import { Playlist, PlaylistSong } from '../viewmodel/Playlist'; import { VideoItem } from '../viewmodel/VideoItem'; import PlaylistTable from '../common/util/PlaylistTable'; import MediaTable from '../common/util/MediaTable'; import { emitter } from '@kit.BasicServicesKit'; import { ToastUtil, AppUtil, LogUtil, PreferencesUtil } from '@pura/harmony-utils'; import { showEditPlaylistDialog } from '../dialog/PlaylistDialog'; import { showAddSongsToPlaylistDialog } from '../dialog/AddSongsToPlaylistDialog'; import { router } from '@kit.ArkUI'; import { GlobalContext } from '../common/util/GlobalContext'; import { CommonConstants } from '../common/constants/CommonConstants'; import { EventConstants } from '../common/constants/EventConstants'; import { common } from '@kit.AbilityKit'; /** * 歌单播放事件数据 */ interface PlaylistEventData { playlistId: string; playlistName: string; songCount: number; startIndex: number; songFilePaths: string[]; } /** * 歌单详情页面 * 展示歌单信息和歌曲列表 */ @Entry @Component export struct PlaylistDetailPage { context = this.getUIContext().getHostContext() as common.UIAbilityContext @State playlist: Playlist | null = null @State songList: VideoItem[] = [] @State isLoading: boolean = true @State isShowEditDialog: boolean = false @State isPlaying: boolean = false @State curIndex: number = -1 @State pageOpacity: number = 0 @State contentScale: number = 0.95 @State showContent: boolean = false @State isSortMode: boolean = false // 是否处于排序模式 private playlistTable: PlaylistTable = new PlaylistTable(getContext(this)) private mediaTable: MediaTable = new MediaTable(getContext(this)) private playlistId: string = '' @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; aboutToAppear() { let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR); AppStorage.setOrCreate('themeColor', themeColor); this.themeColor = themeColor // 获取传入的歌单对象 const params = router.getParams() as Record if (params && params['playlist']) { this.playlist = params['playlist'] as Playlist this.playlistId = this.playlist.id this.loadPlaylistDetail() } // 监听播放状态变化 this.setupPlaybackStatusListener() } aboutToDisappear() { // 移除事件监听 this.removePlaybackStatusListener() } /** * 加载歌单详情 */ async loadPlaylistDetail() { try { this.isLoading = true if (!this.playlist) { ToastUtil.showToast('歌单不存在') router.back() return } // 加载歌单歌曲 const playlistSongs = await this.playlistTable.queryPlaylistSongs(this.playlistId) // 将 PlaylistSong 转换为 VideoItem this.songList = await convertPlaylistSongsToVideoItems(this.context,playlistSongs) // 歌单加载完成后,加载当前播放状态 this.loadCurrentPlaybackStatus() } catch (error) { LogUtil.error('heanup 加载歌单详情失败: ' + error) ToastUtil.showToast('加载歌单详情失败') } finally { this.isLoading = false // 启动页面进入动画 this.animatePageEntry() } } /** * 格式化时长显示 */ formatDuration(duration?: number): string { if (!duration || duration <= 0) { return '' } const minutes = Math.floor(duration / 60) const seconds = Math.floor(duration % 60) return `${minutes}:${seconds.toString().padStart(2, '0')}` } /** * 计算歌单总时长 */ calculateTotalDuration(): number { let total = 0 for (const song of this.songList) { const duration = song.duration || 0 total = total + (typeof duration === 'number' ? duration : 0) } return total } /** * 格式化总时长显示 */ formatTotalDuration(totalSeconds: number): string { if (totalSeconds <= 0) { return '' } const hours = Math.floor(totalSeconds / 3600) const minutes = Math.floor((totalSeconds % 3600) / 60) if (hours > 0) { return `${hours}小时${minutes}分钟` } else { return `${minutes}分钟` } } /** * 页面入场动画 */ animatePageEntry() { animateTo({ duration: 600, curve: Curve.EaseOut, delay: 100, onFinish: () => { this.showContent = true } }, () => { this.pageOpacity = 1 this.contentScale = 1 }) } /** * 设置播放状态监听 */ setupPlaybackStatusListener() { try { // 监听播放状态变化事件 const eventPlaybackStatus: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYBACK_STATUS } emitter.on(eventPlaybackStatus, (eventData: emitter.EventData) => { LogUtil.info('heanup PlaylistDetailPage 收到播放状态变化事件:'+ JSON.stringify(eventData)) if (eventData.data) { const data = eventData.data as Record const oldIsPlaying = this.isPlaying const oldCurIndex = this.curIndex this.isPlaying = data['isPlaying'] as boolean const currentFilePath = data['currentFilePath'] as string LogUtil.info(`heanup 原始播放状态: isPlaying=${this.isPlaying}, currentFilePath=${currentFilePath}`) // 通过filePath在当前歌单中查找对应的索引 if (currentFilePath) { const matchedIndex = this.songList.findIndex(song => song.filePath === currentFilePath) if (matchedIndex !== -1) { this.curIndex = matchedIndex LogUtil.info(`heanup 在歌单中找到匹配的歌曲: ${this.songList[matchedIndex].name}, 索引: ${matchedIndex}`) } else { // 当前播放的歌曲不在本歌单中,重置索引 this.curIndex = -1 LogUtil.info(`heanup 当前播放的歌曲不在本歌单中: ${currentFilePath}`) } } else { this.curIndex = -1 LogUtil.info(`heanup 当前没有播放歌曲`) } LogUtil.info(`heanup 播放状态更新: isPlaying=${oldIsPlaying}->${this.isPlaying}, curIndex=${oldCurIndex}->${this.curIndex}`) } }) } catch (error) { LogUtil.error('heanup 设置播放状态监听失败: ' + error) } } /** * 移除播放状态监听 */ removePlaybackStatusListener() { try { emitter.off(EventConstants.EVENT_PLAYBACK_STATUS) } catch (error) { LogUtil.error('heanup 移除播放状态监听失败: ' + error) } } /** * 加载当前播放状态 */ loadCurrentPlaybackStatus() { try { // 从AppStorage获取当前播放的歌曲 const currentSong = AppStorage.get('currentSong') if (currentSong && currentSong.filePath) { LogUtil.info(`heanup 获取到当前播放歌曲: ${currentSong.name}, filePath: ${currentSong.filePath}`) // 在歌单中查找匹配的歌曲 const matchedIndex = this.songList.findIndex(song => song.filePath === currentSong.filePath) if (matchedIndex !== -1) { this.curIndex = matchedIndex // 假设如果有currentSong说明正在播放 this.isPlaying = true LogUtil.info(`heanup 在歌单中找到当前播放的歌曲: ${this.songList[matchedIndex].name}, 索引: ${matchedIndex}`) } else { LogUtil.info(`heanup 当前播放的歌曲不在本歌单中`) } } else { LogUtil.info(`heanup 当前没有播放歌曲`) } } catch (error) { LogUtil.error('heanup 加载当前播放状态失败: ' + error) } } /** * 播放歌单 */ playPlaylist() { if (this.songList.length === 0) { ToastUtil.showToast('歌单为空') return } // 发送播放歌单事件 const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY } LogUtil.info(`heanup 准备发送播放事件,playlist: ${this.playlist ? '存在' : '不存在'}, songs: ${this.songList.length}首`) // 构建歌单播放事件数据 const playlistData: PlaylistEventData = { playlistId: this.playlist?.id || '', playlistName: this.playlist?.name || '', songCount: this.songList.length, startIndex: 0, // 只发送歌曲的必要信息 songFilePaths: this.songList.map(song => song.filePath) }; LogUtil.info(`heanup 发送歌单播放事件数据: ${JSON.stringify(playlistData)}`) const eventData: emitter.EventData = { data: playlistData }; emitter.emit(eventPlaylistPlay, eventData) ToastUtil.showToast('开始播放歌单') } /** * 撌放指定歌曲 */ playSong(song: VideoItem, index: number) { LogUtil.info(`heanup === playSong 方法被调用 ===`) LogUtil.info(`heanup 播放指定歌曲: ${song.name}, 索引: ${index}`) LogUtil.info(`heanup 歌曲列表长度: ${this.songList.length}`) LogUtil.info(`heanup 歌单ID: ${this.playlist?.id}, 歌单名称: ${this.playlist?.name}`) // 检查歌曲文件路径 LogUtil.info(`heanup 所有歌曲文件路径: ${JSON.stringify(this.songList.map(s => s.filePath))}`) // 发送播放歌单事件 const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY } const playlistData: PlaylistEventData = { playlistId: this.playlist?.id || '', playlistName: this.playlist?.name || '', songCount: this.songList.length, startIndex: index, // 只发送所有歌曲的文件路径 songFilePaths: this.songList.map(s => s.filePath) }; LogUtil.info(`heanup 准备发送事件,eventId: ${eventPlaylistPlay.eventId}`) LogUtil.info(`heanup 发送歌单播放事件数据: ${JSON.stringify(playlistData)}`) const eventData: emitter.EventData = { data: playlistData }; LogUtil.info(`heanup eventData对象: ${JSON.stringify(eventData)}`) emitter.emit(eventPlaylistPlay, eventData) } /** * 编辑歌单 */ editPlaylist() { if (this.playlist) { showEditPlaylistDialog( this.playlist, async (name: string, description: string) => { if (this.playlist) { this.playlist.name = name this.playlist.description = description const success = await this.playlistTable.updatePlaylist(this.playlist.id, name, description) if (success) { ToastUtil.showToast('歌单更新成功') // 发送刷新事件 const eventRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_REFRESH } emitter.emit(eventRefresh, {}) } else { ToastUtil.showToast('歌单更新失败') } } } ) } } /** * 删除歌单 */ async deletePlaylist() { if (this.playlist) { // 显示确认对话框 AlertDialog.show({ title: '删除歌单', message: `确定要删除歌单"${this.playlist.name}"吗?此操作不可撤销。`, primaryButton: { value: '取消', action: () => {} }, secondaryButton: { value: '删除', fontColor: Color.Red, action: async () => { const success = await this.playlistTable.deletePlaylist(this.playlistId) if (success) { ToastUtil.showToast('歌单删除成功') // 发送刷新事件 emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, {}) router.back() } else { ToastUtil.showToast('歌单删除失败') } } } }) } } /** * 从歌单移除歌曲 */ async removeSongFromPlaylist(song: VideoItem) { if (this.playlist) { const success = await this.playlistTable.removeSongFromPlaylist(this.playlistId, song.filePath) if (success) { // 从本地列表移除 const index = this.songList.findIndex(s => s.filePath === song.filePath) if (index !== -1) { this.songList.splice(index, 1) this.songList = [...this.songList] // 触发UI更新 } // 更新歌单信息 this.playlist.songCount = this.songList.length await this.playlistTable.updatePlaylist(this.playlist.id, this.playlist.name, this.playlist.description) ToastUtil.showToast('已从歌单移除') } else { ToastUtil.showToast('移除失败') } } } build() { Column() { // 顶部安全区和标题栏 Column() { Blank() .height(px2vp(AppUtil.getStatusBarHeight())) .backgroundColor($r('app.color.title_bar_bg')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) // 标题栏 Row() { Image($r('app.media.back')) .width(24) .height(24) .margin({ left: 12, right: 8 }) .onClick(() => { // 如果在排序模式,先退出排序模式 if (this.isSortMode) { this.exitSortMode() } else { router.back() } }) Text(this.isSortMode ? '排序模式' : (this.playlist?.name || '歌单详情')) .fontSize(18) .fontColor(Color.White) .fontWeight(FontWeight.Medium) .layoutWeight(1) .textAlign(TextAlign.Center) // 排序/完成按钮 Text(this.isSortMode ? '完成' : '排序') .fontSize(16) .fontColor(Color.White) .margin({ right: 12 }) .onClick(() => { if (this.isSortMode) { this.exitSortMode() } else { this.enterSortMode() } }) } .height(48) .width('100%') .alignItems(VerticalAlign.Center) .backgroundColor($r('app.color.title_bar_bg')) } if (this.isLoading) { // 加载状态 Column() { LoadingProgress() .width(40) .height(40) .color(this.themeColor ) Text('加载中...') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ top: 12 }) } .layoutWeight(1) .justifyContent(FlexAlign.Center) } else if (this.playlist) { // 歌单信息区域 - 参考LocalMusic的封面标题区域设计 Column() { // 背景区域 Row() { Column() { // 歌单封面 Image(this.playlist.coverPath || $r('app.media.hm_playlist')) .width(100) .height(100) .borderRadius(12) .clip(true) .interpolation(ImageInterpolation.High) .autoResize(true) .shadow({ radius: 15, color: '#0000001a', offsetX: 0, offsetY: 6 }) } .alignItems(HorizontalAlign.Start) .margin({ left: 24, top: 16, bottom: 16 }) Column() { // 歌单名称 Text(this.playlist.name) .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_color')) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ bottom: 8 }) // 歌单描述 if (this.playlist.description) { Text(this.playlist.description) .fontSize(14) .fontColor($r('app.color.text_color')) .opacity(0.8) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ bottom: 12 }) .lineHeight(18) } // 歌单统计信息 Row() { Text(`共${this.playlist.songCount}首歌`) .fontSize(13) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_color')) .opacity(0.8) if (this.calculateTotalDuration() > 0) { Text(` · ${this.formatTotalDuration(this.calculateTotalDuration())}`) .fontSize(13) .fontColor($r('app.color.text_color')) .opacity(0.7) .margin({ left: 4 }) } } .margin({ top: 8 }) } .height('100%') .layoutWeight(1) .margin({ left: 16 }) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .width('100%') .height(140) .justifyContent(FlexAlign.SpaceBetween) .padding({ right: 24 }) // 操作按钮区域 Row({ space: 12 }) { // 播放全部按钮 - 参考LocalMusic的按钮样式 Button() { Row({ space: 8 }) { Image($r('app.media.ic_play')) .width(16) .height(16) .fillColor(Color.White) Text('播放全部') .fontSize(14) .fontColor(Color.White) .fontWeight(FontWeight.Medium) } .justifyContent(FlexAlign.Center) } .layoutWeight(1) .height(44) .backgroundColor(this.themeColor ) .borderRadius(22) .shadow({ radius: 8, color: '#0a59f740', offsetX: 0, offsetY: 4 }) .onClick(() => { this.playPlaylist() }) .stateStyles({ normal: { .backgroundColor(this.themeColor ) .scale({ x: 1, y: 1 }) }, pressed: { .backgroundColor('#0a59f7cc') .scale({ x: 0.96, y: 0.96 }) } }) .animation({ duration: 150, curve: Curve.EaseInOut }) // 编辑歌单按钮 Button() { Row({ space: 6 }) { Text('✏️') .fontSize(16) .fontColor(this.themeColor ) Text('编辑') .fontSize(14) .fontColor(this.themeColor ) .fontWeight(FontWeight.Medium) } .justifyContent(FlexAlign.Center) } .layoutWeight(1) .height(44) .backgroundColor(Color.Transparent) .borderRadius(22) .border({ width: 1.5, color: this.themeColor }) .onClick(() => { this.editPlaylist() }) .stateStyles({ normal: { .backgroundColor(Color.Transparent) .scale({ x: 1, y: 1 }) }, pressed: { .backgroundColor('#0a59f715') .scale({ x: 0.96, y: 0.96 }) } }) .animation({ duration: 150, curve: Curve.EaseInOut }) // 删除按钮 Button() { Row({ space: 6 }) { Text('🗑️') .fontSize(16) Text('删除') .fontSize(14) .fontColor(Color.Red) .fontWeight(FontWeight.Medium) } .justifyContent(FlexAlign.Center) } .layoutWeight(1) .height(44) .backgroundColor(Color.Transparent) .borderRadius(22) .border({ width: 1.5, color: Color.Red }) .onClick(() => { this.deletePlaylist() }) .stateStyles({ normal: { .backgroundColor(Color.Transparent) .scale({ x: 1, y: 1 }) }, pressed: { .backgroundColor('#ff000015') .scale({ x: 0.96, y: 0.96 }) } }) .animation({ duration: 150, curve: Curve.EaseInOut }) } .width('100%') .padding({ left: 24, right: 24, bottom: 20 }) .justifyContent(FlexAlign.Start) } .backgroundColor($r('app.color.bg_card')) .margin({ left: 16, right: 16, top: 16 }) .borderRadius(16) .shadow({ radius: 12, color: '#00000014', offsetX: 0, offsetY: 4 }) .scale({ x: this.contentScale, y: this.contentScale }) .opacity(this.pageOpacity) .transition(TransitionEffect.OPACITY.animation({ duration: 600, curve: Curve.EaseOut })) .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 600, curve: Curve.EaseOut })) // 歌曲列表标题 - 简化设计,与LocalMusic保持一致 if (this.songList.length > 0) { Row() { Text('歌曲列表') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.text_color')) .opacity(0.9) .layoutWeight(1) Text(`${this.songList.length}首`) .fontSize(14) .fontColor($r('app.color.text_color')) .opacity(0.6) // 添加歌曲按钮 Button() { Row({ space: 6 }) { Text('+') .fontSize(18) .fontColor(Color.White) Text('添加歌曲') .fontSize(14) .fontColor(Color.White) .fontWeight(FontWeight.Medium) } .justifyContent(FlexAlign.Center) .padding({ left: 16, right: 16, top: 8, bottom: 8 }) } .height(36) .backgroundColor(this.themeColor) .borderRadius(18) .margin({ left: 12 }) .onClick(() => { this.addSongsToPlaylist() }) .stateStyles({ normal: { .backgroundColor(this.themeColor) .scale({ x: 1, y: 1 }) }, pressed: { .backgroundColor('#0a59f7cc') .scale({ x: 0.96, y: 0.96 }) } }) .animation({ duration: 150, curve: Curve.EaseInOut }) } .width('100%') .padding({ left: 24, right: 24, top: 16, bottom: 12 }) .opacity(this.pageOpacity) .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 })) } // 歌曲列表 - 参考LocalMusic的MusicItem样式 if (this.songList.length > 0) { List({ space: 0 }) { ForEach(this.songList, (song: VideoItem, index: number) => { ListItem() { Button({ type: ButtonType.Normal, stateEffect: true }) { Row() { // 歌曲封面 - 改为圆形 Stack() { Image(song.pixelMapPath || $r('app.media.music_red')) .width(48) .height(48) .borderRadius(24) // 改为圆形 .objectFit(ImageFit.Cover) .interpolation(ImageInterpolation.High) .autoResize(true) .margin({ left: 16 }) .onClick(() => { this.playSong(song, index) }) } .width(64) // 歌曲信息 - 参考LocalMusic的布局 Column() { Row() { // 歌曲名称 Text(song.name || song.fileName || '') .fontSize(16) .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color')) .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Bold : FontWeight.Normal) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) .margin({ top: 8, left: 12 }) // 音质标签 if (song.md5Str) { Text(song.md5Str?.includes('Lossless') ? '无损' : song.md5Str) .fontSize(10) .fontColor($r('app.color.text_color')) .fontWeight(500) .padding({ top: 2, right: 6, left: 6, bottom: 2 }) .borderRadius(4) .margin({ top: 8, left: 8 }) .backgroundColor( '#FFC107') .visibility(song.md5Str ? Visibility.Visible : Visibility.None) } } .width('100%') // 歌手和专辑信息 - 参考LocalMusic的第二行布局 Row() { Text(song.artist) .fontSize(13) .fontColor(this.isPlaying && this.curIndex === index ?this.themeColor : $r('app.color.text_color')) .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8) Text(song.album) .fontSize(13) .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color')) .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8) // 时长显示 if (song.duration && typeof song.duration === 'number' && song.duration > 0) { Text(this.formatDuration(song.duration)) .fontSize(13) .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color')) .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal) .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.7) .margin({ right: 20 }) } } .width('100%') .margin({ left: 12, top: 4 }) .alignItems(VerticalAlign.Center) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) .justifyContent(FlexAlign.Center) // 排序模式下显示上下移动按钮,否则显示更多按钮 if (this.isSortMode) { Row({ space: 8 }) { // 上移按钮 Text('▲') .fontSize(16) .fontColor(index === 0 ? '#cccccc' : this.themeColor) .onClick(() => { if (index > 0) { this.moveSong(index, index - 1) } }) .enabled(index > 0) // 下移按钮 Text('▼') .fontSize(16) .fontColor(index === this.songList.length - 1 ? '#cccccc' : this.themeColor) .onClick(() => { if (index < this.songList.length - 1) { this.moveSong(index, index + 1) } }) .enabled(index < this.songList.length - 1) } .margin({ right: 15 }) } else { Text('⋯') .fontSize(20) .fontColor($r('app.color.text_color')) .opacity(0.6) .margin({ right: 15 }) .onClick(() => { this.showSongMenu(song) }) } } .width('100%') .height(70) .justifyContent(FlexAlign.Start) .alignItems(VerticalAlign.Center) } .backgroundColor(Color.Transparent) .height(70) .width('100%') .onClick(() => { // 排序模式下禁用点击播放 if (!this.isSortMode) { this.playSong(song, index) } }) .gesture( LongPressGesture() .onAction(() => { // 排序模式下不显示菜单 if (!this.isSortMode) { this.showSongMenu(song) } }) ) .stateStyles({ normal: { .backgroundColor(Color.Transparent) }, pressed: { .backgroundColor(this.isSortMode ? Color.Transparent : '#f1f3f5') } }) // 当前播放歌曲的背景高亮 .backgroundColor(this.isPlaying && this.curIndex === index ? '#f0f8ff' : Color.Transparent) .border({ width: { left: this.isPlaying && this.curIndex === index ? 3 : 0 }, color: this.themeColor }) .opacity(this.pageOpacity) .translate({ x: 0, y: this.showContent ? 0 : 20 }) .transition(TransitionEffect.OPACITY.animation({ duration: 600, curve: Curve.EaseOut, delay: 300 + index * 30 })) .transition(TransitionEffect.translate({ y: 20 }).animation({ duration: 600, curve: Curve.EaseOut, delay: 300 + index * 30 })) } }) } .width('100%') .layoutWeight(1) .backgroundColor($r('app.color.bg_card')) .margin({ left: 16, right: 16 }) .borderRadius(12) .divider({ strokeWidth: 0.5, color: '#0000001a', startMargin: 80, endMargin: 20 }) .opacity(this.pageOpacity) .scale({ x: this.contentScale, y: this.contentScale }) .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 })) .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 800, curve: Curve.EaseOut, delay: 200 })) } else { // 空状态 Column() { // 空状态容器 Column() { // 空状态图标容器 Stack() { // 背景圆圈 Circle({ width: 160, height: 160 }) .fill('#0a59f715') .border({ width: 2, color: '#0a59f71a' }) // 中间圆圈 Circle({ width: 120, height: 120 }) .fill('#0a59f722') // 音符图标 Image($r('app.media.music_red')) .width(64) .height(64) .opacity(0.6) .fillColor(this.themeColor ) } .margin({ bottom: 32 }) // 空状态标题 Text('歌单还是空的') .fontSize(22) .fontColor($r('app.color.text_color')) .fontWeight(FontWeight.Bold) .margin({ bottom: 12 }) .letterSpacing(0.5) // 空状态描述 Text('快来添加你喜欢的音乐吧\n让这个歌单充满美妙的旋律') .fontSize(15) .fontColor($r('app.color.text_color')) .opacity(0.8) .margin({ bottom: 40 }) .textAlign(TextAlign.Center) .lineHeight(24) .maxLines(2) // 添加歌曲按钮 Button() { Row({ space: 8 }) { Text('+') .fontSize(20) .fontColor(Color.White) Text('添加歌曲') .fontSize(16) .fontColor(Color.White) .fontWeight(FontWeight.Medium) } .justifyContent(FlexAlign.Center) } .width(160) .height(48) .backgroundColor(this.themeColor ) .borderRadius(24) .shadow({ radius: 12, color: '#0a59f74d', offsetX: 0, offsetY: 6 }) .onClick(() => { showAddSongsToPlaylistDialog( this.playlist!, async (songs: VideoItem[]) => { // 添加选中的歌曲到歌单 const success = await this.playlistTable.addSongsToPlaylist(this.playlistId, songs.map(song => song.filePath)) if (success) { ToastUtil.showToast(`成功添加 ${songs.length} 首歌曲`) // 重新加载歌单详情 this.loadPlaylistDetail() } else { ToastUtil.showToast('添加歌曲失败') } } ) }) .stateStyles({ normal: { .backgroundColor(this.themeColor ) .scale({ x: 1, y: 1 }) }, pressed: { .backgroundColor('#0a59f7cc') .scale({ x: 0.96, y: 0.96 }) } }) .animation({ duration: 200, curve: Curve.EaseInOut }) // 快速操作提示 Text('或长按歌单选择更多操作') .fontSize(13) .fontColor($r('app.color.text_color')) .margin({ top: 16 }) .opacity(0.7) } .width('100%') .padding(32) .backgroundColor($r('app.color.bg_card')) .borderRadius(20) .margin({ left: 16, right: 16 }) .shadow({ radius: 16, color: '#0000000f', offsetX: 0, offsetY: 8 }) .opacity(this.pageOpacity) .scale({ x: this.contentScale, y: this.contentScale }) .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 })) .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 800, curve: Curve.EaseOut, delay: 200 })) } .layoutWeight(1) .margin({ left: 16, right: 16 }) .justifyContent(FlexAlign.Center) .padding({ top: 20, bottom: 20 }) } } } .width('100%') .height('100%') .backgroundColor($r('app.color.index_background')) } /** * 显示更多菜单 */ showMoreMenu() { // 简单直接的二选一,避免复杂的多级菜单 AlertDialog.show({ title: '歌单操作', message: '🎵 添加歌曲:向歌单添加新音乐\n✏️ 编辑歌单:修改歌单信息\n\n点击"确定"添加歌曲,点击"取消"编辑歌单', primaryButton: { value: '取消 (编辑)', action: () => { this.editPlaylist() } }, secondaryButton: { value: '确定 (添加)', action: () => { this.addSongsToPlaylist() } } }) } /** * 添加歌曲到歌单 */ addSongsToPlaylist() { if (this.playlist) { showAddSongsToPlaylistDialog( this.playlist, async (songs: VideoItem[]) => { // 添加选中的歌曲到歌单 const success = await this.playlistTable.addSongsToPlaylist(this.playlistId, songs.map(song => song.filePath)) if (success) { ToastUtil.showToast(`成功添加 ${songs.length} 首歌曲`) // 重新加载歌单详情 this.loadPlaylistDetail() } else { ToastUtil.showToast('添加歌曲失败') } } ) } } /** * 显示歌曲菜单 */ showSongMenu(song: VideoItem) { AlertDialog.show({ title: song.name, message: '', primaryButton: { value: '取消', action: () => {} }, secondaryButton: { value: '从歌单移除', fontColor: Color.Red, action: () => { this.removeSongFromPlaylist(song) } } }) } /** * 进入排序模式 */ enterSortMode() { this.isSortMode = true ToastUtil.showToast('点击上下箭头调整歌曲顺序') LogUtil.info('heanup 进入排序模式') } /** * 退出排序模式 */ exitSortMode() { this.isSortMode = false ToastUtil.showToast('排序已保存') LogUtil.info('heanup 退出排序模式') } /** * 移动歌曲位置 */ async moveSong(fromIndex: number, toIndex: number) { if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= this.songList.length || toIndex >= this.songList.length) { return } LogUtil.info(`heanup 移动歌曲: from ${fromIndex} to ${toIndex}`) // 1. 在本地数组中移动 const movedSong = this.songList.splice(fromIndex, 1)[0] this.songList.splice(toIndex, 0, movedSong) // 2. 触发UI更新 this.songList = [...this.songList] // 3. 更新数据库中的sortOrder await this.updateAllSongsSortOrder() } /** * 更新所有歌曲的sortOrder到数据库 */ async updateAllSongsSortOrder() { try { LogUtil.info('heanup 开始更新所有歌曲的sortOrder') for (let i = 0; i < this.songList.length; i++) { const song = this.songList[i] const success = await this.playlistTable.updatePlaylistSongSortOrder( this.playlistId, song.filePath, i ) if (success) { LogUtil.info(`heanup 更新歌曲[${i}] ${song.name} sortOrder成功`) } else { LogUtil.error(`heanup 更新歌曲[${i}] ${song.name} sortOrder失败`) } } LogUtil.info('heanup 所有歌曲sortOrder更新完成') } catch (error) { LogUtil.error('heanup 更新歌曲sortOrder失败: ' + error) } } } /** * 将 PlaylistSong 转换为 VideoItem */ export async function convertPlaylistSongsToVideoItems(context: Context,playlistSongs: PlaylistSong[]): Promise { const videoItems: VideoItem[] = [] LogUtil.info(`heanup 开始转换歌曲,共 ${playlistSongs.length} 首`) const mediaTable: MediaTable = new MediaTable(context) await new Promise((resolve, reject) => { mediaTable.getRdbStore(context, (err:Error) => { err ? reject(err) : resolve(); }); }); for (const playlistSong of playlistSongs) { try { LogUtil.info(`heanup 查询歌曲: ${playlistSong.songFilePath}`) // 从数据库查询完整的歌曲信息 const videoItem = await mediaTable.queryVideoByFilePath(playlistSong.songFilePath) if (videoItem) { LogUtil.info(`heanup 找到歌曲: ${videoItem.name}`) videoItems.push(videoItem) } else { LogUtil.warn(`heanup 数据库中未找到歌曲: ${playlistSong.songFilePath}`) // 如果数据库中没有,创建一个基本的 VideoItem const fileName = playlistSong.songFilePath.split('/').pop() || '' const name = fileName.replace(/\.[^/.]+$/, "") // 移除文件扩展名 const basicVideoItem = new VideoItem( name, // name Date.now().toString() + Math.random(), // id playlistSong.songFilePath, // filePath 0, // type (音乐类型) 0, // videoSize playlistSong.addTime, // cTime undefined, // pixelMap undefined, // size undefined, // pixelMapPath undefined, // artist undefined, // album fileName, // fileName undefined // lastPlayed ) videoItems.push(basicVideoItem) } } catch (error) { LogUtil.error('heanup 转换歌曲失败: ' + error) } } LogUtil.info(`heanup 转换完成,共 ${videoItems.length} 首歌曲`) return videoItems }