feat(zen-box): 新增云端配置同步功能,完善应用构建与配置
本次提交实现了观空禅音应用的核心云端同步功能,包括: 1. 添加用户登录验证接口与本地存储管理 2. 新增云端配置存储与拉取接口,支持跨设备同步歌单、播放进度等设置 3. 新增登录页面与设置页账号管理模块 4. 更新依赖包版本与安卓、iOS打包配置 5. 优化缓存下载逻辑,修复OSS链接编码问题 6. 新增离线模式支持,自动过滤无缓存歌单 7. 新增歌曲排序偏好本地与云端同步功能
This commit is contained in:
+125
-5
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user