HarmonyOS音乐播放器项目文件2
**项目概述
这个音乐播放器应用支持本地音乐扫描、播放控制、歌词显示、主题切换等核心功能,采用了现代化的架构设计和用户界面,为用户提供流畅的音乐播放体验。**
**技术架构
核心技术栈
开发语言: ArkTS
开发框架: HarmonyOS UI Kit
开发工具: DevEco Studio
音频播放: AVPlayer
数据存储: SQLite**


// utils/FileUtils.ets - 文件处理工具类 import { fileIo as fs } from '@kit.CoreFileKit'; import { util } from '@kit.ArkTS'; import { BusinessError } from '@kit.BasicServicesKit'; /** * 音乐信息接口 */ export interface MusicInfo { title: string; artist: string; } /** * 文件处理工具类 */ export class FileUtils { /** * 从 URI 提取文件名 */ static getFileNameFromUri(uri: string): string { const parts = uri.split('/'); const fileName = parts[parts.length - 1]; // URL 解码 try { return decodeURIComponent(fileName); } catch { return fileName; } } /** * 从文件名提取音乐信息(标题和歌手) */ static getMusicInfoFromFileName(fileName: string): MusicInfo { // 移除扩展名 const lastDot = fileName.lastIndexOf('.'); let nameWithoutExt = lastDot > 0 ? fileName.substring(0, lastDot) : fileName; // 常见格式: "歌手 - 歌名", "歌手-歌名" // 尝试用 " - " 分割 let parts = nameWithoutExt.split(' - '); if (parts.length >= 2) { return { artist: parts[0].trim(), title: parts.slice(1).join(' - ').trim() }; } // 尝试用 "-" 分割 parts = nameWithoutExt.split('-'); if (parts.length >= 2) { return { artist: parts[0].trim(), title: parts.slice(1).join('-').trim() }; } // 尝试用 "_" 分割 parts = nameWithoutExt.split('_'); if (parts.length >= 2) { return { artist: parts[0].trim(), title: parts.slice(1).join('_').trim() }; } // 无法解析,返回文件名作为标题 return { title: nameWithoutExt, artist: '未知歌手' }; } /** * 检查是否是音频文件 */ static isAudioFile(fileName: string): boolean { const audioExtensions = ['.mp3', '.flac', '.wav', '.aac', '.m4a', '.ogg', '.wma', '.ape']; const lowerName = fileName.toLowerCase(); return audioExtensions.some(ext => lowerName.endsWith(ext)); } /** * 获取文件大小 */ static async getFileSize(uri: string): Promise<number> { try { const file = fs.openSync(uri, fs.OpenMode.READ_ONLY); const stat = fs.statSync(file.fd); const size = stat.size; fs.closeSync(file); return size; } catch (error) { const err = error as BusinessError; console.warn(`[FileUtils] 获取文件大小失败: code=${err.code}, message=${err.message}`); return 0; } } /** * 支持的音频文件扩展名列表 */ static readonly AUDIO_EXTENSIONS = ['.mp3', '.flac', '.wav', '.aac', '.m4a', '.ogg', '.wma', '.ape']; /** * 检查是否是歌词文件 */ static isLyricFile(fileName: string): boolean { return fileName.toLowerCase().endsWith('.lrc'); } /** * 根据音乐文件名获取对应的歌词文件名 * 例:陈奕迅-富士山下.mp3 -> 陈奕迅-富士山下.lrc */ static getLyricFileName(musicFileName: string): string { const lastDot = musicFileName.lastIndexOf('.'); if (lastDot > 0) { return musicFileName.substring(0, lastDot) + '.lrc'; } return musicFileName + '.lrc'; } /** * 读取歌词文件内容(支持多种编码) */ static async readLyricFile(uri: string): Promise<string> { try { const file = fs.openSync(uri, fs.OpenMode.READ_ONLY); const buffer = new ArrayBuffer(1024 * 100); // 100KB缓冲区 const readLen = fs.readSync(file.fd, buffer); fs.closeSync(file); const uint8Array = new Uint8Array(buffer, 0, readLen); // 检测 BOM 标记 const hasBOM = uint8Array.length >= 3 && uint8Array[0] === 0xEF && uint8Array[1] === 0xBB && uint8Array[2] === 0xBF; // 先尝试 UTF-8 解码 try { const decoder = new util.TextDecoder('utf-8', { fatal: true, ignoreBOM: true }); const content = decoder.decodeWithStream(uint8Array); // 验证解码结果:检查是否包含常见 LRC 标签 if (content.includes('[') && content.includes(']')) { console.info(`[FileUtils] UTF-8 解码成功${hasBOM ? ' (with BOM)' : ''}`); return content; } } catch (utf8Error) { console.warn(`[FileUtils] UTF-8 解码失败,尝试其他编码`); } // 尝试 GBK/GB2312 等中文编码(通过 GB18030 解码) try { const decoder = new util.TextDecoder('gb18030', { fatal: false }); const content = decoder.decodeWithStream(uint8Array); if (content.includes('[') && content.includes(']')) { console.info(`[FileUtils] GB18030 解码成功`); return content; } } catch (gbError) { console.warn(`[FileUtils] GB18030 解码失败`); } // 最后尝试宽松的 UTF-8 解码(不抛错误) const decoder = new util.TextDecoder('utf-8', { fatal: false, ignoreBOM: true }); const content = decoder.decodeWithStream(uint8Array); console.info(`[FileUtils] 使用宽松 UTF-8 解码`); return content; } catch (error) { const err = error as BusinessError; console.error(`[FileUtils] 读取歌词文件失败: code=${err.code}, message=${err.message}`); return ''; } } /** * 匹配歌词文件:支持多种匹配策略 * @param musicFilePath 音乐文件完整路径 * @returns 歌词文件路径,如果不存在则返回 null */ static async findMatchingLyricFile(musicFilePath: string): Promise<string | null> { try { // 获取音乐文件所在目录 const lastSlash = musicFilePath.lastIndexOf('/'); if (lastSlash === -1) { return null; } const dirPath = musicFilePath.substring(0, lastSlash); const musicFileName = musicFilePath.substring(lastSlash + 1); // 移除扩展名 const lastDot = musicFileName.lastIndexOf('.'); const nameWithoutExt = lastDot > 0 ? musicFileName.substring(0, lastDot) : musicFileName; console.info(`[FileUtils] 开始匹配歌词: ${nameWithoutExt}`); // 策略 1:同名匹配(歌手-歌名.lrc) const strategy1 = `${dirPath}/${nameWithoutExt}.lrc`; if (await FileUtils.checkFileExists(strategy1)) { console.info(`[FileUtils] ✅ 策略 1 匹配成功: ${nameWithoutExt}.lrc`); return strategy1; } // 策略 2:尝试颜倒顺序(如果有 - 分隔符) if (nameWithoutExt.includes('-')) { const parts = nameWithoutExt.split('-'); if (parts.length === 2) { const artist = parts[0].trim(); const title = parts[1].trim(); // 策略 2a:歌名-歌手.lrc const strategy2a = `${dirPath}/${title}-${artist}.lrc`; if (await FileUtils.checkFileExists(strategy2a)) { console.info(`[FileUtils] ✅ 策略 2a 匹配成功: ${title}-${artist}.lrc`); return strategy2a; } // 策略 2b:歌名 - 歌手.lrc(带空格) const strategy2b = `${dirPath}/${title} - ${artist}.lrc`; if (await FileUtils.checkFileExists(strategy2b)) { console.info(`[FileUtils] ✅ 策略 2b 匹配成功: ${title} - ${artist}.lrc`); return strategy2b; } // 策略 2c:只有歌名.lrc const strategy2c = `${dirPath}/${title}.lrc`; if (await FileUtils.checkFileExists(strategy2c)) { console.info(`[FileUtils] ✅ 策略 2c 匹配成功: ${title}.lrc`); return strategy2c; } } } // 策略 3:尝试空格分隔 if (nameWithoutExt.includes(' ')) { const parts = nameWithoutExt.split(' '); if (parts.length >= 2) { // 去掉空元素 const cleanParts = parts.filter(p => p.length > 0); if (cleanParts.length === 2) { const part1 = cleanParts[0]; const part2 = cleanParts[1]; // 策略 3a:part2-part1.lrc const strategy3a = `${dirPath}/${part2}-${part1}.lrc`; if (await FileUtils.checkFileExists(strategy3a)) { console.info(`[FileUtils] ✅ 策略 3a 匹配成功: ${part2}-${part1}.lrc`); return strategy3a; } // 策略 3b:part2 part1.lrc const strategy3b = `${dirPath}/${part2} ${part1}.lrc`; if (await FileUtils.checkFileExists(strategy3b)) { console.info(`[FileUtils] ✅ 策略 3b 匹配成功: ${part2} ${part1}.lrc`); return strategy3b; } } } } console.warn(`[FileUtils] ⚠️ 未找到匹配的歌词文件: ${nameWithoutExt}`); return null; } catch (error) { const err = error as BusinessError; console.warn(`[FileUtils] 匹配歌词失败: code=${err.code}, message=${err.message}`); return null; } } /** * 检查文件是否存在 */ private static async checkFileExists(filePath: string): Promise<boolean> { try { const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); fs.closeSync(file); return true; } catch (error) { return false; } } }
import media from '@ohos.multimedia.media'; // 音乐项接口 export interface MusicItem { id: string; title: string; artist: string; duration: number; } // 播放状态接口 export interface PlaybackState { currentSong: MusicItem | null; isPlaying: boolean; playlist: MusicItem[]; } // 音乐服务类 export class MusicService { private static instance: MusicService; private player: media.AVPlayer | null = null; private currentSong: MusicItem | null = null; private isPlaying: boolean = false; private playlist: MusicItem[] = []; private constructor() { this.initPlaylist(); } static getInstance(): MusicService { if (!MusicService.instance) { MusicService.instance = new MusicService(); } return MusicService.instance; } // 初始化播放列表(使用模拟数据) private initPlaylist(): void { this.playlist = [ { id: '1', title: '青花瓷', artist: '周杰伦', duration: 245000 }, { id: '2', title: '晴天', artist: '周杰伦', duration: 258000 }, { id: '3', title: '七里香', artist: '周杰伦', duration: 295000 }, { id: '4', title: '夜曲', artist: '周杰伦', duration: 223000 }, { id: '5', title: '简单爱', artist: '周杰伦', duration: 268000 } ]; } // 获取播放列表 getPlaylist(): MusicItem[] { return this.playlist; } // 模拟播放(不使用真实文件) async play(song: MusicItem): Promise<boolean> { try { console.log('开始播放:', song.title); this.currentSong = song; // 创建播放器 if (!this.player) { this.player = await media.createAVPlayer(); } // 使用一个虚拟的URL(避免文件权限问题) // 在实际应用中,这里应该是真实的音乐文件路径 this.player.url = 'resource://rawfile/music_sample.mp3'; // 准备播放 await this.player.prepare(); // 开始播放 await this.player.play(); this.isPlaying = true; return true; } catch (error) { console.error('播放失败:', error); // 播放失败时,模拟播放成功状态 this.currentSong = song; this.isPlaying = true; // 模拟播放进度 this.simulatePlayback(); return true; // 返回true让UI可以更新 } } // 模拟播放进度 private simulatePlayback(): void { // 在真实应用中,这里应该使用真实的播放器事件 // 这里使用setTimeout模拟 setTimeout(() => { this.isPlaying = false; // 通知UI播放结束 }, 30000); // 30秒后自动停止 } // 暂停播放 async pause(): Promise<void> { if (this.player && this.isPlaying) { try { await this.player.pause(); } catch (error) { console.error('暂停失败:', error); } } this.isPlaying = false; } // 继续播放 async resume(): Promise<void> { if (this.player && !this.isPlaying) { try { await this.player.play(); this.isPlaying = true; } catch (error) { console.error('继续播放失败:', error); } } } // 下一曲 async next(): Promise<boolean> { if (!this.currentSong || this.playlist.length === 0) { return false; } const currentIndex = this.playlist.findIndex(song => song.id === this.currentSong!.id); const nextIndex = (currentIndex + 1) % this.playlist.length; if (nextIndex < this.playlist.length) { return await this.play(this.playlist[nextIndex]); } return false; } // 上一曲 async previous(): Promise<boolean> { if (!this.currentSong || this.playlist.length === 0) { return false; } const currentIndex = this.playlist.findIndex(song => song.id === this.currentSong!.id); const prevIndex = currentIndex > 0 ? currentIndex - 1 : this.playlist.length - 1; return await this.play(this.playlist[prevIndex]); } // 获取当前播放状态 getCurrentState(): PlaybackState { const state: PlaybackState = { currentSong: this.currentSong, isPlaying: this.isPlaying, playlist: this.playlist }; return state; } // 停止播放并释放资源 async stop(): Promise<void> { if (this.player) { try { await this.player.stop(); await this.player.release(); this.player = null; } catch (error) { console.error('停止播放失败:', error); } } this.isPlaying = false; this.currentSong = null; } }
// viewmodels/MusicViewModel.ets - 音乐业务逻辑视图模型 import { common, abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; import { picker } from '@kit.CoreFileKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { dataSharePredicates } from '@kit.ArkData'; import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { MusicItem, PlayMode } from '../models/MusicItem'; import { MusicDatabase } from '../services/Database'; import { AudioPlayer } from '../services/AudioPlayer'; import { FileUtils } from '../utils/FileUtils'; const TAG = 'MusicViewModel'; const DOMAIN = 0x0001; /** * 音乐业务逻辑视图模型 * 负责管理音乐列表、播放控制、文件扫描等业务逻辑 */ export class MusicViewModel { // 数据列表 public musicList: MusicItem[] = []; public favoriteList: MusicItem[] = []; public isLoading: boolean = true; // 上下文和服务 private context: common.UIAbilityContext; private database: MusicDatabase; private audioPlayer: AudioPlayer; // 回调函数 private onDataChanged?: () => void; private onToast?: (message: string) => void; constructor(context: common.UIAbilityContext) { this.context = context; this.database = new MusicDatabase(context); this.audioPlayer = AudioPlayer.getInstance(context); } /** * 设置数据变化回调 */ setOnDataChanged(callback: () => void): void { this.onDataChanged = callback; } /** * 设置 Toast 提示回调 */ setOnToast(callback: (message: string) => void): void { this.onToast = callback; } /** * 显示提示消息 */ private showTip(message: string): void { if (this.onToast) { this.onToast(message); } } /** * 通知数据变化 */ private notifyDataChanged(): void { if (this.onDataChanged) { this.onDataChanged(); } } /** * 获取收藏列表 */ async getFavoriteList(): Promise<MusicItem[]> { return await this.database.getFavoriteMusic(); } /** * 初始化 */ async init(): Promise<void> { hilog.info(DOMAIN, TAG, '========== ViewModel 初始化 =========='); try { await this.requestPermissions(); await this.database.initDatabase(); await this.audioPlayer.init(); await this.loadMusicList(); this.setupPlayerListeners(); hilog.info(DOMAIN, TAG, '========== ViewModel 初始化完成 =========='); } catch (error) { hilog.error(DOMAIN, TAG, '初始化失败: %{public}s', JSON.stringify(error)); throw new Error('ViewModel 初始化失败: ' + JSON.stringify(error)); } } /** * 请求媒体权限 */ async requestPermissions(): Promise<void> { console.info('[MusicViewModel] ===== 开始请求权限 ====='); const permissions: Permissions[] = [ 'ohos.permission.READ_MEDIA', 'ohos.permission.WRITE_MEDIA', 'ohos.permission.READ_AUDIO', 'ohos.permission.READ_IMAGEVIDEO' ]; try { const atManager = abilityAccessCtrl.createAtManager(); const result = await atManager.requestPermissionsFromUser(this.context, permissions); let allGranted = true; for (let i = 0; i < result.authResults.length; i++) { const granted = result.authResults[i] === 0; console.info(`[MusicViewModel] ${permissions[i]}: ${granted ? '✅' : '❌'}`); if (!granted) { allGranted = false; } } if (!allGranted) { this.showTip('部分权限未授权,可能无法扫描本地音乐'); } } catch (error) { const err = error as BusinessError; console.error(`[MusicViewModel] 请求权限失败: ${err.message}`); } console.info('[MusicViewModel] ===== 权限请求结束 ====='); } /** * 加载音乐列表 */ async loadMusicList(): Promise<void> { this.isLoading = true; this.notifyDataChanged(); try { this.musicList = await this.database.getAllMusic(); this.favoriteList = await this.database.getFavoriteMusic(); this.audioPlayer.setPlaylist(this.musicList); hilog.info(DOMAIN, TAG, '加载音乐列表: %{public}d 首', this.musicList.length); } catch (error) { console.error(`加载音乐列表失败: ${JSON.stringify(error)}`); } finally { this.isLoading = false; this.notifyDataChanged(); } } /** * 设置播放器监听器 */ setupPlayerListeners(): void { this.audioPlayer.setOnStateChangeListener((state: string) => { hilog.info(DOMAIN, TAG, '播放状态变化: %{public}s', state); }); this.audioPlayer.setOnMusicChangeListener((music: MusicItem) => { hilog.info(DOMAIN, TAG, '音乐变化: %{public}s', music.title); }); // 监听收藏状态变化 this.audioPlayer.setOnFavoriteChangeListener((musicId: number, isFavorite: boolean) => { console.info(`[MusicViewModel] 收藏状态变化: musicId=${musicId}, isFavorite=${isFavorite}`); this.updateMusicFavoriteState(musicId, isFavorite); }); } /** * 更新音乐收藏状态 */ private updateMusicFavoriteState(musicId: number, isFavorite: boolean): void { const newList: MusicItem[] = []; for (const m of this.musicList) { if (m.id === musicId) { newList.push({ id: m.id, title: m.title, artist: m.artist, album: m.album, duration: m.duration, filePath: m.filePath, cover: m.cover, fileName: m.fileName, fileSize: m.fileSize, createTime: m.createTime, playCount: m.playCount, isFavorite: isFavorite, lyrics: m.lyrics }); } else { newList.push(m); } } this.musicList = newList; // 重新加载收藏列表 this.database.getFavoriteMusic().then((favorites) => { this.favoriteList = favorites; this.notifyDataChanged(); }); } /** * 播放音乐 */ async playMusic(music: MusicItem): Promise<void> { console.info(`[MusicViewModel] 播放音乐: ${music.title}`); try { // 如果该音乐没有歌词,尝试自动匹配 if (!music.lyrics || music.lyrics.length === 0) { await this.autoMatchLyrics(music); } await this.audioPlayer.play(music); await this.database.updatePlayCount(music.id); console.info(`[MusicViewModel] ✅ 播放已启动`); } catch (error) { const err = error as BusinessError; console.error(`[MusicViewModel] 播放失败: ${err.message}`); this.showTip('播放失败: ' + (err.message || '未知错误')); throw new Error('播放音乐失败: ' + err.message); } } /** * 自动匹配单首音乐的歌词 */ private async autoMatchLyrics(music: MusicItem): Promise<void> { try { const lyricPath = await FileUtils.findMatchingLyricFile(music.filePath); if (lyricPath) { const lyricsContent = await FileUtils.readLyricFile(lyricPath); if (lyricsContent) { // 更新数据库 await this.database.updateMusicLyrics(music.id, lyricsContent); // 更新内存中的对象 music.lyrics = lyricsContent; // 更新列表 const index = this.musicList.findIndex(m => m.id === music.id); if (index !== -1) { this.musicList[index].lyrics = lyricsContent; } // 同步到全局状态(关键:确保 PlayPage 立即更新) AppStorage.setOrCreate('currentMusicLyrics', lyricsContent); console.info(`[MusicViewModel] 自动匹配歌词: ${music.title}`); } } } catch (error) { // 静默失败,不影响播放 console.warn(`[MusicViewModel] 自动匹配歌词失败: ${JSON.stringify(error)}`); } } /** * 切换播放/暂停 */ async togglePlayPause(isPlaying: boolean, currentMusicId: number): Promise<void> { try { if (isPlaying) { await this.audioPlayer.pause(); } else { if (currentMusicId > 0) { await this.audioPlayer.resume(); } else if (this.musicList.length > 0) { await this.playMusic(this.musicList[0]); } else { this.showTip('请先添加音乐'); } } } catch (error) { console.error(`[MusicViewModel] 切换播放/暂停失败: ${JSON.stringify(error)}`); this.showTip('操作失败'); throw new Error('切换播放状态失败: ' + JSON.stringify(error)); } } /** * 切换收藏 */ async toggleFavorite(music: MusicItem): Promise<boolean> { console.info(`[MusicViewModel] 切换收藏: ${music.title}`); const newFavoriteState = await this.database.toggleFavorite(music.id); // 更新列表 this.updateMusicFavoriteState(music.id, newFavoriteState); // 通知播放器 this.audioPlayer.notifyFavoriteChanged(music.id, newFavoriteState); this.showTip(newFavoriteState ? '已添加到收藏' : '已取消收藏'); return newFavoriteState; } /** * 删除音乐 */ async deleteMusic(music: MusicItem, currentMusicId: number): Promise<void> { try { // 如果正在播放该音乐,先停止播放 if (currentMusicId === music.id) { await this.audioPlayer.stop(); } const success = await this.database.deleteMusic(music.id); if (success) { await this.loadMusicList(); this.showTip(`已删除: ${music.title}`); } else { this.showTip('删除失败'); } } catch (error) { console.error(`删除音乐失败: ${JSON.stringify(error)}`); this.showTip('删除失败'); throw new Error('删除音乐失败: ' + JSON.stringify(error)); } } /** * 搜索音乐 */ async searchMusic(keyword: string): Promise<void> { if (keyword.trim()) { this.isLoading = true; this.notifyDataChanged(); this.musicList = await this.database.searchMusic(keyword); this.isLoading = false; this.notifyDataChanged(); } else { await this.loadMusicList(); } } /** * 扫描媒体库 - 自动添加歌曲(一键全自动,无需手动选择) */ async scanMediaLibrary(): Promise<void> { console.info('[MusicViewModel] ===== 自动添加歌曲(一键全自动) ====='); // 先尝试使用选择器让用户选择 // 如果用户取消,则提示使用"选择音乐文件"功能 try { const audioPicker = new picker.AudioViewPicker(this.context); const selectOptions = new picker.AudioSelectOptions(); selectOptions.maxSelectNumber = 500; console.info('[MusicViewModel] 打开音频选择器(支持批量选择,最多500首)...'); const result = await audioPicker.select(selectOptions); if (!result || result.length === 0) { console.info('[MusicViewModel] 用户取消选择'); this.showTip('已取消'); return; } await this.processMusicFiles(result); } catch (error) { const err = error as BusinessError; console.error(`[MusicViewModel] 选择音频失败: ${err.message}`); this.showTip('选择音频失败'); } } /** * 处理音乐文件(通用方法) */ private async processMusicFiles(uriList: string[]): Promise<void> { let addedCount = 0; let skippedCount = 0; let lyricsCount = 0; console.info(`[MusicViewModel] 开始处理 ${uriList.length} 个音频文件...`); for (const uri of uriList) { try { // 检查是否已存在 const exists = await this.database.isMusicExists(uri); if (exists) { skippedCount++; continue; } // 获取文件信息 const fileName = FileUtils.getFileNameFromUri(uri); const musicInfo = FileUtils.getMusicInfoFromFileName(fileName); const fileSize = await FileUtils.getFileSize(uri); // 自动匹配歌词文件 const lyricPath = await FileUtils.findMatchingLyricFile(uri); let lyricsContent = ''; if (lyricPath) { lyricsContent = await FileUtils.readLyricFile(lyricPath); if (lyricsContent) { lyricsCount++; console.info(`[MusicViewModel] 自动匹配歌词: ${fileName}`); } } const musicItem: MusicItem = { id: Date.now() + addedCount, title: musicInfo.title, artist: musicInfo.artist, album: '未知专辑', duration: 0, filePath: uri, cover: $r('app.media.ic_album'), fileName: fileName, fileSize: fileSize, createTime: Date.now(), playCount: 0, isFavorite: false, lyrics: lyricsContent }; await this.database.addMusic(musicItem); addedCount++; console.info(`[MusicViewModel] 已添加: ${fileName}`); } catch (fileError) { const err = fileError as BusinessError; console.error(`[MusicViewModel] 处理文件失败: ${err.message}`); } } await this.loadMusicList(); console.info(`[MusicViewModel] ========== 处理结果 ==========`); console.info(`[MusicViewModel] 总文件: ${uriList.length}`); console.info(`[MusicViewModel] 新增: ${addedCount}`); console.info(`[MusicViewModel] 跳过: ${skippedCount}`); console.info(`[MusicViewModel] 匹配歌词: ${lyricsCount}`); console.info(`[MusicViewModel] ================================`); if (addedCount > 0) { let message = `成功添加 ${addedCount} 首歌曲`; if (lyricsCount > 0) { message += `,${lyricsCount} 首已匹配歌词`; } if (skippedCount > 0) { message += `,跳过 ${skippedCount} 首已存在`; } this.showTip(message); } else if (skippedCount > 0) { this.showTip(`所有歌曲已存在`); } else { this.showTip('未添加任何文件'); } } /** * 选择音乐文件 (AudioViewPicker) */ async selectMusicFiles(): Promise<void> { console.info('[MusicViewModel] ===== AudioViewPicker 选择音乐 ====='); try { const audioPicker = new picker.AudioViewPicker(this.context); const selectOptions = new picker.AudioSelectOptions(); selectOptions.maxSelectNumber = 500; // 最多选择 500 首 const result = await audioPicker.select(selectOptions); if (result && result.length > 0) { let addedCount = 0; let skippedCount = 0; let lyricsCount = 0; for (const uri of result) { try { // 检查是否已存在 const exists = await this.database.isMusicExists(uri); if (exists) { skippedCount++; continue; } // 获取文件信息 const fileName = FileUtils.getFileNameFromUri(uri); const musicInfo = FileUtils.getMusicInfoFromFileName(fileName); const fileSize = await FileUtils.getFileSize(uri); // 自动匹配歌词文件 const lyricPath = await FileUtils.findMatchingLyricFile(uri); let lyricsContent = ''; if (lyricPath) { lyricsContent = await FileUtils.readLyricFile(lyricPath); if (lyricsContent) { lyricsCount++; console.info(`[MusicViewModel] 自动匹配歌词: ${fileName}`); } } const musicItem: MusicItem = { id: Date.now() + addedCount, title: musicInfo.title, artist: musicInfo.artist, album: '未知专辑', duration: 0, filePath: uri, cover: $r('app.media.ic_album'), fileName: fileName, fileSize: fileSize, createTime: Date.now(), playCount: 0, isFavorite: false, lyrics: lyricsContent }; await this.database.addMusic(musicItem); addedCount++; } catch (fileError) { const err = fileError as BusinessError; console.error(`[MusicViewModel] 处理文件失败: ${err.message}`); } } await this.loadMusicList(); if (addedCount > 0) { let message = `添加 ${addedCount} 首音乐`; if (lyricsCount > 0) { message += `,${lyricsCount} 首已匹配歌词`; } if (skippedCount > 0) { message += `,跳过 ${skippedCount} 首已存在`; } this.showTip(message); } else if (skippedCount > 0) { this.showTip(`所有歌曲已存在`); } else { this.showTip('未添加任何文件'); } } else { this.showTip('未选择文件'); } } catch (error) { const err = error as BusinessError; console.error(`[MusicViewModel] 选择文件失败: ${err.message}`); this.showTip('选择文件失败'); throw new Error('选择文件失败: ' + err.message); } } /** * 扫描本地音乐 (DocumentViewPicker) */ async scanLocalMusic(): Promise<void> { console.info('[MusicViewModel] ===== DocumentViewPicker 扫描本地音乐 ====='); try { const documentSelectOptions = new picker.DocumentSelectOptions(); documentSelectOptions.maxSelectNumber = 100; documentSelectOptions.fileSuffixFilters = FileUtils.AUDIO_EXTENSIONS; const documentPicker = new picker.DocumentViewPicker(this.context); const result = await documentPicker.select(documentSelectOptions); if (result && result.length > 0) { let addedCount = 0; let skippedCount = 0; let lyricsCount = 0; for (const uri of result) { try { // 检查是否已存在 const exists = await this.database.isMusicExists(uri); if (exists) { skippedCount++; continue; } const fileName = FileUtils.getFileNameFromUri(uri); // 检查是否是音频文件 if (!FileUtils.isAudioFile(fileName)) { continue; } const musicInfo = FileUtils.getMusicInfoFromFileName(fileName); const fileSize = await FileUtils.getFileSize(uri); // 自动匹配歌词文件 const lyricPath = await FileUtils.findMatchingLyricFile(uri); let lyricsContent = ''; if (lyricPath) { lyricsContent = await FileUtils.readLyricFile(lyricPath); if (lyricsContent) { lyricsCount++; console.info(`[MusicViewModel] 自动匹配歌词: ${fileName}`); } } const musicItem: MusicItem = { id: Date.now() + addedCount, title: musicInfo.title, artist: musicInfo.artist, album: '未知专辑', duration: 0, filePath: uri, cover: $r('app.media.ic_album'), fileName: fileName, fileSize: fileSize, createTime: Date.now(), playCount: 0, isFavorite: false, lyrics: lyricsContent }; await this.database.addMusic(musicItem); addedCount++; } catch (fileError) { const err = fileError as BusinessError; console.error(`[MusicViewModel] 处理文件失败: ${err.message}`); } } await this.loadMusicList(); if (addedCount > 0) { let message = `扫描完成,添加 ${addedCount} 首`; if (lyricsCount > 0) { message += `,${lyricsCount} 首已匹配歌词`; } if (skippedCount > 0) { message += `,跳过 ${skippedCount} 首`; } this.showTip(message); } else if (skippedCount > 0) { this.showTip('所有歌曲已存在'); } else { this.showTip('未找到音乐文件'); } } else { this.showTip('未选择文件'); } } catch (error) { const err = error as BusinessError; console.error(`[MusicViewModel] 扫描本地音乐失败: ${err.message}`); this.showTip('扫描失败'); throw new Error('扫描本地音乐失败: ' + err.message); } } /** * 清空音乐库 */ async clearAllMusic(currentMusicId: number): Promise<void> { console.info('[MusicViewModel] ===== 清空音乐库 ====='); try { // 停止当前播放 if (currentMusicId > 0) { await this.audioPlayer.stop(); } const success = await this.database.clearAllMusic(); if (success) { this.musicList = []; this.favoriteList = []; this.notifyDataChanged(); this.showTip('已清空音乐库'); } else { this.showTip('清空失败'); } } catch (error) { const err = error as BusinessError; console.error(`[MusicViewModel] 清空音乐库失败: ${err.message}`); this.showTip('清空失败'); throw new Error('清空音乐库失败: ' + err.message); } } /** * 为已有音乐扫描匹配歌词 */ async scanLyricsForExistingMusic(): Promise<void> { console.info('[MusicViewModel] ===== 扫描匹配歌词 ====='); try { this.isLoading = true; this.notifyDataChanged(); let matchedCount = 0; let totalCount = this.musicList.length; const currentMusic = this.audioPlayer.getCurrentMusic(); // 获取当前播放的音乐 for (const music of this.musicList) { // 跳过已有歌词的音乐 if (music.lyrics && music.lyrics.length > 0) { continue; } // 尝试匹配歌词 const lyricPath = await FileUtils.findMatchingLyricFile(music.filePath); if (lyricPath) { const lyricsContent = await FileUtils.readLyricFile(lyricPath); if (lyricsContent) { // 更新数据库 await this.database.updateMusicLyrics(music.id, lyricsContent); matchedCount++; console.info(`[MusicViewModel] 匹配歌词: ${music.title}`); // 如果是当前播放的音乐,立即同步到全局状态 if (currentMusic && currentMusic.id === music.id) { AppStorage.setOrCreate('currentMusicLyrics', lyricsContent); console.info(`[MusicViewModel] 已同步当前播放音乐的歌词`); } } } } // 重新加载音乐列表 await this.loadMusicList(); this.isLoading = false; this.notifyDataChanged(); if (matchedCount > 0) { this.showTip(`扫描完成,为 ${matchedCount}/${totalCount} 首音乐匹配了歌词`); } else { this.showTip('未找到新的歌词文件'); } } catch (error) { this.isLoading = false; this.notifyDataChanged(); const err = error as BusinessError; console.error(`[MusicViewModel] 扫描歌词失败: ${err.message}`); this.showTip('扫描歌词失败'); throw new Error('扫描歌词失败: ' + err.message); } } /** * 添加模拟音乐数据 */ async addMockMusic(): Promise<void> { const mockMusics: MusicItem[] = [ { id: Date.now(), title: '示例歌曲1', artist: '未知歌手', album: '未知专辑', duration: 269000, filePath: '/data/storage/el2/base/files/song1.mp3', cover: $r('app.media.ic_album'), fileName: 'song1.mp3', fileSize: 1024 * 1024 * 8, createTime: Date.now(), playCount: 0, isFavorite: false, lyrics: '' }, { id: Date.now() + 1, title: '示例歌曲2', artist: '未知歌手', album: '未知专辑', duration: 223000, filePath: '/data/storage/el2/base/files/song2.mp3', cover: $r('app.media.ic_album'), fileName: 'song2.mp3', fileSize: 1024 * 1024 * 6, createTime: Date.now() + 1, playCount: 0, isFavorite: true, lyrics: '' } ]; try { for (const music of mockMusics) { await this.database.addMusic(music); } await this.loadMusicList(); this.showTip('添加 2 首示例音乐'); } catch (error) { console.error(`添加音乐失败: ${JSON.stringify(error)}`); this.showTip('添加失败'); throw new Error('添加模拟音乐失败: ' + JSON.stringify(error)); } } /** * 获取播放器实例 */ getAudioPlayer(): AudioPlayer { return this.audioPlayer; } /** * 释放资源 */ release(): void { this.audioPlayer.release(); } }
// pages/Index.ets - 音乐播放器主页面(重构版) import { router } from '@kit.ArkUI'; import { common } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { window } from '@kit.ArkUI'; import { preferences } from '@kit.ArkData'; import { MusicItem, PlayMode, formatTime } from './models/MusicItem'; import { MusicViewModel } from './viewmodels/MusicViewModel'; import { PlaylistDrawer } from './components/PlaylistDrawer'; import { BusinessError } from '@kit.BasicServicesKit'; const TAG = 'MusicApp'; const DOMAIN = 0x0001; @Entry @Component struct Index { // ViewModel private viewModel: MusicViewModel = new MusicViewModel(AppStorage.get<common.UIAbilityContext>('context') as common.UIAbilityContext); // UI 状态 @State musicList: MusicItem[] = []; @State favoriteList: MusicItem[] = []; @State isLoading: boolean = true; @State searchKeyword: string = ''; @State currentTab: number = 0; // 0: 全部, 1: 收藏 @State isScanning: boolean = false; @State showScanMenu: boolean = false; @State showPlaylistDrawer: boolean = false; @State toastMessage: string = ''; @State showToast: boolean = false; @State isDarkTheme: boolean = false; // 主题切换 // 使用 @StorageLink 实现全局播放状态同步 @StorageLink('currentMusicId') currentMusicId: number = 0; @StorageLink('currentMusicTitle') currentMusicTitle: string = ''; @StorageLink('currentMusicArtist') currentMusicArtist: string = ''; @StorageLink('currentMusicCover') currentMusicCover: ResourceStr = ''; @StorageLink('isPlaying') isPlaying: boolean = false; @StorageLink('currentPosition') currentPosition: number = 0; @StorageLink('playMode') playMode: PlayMode = PlayMode.LOOP; async aboutToAppear(): Promise<void> { hilog.info(DOMAIN, TAG, '========== 应用启动 =========='); // 从本地存储加载主题设置 try { const context = AppStorage.get<common.UIAbilityContext>('context'); if (context) { const dataPreferences = preferences.getPreferencesSync(context, { name: 'app_settings' }); const savedTheme = dataPreferences.getSync('isDarkTheme', false) as boolean; this.isDarkTheme = savedTheme; hilog.info(DOMAIN, TAG, '加载保存的主题设置: %{public}s', savedTheme ? '暗色' : '浅色'); } } catch (error) { hilog.error(DOMAIN, TAG, '加载主题设置失败: %{public}s', JSON.stringify(error)); } // 初始化主题状态到 AppStorage AppStorage.setOrCreate('isDarkTheme', this.isDarkTheme); // 更新状态栏 this.updateStatusBar(); try { // 设置 ViewModel 回调 this.viewModel.setOnDataChanged(() => { this.syncFromViewModel(); }); this.viewModel.setOnToast((message: string) => { this.showTip(message); }); // 初始化 ViewModel await this.viewModel.init(); // 同步数据 this.syncFromViewModel(); hilog.info(DOMAIN, TAG, '========== 应用初始化完成 =========='); } catch (error) { hilog.error(DOMAIN, TAG, '初始化失败: %{public}s', JSON.stringify(error)); } } // 更新系统状态栏 private updateStatusBar(): void { const mainWindow = AppStorage.get<window.Window>('mainWindow'); if (mainWindow) { const statusBarColor = this.isDarkTheme ? '#1a1a2e' : '#F8F9FA'; const statusBarContentColor = this.isDarkTheme ? '#FFFFFF' : '#000000'; mainWindow.setWindowSystemBarProperties({ statusBarColor: statusBarColor, statusBarContentColor: statusBarContentColor }).catch((error: Error) => { hilog.error(DOMAIN, TAG, '设置状态栏失败: %{public}s', JSON.stringify(error)); }); } } // 页面显示时刷新数据(从播放页返回时) onPageShow(): void { hilog.info(DOMAIN, TAG, '========== 页面显示 - 刷新数据 =========='); this.viewModel.loadMusicList(); hilog.info(DOMAIN, TAG, '当前播放: %{public}s, 状态: %{public}s', this.currentMusicTitle || '无', this.isPlaying ? '播放中' : '暂停'); } // 同步 ViewModel 数据到 UI private syncFromViewModel(): void { // 使用数组切片创建新数组,触发响应式更新 this.musicList = this.viewModel.musicList.slice(); this.favoriteList = this.viewModel.favoriteList.slice(); this.isLoading = this.viewModel.isLoading; } // 播放音乐 async playMusic(music: MusicItem): Promise<void> { try { await this.viewModel.playMusic(music); } catch (error) { // 错误已在 ViewModel 中处理 } } // 切换播放/暂停 async togglePlayPause(): Promise<void> { try { await this.viewModel.togglePlayPause(this.isPlaying, this.currentMusicId); } catch (error) { // 错误已在 ViewModel 中处理 } } // 切换收藏 async toggleFavorite(music: MusicItem): Promise<void> { console.info(`[Index] 切换收藏: ${music.title}, 当前状态: ${music.isFavorite}`); const newFavoriteState = await this.viewModel.toggleFavorite(music); // 找到数组索引 const musicIndex = this.musicList.findIndex(m => m.id === music.id); if (musicIndex !== -1) { // 创建新对象(避免展开语法) const oldMusic = this.musicList[musicIndex]; const updatedMusic: MusicItem = { id: oldMusic.id, title: oldMusic.title, artist: oldMusic.artist, album: oldMusic.album, duration: oldMusic.duration, filePath: oldMusic.filePath, cover: oldMusic.cover, fileName: oldMusic.fileName, fileSize: oldMusic.fileSize, createTime: oldMusic.createTime, playCount: oldMusic.playCount, isFavorite: newFavoriteState, lyrics: oldMusic.lyrics }; // 使用 splice 替换指定索引的元素(触发 UI 更新) this.musicList.splice(musicIndex, 1, updatedMusic); console.info(`[Index] ✅ 索引 ${musicIndex} 已更新, isFavorite=${newFavoriteState}`); } // 同步收藏列表 this.favoriteList = await this.viewModel.getFavoriteList(); console.info(`[Index] ✅ 收藏列表已更新: ${this.favoriteList.length} 首`); } // 搜索音乐 async searchMusic(): Promise<void> { await this.viewModel.searchMusic(this.searchKeyword); } // 删除音乐 async deleteMusic(music: MusicItem): Promise<void> { try { await this.viewModel.deleteMusic(music, this.currentMusicId); } catch (error) { // 错误已在 ViewModel 中处理 } } // 清空音乐库 async clearAllMusic(): Promise<void> { try { await this.viewModel.clearAllMusic(this.currentMusicId); } catch (error) { // 错误已在 ViewModel 中处理 } } // 扫描媒体库 async scanMediaLibrary(): Promise<void> { this.isScanning = true; try { await this.viewModel.scanMediaLibrary(); } catch (error) { // 回退到 AudioViewPicker await this.selectMusicFiles(); } finally { this.isScanning = false; } } // 选择音乐文件 async selectMusicFiles(): Promise<void> { this.isScanning = true; try { await this.viewModel.selectMusicFiles(); } catch (error) { // 错误已在 ViewModel 中处理 } finally { this.isScanning = false; } } // 扫描本地音乐 async scanLocalMusic(): Promise<void> { this.isScanning = true; try { await this.viewModel.scanLocalMusic(); } catch (error) { // 错误已在 ViewModel 中处理 } finally { this.isScanning = false; } } // 扫描匹配歌词 async scanLyrics(): Promise<void> { this.isScanning = true; try { await this.viewModel.scanLyricsForExistingMusic(); } catch (error) { // 错误已在 ViewModel 中处理 } finally { this.isScanning = false; } } // 添加模拟数据 async addMockMusic(): Promise<void> { try { await this.viewModel.addMockMusic(); } catch (error) { // 错误已在 ViewModel 中处理 } } // 获取播放模式图标 getPlayModeIcon(): Resource { switch (this.playMode) { case PlayMode.SINGLE: return $r('app.media.ic_repeat_one'); case PlayMode.RANDOM: return $r('app.media.ic_shuffle'); case PlayMode.LOOP: return $r('app.media.ic_repeat'); default: return $r('app.media.ic_repeat'); } } // 获取播放模式名称 getPlayModeName(): string { switch (this.playMode) { case PlayMode.SINGLE: return '单曲循环'; case PlayMode.RANDOM: return '随机播放'; case PlayMode.LOOP: return '列表循环'; default: return '顺序播放'; } } build() { Stack({ alignContent: Alignment.Bottom }) { Column() { // 顶部标题栏 this.buildHeader() // 搜索栏 this.buildSearchBar() // Tab切换 this.buildTabs() // 音乐列表 if (this.isLoading) { this.buildLoading() } else if (this.getCurrentList().length === 0) { this.buildEmpty() } else { this.buildMusicList() } } .width('100%') .height('100%') .linearGradient({ angle: 135, colors: this.isDarkTheme ? [ ['#1a1a2e', 0.0], // 深蓝灰 ['#16213e', 0.3], // 暗蓝色 ['#0f3460', 0.7], // 深蓝色 ['#c41e3a', 1.0] // 鲜红色 ] : [ ['#F8F9FA', 0.0], // 浅灰白 ['#E8EAF6', 0.5], // 淡紫色 ['#E3F2FD', 1.0] // 淡蓝色 ] }) .padding({ bottom: this.currentMusicId > 0 ? 80 : 0 }) // 底部播放控制栏 if (this.currentMusicId > 0) { this.buildPlayerBar() } // 播放列表抽屉 if (this.showPlaylistDrawer) { PlaylistDrawer({ showDrawer: $showPlaylistDrawer, playlist: this.viewModel.getAudioPlayer().getPlaylist(), currentMusicId: this.currentMusicId, onPlayMusic: (music: MusicItem) => { this.playMusic(music); } }) } // Toast 提示 if (this.showToast) { Text(this.toastMessage) .fontSize(14) .fontColor('#FFFFFF') .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.9)' : 'rgba(0, 0, 0, 0.75)') .padding({ left: 20, right: 20, top: 12, bottom: 12 }) .borderRadius(20) .position({ x: '50%', y: '45%' }) .translate({ x: '-50%' }) .zIndex(100) } // 扫描菜单(移到Stack顶层) if (this.showScanMenu) { // 透明背景层,点击关闭菜单 Column() .width('100%') .height('100%') .backgroundColor('rgba(0,0,0,0.3)') .onClick(() => { this.showScanMenu = false; }) .zIndex(200) Column() { // 自动添加歌曲(批量选择) Row() { Image($r('app.media.ic_scan')) .width(24) .height(24) .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA') Column() { Text('自动添加歌曲') .fontSize(15) .fontColor(this.isDarkTheme ? '#FF4158' : '#667EEA') .fontWeight(FontWeight.Medium) Text('批量选择,最多500首') .fontSize(11) .fontColor(this.isDarkTheme ? '#FF8C82' : '#667EEA') .opacity(0.7) .margin({ top: 2 }) } .alignItems(HorizontalAlign.Start) .margin({ left: 12 }) } .width('100%') .height(60) .padding({ left: 16, right: 16 }) .onClick(() => { this.showScanMenu = false; this.scanMediaLibrary(); }) Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0') // 选择音乐文件 Row() { Image($r('app.media.ic_folder')) .width(24) .height(24) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666') Text('选择音乐文件') .fontSize(15) .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333') .margin({ left: 12 }) } .width('100%') .height(52) .padding({ left: 16, right: 16 }) .onClick(() => { this.showScanMenu = false; this.selectMusicFiles(); }) Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0') // 扫描本地音乐 Row() { Image($r('app.media.ic_search')) .width(24) .height(24) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666') Text('文档选择器') .fontSize(15) .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333') .margin({ left: 12 }) } .width('100%') .height(52) .padding({ left: 16, right: 16 }) .onClick(() => { this.showScanMenu = false; this.scanLocalMusic(); }) Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0') // 扫描歌词 Row() { Image($r('app.media.ic_music_note')) .width(24) .height(24) .fillColor('#4CAF50') Text('扫描匹配歌词') .fontSize(15) .fontColor('#4CAF50') .margin({ left: 12 }) } .width('100%') .height(52) .padding({ left: 16, right: 16 }) .onClick(() => { this.showScanMenu = false; this.scanLyrics(); }) Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0') // 清空音乐库 Row() { Image($r('app.media.ic_delete')) .width(24) .height(24) .fillColor(this.isDarkTheme ? '#FF4158' : '#F44336') Text('清空音乐库') .fontSize(15) .fontColor(this.isDarkTheme ? '#FF4158' : '#F44336') .margin({ left: 12 }) } .width('100%') .height(52) .padding({ left: 16, right: 16 }) .onClick(() => { this.showScanMenu = false; this.clearAllMusic(); }) } .width(200) .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF') .borderRadius(8) .shadow({ radius: 12, color: this.isDarkTheme ? '#40000000' : '#20000000', offsetX: 0, offsetY: 4 }) .position({ x: '50%', y: 100 }) .translate({ x: '-50%' }) .zIndex(201) } // 扫描进度遮罩 if (this.isScanning) { Column() { LoadingProgress() .width(60) .height(60) .color(this.isDarkTheme ? '#FF4158' : '#667EEA') Text('正在自动添加歌曲...') .fontSize(16) .fontColor('#FFFFFF') .margin({ top: 16 }) } .width('100%') .height('100%') .backgroundColor(this.isDarkTheme ? 'rgba(15, 52, 96, 0.85)' : 'rgba(0, 0, 0, 0.5)') .backdropBlur(10) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .zIndex(300) } } .width('100%') .height('100%') } @Builder buildHeader() { Row() { // Logo 和标题 Row() { // 音乐图标 Image($r('app.media.ic_music_note')) .width(28) .height(28) .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA') .margin({ right: 10 }) Column() { Text('音乐') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333') Text(`${this.musicList.length} 首歌曲`) .fontSize(11) .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999') .margin({ top: 2 }) } .alignItems(HorizontalAlign.Start) } Blank() // 主题切换按钮 Button({ type: ButtonType.Circle }) { Image(this.isDarkTheme ? $r('app.media.ic_music_note') : $r('app.media.ic_music_note')) .width(20) .height(20) .fillColor('#FFFFFF') } .width(36) .height(36) .backgroundColor(this.isDarkTheme ? '#FF4158' : '#667EEA') .margin({ right: 10 }) .shadow({ radius: 8, color: this.isDarkTheme ? '#40FF4158' : '#30667EEA', offsetX: 0, offsetY: 3 }) .onClick(() => { this.isDarkTheme = !this.isDarkTheme; // 同步主题状态到 AppStorage AppStorage.setOrCreate('isDarkTheme', this.isDarkTheme); // 保存主题设置到本地存储 try { const context = AppStorage.get<common.UIAbilityContext>('context'); if (context) { const dataPreferences = preferences.getPreferencesSync(context, { name: 'app_settings' }); dataPreferences.putSync('isDarkTheme', this.isDarkTheme); dataPreferences.flush(); hilog.info(DOMAIN, TAG, '主题设置已保存: %{public}s', this.isDarkTheme ? '暗色' : '浅色'); } } catch (error) { hilog.error(DOMAIN, TAG, '保存主题设置失败: %{public}s', JSON.stringify(error)); } // 更新系统状态栏 this.updateStatusBar(); }) // 扫描音乐按钮 Button({ type: ButtonType.Normal }) { Row() { Image($r('app.media.ic_scan')) .width(18) .height(18) .fillColor('#FFFFFF') Text('扫描') .fontSize(13) .fontColor('#FFFFFF') .margin({ left: 4 }) } } .height(36) .padding({ left: 16, right: 16 }) .linearGradient({ angle: 135, colors: this.isDarkTheme ? [ ['#FF4158', 0.0], ['#C41E3A', 1.0] ] : [ ['#667EEA', 0.0], ['#764BA2', 1.0] ] }) .borderRadius(18) .shadow({ radius: 8, color: this.isDarkTheme ? '#40FF4158' : '#30667EEA', offsetX: 0, offsetY: 3 }) .onClick(() => { this.showScanMenu = !this.showScanMenu; }) // 菜单按钮 Button({ type: ButtonType.Circle }) { Image($r('app.media.ic_add')) .width(20) .height(20) .fillColor('#FFFFFF') } .width(36) .height(36) .backgroundColor(this.isDarkTheme ? '#FF4158' : '#667EEA') .margin({ left: 10 }) .shadow({ radius: 6, color: this.isDarkTheme ? '#40FF4158' : '#30667EEA', offsetX: 0, offsetY: 2 }) .onClick(() => { this.addMockMusic(); }) } .width('100%') .height(64) .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .backgroundColor(this.isDarkTheme ? 'rgba(26, 26, 46, 0.85)' : 'rgba(255, 255, 255, 0.9)') .backdropBlur(20) } @Builder buildSearchBar() { Row() { Image($r('app.media.ic_search')) .width(20) .height(20) .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA') .margin({ right: 10 }) TextInput({ placeholder: '搜索歌曲、歌手、专辑', text: this.searchKeyword }) .layoutWeight(1) .height(40) .backgroundColor(Color.Transparent) .placeholderColor(this.isDarkTheme ? '#666666' : '#AAAAAA') .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333') .fontSize(15) .onChange((value: string) => { this.searchKeyword = value; // 当输入框清空时,自动恢复全部列表 if (!value.trim()) { this.viewModel.loadMusicList(); } }) .onSubmit(() => { this.searchMusic(); }) if (this.searchKeyword) { Image($r('app.media.ic_close')) .width(20) .height(20) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#999999') .margin({ left: 8 }) .onClick(() => { this.searchKeyword = ''; this.viewModel.loadMusicList(); }) } } .width('100%') .height(48) .padding({ left: 16, right: 16 }) .margin({ left: 16, right: 16, top: 12, bottom: 8 }) .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.6)' : 'rgba(255, 255, 255, 0.8)') .borderRadius(24) .shadow({ radius: 8, color: this.isDarkTheme ? '#20000000' : '#10000000', offsetX: 0, offsetY: 2 }) } @Builder buildTabs() { Row() { ForEach(['全部', '收藏'], (item: string, index: number) => { Column() { Text(item) .fontSize(16) .fontWeight(this.currentTab === index ? FontWeight.Bold : FontWeight.Normal) .fontColor(this.currentTab === index ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#AAAAAA' : '#666666')) if (this.currentTab === index) { Row() .width(28) .height(3) .linearGradient({ angle: 90, colors: this.isDarkTheme ? [ ['#FF4158', 0.0], ['#C41E3A', 1.0] ] : [ ['#667EEA', 0.0], ['#764BA2', 1.0] ] }) .borderRadius(2) .margin({ top: 6 }) } } .padding({ left: 20, right: 20, top: 8, bottom: 8 }) .onClick(() => { this.currentTab = index; }) }) Blank() // 播放模式按钮 Row() { Image(this.getPlayModeIcon()) .width(18) .height(18) .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA') Text(this.getPlayModeName()) .fontSize(12) .fontColor(this.isDarkTheme ? '#FF4158' : '#667EEA') .margin({ left: 6 }) } .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)') .borderRadius(16) .margin({ right: 12 }) .onClick(() => { this.playMode = this.viewModel.getAudioPlayer().togglePlayMode(); }) } .width('100%') .height(52) .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.5)' : 'rgba(255, 255, 255, 0.7)') .backdropBlur(10) .alignItems(VerticalAlign.Center) } @Builder buildLoading() { Column() { LoadingProgress() .width(56) .height(56) .color(this.isDarkTheme ? '#FF4158' : '#667EEA') Text('加载中...') .margin({ top: 16 }) .fontSize(15) .fontColor(this.isDarkTheme ? '#FF4158' : '#667EEA') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } @Builder buildEmpty() { Column() { // 空状态图标 Column() { Image($r('app.media.ic_music_note')) .width(72) .height(72) .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA') } .width(120) .height(120) .justifyContent(FlexAlign.Center) .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)') .borderRadius(60) .margin({ bottom: 24 }) Text(this.currentTab === 0 ? '暂无音乐' : '暂无收藏') .fontSize(18) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333') .margin({ bottom: 8 }) Text(this.currentTab === 0 ? '点击右上角扫描按钮添加音乐' : '收藏喜欢的歌曲吧') .fontSize(14) .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } @Builder buildMusicList() { List({ space: 1 }) { ForEach(this.getCurrentList(), (item: MusicItem) => { ListItem() { this.buildMusicItem(item) } .swipeAction({ end: { builder: () => { this.buildSwipeActions(item) } } }) }, (item: MusicItem) => `${item.id}_${item.isFavorite}`) // ✅ 加入 isFavorite 状态 } .width('100%') .layoutWeight(1) .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.3)' : '#FFFFFF') .divider({ strokeWidth: 0.5, color: this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0', startMargin: 72, endMargin: 16 }) } @Builder buildSwipeActions(music: MusicItem) { Row() { // 收藏按钮 Column() { Image(music.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border')) .width(24) .height(24) .fillColor('#FFFFFF') Text(music.isFavorite ? '取消' : '收藏') .fontSize(11) .fontColor('#FFFFFF') .margin({ top: 4 }) } .width(72) .height('100%') .backgroundColor(this.isDarkTheme ? '#FF8C42' : '#FFA726') .justifyContent(FlexAlign.Center) .onClick(() => { this.toggleFavorite(music); }) // 删除按钮 Column() { Image($r('app.media.ic_delete')) .width(24) .height(24) .fillColor('#FFFFFF') Text('删除') .fontSize(11) .fontColor('#FFFFFF') .margin({ top: 4 }) } .width(72) .height('100%') .backgroundColor(this.isDarkTheme ? '#FF4158' : '#F44336') .justifyContent(FlexAlign.Center) .onClick(() => { this.deleteMusic(music); }) } .height('100%') } @Builder buildMusicItem(music: MusicItem) { Row() { // 专辑封面 Stack() { Image(music.cover) .width(50) .height(50) .borderRadius(8) .objectFit(ImageFit.Cover) if (this.currentMusicId === music.id && this.isPlaying) { Row() .width(50) .height(50) .borderRadius(8) .backgroundColor('rgba(0,0,0,0.3)') Image($r('app.media.ic_playing')) .width(24) .height(24) .fillColor('#FFFFFF') } } .margin({ right: 12 }) // 音乐信息 Column() { Text(music.title) .fontSize(16) .fontWeight(this.currentMusicId === music.id ? FontWeight.Bold : FontWeight.Normal) .fontColor(this.currentMusicId === music.id ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#FFFFFF' : '#333333')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') Row() { Text(`${music.artist}`) .fontSize(13) .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(` · ${formatTime(music.duration)}`) .fontSize(13) .fontColor(this.isDarkTheme ? '#888888' : '#CCCCCC') } .width('100%') .margin({ top: 4 }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) // 收藏按钮 Image(music.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border')) .width(24) .height(24) .fillColor(music.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#666666' : '#CCCCCC')) .margin({ right: 8 }) .onClick((event: ClickEvent) => { // event.stopPropagation(); // HarmonyOS 4.0+ 已移除此方法 this.toggleFavorite(music); }) // 更多按钮 Image($r('app.media.ic_more')) .width(24) .height(24) .fillColor(this.isDarkTheme ? '#666666' : '#CCCCCC') } .width('100%') .height(72) .padding({ left: 16, right: 16 }) .backgroundColor(this.currentMusicId === music.id ? (this.isDarkTheme ? 'rgba(255, 65, 88, 0.2)' : 'rgba(102, 126, 234, 0.1)') : (this.isDarkTheme ? 'rgba(22, 33, 62, 0.4)' : '#FFFFFF') ) .onClick(() => { console.info('[MusicApp] ======================================'); console.info('[MusicApp] 点击音乐列表项'); console.info(`[MusicApp] 标题: ${music.title}`); console.info(`[MusicApp] ID: ${music.id}`); console.info(`[MusicApp] 文件: ${music.filePath}`); console.info('[MusicApp] ======================================'); this.playMusic(music); }) } @Builder buildPlayerBar() { Row() { // 专辑封面 - 点击跳转到播放页 Image(this.currentMusicCover || $r('app.media.ic_album')) .width(48) .height(48) .borderRadius(24) .objectFit(ImageFit.Cover) .margin({ right: 12 }) .rotate({ angle: this.isPlaying ? 360 : 0 }) .animation({ duration: 10000, iterations: -1, curve: Curve.Linear }) .onClick(() => { this.navigateToPlayPage(); }) // 歌曲信息 - 点击跳转到播放页 Column() { Text(this.currentMusicTitle || '') .fontSize(15) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') Text(this.currentMusicArtist || '') .fontSize(12) .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') .margin({ top: 2 }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) .onClick(() => { this.navigateToPlayPage(); }) // 控制按钮区域 - 不触发页面跳转 Row() { // 上一首 Image($r('app.media.ic_previous')) .width(28) .height(28) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#333333') .onClick(() => { console.info('[MusicApp] 点击上一首'); this.viewModel.getAudioPlayer().playPrevious(); }) // 播放/暂停 Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play')) .width(36) .height(36) .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA') .margin({ left: 16 }) .onClick(() => { console.info('[MusicApp] 点击播放/暂停按钮'); this.togglePlayPause(); }) // 下一首 Image($r('app.media.ic_next')) .width(28) .height(28) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#333333') .margin({ left: 16 }) .onClick(() => { console.info('[MusicApp] 点击下一首'); this.viewModel.getAudioPlayer().playNext(); }) // 播放列表 Image($r('app.media.ic_playlist')) .width(24) .height(24) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666') .margin({ left: 16 }) .onClick(() => { console.info('[MusicApp] 点击播放列表'); this.showPlaylistDrawer = true; }) } .height('100%') .alignItems(VerticalAlign.Center) } .width('100%') .height(72) .padding({ left: 16, right: 16 }) .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF') .backdropBlur(20) .shadow({ radius: 12, color: this.isDarkTheme ? '#40000000' : '#20000000', offsetX: 0, offsetY: -3 }) } // 获取当前显示列表 getCurrentList(): MusicItem[] { return this.currentTab === 0 ? this.musicList : this.favoriteList; } // 显示提示消息 showTip(message: string): void { this.toastMessage = message; this.showToast = true; setTimeout(() => { this.showToast = false; }, 2000); } // 跳转到播放页面 navigateToPlayPage(): void { console.info('[MusicApp] 跳转到播放页面'); router.pushUrl({ url: 'pages/PlayPage', params: { musicId: this.currentMusicId } }).then(() => { // 成功跳转 }).catch((error: Error) => { console.error(`页面跳转失败: ${JSON.stringify(error)}`); }); } aboutToDisappear(): void { this.viewModel.release(); } }
// pages/PlayPage.ets import { router } from '@kit.ArkUI'; import { common } from '@kit.AbilityKit'; import { MusicItem, PlayMode, LyricLine, parseLyrics, formatTime } from './models/MusicItem'; import { AudioPlayer } from './services/AudioPlayer'; import { MusicDatabase } from './services/Database'; import { FileUtils } from './utils/FileUtils'; // 导入 FileUtils // 路由参数接口 interface RouteParams { musicId: number; } @Entry @Component struct PlayPage { @State currentMusic: MusicItem | null = null; @State isPlaying: boolean = false; @State currentPosition: number = 0; @State duration: number = 0; @State volume: number = 0.7; @State playMode: PlayMode = PlayMode.LOOP; @State isFavorite: boolean = false; @State showLyrics: boolean = true; @State showVolumeSlider: boolean = false; @State showPlaylistDrawer: boolean = false; @State lyrics: LyricLine[] = []; @State currentLyricIndex: number = 0; @State rotationAngle: number = 0; @State musicList: MusicItem[] = []; @State toastMessage: string = ''; @State showToast: boolean = false; @State isProgressHovered: boolean = false; // 进度条悬停状态 @State isDraggingProgress: boolean = false; // 是否正在拖动进度条 @State dragProgressValue: number = 0; // 拖动时的临时进度值 // 防抖控制:防止快速点击 private isToggling: boolean = false; private lastToggleTime: number = 0; private TOGGLE_DEBOUNCE_MS: number = 300; // 300ms 防抖时间 // 从 AppStorage 读取主题状态 @StorageLink('isDarkTheme') isDarkTheme: boolean = false; // 歌词滚动控制器 private lyricScroller: Scroller = new Scroller(); // 使用 @StorageLink 监听全局歌词变化,并自动解析 @StorageLink('currentMusicLyrics') @Watch('onLyricsChanged') globalLyrics: string = ''; private rotationTimer: number = -1; private context: common.UIAbilityContext = AppStorage.get<common.UIAbilityContext>('context') as common.UIAbilityContext; private audioPlayer: AudioPlayer = AudioPlayer.getInstance(this.context); private database: MusicDatabase = new MusicDatabase(this.context); async aboutToAppear(): Promise<void> { console.info('[PlayPage] ===== 页面初始化 ====='); try { await this.database.initDatabase(); // 初始化播放器(如果尚未初始化) if (!this.audioPlayer.isReady()) { await this.audioPlayer.init(); } // 设置初始音量 await this.audioPlayer.setVolume(this.volume); console.info(`[PlayPage] 初始音量已设置: ${Math.round(this.volume * 100)}%`); // 加载音乐列表 this.musicList = await this.database.getAllMusic(); // 关键:设置播放列表到 AudioPlayer this.audioPlayer.setPlaylist(this.musicList); console.info(`[PlayPage] 播放列表已设置: ${this.musicList.length} 首音乐`); // 同步当前播放状态 const currentPlaying = this.audioPlayer.getCurrentMusic(); if (currentPlaying) { console.info(`[PlayPage] 同步当前播放: ${currentPlaying.title}`); this.currentMusic = currentPlaying; this.isPlaying = this.audioPlayer.getIsPlaying(); this.duration = currentPlaying.duration || this.audioPlayer.getDuration(); this.isFavorite = currentPlaying.isFavorite; this.playMode = this.audioPlayer.getPlayMode(); this.parseMusicLyrics(); } // 获取路由参数 const params: RouteParams = router.getParams() as RouteParams; if (params && params.musicId) { console.info(`[PlayPage] 路由参数 musicId: ${params.musicId}`); const music = this.musicList.find(m => m.id === params.musicId); if (music) { // 如果路由参数中的音乐与当前播放的不同,则播放新的 if (!currentPlaying || currentPlaying.id !== music.id) { console.info(`[PlayPage] 播放路由指定的音乐: ${music.title}`); this.currentMusic = music; this.duration = music.duration; this.isFavorite = music.isFavorite; this.parseMusicLyrics(); await this.playMusic(music); } } } // 设置监听器 this.setupPlayerListeners(); this.startRotation(); console.info('[PlayPage] ===== 初始化完成 ====='); } catch (error) { console.error(`[PlayPage] 初始化失败: ${JSON.stringify(error)}`); } } parseMusicLyrics(): void { console.info(`[PlayPage] ===== 开始解析歌词 =====`); // 优先使用全局歌词(从 AppStorage 同步) const lyricsContent = this.globalLyrics || (this.currentMusic?.lyrics || ''); console.info(`[PlayPage] globalLyrics 长度: ${this.globalLyrics.length}`); console.info(`[PlayPage] currentMusic.lyrics 长度: ${this.currentMusic?.lyrics?.length || 0}`); console.info(`[PlayPage] 最终使用歌词长度: ${lyricsContent.length}`); if (lyricsContent && lyricsContent.length > 0) { // 创建新数组,触发 UI 更新 this.lyrics = parseLyrics(lyricsContent); console.info(`[PlayPage] ✅ 歌词已解析: ${this.lyrics.length} 行`); // 输出前 3 行歌词作为样例 if (this.lyrics.length > 0) { console.info(`[PlayPage] 前 3 行歌词:`); for (let i = 0; i < Math.min(3, this.lyrics.length); i++) { console.info(` [${i}] ${this.lyrics[i].text}`); } } } else { // 默认歌词,创建新数组 this.lyrics = [ { time: 0, text: '暂无歌词' }, { time: 3000, text: '请添加歌词文件' } ]; console.info(`[PlayPage] ⚠️ 歌词为空,显示默认歌词`); } console.info(`[PlayPage] ===== 歌词解析完成 =====`); } /** * 全局歌词变化回调 - 自动重新解析歌词并刷新 UI */ onLyricsChanged(): void { console.info(`[PlayPage] 全局歌词变化,重新解析歌词`); // 重置歌词索引 this.currentLyricIndex = 0; // 重新解析歌词(会创建新数组,触发 UI 刷新) this.parseMusicLyrics(); console.info(`[PlayPage] ✅ UI 已通知刷新,当前 ${this.lyrics.length} 行歌词`); } startRotation(): void { if (this.rotationTimer === -1) { this.rotationTimer = setInterval(() => { if (this.isPlaying) { this.rotationAngle = (this.rotationAngle + 1) % 360; } }, 50); } } stopRotation(): void { if (this.rotationTimer !== -1) { clearInterval(this.rotationTimer); this.rotationTimer = -1; } } setupPlayerListeners(): void { this.audioPlayer.setOnStateChangeListener((state: string) => { if (state === 'playing') { this.isPlaying = true; this.currentMusic = this.audioPlayer.getCurrentMusic(); if (this.currentMusic) { this.duration = this.currentMusic.duration; this.isFavorite = this.currentMusic.isFavorite; // 在播放状态变化时也重新解析歌词 this.parseMusicLyrics(); } } else if (state === 'paused') { this.isPlaying = false; } else if (state === 'completed') { this.isPlaying = false; } }); this.audioPlayer.setOnPositionChangeListener((position: number) => { this.currentPosition = position; this.updateCurrentLyricIndex(); }); // 关键修改:音乐变化时从数据库重新加载歌词并自动匹配 this.audioPlayer.setOnMusicChangeListener(async (music: MusicItem) => { console.info(`[PlayPage] ==== 音乐变化: ${music.title} ====`); // 重置歌词状态 this.currentLyricIndex = 0; try { // ① 从数据库重新加载当前音乐的最新数据 const freshMusic = await this.database.getMusicById(music.id); if (!freshMusic) { console.warn(`[PlayPage] 未找到音乐: id=${music.id}`); return; } console.info(`[PlayPage] 从数据库加载: ${freshMusic.title}`); console.info(`[PlayPage] 歌词长度: ${freshMusic.lyrics?.length || 0}`); console.info(`[PlayPage] 文件路径: ${freshMusic.filePath}`); // ② 如果没有歌词,尝试自动匹配 let finalLyrics = freshMusic.lyrics || ''; if (!finalLyrics || finalLyrics.length === 0) { console.info(`[PlayPage] 歌词为空,尝试自动匹配...`); const lyricPath = await FileUtils.findMatchingLyricFile(freshMusic.filePath); if (lyricPath) { console.info(`[PlayPage] 找到歌词文件: ${lyricPath}`); const lyricsContent = await FileUtils.readLyricFile(lyricPath); if (lyricsContent && lyricsContent.length > 0) { console.info(`[PlayPage] ✅ 自动匹配成功,歌词长度: ${lyricsContent.length}`); // 更新到数据库 await this.database.updateMusicLyrics(freshMusic.id, lyricsContent); finalLyrics = lyricsContent; } else { console.warn(`[PlayPage] 歌词文件为空`); } } else { console.warn(`[PlayPage] 未找到匹配的歌词文件`); } } // ③ 更新内存中的歌词 freshMusic.lyrics = finalLyrics; // ④ 更新当前音乐信息 this.currentMusic = freshMusic; this.duration = freshMusic.duration; this.isFavorite = freshMusic.isFavorite; // ⑤ 🔑 关键:更新 PlayPage 的 musicList 和 AudioPlayer 的 playlist const listIndex = this.musicList.findIndex(m => m.id === freshMusic.id); if (listIndex !== -1) { this.musicList[listIndex] = freshMusic; console.info(`[PlayPage] 已更新 musicList[${listIndex}]`); } // 同步到 AudioPlayer 的播放列表 this.audioPlayer.setPlaylist(this.musicList); console.info(`[PlayPage] 已同步播放列表到 AudioPlayer`); // ⑥ 同步到全局状态(必须在 parseMusicLyrics 之前) AppStorage.setOrCreate('currentMusicLyrics', finalLyrics); console.info(`[PlayPage] 全局状态已同步,歌词长度: ${finalLyrics.length}`); // ⑦ 解析并显示歌词 this.parseMusicLyrics(); console.info(`[PlayPage] ✅ 歌词已加载并通知 UI 刷新`); console.info(`[PlayPage] 当前显示 ${this.lyrics.length} 行歌词`); } catch (error) { console.error(`[PlayPage] 加载歌词失败: ${JSON.stringify(error)}`); // 失败时显示默认歌词 this.lyrics = [ { time: 0, text: '加载失败' }, { time: 3000, text: '请重试' } ]; } console.info(`[PlayPage] ==== 音乐变化处理完成 ====`); }); this.audioPlayer.setOnPlayModeChangeListener((mode: PlayMode) => { this.playMode = mode; }); // 监听实际时长 this.audioPlayer.setOnDurationChangeListener((duration: number) => { this.duration = duration; // 更新数据库中的时长 if (this.currentMusic && this.currentMusic.duration === 0) { this.database.updateMusicDuration(this.currentMusic.id, duration); } }); // 监听收藏状态变化(从其他页面同步) this.audioPlayer.setOnFavoriteChangeListener((musicId: number, isFavorite: boolean) => { console.info(`[PlayPage] 收到收藏状态变化: musicId=${musicId}, isFavorite=${isFavorite}`); if (this.currentMusic && this.currentMusic.id === musicId) { this.isFavorite = isFavorite; // 更新 currentMusic this.currentMusic.isFavorite = isFavorite; } // 更新音乐列表中的状态 const index = this.musicList.findIndex(m => m.id === musicId); if (index !== -1) { this.musicList[index].isFavorite = isFavorite; } }); } updateCurrentLyricIndex(): void { const oldIndex = this.currentLyricIndex; for (let i = this.lyrics.length - 1; i >= 0; i--) { if (this.currentPosition >= this.lyrics[i].time) { this.currentLyricIndex = i; break; } } // 如果歌词索引变化,自动滚动到当前歌词 if (oldIndex !== this.currentLyricIndex && this.showLyrics) { this.scrollToCurrentLyric(); } } /** * 滚动到当前歌词(居中显示) */ scrollToCurrentLyric(): void { try { // 每行歌词高度约 50vp(包括 padding 和 space) const itemHeight = 50; const yOffset = this.currentLyricIndex * itemHeight; // 滚动到当前歌词,并且居中显示 this.lyricScroller.scrollTo({ xOffset: 0, yOffset: yOffset, animation: { duration: 300, curve: Curve.EaseInOut } }); } catch (error) { console.error(`[PlayPage] 歌词滚动失败: ${JSON.stringify(error)}`); } } async playMusic(music: MusicItem): Promise<void> { console.info(`[PlayPage] 播放音乐: ${music.title}`); try { await this.audioPlayer.play(music); await this.database.updatePlayCount(music.id); this.currentMusic = music; this.isPlaying = true; this.parseMusicLyrics(); console.info(`[PlayPage] ✅ 播放已启动`); } catch (error) { console.error(`[PlayPage] 播放失败: ${JSON.stringify(error)}`); this.showTip('播放失败'); } } async togglePlayPause(): Promise<void> { const now = Date.now(); // 防抖:如果正在执行或距离上次操作不足 300ms,则忽略 if (this.isToggling || (now - this.lastToggleTime < this.TOGGLE_DEBOUNCE_MS)) { console.warn(`[PlayPage] 操作过于频繁,已忽略(距上次 ${now - this.lastToggleTime}ms)`); return; } this.isToggling = true; this.lastToggleTime = now; try { // ⚠️ 关键:基于 AudioPlayer 的真实状态判断,而不是本地 UI 状态 const realIsPlaying = this.audioPlayer.getIsPlaying(); console.info(`[PlayPage] 切换播放/暂停:`); console.info(`[PlayPage] UI状态 isPlaying=${this.isPlaying}`); console.info(`[PlayPage] 真实状态 realIsPlaying=${realIsPlaying}`); if (realIsPlaying) { // 真实在播放,执行暂停 console.info(`[PlayPage] → 执行暂停`); await this.audioPlayer.pause(); // ✅ 不立即修改 this.isPlaying,等待回调更新 } else if (this.currentMusic) { // 真实已暂停或停止,执行恢复 console.info(`[PlayPage] → 执行恢复`); await this.audioPlayer.resume(); // ✅ 不立即修改 this.isPlaying,等待回调更新 } else if (this.musicList.length > 0) { // 没有当前音乐,播放第一首 console.info(`[PlayPage] → 播放第一首`); await this.playMusic(this.musicList[0]); } else { this.showTip('请先添加音乐'); } // 延迟 100ms 后同步状态(给 AudioPlayer 回调时间) setTimeout(() => { this.isPlaying = this.audioPlayer.getIsPlaying(); console.info(`[PlayPage] ✅ 状态已同步: isPlaying=${this.isPlaying}`); }, 100); } catch (error) { console.error(`[PlayPage] ❌ 切换失败: ${JSON.stringify(error)}`); // 发生错误时也同步状态 this.isPlaying = this.audioPlayer.getIsPlaying(); } finally { // 延迟 300ms 后解锁(防抖时间) setTimeout(() => { this.isToggling = false; }, this.TOGGLE_DEBOUNCE_MS); } } async seekTo(position: number): Promise<void> { await this.audioPlayer.seekTo(position); this.currentPosition = position; } async setVolume(value: number): Promise<void> { this.volume = value; await this.audioPlayer.setVolume(value); } async toggleFavorite(): Promise<void> { if (this.currentMusic) { console.info(`[PlayPage] 切换收藏: ${this.currentMusic.title}`); const newFavoriteState = await this.database.toggleFavorite(this.currentMusic.id); this.isFavorite = newFavoriteState; // 通知 AudioPlayer 同步到其他页面 this.audioPlayer.notifyFavoriteChanged(this.currentMusic.id, newFavoriteState); this.showTip(newFavoriteState ? '已添加到收藏' : '已取消收藏'); } } /** * 处理上一曲点击 - 直接加载歌词 */ async handlePlayPrevious(): Promise<void> { console.info(`[PlayPage] ===== 点击上一曲 =====`); // 调用 AudioPlayer 播放上一曲 await this.audioPlayer.playPrevious(); // 等待 100ms 让 AudioPlayer 完成切歌 await new Promise<void>((resolve) => setTimeout(resolve, 100)); // 直接从 AudioPlayer 获取当前正在播放的音乐 const currentMusic = this.audioPlayer.getCurrentMusic(); if (!currentMusic) { console.warn(`[PlayPage] 未获取到当前播放的音乐`); return; } console.info(`[PlayPage] 当前播放: ${currentMusic.title}`); // 从数据库重新加载歌词 await this.loadAndMatchLyrics(currentMusic.id); } /** * 处理下一曲点击 - 直接加载歌词 */ async handlePlayNext(): Promise<void> { console.info(`[PlayPage] ===== 点击下一曲 =====`); // 调用 AudioPlayer 播放下一曲 await this.audioPlayer.playNext(); // 等待 100ms 让 AudioPlayer 完成切歌 await new Promise<void>((resolve) => setTimeout(resolve, 100)); // 直接从 AudioPlayer 获取当前正在播放的音乐 const currentMusic = this.audioPlayer.getCurrentMusic(); if (!currentMusic) { console.warn(`[PlayPage] 未获取到当前播放的音乐`); return; } console.info(`[PlayPage] 当前播放: ${currentMusic.title}`); // 从数据库重新加载歌词 await this.loadAndMatchLyrics(currentMusic.id); } /** * 加载并匹配歌词 - 通用方法 */ async loadAndMatchLyrics(musicId: number): Promise<void> { console.info(`[PlayPage] 加载歌词: musicId=${musicId}`); try { // ① 从数据库加载最新数据 const freshMusic = await this.database.getMusicById(musicId); if (!freshMusic) { console.warn(`[PlayPage] 未找到音乐: id=${musicId}`); return; } console.info(`[PlayPage] 从数据库加载: ${freshMusic.title}`); console.info(`[PlayPage] 歌词长度: ${freshMusic.lyrics?.length || 0}`); // ② 如果没有歌词,尝试自动匹配 let finalLyrics = freshMusic.lyrics || ''; if (!finalLyrics || finalLyrics.length === 0) { console.info(`[PlayPage] 歌词为空,尝试自动匹配...`); const lyricPath = await FileUtils.findMatchingLyricFile(freshMusic.filePath); if (lyricPath) { console.info(`[PlayPage] 找到歌词文件: ${lyricPath}`); const lyricsContent = await FileUtils.readLyricFile(lyricPath); if (lyricsContent && lyricsContent.length > 0) { console.info(`[PlayPage] ✅ 自动匹配成功,歌词长度: ${lyricsContent.length}`); // 更新到数据库 await this.database.updateMusicLyrics(freshMusic.id, lyricsContent); finalLyrics = lyricsContent; } else { console.warn(`[PlayPage] 歌词文件为空`); } } else { console.warn(`[PlayPage] 未找到匹配的歌词文件`); } } // ③ 更新当前音乐信息 freshMusic.lyrics = finalLyrics; this.currentMusic = freshMusic; this.duration = freshMusic.duration; this.isFavorite = freshMusic.isFavorite; this.currentLyricIndex = 0; // ④ 同步到全局状态 AppStorage.setOrCreate('currentMusicLyrics', finalLyrics); console.info(`[PlayPage] 全局状态已同步,歌词长度: ${finalLyrics.length}`); // ⑤ 解析并显示歌词 this.parseMusicLyrics(); console.info(`[PlayPage] ✅ 歌词已加载,显示 ${this.lyrics.length} 行`); } catch (error) { console.error(`[PlayPage] 加载歌词失败: ${JSON.stringify(error)}`); this.lyrics = [ { time: 0, text: '加载失败' }, { time: 3000, text: '请重试' } ]; } } showTip(message: string): void { this.toastMessage = message; this.showToast = true; setTimeout(() => { this.showToast = false; }, 1500); } getPlayModeIcon(): Resource { switch (this.playMode) { case PlayMode.SINGLE: return $r('app.media.ic_repeat_one'); case PlayMode.RANDOM: return $r('app.media.ic_shuffle'); case PlayMode.LOOP: return $r('app.media.ic_repeat'); default: return $r('app.media.ic_repeat'); } } getPlayModeName(): string { switch (this.playMode) { case PlayMode.SINGLE: return '单曲循环'; case PlayMode.RANDOM: return '随机播放'; case PlayMode.LOOP: return '列表循环'; default: return '顺序播放'; } } goBack(): void { router.back(); } build() { Stack({ alignContent: Alignment.Bottom }) { Column() { // 顶部导航栏 this.buildHeader() if (this.showLyrics) { // 歌词模式 this.buildLyricsView() } else { // 封面模式 this.buildCoverView() } // 进度条 this.buildProgressBar() // 控制按钮 this.buildControls() // 底部功能按钮 this.buildBottomActions() } .width('100%') .height('100%') .linearGradient({ direction: GradientDirection.Bottom, colors: this.isDarkTheme ? [ ['#1a1a2e', 0.0], // 深蓝灰 ['#16213e', 0.3], // 暗蓝色 ['#0f3460', 0.7], // 深蓝色 ['#c41e3a', 1.0] // 鲜红色 ] : [ ['#667eea', 0], ['#764ba2', 0.5], ['#f093fb', 1] ] }) // 音量滑块 if (this.showVolumeSlider) { this.buildVolumeSlider() } // 播放列表抽屉 if (this.showPlaylistDrawer) { this.buildPlaylistDrawer() } // Toast 提示 if (this.showToast) { Text(this.toastMessage) .fontSize(14) .fontColor('#FFFFFF') .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.9)' : 'rgba(0, 0, 0, 0.7)') .padding({ left: 20, right: 20, top: 12, bottom: 12 }) .borderRadius(20) .position({ x: '50%', y: '40%' }) .translate({ x: '-50%' }) } } .width('100%') .height('100%') } @Builder buildHeader(): void { Row() { // 返回按钮 Image($r('app.media.ic_back')) .width(28) .height(28) .fillColor('#FFFFFF') .onClick(() => { this.goBack(); }) // 标题区域 Column() { Text(this.currentMusic?.title || '未知歌曲') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.currentMusic?.artist || '未知歌手') .fontSize(13) .fontColor('rgba(255,255,255,0.8)') .margin({ top: 2 }) } .layoutWeight(1) .alignItems(HorizontalAlign.Center) // 分享按钮 Image($r('app.media.ic_share')) .width(24) .height(24) .fillColor('#FFFFFF') } .width('100%') .height(56) .padding({ left: 16, right: 16 }) } @Builder buildCoverView(): void { Column() { // 旋转唱片效果 Stack() { // 唱片底座 Circle() .width(300) .height(300) .fill('#000000') .opacity(0.3) // 唱片 Stack() { // 黑胶唱片背景 Circle() .width(280) .height(280) .fill('#1a1a1a') // 唱片纹理 Circle() .width(260) .height(260) .fill(Color.Transparent) .stroke('#333333') .strokeWidth(40) // 专辑封面 Image(this.currentMusic?.cover || $r('app.media.ic_album')) .width(160) .height(160) .borderRadius(80) .objectFit(ImageFit.Cover) } .rotate({ angle: this.rotationAngle }) } .margin({ top: 40, bottom: 40 }) // 歌曲信息 Column() { Text(this.currentMusic?.title || '未知歌曲') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text(this.currentMusic?.artist || '未知歌手') .fontSize(16) .fontColor('rgba(255,255,255,0.8)') Text(' - ') .fontSize(16) .fontColor('rgba(255,255,255,0.6)') Text(this.currentMusic?.album || '未知专辑') .fontSize(16) .fontColor('rgba(255,255,255,0.8)') } .margin({ top: 8 }) // 收藏按钮 Image(this.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border')) .width(32) .height(32) .fillColor(this.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : '#FFFFFF') .margin({ top: 20 }) .onClick(() => { this.toggleFavorite(); }) } .width('100%') .alignItems(HorizontalAlign.Center) } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .onClick(() => { this.showLyrics = true; }) } @Builder buildLyricsView(): void { Column() { // 顶部小封面 Row() { Image(this.currentMusic?.cover || $r('app.media.ic_album')) .width(60) .height(60) .borderRadius(8) .objectFit(ImageFit.Cover) .rotate({ angle: this.rotationAngle }) Column() { Text(this.currentMusic?.title || '未知歌曲') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.currentMusic?.artist || '未知歌手') .fontSize(14) .fontColor('rgba(255,255,255,0.8)') .margin({ top: 4 }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) .margin({ left: 16 }) // 收藏按钮 Image(this.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border')) .width(28) .height(28) .fillColor(this.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : '#FFFFFF') .onClick(() => { this.toggleFavorite(); }) } .width('100%') .padding({ left: 20, right: 20, top: 10, bottom: 20 }) .onClick(() => { this.showLyrics = false; }) // 歌词滚动区域(使用 Scroll + Column 实现居中) Scroll(this.lyricScroller) { Column() { // 顶部占位空间(让第一行歌词能显示在中间) Row() .height('40%') // 歌词列表 ForEach(this.lyrics, (item: LyricLine, index: number) => { Text(item.text) .fontSize(this.currentLyricIndex === index ? 22 : 17) .fontWeight(this.currentLyricIndex === index ? FontWeight.Bold : FontWeight.Normal) .fontColor(this.getLyricColor(index)) .textAlign(TextAlign.Center) .width('100%') .padding({ left: 30, right: 30, top: 12, bottom: 12 }) .transition({ type: TransitionType.All, opacity: 1, scale: { x: 1, y: 1 } }) .animation({ duration: 300, curve: Curve.EaseInOut }) .onClick(() => { // 点击歌词跳转到对应时间 this.seekTo(item.time); }) }, (item: LyricLine, index: number) => `${index}_${item.time}`) // 底部占位空间(让最后一行歌词能显示在中间) Row() .height('40%') } .width('100%') } .width('100%') .layoutWeight(1) .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) } .width('100%') .layoutWeight(1) } /** * 获取歌词颜色(渐变效果) */ getLyricColor(index: number): string { if (index === this.currentLyricIndex) { // 当前歌词:纯白色 return '#FFFFFF'; } else if (index === this.currentLyricIndex + 1) { // 下一句:略淡 return 'rgba(255,255,255,0.7)'; } else if (index === this.currentLyricIndex - 1) { // 上一句:略淡 return 'rgba(255,255,255,0.6)'; } else { // 其他:很淡 return 'rgba(255,255,255,0.4)'; } } @Builder buildProgressBar(): void { Column() { Slider({ value: this.isDraggingProgress ? this.dragProgressValue : this.currentPosition, // 拖动时使用临时值 min: 0, max: this.duration > 0 ? this.duration : 1, step: 1000, style: SliderStyle.OutSet }) .width('90%') .height(this.isProgressHovered ? 22 : 18) // 悬停时22,默认18 .blockSize({ width: 24, height: 24 }) .blockColor('#FFFFFF') // 白色滑块 .trackColor('rgba(255,255,255,0.3)') // 半透明白色未播放轨道 .selectedColor('#FFFFFF') // 白色已播放轨道 .trackThickness(this.isProgressHovered ? 8 : 6) // 悬停时轨道也变粗 .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { this.isProgressHovered = true; this.isDraggingProgress = true; // 开始拖动 this.dragProgressValue = this.currentPosition; } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { this.isProgressHovered = false; this.isDraggingProgress = false; // 结束拖动 } }) .onChange((value: number, mode: SliderChangeMode) => { if (mode === SliderChangeMode.Moving) { // 拖动中,只更新临时值 this.dragProgressValue = value; } else if (mode === SliderChangeMode.End) { // 拖动结束,执行跳转 this.isDraggingProgress = false; this.seekTo(value); } }) Row() { Text(formatTime(this.isDraggingProgress ? this.dragProgressValue : this.currentPosition)) // 拖动时显示临时值 .fontSize(14) .fontColor('rgba(255,255,255,0.8)') // 白色半透明文字 Blank() Text(formatTime(this.duration)) .fontSize(14) .fontColor('rgba(255,255,255,0.8)') // 白色半透明文字 } .width('90%') .margin({ top: 8 }) } .width('100%') .padding({ top: 16 }) } @Builder buildControls(): void { Row() { // 播放模式 Column() { Image(this.getPlayModeIcon()) .width(28) .height(28) .fillColor('#FFFFFF') } .onClick(() => { this.playMode = this.audioPlayer.togglePlayMode(); }) // 上一首 Image($r('app.media.ic_previous')) .width(40) .height(40) .fillColor('#FFFFFF') .margin({ left: 24 }) .onClick(() => { this.handlePlayPrevious(); }) // 播放/暂停 Stack() { Circle() .width(72) .height(72) .fill('#FFFFFF') Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play')) .width(36) .height(36) .fillColor(this.isDarkTheme ? '#FF4158' : '#764ba2') } .margin({ left: 24, right: 24 }) .onClick(() => { this.togglePlayPause(); }) // 下一首 Image($r('app.media.ic_next')) .width(40) .height(40) .fillColor('#FFFFFF') .margin({ right: 24 }) .onClick(() => { this.handlePlayNext(); }) // 播放列表 Image($r('app.media.ic_playlist')) .width(28) .height(28) .fillColor('#FFFFFF') .onClick(() => { this.showPlaylistDrawer = true; }) } .width('100%') .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) .padding({ top: 24, bottom: 16 }) } @Builder buildBottomActions(): void { Row() { // 收藏 Column() { Image(this.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border')) .width(24) .height(24) .fillColor(this.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : '#FFFFFF') Text('收藏') .fontSize(10) .fontColor('#FFFFFF') .margin({ top: 4 }) } .onClick(() => { this.toggleFavorite(); }) // 下载 Column() { Image($r('app.media.ic_download')) .width(24) .height(24) .fillColor('#FFFFFF') Text('下载') .fontSize(10) .fontColor('#FFFFFF') .margin({ top: 4 }) } // 评论 Column() { Image($r('app.media.ic_comment')) .width(24) .height(24) .fillColor('#FFFFFF') Text('评论') .fontSize(10) .fontColor('#FFFFFF') .margin({ top: 4 }) } // 音量 Column() { Image($r('app.media.ic_volume_high')) .width(24) .height(24) .fillColor('#FFFFFF') Text('音量') .fontSize(10) .fontColor('#FFFFFF') .margin({ top: 4 }) } .onClick(() => { this.showVolumeSlider = !this.showVolumeSlider; }) // 更多 Column() { Image($r('app.media.ic_more')) .width(24) .height(24) .fillColor('#FFFFFF') Text('更多') .fontSize(10) .fontColor('#FFFFFF') .margin({ top: 4 }) } } .width('100%') .justifyContent(FlexAlign.SpaceAround) .padding({ top: 16, bottom: 32 }) } @Builder buildVolumeSlider(): void { Column() { Row() .width('100%') .layoutWeight(1) .backgroundColor('rgba(0,0,0,0.3)') .onClick(() => { this.showVolumeSlider = false; }) Column() { Row() { Text('音量') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#333333') Blank() Text(`${Math.round(this.volume * 100)}%`) .fontSize(14) .fontColor('#666666') } .width('100%') .padding({ left: 20, right: 20, top: 20, bottom: 16 }) Row() { Image($r('app.media.ic_volume_low')) .width(24) .height(24) .fillColor('#999999') .margin({ right: 12 }) Slider({ value: this.volume * 100, min: 0, max: 100, step: 1, style: SliderStyle.OutSet }) .layoutWeight(1) .blockColor('#764ba2') .trackColor('#E0E0E0') .selectedColor('#764ba2') .onChange((value: number) => { this.setVolume(value / 100); }) Image($r('app.media.ic_volume_high')) .width(24) .height(24) .fillColor('#333333') .margin({ left: 12 }) } .width('100%') .padding({ left: 20, right: 20, bottom: 32 }) } .width('100%') .backgroundColor('#FFFFFF') .borderRadius({ topLeft: 20, topRight: 20 }) } .width('100%') .height('100%') } @Builder buildPlaylistDrawer(): void { Column() { Row() .width('100%') .layoutWeight(1) .backgroundColor(this.isDarkTheme ? 'rgba(15, 52, 96, 0.85)' : 'rgba(0, 0, 0, 0.5)') .onClick(() => { this.showPlaylistDrawer = false; }) Column() { // 标题栏 Row() { Text('播放列表') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333') Blank() Row() { Image(this.getPlayModeIcon()) .width(18) .height(18) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666') Text(this.getPlayModeName()) .fontSize(12) .fontColor(this.isDarkTheme ? '#AAAAAA' : '#666666') .margin({ left: 4 }) } .onClick(() => { this.playMode = this.audioPlayer.togglePlayMode(); }) Image($r('app.media.ic_close')) .width(24) .height(24) .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666') .margin({ left: 16 }) .onClick(() => { this.showPlaylistDrawer = false; }) } .width('100%') .height(56) .padding({ left: 20, right: 20 }) // 列表 List({ space: 1 }) { ForEach(this.musicList, (item: MusicItem, index: number) => { ListItem() { Row() { Text(`${index + 1}`) .fontSize(14) .fontColor(this.currentMusic?.id === item.id ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#AAAAAA' : '#999999') ) .width(30) Column() { Text(item.title) .fontSize(15) .fontColor(this.currentMusic?.id === item.id ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#FFFFFF' : '#333333') ) .fontWeight(this.currentMusic?.id === item.id ? FontWeight.Bold : FontWeight.Normal) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(`${item.artist} · ${formatTime(item.duration)}`) .fontSize(12) .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999') .margin({ top: 2 }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) if (this.currentMusic?.id === item.id && this.isPlaying) { Image($r('app.media.ic_playing')) .width(20) .height(20) .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA') } } .width('100%') .height(60) .padding({ left: 20, right: 20 }) .backgroundColor(this.currentMusic?.id === item.id ? (this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)') : 'transparent' ) .onClick(() => { this.playMusic(item); }) } }, (item: MusicItem) => item.id.toString()) } .width('100%') .layoutWeight(1) .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.3)' : '#FFFFFF') .divider({ strokeWidth: 0.5, color: this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0', startMargin: 50, endMargin: 20 }) } .width('100%') .height('60%') .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF') .borderRadius({ topLeft: 20, topRight: 20 }) .shadow({ radius: 12, color: this.isDarkTheme ? '#40000000' : '#20000000', offsetX: 0, offsetY: -3 }) } .width('100%') .height('100%') } aboutToDisappear(): void { this.stopRotation(); // 保存播放历史 if (this.currentMusic && this.currentPosition > 0) { this.database.addPlayHistory(this.currentMusic.id, this.currentPosition); } } }