33625e3438
1. 添加安卓端启动图标、启动页资源与配置文件 2. 更新API接口地址为正式服务器地址 3. 调整package.json脚本与依赖项 4. 完善安卓项目构建配置与原生代码 5. 修复剪贴板下载按钮逻辑,改为流式下载
206 lines
6.5 KiB
TypeScript
206 lines
6.5 KiB
TypeScript
import axios from 'axios';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { Track } from './types';
|
|
|
|
const API_BASE_URL = 'https://traesite.umersoft.com:8975';
|
|
const PLAYLIST_LIST_PATH = '/api/zen_box/playlists';
|
|
const DEFAULT_PLAYLIST_FILE = 'playlist.json';
|
|
const PLAYLIST_SNAPSHOT_KEY_PREFIX = '@zen_box_playlist_snapshot:';
|
|
const SELECTED_PLAYLIST_KEY = '@zen_box_selected_playlist';
|
|
const RESUME_ENABLED_KEY = '@zen_box_resume_enabled';
|
|
const PLAYBACK_POSITION_KEY_PREFIX = '@zen_box_playback_position:';
|
|
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
timeout: 10000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
export const formatError = (error: any): string => {
|
|
if (error.response) {
|
|
return `服务器响应 ${error.response.status}: ${error.response.statusText}`;
|
|
} else if (error.request) {
|
|
return `服务器无响应: 无法连接到 ${API_BASE_URL}`;
|
|
} else if (error.message) {
|
|
return `请求错误: ${error.message}`;
|
|
} else {
|
|
return `未知错误`;
|
|
}
|
|
};
|
|
|
|
const normalizePlaylistFile = (fileName?: string): string => {
|
|
return String(fileName || DEFAULT_PLAYLIST_FILE).trim() || DEFAULT_PLAYLIST_FILE;
|
|
};
|
|
|
|
const buildPlaylistPath = (fileName?: string): string => {
|
|
return `/tools/zen_box/${encodeURIComponent(normalizePlaylistFile(fileName))}`;
|
|
};
|
|
|
|
const getPlaylistSnapshotKey = (fileName?: string): string => {
|
|
return `${PLAYLIST_SNAPSHOT_KEY_PREFIX}${normalizePlaylistFile(fileName)}`;
|
|
};
|
|
|
|
const getPlaybackPositionKey = (playlistFile?: string, trackSrc?: string): string => {
|
|
return `${PLAYBACK_POSITION_KEY_PREFIX}${normalizePlaylistFile(playlistFile)}::${String(trackSrc || '').trim()}`;
|
|
};
|
|
|
|
export const getPlaylistFiles = async (): Promise<string[]> => {
|
|
try {
|
|
const response = await api.get<{ ok?: boolean; playlists?: string[] }>(PLAYLIST_LIST_PATH);
|
|
const raw = Array.isArray(response.data?.playlists) ? response.data.playlists : [];
|
|
const files = raw.map(item => String(item || '')).filter(Boolean);
|
|
if (!files.includes(DEFAULT_PLAYLIST_FILE)) {
|
|
files.unshift(DEFAULT_PLAYLIST_FILE);
|
|
}
|
|
return files;
|
|
} catch (error) {
|
|
console.error('读取歌单列表失败:', error);
|
|
return [DEFAULT_PLAYLIST_FILE];
|
|
}
|
|
};
|
|
|
|
export const getSavedSelectedPlaylist = async (): Promise<string> => {
|
|
try {
|
|
const value = await AsyncStorage.getItem(SELECTED_PLAYLIST_KEY);
|
|
return normalizePlaylistFile(value || DEFAULT_PLAYLIST_FILE);
|
|
} catch (error) {
|
|
console.error('读取当前歌单失败:', error);
|
|
return DEFAULT_PLAYLIST_FILE;
|
|
}
|
|
};
|
|
|
|
export const saveSelectedPlaylist = async (fileName: string): Promise<void> => {
|
|
try {
|
|
await AsyncStorage.setItem(SELECTED_PLAYLIST_KEY, normalizePlaylistFile(fileName));
|
|
} catch (error) {
|
|
console.error('保存当前歌单失败:', error);
|
|
}
|
|
};
|
|
|
|
export const getResumePlaybackEnabled = async (): Promise<boolean> => {
|
|
try {
|
|
const value = await AsyncStorage.getItem(RESUME_ENABLED_KEY);
|
|
if (value === null) {
|
|
return true;
|
|
}
|
|
return value === '1';
|
|
} catch (error) {
|
|
console.error('读取断点续播设置失败:', error);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
export const saveResumePlaybackEnabled = async (enabled: boolean): Promise<void> => {
|
|
try {
|
|
await AsyncStorage.setItem(RESUME_ENABLED_KEY, enabled ? '1' : '0');
|
|
} catch (error) {
|
|
console.error('保存断点续播设置失败:', error);
|
|
}
|
|
};
|
|
|
|
export const getSavedPlaybackPosition = async (playlistFile?: string, trackSrc?: string): Promise<number> => {
|
|
try {
|
|
const key = getPlaybackPositionKey(playlistFile, trackSrc);
|
|
const value = await AsyncStorage.getItem(key);
|
|
const seconds = Number(value || 0);
|
|
return Number.isFinite(seconds) && seconds > 0 ? seconds : 0;
|
|
} catch (error) {
|
|
console.error('读取播放进度失败:', error);
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
export const savePlaybackPosition = async (
|
|
playlistFile: string,
|
|
trackSrc: string,
|
|
positionSeconds: number
|
|
): Promise<void> => {
|
|
try {
|
|
const key = getPlaybackPositionKey(playlistFile, trackSrc);
|
|
const safeSeconds = Math.max(0, Math.floor(positionSeconds || 0));
|
|
await AsyncStorage.setItem(key, String(safeSeconds));
|
|
} catch (error) {
|
|
console.error('保存播放进度失败:', error);
|
|
}
|
|
};
|
|
|
|
export const clearPlaybackPosition = async (playlistFile: string, trackSrc: string): Promise<void> => {
|
|
try {
|
|
const key = getPlaybackPositionKey(playlistFile, trackSrc);
|
|
await AsyncStorage.removeItem(key);
|
|
} catch (error) {
|
|
console.error('清除播放进度失败:', error);
|
|
}
|
|
};
|
|
|
|
export const getPlaylist = async (fileName?: string): Promise<Track[]> => {
|
|
try {
|
|
const response = await api.get<Track[]>(buildPlaylistPath(fileName));
|
|
return response.data;
|
|
} catch (error) {
|
|
throw new Error(formatError(error));
|
|
}
|
|
};
|
|
|
|
const isValidPlaylist = (data: unknown): data is Track[] => {
|
|
return Array.isArray(data) && data.every((item) => {
|
|
if (!item || typeof item !== 'object') {
|
|
return false;
|
|
}
|
|
const track = item as Track;
|
|
return typeof track.title === 'string'
|
|
&& typeof track.artist === 'string'
|
|
&& typeof track.src === 'string';
|
|
});
|
|
};
|
|
|
|
export const savePlaylistSnapshot = async (tracks: Track[], fileName?: string): Promise<void> => {
|
|
try {
|
|
await AsyncStorage.setItem(getPlaylistSnapshotKey(fileName), JSON.stringify(tracks));
|
|
} catch (error) {
|
|
console.error('保存歌单快照失败:', error);
|
|
}
|
|
};
|
|
|
|
export const getPlaylistSnapshot = async (fileName?: string): Promise<Track[]> => {
|
|
try {
|
|
const raw = await AsyncStorage.getItem(getPlaylistSnapshotKey(fileName));
|
|
if (!raw) {
|
|
return [];
|
|
}
|
|
|
|
const parsed: unknown = JSON.parse(raw);
|
|
return isValidPlaylist(parsed) ? parsed : [];
|
|
} catch (error) {
|
|
console.error('读取歌单快照失败:', error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
export const getPlaylistWithFallback = async (fileName?: string): Promise<{
|
|
tracks: Track[];
|
|
source: 'remote' | 'snapshot';
|
|
fallbackReason?: string;
|
|
playlistFile: string;
|
|
}> => {
|
|
const normalizedFile = normalizePlaylistFile(fileName);
|
|
try {
|
|
const tracks = await getPlaylist(normalizedFile);
|
|
await savePlaylistSnapshot(tracks, normalizedFile);
|
|
return { tracks, source: 'remote', playlistFile: normalizedFile };
|
|
} catch (error) {
|
|
const fallbackTracks = await getPlaylistSnapshot(normalizedFile);
|
|
if (fallbackTracks.length > 0) {
|
|
return {
|
|
tracks: fallbackTracks,
|
|
source: 'snapshot',
|
|
fallbackReason: error instanceof Error ? error.message : '远程歌单加载失败',
|
|
playlistFile: normalizedFile,
|
|
};
|
|
}
|
|
throw error;
|
|
}
|
|
};
|