feat(zen-box): 新增云端配置同步功能,完善应用构建与配置

本次提交实现了观空禅音应用的核心云端同步功能,包括:
1.  添加用户登录验证接口与本地存储管理
2.  新增云端配置存储与拉取接口,支持跨设备同步歌单、播放进度等设置
3.  新增登录页面与设置页账号管理模块
4.  更新依赖包版本与安卓、iOS打包配置
5.  优化缓存下载逻辑,修复OSS链接编码问题
6.  新增离线模式支持,自动过滤无缓存歌单
7.  新增歌曲排序偏好本地与云端同步功能
This commit is contained in:
yangxiangyuan
2026-07-11 10:57:02 +08:00
parent 33625e3438
commit 6a043687f5
18 changed files with 880 additions and 66 deletions
+125 -5
View File
@@ -6,12 +6,13 @@ import { Audio, AVPlaybackStatus } from 'expo-av';
import * as SplashScreen from 'expo-splash-screen';
import Svg, { Path } from 'react-native-svg';
import { theme } from './theme';
import { Track, RepeatMode, CacheInfo } from './types';
import { Track, RepeatMode, CacheInfo, UserConfig, SortMode } from './types';
import * as API from './api';
import * as Cache from './utils/cache';
import { PlayerScreen } from './screens/PlayerScreen';
import { PlaylistScreen } from './screens/PlaylistScreen';
import { SettingsScreen } from './screens/SettingsScreen';
import { LoginScreen } from './screens/LoginScreen';
// 安全地编码 URL 中的路径部分
const encodeUrlPath = (url: string): string => {
@@ -79,8 +80,6 @@ const StartupLoadingScreen = () => (
</ImageBackground>
);
type SortMode = 'original' | 'asc' | 'desc';
const TabNavigator = () => {
const [tracks, setTracks] = useState<Track[]>([]);
const [cacheInfo, setCacheInfo] = useState<CacheInfo>({});
@@ -98,6 +97,11 @@ const TabNavigator = () => {
const [playlistFiles, setPlaylistFiles] = useState<string[]>(['playlist.json']);
const [currentPlaylistFile, setCurrentPlaylistFile] = useState('playlist.json');
const [resumePlaybackEnabled, setResumePlaybackEnabled] = useState(true);
const [sortMode, setSortMode] = useState<SortMode>('original');
const [showLoginScreen, setShowLoginScreen] = useState(false);
const [loggedInUser, setLoggedInUser] = useState<string | null>(null);
const [isOffline, setIsOffline] = useState(false);
const [offlineAvailablePlaylists, setOfflineAvailablePlaylists] = useState<string[]>([]);
const navigation = useNavigation();
const soundRef = useRef<Audio.Sound | null>(null);
@@ -158,6 +162,57 @@ const TabNavigator = () => {
const refreshCacheInfo = () => {
setCacheInfo(Cache.getCacheInfo());
};
const applyCloudConfig = async (config: UserConfig) => {
try {
if (config.selected_playlist) {
await API.saveSelectedPlaylist(config.selected_playlist);
setCurrentPlaylistFileState(config.selected_playlist);
}
if (config.resume_enabled !== undefined) {
await API.saveResumePlaybackEnabled(config.resume_enabled);
setResumePlaybackEnabledState(config.resume_enabled);
}
if (config.cache_only !== undefined) {
await Cache.setCacheOnlySetting(config.cache_only);
setCacheOnlyState(config.cache_only);
}
if (config.sort_mode) {
await API.saveSortPreference(config.sort_mode);
setSortMode(config.sort_mode);
}
// 恢复播放进度
if (config.playback_positions) {
for (const [key, seconds] of Object.entries(config.playback_positions)) {
const [playlistFile, trackSrc] = key.split('::');
if (playlistFile && trackSrc && seconds > 0) {
await API.savePlaybackPosition(playlistFile, trackSrc, seconds);
}
}
}
} catch (error) {
console.error('应用云端配置失败:', error);
}
};
const buildUserConfig = (): UserConfig => {
return {
selected_playlist: currentPlaylistFileRef.current,
resume_enabled: resumePlaybackEnabledRef.current,
cache_only: cacheOnlyRef.current,
sort_mode: sortMode,
playback_positions: {},
};
};
const pushConfigIfNeeded = async () => {
try {
const config = buildUserConfig();
await API.pushCloudConfig(config);
} catch (error) {
console.error('推送云端配置失败:', error);
}
};
const persistPlaybackPosition = async (
trackIndex = currentIndexRef.current,
positionSeconds = positionRef.current,
@@ -269,16 +324,42 @@ const TabNavigator = () => {
staysActiveInBackground: true,
});
// 检查登录态
const user = await API.getLoggedInUser();
setLoggedInUser(user);
// 检查网络状态
const connected = await API.isNetworkConnected();
setIsOffline(!connected);
// 如果已登录且有网络,拉取云端配置
if (user && connected) {
const cloudConfig = await API.pullCloudConfig();
if (cloudConfig) {
await applyCloudConfig(cloudConfig);
}
}
const cacheOnlySetting = await Cache.getCacheOnlySetting();
setCacheOnlyState(cacheOnlySetting);
const resumePlaybackSetting = await API.getResumePlaybackEnabled();
setResumePlaybackEnabledState(resumePlaybackSetting);
const savedSort = await API.getSortPreference();
setSortMode(savedSort);
const files = await API.getPlaylistFiles();
let files = await API.getPlaylistFiles();
setPlaylistFiles(files);
// 离线模式:反推可用歌单
if (!connected) {
const available = await API.getOfflineAvailablePlaylists((src) => Cache.isCached(src));
setOfflineAvailablePlaylists(available);
files = available;
}
const savedPlaylist = await API.getSavedSelectedPlaylist();
const initialPlaylist = files.includes(savedPlaylist) ? savedPlaylist : 'playlist.json';
await loadPlaylistBundle(initialPlaylist, { showSnapshotAlert: true });
await loadPlaylistBundle(initialPlaylist, { showSnapshotAlert: !connected });
} catch (error) {
console.error('Init failed:', error);
Alert.alert('错误', '无法加载播放列表');
@@ -584,6 +665,7 @@ const TabNavigator = () => {
setCacheOnlyState(value);
try {
await Cache.setCacheOnlySetting(value);
await pushConfigIfNeeded();
} catch (error) {
setCacheOnlyState(!value);
throw error;
@@ -593,6 +675,7 @@ const TabNavigator = () => {
const handlePlaylistChange = async (playlistFile: string) => {
try {
await loadPlaylistBundle(playlistFile, { showSnapshotAlert: true });
await pushConfigIfNeeded();
} catch (error) {
console.error('切换歌单失败:', error);
Alert.alert('错误', '切换歌单失败');
@@ -604,6 +687,7 @@ const TabNavigator = () => {
setResumePlaybackEnabledState(value);
try {
await API.saveResumePlaybackEnabled(value);
await pushConfigIfNeeded();
} catch (error) {
setResumePlaybackEnabledState(!value);
throw error;
@@ -662,6 +746,34 @@ const TabNavigator = () => {
navigation.navigate('Playlist');
};
const handleLoginSuccess = async () => {
setShowLoginScreen(false);
const user = await API.getLoggedInUser();
setLoggedInUser(user);
// 登录成功后拉取云端配置
const connected = await API.isNetworkConnected();
if (connected && user) {
const cloudConfig = await API.pullCloudConfig();
if (cloudConfig) {
await applyCloudConfig(cloudConfig);
}
}
};
const handleLogout = async () => {
await API.logout();
setLoggedInUser(null);
};
if (showLoginScreen) {
return (
<LoginScreen
onLoginSuccess={handleLoginSuccess}
onBack={() => setShowLoginScreen(false)}
/>
);
}
if (isLoading) {
return <StartupLoadingScreen />;
}
@@ -712,10 +824,15 @@ const TabNavigator = () => {
downloadingTrackSrcs={downloadingTrackSrcs}
currentIndex={currentIndex}
isPlaying={isPlaying}
playlistFiles={playlistFiles}
currentPlaylistFile={currentPlaylistFile}
offlineAvailablePlaylists={offlineAvailablePlaylists}
isOffline={isOffline}
onPlaybackOrderChange={handlePlaybackOrderChange}
onSelectTrack={(index) => loadTrack(index, true)}
onDownloadTrack={handleDownloadTrack}
onDownloadAll={downloadAllTracks}
onPlaylistChange={handlePlaylistChange}
isDownloading={isDownloading}
downloadProgress={downloadProgress}
/>
@@ -739,6 +856,9 @@ const TabNavigator = () => {
onPlaylistChange={handlePlaylistChange}
cachedTrackCount={tracks.filter((track) => !!cacheInfo[track.src]).length}
trackCount={tracks.length}
loggedInUser={loggedInUser}
onLoginPress={() => setShowLoginScreen(true)}
onLogout={handleLogout}
/>
)}
options={{