PlaylistDetailPage.ets 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
  1. import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
  2. import { VideoItem } from '../viewmodel/VideoItem';
  3. import PlaylistTable from '../common/util/PlaylistTable';
  4. import MediaTable from '../common/util/MediaTable';
  5. import { emitter } from '@kit.BasicServicesKit';
  6. import { ToastUtil, AppUtil, LogUtil, PreferencesUtil } from '@pura/harmony-utils';
  7. import { showEditPlaylistDialog } from '../dialog/PlaylistDialog';
  8. import { showAddSongsToPlaylistDialog } from '../dialog/AddSongsToPlaylistDialog';
  9. import { router } from '@kit.ArkUI';
  10. import { GlobalContext } from '../common/util/GlobalContext';
  11. import { CommonConstants } from '../common/constants/CommonConstants';
  12. import { EventConstants } from '../common/constants/EventConstants';
  13. import { common } from '@kit.AbilityKit';
  14. /**
  15. * 歌单播放事件数据
  16. */
  17. interface PlaylistEventData {
  18. playlistId: string;
  19. playlistName: string;
  20. songCount: number;
  21. startIndex: number;
  22. songFilePaths: string[];
  23. }
  24. /**
  25. * 歌单详情页面
  26. * 展示歌单信息和歌曲列表
  27. */
  28. @Entry
  29. @Component
  30. export struct PlaylistDetailPage {
  31. context = this.getUIContext().getHostContext() as common.UIAbilityContext
  32. @State playlist: Playlist | null = null
  33. @State songList: VideoItem[] = []
  34. @State isLoading: boolean = true
  35. @State isShowEditDialog: boolean = false
  36. @State isPlaying: boolean = false
  37. @State curIndex: number = -1
  38. @State pageOpacity: number = 0
  39. @State contentScale: number = 0.95
  40. @State showContent: boolean = false
  41. @State isSortMode: boolean = false // 是否处于排序模式
  42. private playlistTable: PlaylistTable = new PlaylistTable(getContext(this))
  43. private mediaTable: MediaTable = new MediaTable(getContext(this))
  44. private playlistId: string = ''
  45. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  46. aboutToAppear() {
  47. let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
  48. AppStorage.setOrCreate('themeColor', themeColor);
  49. this.themeColor = themeColor
  50. // 获取传入的歌单对象
  51. const params = router.getParams() as Record<string, Object>
  52. if (params && params['playlist']) {
  53. this.playlist = params['playlist'] as Playlist
  54. this.playlistId = this.playlist.id
  55. this.loadPlaylistDetail()
  56. }
  57. // 监听播放状态变化
  58. this.setupPlaybackStatusListener()
  59. }
  60. aboutToDisappear() {
  61. // 移除事件监听
  62. this.removePlaybackStatusListener()
  63. }
  64. /**
  65. * 加载歌单详情
  66. */
  67. async loadPlaylistDetail() {
  68. try {
  69. this.isLoading = true
  70. if (!this.playlist) {
  71. ToastUtil.showToast('歌单不存在')
  72. router.back()
  73. return
  74. }
  75. // 加载歌单歌曲
  76. const playlistSongs = await this.playlistTable.queryPlaylistSongs(this.playlistId)
  77. // 将 PlaylistSong 转换为 VideoItem
  78. this.songList = await convertPlaylistSongsToVideoItems(this.context,playlistSongs)
  79. // 歌单加载完成后,加载当前播放状态
  80. this.loadCurrentPlaybackStatus()
  81. } catch (error) {
  82. LogUtil.error('heanup 加载歌单详情失败: ' + error)
  83. ToastUtil.showToast('加载歌单详情失败')
  84. } finally {
  85. this.isLoading = false
  86. // 启动页面进入动画
  87. this.animatePageEntry()
  88. }
  89. }
  90. /**
  91. * 格式化时长显示
  92. */
  93. formatDuration(duration?: number): string {
  94. if (!duration || duration <= 0) {
  95. return ''
  96. }
  97. const minutes = Math.floor(duration / 60)
  98. const seconds = Math.floor(duration % 60)
  99. return `${minutes}:${seconds.toString().padStart(2, '0')}`
  100. }
  101. /**
  102. * 计算歌单总时长
  103. */
  104. calculateTotalDuration(): number {
  105. let total = 0
  106. for (const song of this.songList) {
  107. const duration = song.duration || 0
  108. total = total + (typeof duration === 'number' ? duration : 0)
  109. }
  110. return total
  111. }
  112. /**
  113. * 格式化总时长显示
  114. */
  115. formatTotalDuration(totalSeconds: number): string {
  116. if (totalSeconds <= 0) {
  117. return ''
  118. }
  119. const hours = Math.floor(totalSeconds / 3600)
  120. const minutes = Math.floor((totalSeconds % 3600) / 60)
  121. if (hours > 0) {
  122. return `${hours}小时${minutes}分钟`
  123. } else {
  124. return `${minutes}分钟`
  125. }
  126. }
  127. /**
  128. * 页面入场动画
  129. */
  130. animatePageEntry() {
  131. animateTo({
  132. duration: 600,
  133. curve: Curve.EaseOut,
  134. delay: 100,
  135. onFinish: () => {
  136. this.showContent = true
  137. }
  138. }, () => {
  139. this.pageOpacity = 1
  140. this.contentScale = 1
  141. })
  142. }
  143. /**
  144. * 设置播放状态监听
  145. */
  146. setupPlaybackStatusListener() {
  147. try {
  148. // 监听播放状态变化事件
  149. const eventPlaybackStatus: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYBACK_STATUS }
  150. emitter.on(eventPlaybackStatus, (eventData: emitter.EventData) => {
  151. LogUtil.info('heanup PlaylistDetailPage 收到播放状态变化事件:'+ JSON.stringify(eventData))
  152. if (eventData.data) {
  153. const data = eventData.data as Record<string, Object>
  154. const oldIsPlaying = this.isPlaying
  155. const oldCurIndex = this.curIndex
  156. this.isPlaying = data['isPlaying'] as boolean
  157. const currentFilePath = data['currentFilePath'] as string
  158. LogUtil.info(`heanup 原始播放状态: isPlaying=${this.isPlaying}, currentFilePath=${currentFilePath}`)
  159. // 通过filePath在当前歌单中查找对应的索引
  160. if (currentFilePath) {
  161. const matchedIndex = this.songList.findIndex(song => song.filePath === currentFilePath)
  162. if (matchedIndex !== -1) {
  163. this.curIndex = matchedIndex
  164. LogUtil.info(`heanup 在歌单中找到匹配的歌曲: ${this.songList[matchedIndex].name}, 索引: ${matchedIndex}`)
  165. } else {
  166. // 当前播放的歌曲不在本歌单中,重置索引
  167. this.curIndex = -1
  168. LogUtil.info(`heanup 当前播放的歌曲不在本歌单中: ${currentFilePath}`)
  169. }
  170. } else {
  171. this.curIndex = -1
  172. LogUtil.info(`heanup 当前没有播放歌曲`)
  173. }
  174. LogUtil.info(`heanup 播放状态更新: isPlaying=${oldIsPlaying}->${this.isPlaying}, curIndex=${oldCurIndex}->${this.curIndex}`)
  175. }
  176. })
  177. } catch (error) {
  178. LogUtil.error('heanup 设置播放状态监听失败: ' + error)
  179. }
  180. }
  181. /**
  182. * 移除播放状态监听
  183. */
  184. removePlaybackStatusListener() {
  185. try {
  186. emitter.off(EventConstants.EVENT_PLAYBACK_STATUS)
  187. } catch (error) {
  188. LogUtil.error('heanup 移除播放状态监听失败: ' + error)
  189. }
  190. }
  191. /**
  192. * 加载当前播放状态
  193. */
  194. loadCurrentPlaybackStatus() {
  195. try {
  196. // 从AppStorage获取当前播放的歌曲
  197. const currentSong = AppStorage.get<VideoItem>('currentSong')
  198. if (currentSong && currentSong.filePath) {
  199. LogUtil.info(`heanup 获取到当前播放歌曲: ${currentSong.name}, filePath: ${currentSong.filePath}`)
  200. // 在歌单中查找匹配的歌曲
  201. const matchedIndex = this.songList.findIndex(song => song.filePath === currentSong.filePath)
  202. if (matchedIndex !== -1) {
  203. this.curIndex = matchedIndex
  204. // 假设如果有currentSong说明正在播放
  205. this.isPlaying = true
  206. LogUtil.info(`heanup 在歌单中找到当前播放的歌曲: ${this.songList[matchedIndex].name}, 索引: ${matchedIndex}`)
  207. } else {
  208. LogUtil.info(`heanup 当前播放的歌曲不在本歌单中`)
  209. }
  210. } else {
  211. LogUtil.info(`heanup 当前没有播放歌曲`)
  212. }
  213. } catch (error) {
  214. LogUtil.error('heanup 加载当前播放状态失败: ' + error)
  215. }
  216. }
  217. /**
  218. * 播放歌单
  219. */
  220. playPlaylist() {
  221. if (this.songList.length === 0) {
  222. ToastUtil.showToast('歌单为空')
  223. return
  224. }
  225. // 发送播放歌单事件
  226. const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
  227. LogUtil.info(`heanup 准备发送播放事件,playlist: ${this.playlist ? '存在' : '不存在'}, songs: ${this.songList.length}首`)
  228. // 构建歌单播放事件数据
  229. const playlistData: PlaylistEventData = {
  230. playlistId: this.playlist?.id || '',
  231. playlistName: this.playlist?.name || '',
  232. songCount: this.songList.length,
  233. startIndex: 0,
  234. // 只发送歌曲的必要信息
  235. songFilePaths: this.songList.map(song => song.filePath)
  236. };
  237. LogUtil.info(`heanup 发送歌单播放事件数据: ${JSON.stringify(playlistData)}`)
  238. const eventData: emitter.EventData = {
  239. data: playlistData
  240. };
  241. emitter.emit(eventPlaylistPlay, eventData)
  242. ToastUtil.showToast('开始播放歌单')
  243. }
  244. /**
  245. * 撌放指定歌曲
  246. */
  247. playSong(song: VideoItem, index: number) {
  248. LogUtil.info(`heanup === playSong 方法被调用 ===`)
  249. LogUtil.info(`heanup 播放指定歌曲: ${song.name}, 索引: ${index}`)
  250. LogUtil.info(`heanup 歌曲列表长度: ${this.songList.length}`)
  251. LogUtil.info(`heanup 歌单ID: ${this.playlist?.id}, 歌单名称: ${this.playlist?.name}`)
  252. // 检查歌曲文件路径
  253. LogUtil.info(`heanup 所有歌曲文件路径: ${JSON.stringify(this.songList.map(s => s.filePath))}`)
  254. // 发送播放歌单事件
  255. const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
  256. const playlistData: PlaylistEventData = {
  257. playlistId: this.playlist?.id || '',
  258. playlistName: this.playlist?.name || '',
  259. songCount: this.songList.length,
  260. startIndex: index,
  261. // 只发送所有歌曲的文件路径
  262. songFilePaths: this.songList.map(s => s.filePath)
  263. };
  264. LogUtil.info(`heanup 准备发送事件,eventId: ${eventPlaylistPlay.eventId}`)
  265. LogUtil.info(`heanup 发送歌单播放事件数据: ${JSON.stringify(playlistData)}`)
  266. const eventData: emitter.EventData = {
  267. data: playlistData
  268. };
  269. LogUtil.info(`heanup eventData对象: ${JSON.stringify(eventData)}`)
  270. emitter.emit(eventPlaylistPlay, eventData)
  271. }
  272. /**
  273. * 编辑歌单
  274. */
  275. editPlaylist() {
  276. if (this.playlist) {
  277. showEditPlaylistDialog(
  278. this.playlist,
  279. async (name: string, description: string) => {
  280. if (this.playlist) {
  281. this.playlist.name = name
  282. this.playlist.description = description
  283. const success = await this.playlistTable.updatePlaylist(this.playlist.id, name, description)
  284. if (success) {
  285. ToastUtil.showToast('歌单更新成功')
  286. // 发送刷新事件
  287. const eventRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_REFRESH }
  288. emitter.emit(eventRefresh, {})
  289. } else {
  290. ToastUtil.showToast('歌单更新失败')
  291. }
  292. }
  293. }
  294. )
  295. }
  296. }
  297. /**
  298. * 删除歌单
  299. */
  300. async deletePlaylist() {
  301. if (this.playlist) {
  302. // 显示确认对话框
  303. AlertDialog.show({
  304. title: '删除歌单',
  305. message: `确定要删除歌单"${this.playlist.name}"吗?此操作不可撤销。`,
  306. primaryButton: {
  307. value: '取消',
  308. action: () => {}
  309. },
  310. secondaryButton: {
  311. value: '删除',
  312. fontColor: Color.Red,
  313. action: async () => {
  314. const success = await this.playlistTable.deletePlaylist(this.playlistId)
  315. if (success) {
  316. ToastUtil.showToast('歌单删除成功')
  317. // 发送刷新事件
  318. emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, {})
  319. router.back()
  320. } else {
  321. ToastUtil.showToast('歌单删除失败')
  322. }
  323. }
  324. }
  325. })
  326. }
  327. }
  328. /**
  329. * 从歌单移除歌曲
  330. */
  331. async removeSongFromPlaylist(song: VideoItem) {
  332. if (this.playlist) {
  333. const success = await this.playlistTable.removeSongFromPlaylist(this.playlistId, song.filePath)
  334. if (success) {
  335. // 从本地列表移除
  336. const index = this.songList.findIndex(s => s.filePath === song.filePath)
  337. if (index !== -1) {
  338. this.songList.splice(index, 1)
  339. this.songList = [...this.songList] // 触发UI更新
  340. }
  341. // 更新歌单信息
  342. this.playlist.songCount = this.songList.length
  343. await this.playlistTable.updatePlaylist(this.playlist.id, this.playlist.name, this.playlist.description)
  344. ToastUtil.showToast('已从歌单移除')
  345. } else {
  346. ToastUtil.showToast('移除失败')
  347. }
  348. }
  349. }
  350. build() {
  351. Column() {
  352. // 顶部安全区和标题栏
  353. Column() {
  354. Blank()
  355. .height(px2vp(AppUtil.getStatusBarHeight()))
  356. .backgroundColor($r('app.color.title_bar_bg'))
  357. .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  358. // 标题栏
  359. Row() {
  360. Image($r('app.media.back'))
  361. .width(24)
  362. .height(24)
  363. .margin({ left: 12, right: 8 })
  364. .onClick(() => {
  365. // 如果在排序模式,先退出排序模式
  366. if (this.isSortMode) {
  367. this.exitSortMode()
  368. } else {
  369. router.back()
  370. }
  371. })
  372. Text(this.isSortMode ? '排序模式' : (this.playlist?.name || '歌单详情'))
  373. .fontSize(18)
  374. .fontColor(Color.White)
  375. .fontWeight(FontWeight.Medium)
  376. .layoutWeight(1)
  377. .textAlign(TextAlign.Center)
  378. // 排序/完成按钮
  379. Text(this.isSortMode ? '完成' : '排序')
  380. .fontSize(16)
  381. .fontColor(Color.White)
  382. .margin({ right: 12 })
  383. .onClick(() => {
  384. if (this.isSortMode) {
  385. this.exitSortMode()
  386. } else {
  387. this.enterSortMode()
  388. }
  389. })
  390. }
  391. .height(48)
  392. .width('100%')
  393. .alignItems(VerticalAlign.Center)
  394. .backgroundColor($r('app.color.title_bar_bg'))
  395. }
  396. if (this.isLoading) {
  397. // 加载状态
  398. Column() {
  399. LoadingProgress()
  400. .width(40)
  401. .height(40)
  402. .color(this.themeColor )
  403. Text('加载中...')
  404. .fontSize(14)
  405. .fontColor($r('app.color.text_color'))
  406. .margin({ top: 12 })
  407. }
  408. .layoutWeight(1)
  409. .justifyContent(FlexAlign.Center)
  410. } else if (this.playlist) {
  411. // 歌单信息区域 - 参考LocalMusic的封面标题区域设计
  412. Column() {
  413. // 背景区域
  414. Row() {
  415. Column() {
  416. // 歌单封面
  417. Image(this.playlist.coverPath || $r('app.media.hm_playlist'))
  418. .width(100)
  419. .height(100)
  420. .borderRadius(12)
  421. .clip(true)
  422. .interpolation(ImageInterpolation.High)
  423. .autoResize(true)
  424. .shadow({
  425. radius: 15,
  426. color: '#0000001a',
  427. offsetX: 0,
  428. offsetY: 6
  429. })
  430. }
  431. .alignItems(HorizontalAlign.Start)
  432. .margin({ left: 24, top: 16, bottom: 16 })
  433. Column() {
  434. // 歌单名称
  435. Text(this.playlist.name)
  436. .fontSize(20)
  437. .fontWeight(FontWeight.Bold)
  438. .fontColor($r('app.color.text_color'))
  439. .maxLines(2)
  440. .textOverflow({ overflow: TextOverflow.Ellipsis })
  441. .margin({ bottom: 8 })
  442. // 歌单描述
  443. if (this.playlist.description) {
  444. Text(this.playlist.description)
  445. .fontSize(14)
  446. .fontColor($r('app.color.text_color'))
  447. .opacity(0.8)
  448. .maxLines(2)
  449. .textOverflow({ overflow: TextOverflow.Ellipsis })
  450. .margin({ bottom: 12 })
  451. .lineHeight(18)
  452. }
  453. // 歌单统计信息
  454. Row() {
  455. Text(`共${this.playlist.songCount}首歌`)
  456. .fontSize(13)
  457. .fontWeight(FontWeight.Bold)
  458. .fontColor($r('app.color.text_color'))
  459. .opacity(0.8)
  460. if (this.calculateTotalDuration() > 0) {
  461. Text(` · ${this.formatTotalDuration(this.calculateTotalDuration())}`)
  462. .fontSize(13)
  463. .fontColor($r('app.color.text_color'))
  464. .opacity(0.7)
  465. .margin({ left: 4 })
  466. }
  467. }
  468. .margin({ top: 8 })
  469. }
  470. .height('100%')
  471. .layoutWeight(1)
  472. .margin({ left: 16 })
  473. .justifyContent(FlexAlign.Center)
  474. .alignItems(HorizontalAlign.Start)
  475. }
  476. .width('100%')
  477. .height(140)
  478. .justifyContent(FlexAlign.SpaceBetween)
  479. .padding({ right: 24 })
  480. // 操作按钮区域
  481. Row({ space: 12 }) {
  482. // 播放全部按钮 - 参考LocalMusic的按钮样式
  483. Button() {
  484. Row({ space: 8 }) {
  485. Image($r('app.media.ic_play'))
  486. .width(16)
  487. .height(16)
  488. .fillColor(Color.White)
  489. Text('播放全部')
  490. .fontSize(14)
  491. .fontColor(Color.White)
  492. .fontWeight(FontWeight.Medium)
  493. }
  494. .justifyContent(FlexAlign.Center)
  495. }
  496. .layoutWeight(1)
  497. .height(44)
  498. .backgroundColor(this.themeColor )
  499. .borderRadius(22)
  500. .shadow({
  501. radius: 8,
  502. color: '#0a59f740',
  503. offsetX: 0,
  504. offsetY: 4
  505. })
  506. .onClick(() => {
  507. this.playPlaylist()
  508. })
  509. .stateStyles({
  510. normal: {
  511. .backgroundColor(this.themeColor )
  512. .scale({ x: 1, y: 1 })
  513. },
  514. pressed: {
  515. .backgroundColor('#0a59f7cc')
  516. .scale({ x: 0.96, y: 0.96 })
  517. }
  518. })
  519. .animation({
  520. duration: 150,
  521. curve: Curve.EaseInOut
  522. })
  523. // 编辑歌单按钮
  524. Button() {
  525. Row({ space: 6 }) {
  526. Text('✏️')
  527. .fontSize(16)
  528. .fontColor(this.themeColor )
  529. Text('编辑')
  530. .fontSize(14)
  531. .fontColor(this.themeColor )
  532. .fontWeight(FontWeight.Medium)
  533. }
  534. .justifyContent(FlexAlign.Center)
  535. }
  536. .layoutWeight(1)
  537. .height(44)
  538. .backgroundColor(Color.Transparent)
  539. .borderRadius(22)
  540. .border({
  541. width: 1.5,
  542. color: this.themeColor
  543. })
  544. .onClick(() => {
  545. this.editPlaylist()
  546. })
  547. .stateStyles({
  548. normal: {
  549. .backgroundColor(Color.Transparent)
  550. .scale({ x: 1, y: 1 })
  551. },
  552. pressed: {
  553. .backgroundColor('#0a59f715')
  554. .scale({ x: 0.96, y: 0.96 })
  555. }
  556. })
  557. .animation({
  558. duration: 150,
  559. curve: Curve.EaseInOut
  560. })
  561. // 删除按钮
  562. Button() {
  563. Row({ space: 6 }) {
  564. Text('🗑️')
  565. .fontSize(16)
  566. Text('删除')
  567. .fontSize(14)
  568. .fontColor(Color.Red)
  569. .fontWeight(FontWeight.Medium)
  570. }
  571. .justifyContent(FlexAlign.Center)
  572. }
  573. .layoutWeight(1)
  574. .height(44)
  575. .backgroundColor(Color.Transparent)
  576. .borderRadius(22)
  577. .border({
  578. width: 1.5,
  579. color: Color.Red
  580. })
  581. .onClick(() => {
  582. this.deletePlaylist()
  583. })
  584. .stateStyles({
  585. normal: {
  586. .backgroundColor(Color.Transparent)
  587. .scale({ x: 1, y: 1 })
  588. },
  589. pressed: {
  590. .backgroundColor('#ff000015')
  591. .scale({ x: 0.96, y: 0.96 })
  592. }
  593. })
  594. .animation({
  595. duration: 150,
  596. curve: Curve.EaseInOut
  597. })
  598. }
  599. .width('100%')
  600. .padding({ left: 24, right: 24, bottom: 20 })
  601. .justifyContent(FlexAlign.Start)
  602. }
  603. .backgroundColor($r('app.color.bg_card'))
  604. .margin({ left: 16, right: 16, top: 16 })
  605. .borderRadius(16)
  606. .shadow({
  607. radius: 12,
  608. color: '#00000014',
  609. offsetX: 0,
  610. offsetY: 4
  611. })
  612. .scale({ x: this.contentScale, y: this.contentScale })
  613. .opacity(this.pageOpacity)
  614. .transition(TransitionEffect.OPACITY.animation({ duration: 600, curve: Curve.EaseOut }))
  615. .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 600, curve: Curve.EaseOut }))
  616. // 歌曲列表标题 - 简化设计,与LocalMusic保持一致
  617. if (this.songList.length > 0) {
  618. Row() {
  619. Text('歌曲列表')
  620. .fontSize(16)
  621. .fontWeight(FontWeight.Medium)
  622. .fontColor($r('app.color.text_color'))
  623. .opacity(0.9)
  624. .layoutWeight(1)
  625. Text(`${this.songList.length}首`)
  626. .fontSize(14)
  627. .fontColor($r('app.color.text_color'))
  628. .opacity(0.6)
  629. // 添加歌曲按钮
  630. Button() {
  631. Row({ space: 6 }) {
  632. Text('+')
  633. .fontSize(18)
  634. .fontColor(Color.White)
  635. Text('添加歌曲')
  636. .fontSize(14)
  637. .fontColor(Color.White)
  638. .fontWeight(FontWeight.Medium)
  639. }
  640. .justifyContent(FlexAlign.Center)
  641. .padding({ left: 16, right: 16, top: 8, bottom: 8 })
  642. }
  643. .height(36)
  644. .backgroundColor(this.themeColor)
  645. .borderRadius(18)
  646. .margin({ left: 12 })
  647. .onClick(() => {
  648. this.addSongsToPlaylist()
  649. })
  650. .stateStyles({
  651. normal: {
  652. .backgroundColor(this.themeColor)
  653. .scale({ x: 1, y: 1 })
  654. },
  655. pressed: {
  656. .backgroundColor('#0a59f7cc')
  657. .scale({ x: 0.96, y: 0.96 })
  658. }
  659. })
  660. .animation({
  661. duration: 150,
  662. curve: Curve.EaseInOut
  663. })
  664. }
  665. .width('100%')
  666. .padding({ left: 24, right: 24, top: 16, bottom: 12 })
  667. .opacity(this.pageOpacity)
  668. .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
  669. }
  670. // 歌曲列表 - 参考LocalMusic的MusicItem样式
  671. if (this.songList.length > 0) {
  672. List({ space: 0 }) {
  673. ForEach(this.songList, (song: VideoItem, index: number) => {
  674. ListItem() {
  675. Button({ type: ButtonType.Normal, stateEffect: true }) {
  676. Row() {
  677. // 歌曲封面 - 改为圆形
  678. Stack() {
  679. Image(song.pixelMapPath || $r('app.media.music_red'))
  680. .width(48)
  681. .height(48)
  682. .borderRadius(24) // 改为圆形
  683. .objectFit(ImageFit.Cover)
  684. .interpolation(ImageInterpolation.High)
  685. .autoResize(true)
  686. .margin({ left: 16 })
  687. .onClick(() => {
  688. this.playSong(song, index)
  689. })
  690. }
  691. .width(64)
  692. // 歌曲信息 - 参考LocalMusic的布局
  693. Column() {
  694. Row() {
  695. // 歌曲名称
  696. Text(song.name || song.fileName || '')
  697. .fontSize(16)
  698. .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
  699. .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Bold : FontWeight.Normal)
  700. .maxLines(1)
  701. .textOverflow({ overflow: TextOverflow.Ellipsis })
  702. .layoutWeight(1)
  703. .margin({ top: 8, left: 12 })
  704. // 音质标签
  705. if (song.md5Str) {
  706. Text(song.md5Str?.includes('Lossless') ? '无损' : song.md5Str)
  707. .fontSize(10)
  708. .fontColor($r('app.color.text_color'))
  709. .fontWeight(500)
  710. .padding({ top: 2, right: 6, left: 6, bottom: 2 })
  711. .borderRadius(4)
  712. .margin({ top: 8, left: 8 })
  713. .backgroundColor( '#FFC107')
  714. .visibility(song.md5Str ? Visibility.Visible : Visibility.None)
  715. }
  716. }
  717. .width('100%')
  718. // 歌手和专辑信息 - 参考LocalMusic的第二行布局
  719. Row() {
  720. Text(song.artist)
  721. .fontSize(13)
  722. .fontColor(this.isPlaying && this.curIndex === index ?this.themeColor : $r('app.color.text_color'))
  723. .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
  724. .maxLines(1)
  725. .textOverflow({ overflow: TextOverflow.Ellipsis })
  726. .layoutWeight(1)
  727. .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
  728. Text(song.album)
  729. .fontSize(13)
  730. .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
  731. .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
  732. .maxLines(1)
  733. .textOverflow({ overflow: TextOverflow.Ellipsis })
  734. .layoutWeight(1)
  735. .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
  736. // 时长显示
  737. if (song.duration && typeof song.duration === 'number' && song.duration > 0) {
  738. Text(this.formatDuration(song.duration))
  739. .fontSize(13)
  740. .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
  741. .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
  742. .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.7)
  743. .margin({ right: 20 })
  744. }
  745. }
  746. .width('100%')
  747. .margin({ left: 12, top: 4 })
  748. .alignItems(VerticalAlign.Center)
  749. }
  750. .layoutWeight(1)
  751. .alignItems(HorizontalAlign.Start)
  752. .justifyContent(FlexAlign.Center)
  753. // 排序模式下显示上下移动按钮,否则显示更多按钮
  754. if (this.isSortMode) {
  755. Row({ space: 8 }) {
  756. // 上移按钮
  757. Text('▲')
  758. .fontSize(16)
  759. .fontColor(index === 0 ? '#cccccc' : this.themeColor)
  760. .onClick(() => {
  761. if (index > 0) {
  762. this.moveSong(index, index - 1)
  763. }
  764. })
  765. .enabled(index > 0)
  766. // 下移按钮
  767. Text('▼')
  768. .fontSize(16)
  769. .fontColor(index === this.songList.length - 1 ? '#cccccc' : this.themeColor)
  770. .onClick(() => {
  771. if (index < this.songList.length - 1) {
  772. this.moveSong(index, index + 1)
  773. }
  774. })
  775. .enabled(index < this.songList.length - 1)
  776. }
  777. .margin({ right: 15 })
  778. } else {
  779. Text('⋯')
  780. .fontSize(20)
  781. .fontColor($r('app.color.text_color'))
  782. .opacity(0.6)
  783. .margin({ right: 15 })
  784. .onClick(() => {
  785. this.showSongMenu(song)
  786. })
  787. }
  788. }
  789. .width('100%')
  790. .height(70)
  791. .justifyContent(FlexAlign.Start)
  792. .alignItems(VerticalAlign.Center)
  793. }
  794. .backgroundColor(Color.Transparent)
  795. .height(70)
  796. .width('100%')
  797. .onClick(() => {
  798. // 排序模式下禁用点击播放
  799. if (!this.isSortMode) {
  800. this.playSong(song, index)
  801. }
  802. })
  803. .gesture(
  804. LongPressGesture()
  805. .onAction(() => {
  806. // 排序模式下不显示菜单
  807. if (!this.isSortMode) {
  808. this.showSongMenu(song)
  809. }
  810. })
  811. )
  812. .stateStyles({
  813. normal: {
  814. .backgroundColor(Color.Transparent)
  815. },
  816. pressed: {
  817. .backgroundColor(this.isSortMode ? Color.Transparent : '#f1f3f5')
  818. }
  819. })
  820. // 当前播放歌曲的背景高亮
  821. .backgroundColor(this.isPlaying && this.curIndex === index ? '#f0f8ff' : Color.Transparent)
  822. .border({
  823. width: { left: this.isPlaying && this.curIndex === index ? 3 : 0 },
  824. color: this.themeColor
  825. })
  826. .opacity(this.pageOpacity)
  827. .translate({ x: 0, y: this.showContent ? 0 : 20 })
  828. .transition(TransitionEffect.OPACITY.animation({
  829. duration: 600,
  830. curve: Curve.EaseOut,
  831. delay: 300 + index * 30
  832. }))
  833. .transition(TransitionEffect.translate({ y: 20 }).animation({
  834. duration: 600,
  835. curve: Curve.EaseOut,
  836. delay: 300 + index * 30
  837. }))
  838. }
  839. })
  840. }
  841. .width('100%')
  842. .layoutWeight(1)
  843. .backgroundColor($r('app.color.bg_card'))
  844. .margin({ left: 16, right: 16 })
  845. .borderRadius(12)
  846. .divider({
  847. strokeWidth: 0.5,
  848. color: '#0000001a',
  849. startMargin: 80,
  850. endMargin: 20
  851. })
  852. .opacity(this.pageOpacity)
  853. .scale({ x: this.contentScale, y: this.contentScale })
  854. .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
  855. .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
  856. } else {
  857. // 空状态
  858. Column() {
  859. // 空状态容器
  860. Column() {
  861. // 空状态图标容器
  862. Stack() {
  863. // 背景圆圈
  864. Circle({ width: 160, height: 160 })
  865. .fill('#0a59f715')
  866. .border({
  867. width: 2,
  868. color: '#0a59f71a'
  869. })
  870. // 中间圆圈
  871. Circle({ width: 120, height: 120 })
  872. .fill('#0a59f722')
  873. // 音符图标
  874. Image($r('app.media.music_red'))
  875. .width(64)
  876. .height(64)
  877. .opacity(0.6)
  878. .fillColor(this.themeColor )
  879. }
  880. .margin({ bottom: 32 })
  881. // 空状态标题
  882. Text('歌单还是空的')
  883. .fontSize(22)
  884. .fontColor($r('app.color.text_color'))
  885. .fontWeight(FontWeight.Bold)
  886. .margin({ bottom: 12 })
  887. .letterSpacing(0.5)
  888. // 空状态描述
  889. Text('快来添加你喜欢的音乐吧\n让这个歌单充满美妙的旋律')
  890. .fontSize(15)
  891. .fontColor($r('app.color.text_color'))
  892. .opacity(0.8)
  893. .margin({ bottom: 40 })
  894. .textAlign(TextAlign.Center)
  895. .lineHeight(24)
  896. .maxLines(2)
  897. // 添加歌曲按钮
  898. Button() {
  899. Row({ space: 8 }) {
  900. Text('+')
  901. .fontSize(20)
  902. .fontColor(Color.White)
  903. Text('添加歌曲')
  904. .fontSize(16)
  905. .fontColor(Color.White)
  906. .fontWeight(FontWeight.Medium)
  907. }
  908. .justifyContent(FlexAlign.Center)
  909. }
  910. .width(160)
  911. .height(48)
  912. .backgroundColor(this.themeColor )
  913. .borderRadius(24)
  914. .shadow({
  915. radius: 12,
  916. color: '#0a59f74d',
  917. offsetX: 0,
  918. offsetY: 6
  919. })
  920. .onClick(() => {
  921. showAddSongsToPlaylistDialog(
  922. this.playlist!,
  923. async (songs: VideoItem[]) => {
  924. // 添加选中的歌曲到歌单
  925. const success = await this.playlistTable.addSongsToPlaylist(this.playlistId, songs.map(song => song.filePath))
  926. if (success) {
  927. ToastUtil.showToast(`成功添加 ${songs.length} 首歌曲`)
  928. // 重新加载歌单详情
  929. this.loadPlaylistDetail()
  930. } else {
  931. ToastUtil.showToast('添加歌曲失败')
  932. }
  933. }
  934. )
  935. })
  936. .stateStyles({
  937. normal: {
  938. .backgroundColor(this.themeColor )
  939. .scale({ x: 1, y: 1 })
  940. },
  941. pressed: {
  942. .backgroundColor('#0a59f7cc')
  943. .scale({ x: 0.96, y: 0.96 })
  944. }
  945. })
  946. .animation({
  947. duration: 200,
  948. curve: Curve.EaseInOut
  949. })
  950. // 快速操作提示
  951. Text('或长按歌单选择更多操作')
  952. .fontSize(13)
  953. .fontColor($r('app.color.text_color'))
  954. .margin({ top: 16 })
  955. .opacity(0.7)
  956. }
  957. .width('100%')
  958. .padding(32)
  959. .backgroundColor($r('app.color.bg_card'))
  960. .borderRadius(20)
  961. .margin({ left: 16, right: 16 })
  962. .shadow({
  963. radius: 16,
  964. color: '#0000000f',
  965. offsetX: 0,
  966. offsetY: 8
  967. })
  968. .opacity(this.pageOpacity)
  969. .scale({ x: this.contentScale, y: this.contentScale })
  970. .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
  971. .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
  972. }
  973. .layoutWeight(1)
  974. .margin({ left: 16, right: 16 })
  975. .justifyContent(FlexAlign.Center)
  976. .padding({ top: 20, bottom: 20 })
  977. }
  978. }
  979. }
  980. .width('100%')
  981. .height('100%')
  982. .backgroundColor($r('app.color.index_background'))
  983. }
  984. /**
  985. * 显示更多菜单
  986. */
  987. showMoreMenu() {
  988. // 简单直接的二选一,避免复杂的多级菜单
  989. AlertDialog.show({
  990. title: '歌单操作',
  991. message: '🎵 添加歌曲:向歌单添加新音乐\n✏️ 编辑歌单:修改歌单信息\n\n点击"确定"添加歌曲,点击"取消"编辑歌单',
  992. primaryButton: {
  993. value: '取消 (编辑)',
  994. action: () => {
  995. this.editPlaylist()
  996. }
  997. },
  998. secondaryButton: {
  999. value: '确定 (添加)',
  1000. action: () => {
  1001. this.addSongsToPlaylist()
  1002. }
  1003. }
  1004. })
  1005. }
  1006. /**
  1007. * 添加歌曲到歌单
  1008. */
  1009. addSongsToPlaylist() {
  1010. if (this.playlist) {
  1011. showAddSongsToPlaylistDialog(
  1012. this.playlist,
  1013. async (songs: VideoItem[]) => {
  1014. // 添加选中的歌曲到歌单
  1015. const success = await this.playlistTable.addSongsToPlaylist(this.playlistId, songs.map(song => song.filePath))
  1016. if (success) {
  1017. ToastUtil.showToast(`成功添加 ${songs.length} 首歌曲`)
  1018. // 重新加载歌单详情
  1019. this.loadPlaylistDetail()
  1020. } else {
  1021. ToastUtil.showToast('添加歌曲失败')
  1022. }
  1023. }
  1024. )
  1025. }
  1026. }
  1027. /**
  1028. * 显示歌曲菜单
  1029. */
  1030. showSongMenu(song: VideoItem) {
  1031. AlertDialog.show({
  1032. title: song.name,
  1033. message: '',
  1034. primaryButton: {
  1035. value: '取消',
  1036. action: () => {}
  1037. },
  1038. secondaryButton: {
  1039. value: '从歌单移除',
  1040. fontColor: Color.Red,
  1041. action: () => {
  1042. this.removeSongFromPlaylist(song)
  1043. }
  1044. }
  1045. })
  1046. }
  1047. /**
  1048. * 进入排序模式
  1049. */
  1050. enterSortMode() {
  1051. this.isSortMode = true
  1052. ToastUtil.showToast('点击上下箭头调整歌曲顺序')
  1053. LogUtil.info('heanup 进入排序模式')
  1054. }
  1055. /**
  1056. * 退出排序模式
  1057. */
  1058. exitSortMode() {
  1059. this.isSortMode = false
  1060. ToastUtil.showToast('排序已保存')
  1061. LogUtil.info('heanup 退出排序模式')
  1062. }
  1063. /**
  1064. * 移动歌曲位置
  1065. */
  1066. async moveSong(fromIndex: number, toIndex: number) {
  1067. if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 ||
  1068. fromIndex >= this.songList.length || toIndex >= this.songList.length) {
  1069. return
  1070. }
  1071. LogUtil.info(`heanup 移动歌曲: from ${fromIndex} to ${toIndex}`)
  1072. // 1. 在本地数组中移动
  1073. const movedSong = this.songList.splice(fromIndex, 1)[0]
  1074. this.songList.splice(toIndex, 0, movedSong)
  1075. // 2. 触发UI更新
  1076. this.songList = [...this.songList]
  1077. // 3. 更新数据库中的sortOrder
  1078. await this.updateAllSongsSortOrder()
  1079. }
  1080. /**
  1081. * 更新所有歌曲的sortOrder到数据库
  1082. */
  1083. async updateAllSongsSortOrder() {
  1084. try {
  1085. LogUtil.info('heanup 开始更新所有歌曲的sortOrder')
  1086. for (let i = 0; i < this.songList.length; i++) {
  1087. const song = this.songList[i]
  1088. const success = await this.playlistTable.updatePlaylistSongSortOrder(
  1089. this.playlistId,
  1090. song.filePath,
  1091. i
  1092. )
  1093. if (success) {
  1094. LogUtil.info(`heanup 更新歌曲[${i}] ${song.name} sortOrder成功`)
  1095. } else {
  1096. LogUtil.error(`heanup 更新歌曲[${i}] ${song.name} sortOrder失败`)
  1097. }
  1098. }
  1099. LogUtil.info('heanup 所有歌曲sortOrder更新完成')
  1100. } catch (error) {
  1101. LogUtil.error('heanup 更新歌曲sortOrder失败: ' + error)
  1102. }
  1103. }
  1104. }
  1105. /**
  1106. * 将 PlaylistSong 转换为 VideoItem
  1107. */
  1108. export async function convertPlaylistSongsToVideoItems(context: Context,playlistSongs: PlaylistSong[]): Promise<VideoItem[]> {
  1109. const videoItems: VideoItem[] = []
  1110. LogUtil.info(`heanup 开始转换歌曲,共 ${playlistSongs.length} 首`)
  1111. const mediaTable: MediaTable = new MediaTable(context)
  1112. await new Promise<void>((resolve, reject) => {
  1113. mediaTable.getRdbStore(context, (err:Error) => {
  1114. err ? reject(err) : resolve();
  1115. });
  1116. });
  1117. for (const playlistSong of playlistSongs) {
  1118. try {
  1119. LogUtil.info(`heanup 查询歌曲: ${playlistSong.songFilePath}`)
  1120. // 从数据库查询完整的歌曲信息
  1121. const videoItem = await mediaTable.queryVideoByFilePath(playlistSong.songFilePath)
  1122. if (videoItem) {
  1123. LogUtil.info(`heanup 找到歌曲: ${videoItem.name}`)
  1124. videoItems.push(videoItem)
  1125. } else {
  1126. LogUtil.warn(`heanup 数据库中未找到歌曲: ${playlistSong.songFilePath}`)
  1127. // 如果数据库中没有,创建一个基本的 VideoItem
  1128. const fileName = playlistSong.songFilePath.split('/').pop() || ''
  1129. const name = fileName.replace(/\.[^/.]+$/, "") // 移除文件扩展名
  1130. const basicVideoItem = new VideoItem(
  1131. name, // name
  1132. Date.now().toString() + Math.random(), // id
  1133. playlistSong.songFilePath, // filePath
  1134. 0, // type (音乐类型)
  1135. 0, // videoSize
  1136. playlistSong.addTime, // cTime
  1137. undefined, // pixelMap
  1138. undefined, // size
  1139. undefined, // pixelMapPath
  1140. undefined, // artist
  1141. undefined, // album
  1142. fileName, // fileName
  1143. undefined // lastPlayed
  1144. )
  1145. videoItems.push(basicVideoItem)
  1146. }
  1147. } catch (error) {
  1148. LogUtil.error('heanup 转换歌曲失败: ' + error)
  1149. }
  1150. }
  1151. LogUtil.info(`heanup 转换完成,共 ${videoItems.length} 首歌曲`)
  1152. return videoItems
  1153. }