Parcourir la source

继续优化一些播放细节
增加发现页的心动歌单页面

onecold il y a 4 mois
Parent
commit
80eef47b84

+ 51 - 0
entry/src/main/ets/common/util/PlaylistPlayRequestStore.ets

@@ -0,0 +1,51 @@
+export class PlaylistPlayRequest {
+  playlistId: string;
+  playlistName: string;
+  songCount: number;
+  startIndex: number;
+  songFilePaths: string[];
+  isJump?: boolean;
+  playType?: number;
+
+  constructor(playlistId: string, playlistName: string, songCount: number, startIndex: number, songFilePaths: string[],
+    isJump?: boolean, playType?: number) {
+    this.playlistId = playlistId;
+    this.playlistName = playlistName;
+    this.songCount = songCount;
+    this.startIndex = startIndex;
+    this.songFilePaths = songFilePaths;
+    this.isJump = isJump;
+    this.playType = playType;
+  }
+}
+
+let pendingPlaylistPlayRequest: PlaylistPlayRequest | undefined = undefined;
+
+function clonePlaylistPlayRequest(request: PlaylistPlayRequest): PlaylistPlayRequest {
+  return new PlaylistPlayRequest(
+    request.playlistId,
+    request.playlistName,
+    request.songCount,
+    request.startIndex,
+    request.songFilePaths ? request.songFilePaths.slice() : [],
+    request.isJump,
+    request.playType
+  );
+}
+
+export function savePendingPlaylistPlay(request: PlaylistPlayRequest): void {
+  pendingPlaylistPlayRequest = clonePlaylistPlayRequest(request);
+}
+
+export function consumePendingPlaylistPlay(): PlaylistPlayRequest | undefined {
+  if (!pendingPlaylistPlayRequest) {
+    return undefined;
+  }
+  const request = clonePlaylistPlayRequest(pendingPlaylistPlayRequest);
+  pendingPlaylistPlayRequest = undefined;
+  return request;
+}
+
+export function clearPendingPlaylistPlay(): void {
+  pendingPlaylistPlayRequest = undefined;
+}

+ 48 - 0
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -3105,6 +3105,30 @@ export class RemoteDriveManager {
     return regex.test(value.toLowerCase());
   }
 
+  private buildDiscoverySeedSongs(source: VideoItem[], limitCount: number): VideoItem[] {
+    if (!source || source.length === 0 || limitCount <= 0) {
+      return [];
+    }
+    const deduped: VideoItem[] = [];
+    const seen: Set<string> = new Set<string>();
+    for (let index = 0; index < source.length; index += 1) {
+      const item = source[index];
+      const key = item.remote_rel_path || item.filePath || item.id || '';
+      if (key.length === 0 || seen.has(key)) {
+        continue;
+      }
+      seen.add(key);
+      deduped.push(this.cloneDirectoryPreviewSong(item));
+    }
+    for (let index = deduped.length - 1; index > 0; index -= 1) {
+      const randomIndex = Math.floor(Math.random() * (index + 1));
+      const current = deduped[index];
+      deduped[index] = deduped[randomIndex];
+      deduped[randomIndex] = current;
+    }
+    return deduped.slice(0, Math.min(limitCount, deduped.length));
+  }
+
   private getBreadcrumbsForPath(account: WebDavAccount, targetPath: string): BreadcrumbItem[] {
     const basePath = this.normalizeSearchPathForAccount(account, this.getBasePath(account));
     const normalizedTarget = this.normalizeSearchPathForAccount(account, targetPath);
@@ -3250,6 +3274,30 @@ export class RemoteDriveManager {
     await buildPromise;
   }
 
+  public async getDiscoverySeedSongs(account: WebDavAccount, limitCount: number): Promise<VideoItem[]> {
+    if (!account || limitCount <= 0) {
+      return [];
+    }
+    if (this.supportsGlobalSearchIndex(account)) {
+      const index = this.getOrCreateGlobalSearchIndex(account);
+      if (index.songs.length === 0 || index.lastBuiltAt === 0) {
+        await this.ensureGlobalSearchIndex(account);
+      }
+      return this.buildDiscoverySeedSongs(index.songs, limitCount);
+    }
+    const accountId = account.id ? account.id.toString() : '';
+    const currentSongs = this.webDavSongs.filter((song: VideoItem) => {
+      if (!song || song.type !== account.webType) {
+        return false;
+      }
+      if (accountId.length === 0) {
+        return true;
+      }
+      return song.webdav_account_id === accountId;
+    });
+    return this.buildDiscoverySeedSongs(currentSongs, limitCount);
+  }
+
   private async buildGlobalSearchIndex(account: WebDavAccount, index: RemoteDriveGlobalSearchIndex,
     buildVersion: number): Promise<void> {
     switch (account.webType) {

+ 23 - 2
entry/src/main/ets/common/util/Utility.ets

@@ -1232,6 +1232,24 @@ export class Utility {
 
 
   //新的查询是否收藏的方法
+  static getFavMatchKey(item: VideoItem | undefined): string {
+    if (item === undefined) {
+      return ''
+    }
+    if (StrUtil.isNotEmpty(item.remote_rel_path)) {
+      return `remote:${item.remote_rel_path}`
+    }
+    const filePath = item.filePath || ''
+    if (filePath.length === 0) {
+      return ''
+    }
+    const httpMatch = filePath.match(/^https?:\/\/[^\/]+(?::\d+)?(\/.*)$/i)
+    if (httpMatch && httpMatch[1]) {
+      return `remote:${httpMatch[1]}`
+    }
+    return `path:${filePath}`
+  }
+
   static  getIsFav(favList: Array<VideoItem>,item:VideoItem|undefined){
     if(ArrayUtil.isEmpty(favList)){
       return false
@@ -1239,9 +1257,12 @@ export class Utility {
     if(item===undefined){
       return false
     }
+    const targetKey = Utility.getFavMatchKey(item)
+    if (targetKey.length === 0) {
+      return false
+    }
     for(let i=0;i<favList.length;i++){
-
-      if(favList[i].filePath === item.filePath&&favList[i].isFav===1){
+      if(Utility.getFavMatchKey(favList[i]) === targetKey && favList[i].isFav===1){
 
         return true
       }

+ 61 - 25
entry/src/main/ets/pages/NewIndex.ets

@@ -200,7 +200,9 @@ struct NewIndex {
   onBackPress(): boolean | void {
     console.info('onecold onBackPress isDetailView = '+ this.isDetailView);
     console.info('onecold onBackPress mType = '+  this.mType);
-    if (this.currentPath !== this.rootPath || this.isHistory || this.isFavMusic ||
+    if (this.mType === 4) {
+      this.returnToConfiguredHome()
+    } else if (this.currentPath !== this.rootPath || this.isHistory || this.isFavMusic ||
       (this.modeType !== 0 && this.isCanBack)) {
       console.info('onecold 返回键处理逻辑:发送音频广播通知更新列表');
       const eventData: emitter.EventData = {};
@@ -354,6 +356,10 @@ struct NewIndex {
 
     });
 
+    emitter.on({ eventId: EventConstants.EVENT_SETTING_BACK_TO_HOME }, () => {
+      this.returnToConfiguredHome()
+    });
+
     this.logHiCar('info', `getHiCarStatus isHiCarStatus=${this.isHiCarStatus}`)
     if(this.curDisplayIsHiCar||this.isBigScreen()){
       this.getUIContext()?.animateTo({ duration: 555 }, () => {
@@ -388,29 +394,54 @@ struct NewIndex {
   }
 
   private initDefalutType(){
-    if(this.defalut_home_type==0){
-      this.mType = 0
-    }else if(this.defalut_home_type==1){
+    this.applyDefaultHomeState()
+    console.info('onecold 网盘 默认首页2 this.mType='+this.mType )
+  }
+
+  private isRemoteMusicAccount(account: WebDavAccount | null | undefined): boolean {
+    if (!account) {
+      return false
+    }
+    return account.webType === RemoteDriveType.Navidrome
+      || account.webType === RemoteDriveType.Jellyfin
+      || account.webType === RemoteDriveType.Emby
+      || account.webType === RemoteDriveType.AudioStation
+      || account.webType === RemoteDriveType.Plex
+      || account.webType === RemoteDriveType.DaoLiYu
+  }
+
+  private getConfiguredHomeAccount(): WebDavAccount | null {
+    const activatedAccount = this.webdavManager.getActivatedWebDavAccount()
+    if (activatedAccount) {
+      return activatedAccount
+    }
+    if (this.selectedAccount && (this.selectedAccount.id > 0 || this.selectedAccount.host.length > 0)) {
+      return this.selectedAccount
+    }
+    if (this.webDavAccounts.length > 0) {
+      return this.webDavAccounts[0]
+    }
+    return null
+  }
+
+  private applyDefaultHomeState(): void {
+    this.defalut_home_type = PreferencesUtil.getNumberSync('defalut_home_type', 0)
+    const configuredAccount = this.getConfiguredHomeAccount()
+
+    this.currentSongListID = ''
+    this.currentSongListName = ''
+    this.modeType = 0
+    this.tabSelectedIndexes = [0]
+
+    if(this.defalut_home_type==1){
       this.mType = 0
       this.modeType = 1
     }else if(this.defalut_home_type==2){//如果是网盘的话
-      if(this.webDavAccounts.length>0){
-        console.info('onecold 网盘 默认首页2 this.webDavAccounts[0].webType='+this.webDavAccounts[0].webType )
-        if(this.webDavAccounts[0].webType===RemoteDriveType.Navidrome||
-          this.webDavAccounts[0].webType===RemoteDriveType.Jellyfin||
-          this.webDavAccounts[0].webType===RemoteDriveType.Emby||
-          this.webDavAccounts[0].webType===RemoteDriveType.DaoLiYu){
-          this.mType = 7
-        }else{
-          this.mType = 6
-        }
-      }else{
-        this.mType = 6
-
+      this.tabSelectedIndexes = [2]
+      if (configuredAccount) {
+        this.selectedAccount = configuredAccount
       }
-      this.selectedAccount = new WebDavAccount()
-      this.selectWebDavAccount(this.webDavAccounts[0])
-      this.tabSelectedIndexes[0]=2
+      this.mType = this.isRemoteMusicAccount(configuredAccount) ? 7 : 6
     }else if(this.defalut_home_type==3){
       this.mType = 0
       this.modeType = 2
@@ -419,9 +450,15 @@ struct NewIndex {
       this.modeType = 3
     }else if(this.defalut_home_type==5){
       this.mType = 8
-      this.modeType = 0
+    } else {
+      this.mType = 0
     }
-    console.info('onecold 网盘 默认首页2 this.mType='+this.mType )
+  }
+
+  private returnToConfiguredHome(): void {
+    this.getUIContext()?.animateTo({ duration: 555 }, () => {
+      this.applyDefaultHomeState()
+    })
   }
 
 
@@ -453,6 +490,7 @@ struct NewIndex {
     this.breakpointSystem.unregister();
     emitter.off(EventConstants.EVENT_SWIPE_BACK_UPDATE);
     emitter.off(EventConstants.EVENT_USER_STATE_CHANGE);
+    emitter.off(EventConstants.EVENT_SETTING_BACK_TO_HOME);
 
     // 监听歌单刷新事件
     emitter.off(EventConstants.EVENT_PLAYLIST_REFRESH);
@@ -550,7 +588,7 @@ struct NewIndex {
         }
         .width('90%')
         .borderRadius(15)
-        .margin({ bottom:DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_2IN1
+        .margin({ bottom:DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_2IN1||this.curDisplayIsHiCar
           ? 30 :this.bottomSafeHeight })
         .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
         .backgroundImage(StrUtil.isEmpty(this.cover) ?undefined:this.cover)
@@ -2289,5 +2327,3 @@ interface HiCarAspectRatio {
   context: Context;
   playlistId: string;
 }
-
-

+ 25 - 34
entry/src/main/ets/pages/PlaylistDetailPage.ets

@@ -14,6 +14,7 @@ import { LazyDataSource } from '../common/util/LazyDataSource';
 import { ConfigurationConstant } from '@kit.AbilityKit';
 import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
 import { PlayingIndicator } from '../view/PlayingIndicator';
+import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
 
 // 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
 function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
@@ -28,17 +29,6 @@ function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: bool
   return `rgba(${r},${g},${b},${alpha})`;
 }
 
-/**
- * 歌单播放事件数据
- */
-interface PlaylistEventData {
-  playlistId: string;
-  playlistName: string;
-  songCount: number;
-  startIndex: number;
-  songFilePaths: string[];
-}
-
 /**
  * 歌单详情页面
  * 展示歌单信息和歌曲列表
@@ -423,14 +413,13 @@ export struct PlaylistDetailPage {
     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)
-    };
+    const playlistData = new PlaylistPlayRequest(
+      this.playlist?.id || '',
+      this.playlist?.name || '',
+      this.songList.length,
+      0,
+      this.songList.map(song => song.filePath)
+    );
 
     LogUtil.info(`heanup 发送歌单播放事件数据: ${JSON.stringify(playlistData)}`)
 
@@ -438,6 +427,7 @@ export struct PlaylistDetailPage {
       data: playlistData
     };
 
+    savePendingPlaylistPlay(playlistData)
     emitter.emit(eventPlaylistPlay, eventData)
 
     ToastUtil.showToast('开始播放歌单')
@@ -457,14 +447,13 @@ export struct PlaylistDetailPage {
 
     // 发送播放歌单事件
     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)
-    };
+    const playlistData = new PlaylistPlayRequest(
+      this.playlist?.id || '',
+      this.playlist?.name || '',
+      this.songList.length,
+      index,
+      this.songList.map(s => s.filePath)
+    );
 
     LogUtil.info(`heanup 准备发送事件,eventId: ${eventPlaylistPlay.eventId}`)
     LogUtil.info(`heanup 发送歌单播放事件数据: ${JSON.stringify(playlistData)}`)
@@ -474,6 +463,7 @@ export struct PlaylistDetailPage {
     };
 
     LogUtil.info(`heanup eventData对象: ${JSON.stringify(eventData)}`)
+    savePendingPlaylistPlay(playlistData)
     emitter.emit(eventPlaylistPlay, eventData)
   }
 
@@ -572,16 +562,17 @@ export struct PlaylistDetailPage {
           }
 
           // 更新播放列表(重新发送歌单播放事件,保持当前播放位置)
-          const playlistData: PlaylistEventData = {
-            playlistId: this.playlist.id,
-            playlistName: this.playlist.name,
-            songCount: this.songList.length,
-            startIndex: this.curIndex >= this.songList.length ? Math.max(0, this.songList.length - 1) : this.curIndex,
-            songFilePaths: this.songList.map(s => s.filePath)
-          }
+          const playlistData = new PlaylistPlayRequest(
+            this.playlist.id,
+            this.playlist.name,
+            this.songList.length,
+            this.curIndex >= this.songList.length ? Math.max(0, this.songList.length - 1) : this.curIndex,
+            this.songList.map(s => s.filePath)
+          )
 
           const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
           const eventData: emitter.EventData = { data: playlistData }
+          savePendingPlaylistPlay(playlistData)
           emitter.emit(eventPlaylistPlay, eventData)
           LogUtil.info('heanup 已更新播放列表,移除了被删除的歌曲')
         }

+ 2 - 2
entry/src/main/ets/pages/SettingPage.ets

@@ -297,7 +297,7 @@ export struct SettingPage {
     this.isShowHistory = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_HISTORY, true)
     this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false)
     this.customizeBgPath = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '')
-    this.isGridMusic = PreferencesUtil.getBooleanSync(SettingPage.IS_GRID_MUSIC, false)
+    this.isGridMusic = PreferencesUtil.getBooleanSync(SettingPage.IS_GRID_MUSIC, true)
     this.isScrollHide = PreferencesUtil.getBooleanSync(SettingPage.IS_SCROLL_HIDE, false)
     this.isSameTimePlay = PreferencesUtil.getBooleanSync(SettingPage.IS_SAMETIME_PLAY, false)
     this.isSavePlayMode = PreferencesUtil.getBooleanSync(SettingPage.iS_SAVE_PLAY_MODE, true)
@@ -2029,7 +2029,7 @@ export struct SettingPage {
               this.isBackupSheet = true;
             })
             .bindSheet($$this.isBackupSheet, this.backupSheetBuilder(), {
-              height: this.isLandscape ? '95%' : '85%',
+              height: '95%',
               dragBar: true,
               preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM,
               showClose: true,

+ 18 - 28
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -37,19 +37,7 @@ import { DownloadCenter } from '../view/DownloadCenter';
 import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { PlayingIndicator } from '../view/PlayingIndicator';
-
-/**
- * 歌单播放事件数据
- */
-interface PlaylistEventData {
-  playlistId: string;
-  playlistName: string;
-  songCount: number;
-  startIndex: number;
-  isJump: boolean;
-  songFilePaths: string[];
-  // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id
-}
+import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
 
 interface WebDavMetadataUpdatePayload {
   filePath: string;
@@ -1890,14 +1878,15 @@ export struct WebDavMainPage {
     globalWebdavCurrentPlayIndex = startIndex;
 
     const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
-    const playlistData: PlaylistEventData = {
-      playlistId: 'webdav-playlist',
-      playlistName: '下载中心',
-      songCount: playlist.length,
+    const playlistData = new PlaylistPlayRequest(
+      'webdav-playlist',
+      '下载中心',
+      playlist.length,
       startIndex,
-      isJump: true,
-      songFilePaths: playlist.map((item: VideoItem): string => item.filePath)
-    };
+      playlist.map((item: VideoItem): string => item.filePath),
+      true
+    );
+    savePendingPlaylistPlay(playlistData);
     emitter.emit(eventPlaylistPlay, { data: playlistData });
 
 
@@ -2536,14 +2525,14 @@ export struct WebDavMainPage {
       // 直接通过事件传递videoItems数据,不使用GlobalContext
       const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
 
-      const playlistData: PlaylistEventData = {
-        playlistId: 'webdav-playlist', // 使用特殊的ID标识网盘播放列表
-        playlistName: `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
-        songCount: videoItems.length,
-        startIndex: playIndex,
-        isJump: isJump,//设置true会弹出播放页
-        songFilePaths: songFilePaths
-      };
+      const playlistData = new PlaylistPlayRequest(
+        'webdav-playlist',
+        `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
+        videoItems.length,
+        playIndex,
+        songFilePaths,
+        isJump
+      );
 
       // 保存videoItems到全局内存
       globalWebdavVideoItems = videoItems;
@@ -2552,6 +2541,7 @@ export struct WebDavMainPage {
       Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${playIndex}`);
 
       // 发送播放请求事件,只传递索引信息
+      savePendingPlaylistPlay(playlistData);
       emitter.emit(eventPlaylistPlay, { data: playlistData });
 
       Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${playIndex}`);

+ 337 - 23
entry/src/main/ets/view/FindView.ets

@@ -1,13 +1,18 @@
 import { PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'
 import { BusinessError, emitter } from '@kit.BasicServicesKit'
+import { common } from '@kit.AbilityKit'
 import { CommonConstants } from '../common/constants/CommonConstants'
 import { EventConstants } from '../common/constants/EventConstants'
 import MediaTable from '../common/util/MediaTable'
 import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'
 import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem'
 import Logger from '../common/util/Logger'
+import { RemoteDriveManager } from '../common/util/RemoteDriveManager'
+import { cloneVideoItem } from '../common/util/RemotePlayerUtil'
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil'
 import { VideoItem } from '../viewmodel/VideoItem'
+import { WebDavAccount } from '../viewmodel/WebDavAccount'
+import { Playlist } from '../viewmodel/Playlist'
 import { ConfigTitle } from './ConfigTitle'
 import { PointLightActionButton } from './PointLight/PointLightActionButton'
 import { PointLightContentButton } from './PointLight/PointLightContentButton'
@@ -15,6 +20,9 @@ import { SettingPage } from '../pages/SettingPage'
 import { FindAlbumDetail } from './FindAlbumDetail'
 import { PlayingIndicator } from './PlayingIndicator'
 import { setFindPlaylist } from '../common/util/FindPlaylistStore'
+import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore'
+import PlaylistTable from '../common/util/PlaylistTable'
+import { convertPlaylistSongsToVideoItems } from '../pages/PlaylistDetailPage'
 
 @Builder
 export function FindViewBuilder() {
@@ -48,16 +56,6 @@ function getFindSongKey(item: VideoItem, index: number): string {
   return `find_song_${index}`
 }
 
-interface FindPlaylistEventData {
-  playlistId: string
-  playlistName: string
-  songCount: number
-  startIndex: number
-  isJump: boolean
-  songFilePaths: string[]
-  playType?: number
-}
-
 interface FindAlbumGroup {
   id: string
   title: string
@@ -93,6 +91,13 @@ function getFindSongPageKey(item: FindSongPage, index: number): string {
   return 'find_song_page_' + index
 }
 
+function getFindPlaylistKey(item: Playlist, index: number): string {
+  if (StrUtil.isNotEmpty(item.id)) {
+    return item.id
+  }
+  return 'find_playlist_' + index
+}
+
 function getFindAlbumPageKey(item: FindAlbumPage, index: number): string {
   if (StrUtil.isNotEmpty(item.id)) {
     return item.id
@@ -123,6 +128,7 @@ export struct FindView {
   @State private popularSectionPages: FindSongPage[] = []
   @State private favoriteSongs: VideoItem[] = []
   @State private cloudMoodSongs: VideoItem[] = []
+  @State private heartPlaylists: Playlist[] = []
   @State private featuredAlbums: FindAlbumGroup[] = []
   @State private featuredAlbumPages: FindAlbumPage[] = []
   @State private cloudAlbums: FindAlbumGroup[] = []
@@ -163,6 +169,8 @@ export struct FindView {
   @StorageLink('isPlaying') isPlaying: boolean = false
 
   private mediaTable?: MediaTable
+  private playlistTable?: PlaylistTable
+  private remoteDriveManager: RemoteDriveManager = RemoteDriveManager.getInstance()
   private localSongsPool: VideoItem[] = []
   private coveredSongsPool: VideoItem[] = []
   private remoteSongsPool: VideoItem[] = []
@@ -178,6 +186,9 @@ export struct FindView {
   private featuredAlbumPageVersion: number = 0
   private cloudAlbumPageVersion: number = 0
   private popularSectionPageVersion: number = 0
+  private isRemoteBootstrapLoading: boolean = false
+  private playlistSongsCache: Map<string, VideoItem[]> = new Map<string, VideoItem[]>()
+  private heartPlaylistCoverTicket: number = 0
 
   aboutToAppear(): void {
     this.initSetting()
@@ -213,6 +224,13 @@ export struct FindView {
   }
 
   private ensureMediaTable(): void {
+    if (!this.playlistTable) {
+      try {
+        this.playlistTable = new PlaylistTable(getContext(this) as common.Context)
+      } catch (error) {
+        Logger.warn(TAG, `初始化歌单数据库失败: ${this.toErrorMessage(error as Object)}`)
+      }
+    }
     if (this.mediaTable) {
       void this.loadDiscoveryContent(false)
       return
@@ -250,39 +268,44 @@ export struct FindView {
         this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT),
         this.mediaTable.queryRecentPlayedRecordsAsync(RECENT_POOL_COUNT),
         this.queryTopPlayedSongs(TOP_PLAYED_COUNT),
-        this.queryFavoriteSongs()
+        this.queryFavoriteSongs(),
+        this.queryPlaylists()
       ])
       const remoteSongs = requestResults[0] as VideoItem[]
       const recentSongs = requestResults[1] as VideoItem[]
       const topPlayedSongs = requestResults[2] as VideoItem[]
       const favoriteSongs = requestResults[3] as VideoItem[]
+      const playlists = requestResults[4] as Playlist[]
       const uniqueRemoteSongs = this.filterUniqueSongs(remoteSongs)
       const uniqueRecentSongs = this.filterUniqueSongs(recentSongs)
       const uniqueTopPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
       const uniqueFavoriteSongs = this.filterUniqueSongs(favoriteSongs)
 
+      this.playlistSongsCache.clear()
       this.localSongsPool = uniqueLocalSongs
       this.coveredSongsPool = coveredSongs
-      this.remoteSongsPool = uniqueRemoteSongs
+      this.applyRemoteDiscoverySongs(uniqueRemoteSongs)
       this.recentSongsPool = uniqueRecentSongs
       this.topPlayedSongsPool = uniqueTopPlayedSongs
       this.favoriteSongsPool = uniqueFavoriteSongs
+      this.heartPlaylists = playlists
       this.featuredAlbumsPool = this.buildAlbumGroups(this.localSongsPool, false)
-      this.cloudAlbumsPool = this.buildAlbumGroups(this.remoteSongsPool, true)
       this.searchRemoteSongsPool = []
       this.swiperSongs = this.pickRandomSongs(this.coveredSongsPool, SWIPER_MAX_COUNT)
       this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
-      this.cloudMoodSongs = this.pickPreferredSongs(this.remoteSongsPool, HOT_SECTION_COUNT)
-      this.remoteSongs = this.pickPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT)
       this.recentSongs = this.pickPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT)
       this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
       this.favoriteSongs = this.pickPreferredSongs(this.favoriteSongsPool, FAVORITE_SECTION_COUNT)
       this.featuredAlbums = this.pickAlbumGroups(this.featuredAlbumsPool, FEATURED_ALBUM_COUNT)
-      this.cloudAlbums = this.pickAlbumGroups(this.cloudAlbumsPool, CLOUD_ALBUM_COUNT)
       this.swiperIndex = 0
       this.resetPagedSectionIndices()
       this.rebuildPagedSectionSources()
       this.refreshText = ''
+      this.heartPlaylistCoverTicket += 1
+      void this.resolveHeartPlaylistCovers(this.heartPlaylistCoverTicket, playlists)
+      if (uniqueRemoteSongs.length === 0) {
+        void this.bootstrapRemoteDiscoverySongsIfNeeded()
+      }
 
       Logger.info(
         TAG,
@@ -300,6 +323,7 @@ export struct FindView {
       this.popularSongs = []
       this.popularSectionPages = []
       this.favoriteSongs = []
+      this.heartPlaylists = []
       this.localSongsPool = []
       this.coveredSongsPool = []
       this.remoteSongsPool = []
@@ -320,6 +344,103 @@ export struct FindView {
     }
   }
 
+  private async queryPlaylists(): Promise<Playlist[]> {
+    if (!this.playlistTable) {
+      return []
+    }
+    try {
+      const playlists = await this.playlistTable.queryAllPlaylists()
+      return playlists.filter((item: Playlist) => StrUtil.isNotEmpty(item.id) && StrUtil.isNotEmpty(item.name))
+    } catch (error) {
+      Logger.warn(TAG, `发现页查询歌单失败: ${this.toErrorMessage(error as Object)}`)
+      return []
+    }
+  }
+
+  private applyRemoteDiscoverySongs(items: VideoItem[], refreshSections: boolean = false): void {
+    this.remoteSongsPool = items
+    this.searchRemoteSongsPool = items.slice()
+    this.cloudAlbumsPool = this.buildAlbumGroups(this.remoteSongsPool, true)
+    this.cloudMoodSongs = this.pickPreferredSongs(this.remoteSongsPool, HOT_SECTION_COUNT)
+    this.remoteSongs = this.pickPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT)
+    this.cloudAlbums = this.pickAlbumGroups(this.cloudAlbumsPool, CLOUD_ALBUM_COUNT)
+    if (refreshSections) {
+      this.cloudSectionPageIndex = 0
+      this.cloudAlbumPageIndex = 0
+      this.updateCloudSectionPages()
+      this.updateCloudAlbumPages()
+    }
+  }
+
+  private async ensureRemoteDiscoverySongsAvailable(): Promise<void> {
+    if (!this.mediaTable) {
+      return
+    }
+    if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.searchRemoteSongsPool.length > 0) {
+      return
+    }
+    const remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
+    if (remoteSongs.length > 0) {
+      this.applyRemoteDiscoverySongs(remoteSongs, true)
+      return
+    }
+    await this.bootstrapRemoteDiscoverySongsIfNeeded()
+  }
+
+  private async bootstrapRemoteDiscoverySongsIfNeeded(): Promise<void> {
+    if (this.isRemoteBootstrapLoading || !this.mediaTable || this.remoteSongsPool.length > 0) {
+      return
+    }
+    this.isRemoteBootstrapLoading = true
+    try {
+      const context = getContext(this) as common.Context
+      this.remoteDriveManager.setContext(context)
+      await this.remoteDriveManager.createWebDavTableInDB()
+      await this.remoteDriveManager.queryWebDavAccountsFromDB()
+      const accounts: WebDavAccount[] = this.remoteDriveManager.getAllWebDavAccounts()
+      if (accounts.length === 0) {
+        return
+      }
+      const account = this.remoteDriveManager.getActivatedWebDavAccount() || accounts[0]
+      if (!account) {
+        return
+      }
+      Logger.info(TAG, `发现页开始预热远程歌曲: account=${account.name}, type=${account.webType}`)
+      const seedSongs = await this.remoteDriveManager.getDiscoverySeedSongs(account, REMOTE_POOL_COUNT)
+      if (seedSongs.length === 0) {
+        Logger.info(TAG, `发现页远程预热未拿到歌曲: account=${account.name}`)
+        return
+      }
+      let savedCount = 0
+      for (let index = 0; index < seedSongs.length; index += 1) {
+        const seedSong = cloneVideoItem(seedSongs[index])
+        if (!seedSong.webdav_account_id && account.id) {
+          seedSong.webdav_account_id = account.id.toString()
+        }
+        if (StrUtil.isEmpty(seedSong.id)) {
+          seedSong.id = seedSong.remote_rel_path || seedSong.filePath
+        }
+        const saved = await this.mediaTable.saveOrUpdateWebDavItem(seedSong)
+        if (saved) {
+          savedCount += 1
+        }
+      }
+      if (savedCount <= 0) {
+        return
+      }
+      const refreshedRemoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
+      if (refreshedRemoteSongs.length === 0) {
+        return
+      }
+      this.applyRemoteDiscoverySongs(refreshedRemoteSongs, true)
+      Logger.info(TAG, `发现页远程预热完成: saved=${savedCount}, remote=${refreshedRemoteSongs.length}`)
+    } catch (error) {
+      Logger.warn(TAG, `发现页远程预热失败: ${this.toErrorMessage(error)}`)
+    } finally {
+      this.isRemoteBootstrapLoading = false
+    }
+  }
+
   private queryTopPlayedSongs(limitCount: number): Promise<VideoItem[]> {
     return new Promise<VideoItem[]>((resolve) => {
       if (!this.mediaTable) {
@@ -754,15 +875,16 @@ export struct FindView {
       setFindPlaylist(playlistId, playlistName, songs, safeIndex)
     }
     const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
-    const playlistData: FindPlaylistEventData = {
+    const playlistData = new PlaylistPlayRequest(
       playlistId,
       playlistName,
-      songCount: songs.length,
-      startIndex: safeIndex,
-      isJump: false,
-      songFilePaths: songs.map((item: VideoItem): string => item.filePath),
+      songs.length,
+      safeIndex,
+      songs.map((item: VideoItem): string => item.filePath),
+      false,
       playType
-    }
+    )
+    savePendingPlaylistPlay(playlistData)
     emitter.emit(eventPlaylistPlay, { data: playlistData })
   }
 
@@ -785,6 +907,7 @@ export struct FindView {
     if (!this.mediaTable) {
       return []
     }
+    await this.ensureRemoteDiscoverySongsAvailable()
     const remoteSongs = await this.mediaTable.queryRemoteSongsAsync()
     this.searchRemoteSongsPool = this.filterUniqueSongs(remoteSongs)
     return this.searchRemoteSongsPool
@@ -948,6 +1071,115 @@ export struct FindView {
     this.isSearchMode = false
   }
 
+  private getPlaylistCover(playlist: Playlist): string | Resource {
+    if (StrUtil.isNotEmpty(playlist.coverPath)) {
+      return playlist.coverPath as string
+    }
+    const cachedSongs = this.playlistSongsCache.get(playlist.id)
+    const coverSong = cachedSongs ? this.pickAlbumCoverSong(cachedSongs) : undefined
+    if (coverSong && StrUtil.isNotEmpty(coverSong.pixelMapPath)) {
+      return coverSong.pixelMapPath as string
+    }
+    return $r('app.media.alt')
+  }
+
+  private getPlaylistSubtitle(playlist: Playlist): string {
+    if (StrUtil.isNotEmpty(playlist.description)) {
+      return playlist.description as string
+    }
+    return `${playlist.songCount} 首歌曲`
+  }
+
+  private presentPlaylistDetail(playlist: Playlist, songs: VideoItem[]): void {
+    if (songs.length === 0) {
+      ToastUtil.showToast('歌单里还没有可展示歌曲')
+      return
+    }
+    const coverSong = this.pickAlbumCoverSong(songs)
+    this.currentAlbumId = `find-playlist-${playlist.id}`
+    this.currentAlbumTitle = playlist.name
+    this.currentAlbumArtist = '心动歌单'
+    this.currentAlbumCoverPath = StrUtil.isNotEmpty(playlist.coverPath) ? playlist.coverPath as string :
+      (coverSong?.pixelMapPath ?? '')
+    this.currentAlbumSourceLabel = '心动歌单'
+    this.currentAlbumSongs = songs.slice()
+    this.isAlbumMode = true
+    this.isSearchMode = false
+  }
+
+  private async openPlaylistDetail(playlist: Playlist): Promise<void> {
+    if (!this.playlistTable) {
+      ToastUtil.showToast('歌单加载失败')
+      return
+    }
+    if (playlist.songCount <= 0) {
+      ToastUtil.showToast('歌单里还没有歌曲')
+      return
+    }
+    const cachedSongs = this.playlistSongsCache.get(playlist.id)
+    if (cachedSongs && cachedSongs.length > 0) {
+      this.presentPlaylistDetail(playlist, cachedSongs)
+      return
+    }
+    try {
+      const playlistSongs = await this.playlistTable.queryPlaylistSongs(playlist.id)
+      if (playlistSongs.length === 0) {
+        ToastUtil.showToast('歌单里还没有歌曲')
+        return
+      }
+      const songs = await convertPlaylistSongsToVideoItems(getContext(this) as common.Context, playlistSongs)
+      if (songs.length === 0) {
+        ToastUtil.showToast('歌单里还没有可播放歌曲')
+        return
+      }
+      this.playlistSongsCache.set(playlist.id, songs)
+      this.presentPlaylistDetail(playlist, songs)
+    } catch (error) {
+      Logger.error(TAG, `发现页打开歌单失败: ${this.toErrorMessage(error as Object)}`)
+      ToastUtil.showToast('歌单加载失败')
+    }
+  }
+
+  private async resolveHeartPlaylistCovers(ticket: number, playlists: Playlist[]): Promise<void> {
+    if (!this.playlistTable || playlists.length === 0) {
+      return
+    }
+    const context = getContext(this) as common.Context
+    for (let index = 0; index < playlists.length; index += 1) {
+      if (ticket !== this.heartPlaylistCoverTicket) {
+        return
+      }
+      const playlist = playlists[index]
+      if (StrUtil.isNotEmpty(playlist.coverPath) || playlist.songCount <= 0) {
+        continue
+      }
+      try {
+        let songs = this.playlistSongsCache.get(playlist.id)
+        if (!songs || songs.length === 0) {
+          const playlistSongs = await this.playlistTable.queryPlaylistSongs(playlist.id)
+          if (playlistSongs.length === 0) {
+            continue
+          }
+          songs = await convertPlaylistSongsToVideoItems(context, playlistSongs)
+          if (songs.length > 0) {
+            this.playlistSongsCache.set(playlist.id, songs)
+          }
+        }
+        if (!songs || songs.length === 0) {
+          continue
+        }
+        const coverSong = this.pickAlbumCoverSong(songs)
+        if (!coverSong || StrUtil.isEmpty(coverSong.pixelMapPath)) {
+          continue
+        }
+        playlist.coverPath = coverSong.pixelMapPath as string
+        this.heartPlaylists = this.heartPlaylists.slice()
+      } catch (error) {
+        Logger.warn(TAG, `发现页补全歌单封面失败: ${playlist.name}, ${this.toErrorMessage(error as Object)}`)
+      }
+    }
+  }
+
   private exitAlbumMode(): void {
     this.getUIContext()?.animateTo({ duration: 500 }, () => {
       this.isAlbumMode = false
@@ -2204,6 +2436,83 @@ export struct FindView {
     }
   }
 
+  @Builder
+  private buildHeartPlaylistCardContent(playlist: Playlist) {
+    Column({ space: 6 }) {
+      Stack({ alignContent: Alignment.Bottom }) {
+        Image(this.getPlaylistCover(playlist))
+          .aspectRatio(3 / 4)
+          .borderRadius(16)
+          .width(160)
+          .objectFit(ImageFit.Cover)
+
+        Row() {
+          Text('心动歌单')
+            .fontColor($r('app.color.white'))
+            .fontSize(9)
+            .padding({ left: 6, right: 6, top: 3, bottom: 3 })
+            .backgroundColor('#7A000000')
+            .borderRadius(999)
+
+          Text(`${playlist.songCount} 首`)
+            .fontColor($r('app.color.white'))
+            .fontSize(9)
+            .padding({ left: 6, right: 6, top: 3, bottom: 3 })
+            .backgroundColor('#5C000000')
+            .borderRadius(999)
+        }
+        .justifyContent(FlexAlign.SpaceBetween)
+        .padding({ left: 8, right: 8, bottom: 8 })
+        .width(160)
+      }
+
+      Text(playlist.name)
+        .lineHeight(16)
+        .maxLines(1)
+        .width(160)
+        .fontColor(this.getPrimaryTextColor())
+        .fontSize(14)
+        .fontWeight(FontWeight.Bold)
+        .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+      Text(this.getPlaylistSubtitle(playlist))
+        .fontColor(this.getSecondaryTextColor())
+        .fontSize(10)
+        .maxLines(1)
+        .textOverflow({ overflow: TextOverflow.Ellipsis })
+        .lineHeight(11)
+        .width(160)
+        .textAlign(TextAlign.Start)
+    }
+  }
+
+  @Builder
+  private buildHeartPlaylistSection() {
+    ConfigTitle({
+      text: '心动歌单',
+      showAction: false
+    })
+    List({ space: 8 }) {
+      ForEach(this.heartPlaylists, (playlist: Playlist) => {
+        ListItem() {
+          PointLightContentButton({
+            pointColor: this.themeColor,
+            buttonRadius: 16,
+            builder: () => {
+              this.buildHeartPlaylistCardContent(playlist)
+            }
+          })
+          .onClick(() => {
+            void this.openPlaylistDetail(playlist)
+          })
+        }
+      }, getFindPlaylistKey)
+    }
+    .listDirection(Axis.Horizontal)
+    .scrollBar(BarState.Off)
+    .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
+  }
+
   @Builder
   private buildCloudMoodSection() {
     ConfigTitle({
@@ -2541,11 +2850,16 @@ export struct FindView {
           if (this.favoriteSongsPool.length > 0 || this.favoriteSongs.length > 0) {
             this.buildFavoriteSection()
           }
+          if (this.heartPlaylists.length > 0) {
+            this.buildHeartPlaylistSection()
+          }
           if (this.remoteSongsPool.length > 0 || this.remoteSongs.length > 0 || this.cloudMoodSongs.length > 0) {
-            this.buildCloudMoodSection()
             this.buildCloudSection()
           }
           this.buildFeaturedAlbumSection()
+          if (this.remoteSongsPool.length > 0 || this.remoteSongs.length > 0 || this.cloudMoodSongs.length > 0) {
+            this.buildCloudMoodSection()
+          }
           if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.cloudAlbums.length > 0) {
             this.buildCloudAlbumSection()
           }

+ 248 - 74
entry/src/main/ets/view/LocalMusic.ets

@@ -136,6 +136,7 @@ import {
   WebDavMetadataUpdatePayload,
   isDaoLiYuType
 } from '../common/util/RemotePlayerUtil';
+import { PlaylistPlayRequest, consumePendingPlaylistPlay, clearPendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
 
 import { HSBColorPicker, HSBColorPickerLayout } from '@keke/color-picker'
 import app from '@system.app';
@@ -294,20 +295,6 @@ type LocalMusicWorkerMessage =
     | WorkerMessageMetadata
     | WorkerMessageEditResult;
 
-/**
- * 歌单播放事件数据
- */
-interface PlaylistEventData {
-  playlistId: string;
-  playlistName: string;
-  songCount: number;
-  startIndex: number;
-  songFilePaths: string[];
-  isJump: boolean;
-  playType?: number;
-  webDavAuthInfo?: WebDavAuthItem; // 新增WebDAV认证信息
-}
-
 const DEFAULT_INDEX =
   ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
     'X', 'Y', 'Z']
@@ -1070,33 +1057,31 @@ export struct LocalMusic {
 
       // 检查歌单播放数据结构
       if (data.playlistId && data.songFilePaths && data.startIndex !== undefined) {
-        const playlistData: PlaylistEventData = {
-          playlistId: data.playlistId as string,
-          playlistName: data.playlistName as string,
-          songCount: data.songCount as number,
-          startIndex: data.startIndex as number,
-          isJump: data.isJump as boolean,
-          songFilePaths: data.songFilePaths as string[],
-          playType: data.playType as number
-        }
-
-        if (playlistData.playType !== undefined) {
-          this.applyRequestedPlayType(playlistData.playType)
-        }
-
-        await this.handlePlaylistPlayRequest(
-          playlistData.playlistId,
-          playlistData.playlistName,
-          playlistData.songFilePaths,
-          playlistData.startIndex,
-          playlistData.isJump,
-          playlistData.songCount
+        const playlistData = new PlaylistPlayRequest(
+          data.playlistId as string,
+          data.playlistName as string,
+          data.songCount as number,
+          data.startIndex as number,
+          data.songFilePaths as string[],
+          data.isJump as boolean,
+          data.playType as number
         )
+
+        await this.dispatchPlaylistPlayRequest(playlistData)
         return
       }
 
       Logger.error('heanup eventPlaylistPlay: 接收到无效的数据结构')
     });
+    Promise.resolve().then(async (): Promise<void> => {
+      const pendingPlaylistRequest = consumePendingPlaylistPlay();
+      if (!pendingPlaylistRequest) {
+        return;
+      }
+      Logger.info(TAG,
+        `检测到待处理播放请求,准备补偿执行: ${pendingPlaylistRequest.playlistName}, startIndex=${pendingPlaylistRequest.startIndex}`);
+      await this.dispatchPlaylistPlayRequest(pendingPlaylistRequest);
+    });
 
     let eventRefreshSort: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_REFRESH_SORT }
     // 监听广播事件(通用设置配置更新)
@@ -1447,7 +1432,7 @@ export struct LocalMusic {
     this.sortType = PreferencesUtil.getNumberSync(SettingPage.SORT_TYPE, 4)
     this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false)
     this.customizeBgPath = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '')
-    this.isGridMusic = PreferencesUtil.getBooleanSync(SettingPage.IS_GRID_MUSIC, false)
+    this.isGridMusic = PreferencesUtil.getBooleanSync(SettingPage.IS_GRID_MUSIC, true)
     this.isScrollHide = PreferencesUtil.getBooleanSync(SettingPage.IS_SCROLL_HIDE, false)
     this.isSameTimePlay = PreferencesUtil.getBooleanSync(SettingPage.IS_SAMETIME_PLAY, false)
     this.isSavePlayMode = PreferencesUtil.getBooleanSync(SettingPage.iS_SAVE_PLAY_MODE, true)
@@ -2190,7 +2175,11 @@ export struct LocalMusic {
     emitter.off(EventConstants.EVENT_SETTING_UPDATE);
     emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED);
     emitter.off(EventConstants.EVENT_EQUALIZER_CHANGED);
+    emitter.off(EventConstants.EVENT_PLAYLIST_PLAY);
+    emitter.off(EventConstants.EVENT_PLAYLIST_REFRESH_SORT);
     emitter.off(EventConstants.EVENT_PLAY_QUEUE_REFRESH);
+    emitter.off(EventConstants.EVENT_OPEN_LOCAL_SPECIAL_LIST);
+    emitter.off(EventConstants.EVENT_PLAY_PAUSE);
     this.mDestroyPage = true;
     this.mIjkMediaPlayer.setScreenOnWhilePlaying(false);
     if (this.CONTROL_PlayStatus != PlayStatus.INIT) {
@@ -5541,7 +5530,8 @@ export struct LocalMusic {
 
   private syncFavStateLocal(item: VideoItem, isFav: number): void {
     const nextFavList = [...this.favList];
-    const favIndex = nextFavList.findIndex((favItem: VideoItem) => favItem.filePath === item.filePath);
+    const itemFavKey = Utility.getFavMatchKey(item);
+    const favIndex = nextFavList.findIndex((favItem: VideoItem) => Utility.getFavMatchKey(favItem) === itemFavKey);
     if (isFav === 1) {
       if (favIndex >= 0) {
         nextFavList[favIndex].isFav = 1;
@@ -5555,12 +5545,112 @@ export struct LocalMusic {
     }
     this.favList = nextFavList;
     item.isFav = isFav;
-    if (this.currentSong && this.currentSong.filePath === item.filePath) {
+    if (this.currentSong && Utility.getFavMatchKey(this.currentSong) === itemFavKey) {
       this.currentSong.isFav = isFav;
       this.isFac = isFav === 1;
     }
   }
 
+  private syncFavoriteStateForPlainList(list: Array<VideoItem> | undefined, targetKey: string, isFav: number): boolean {
+    if (!list || ArrayUtil.isEmpty(list) || StrUtil.isEmpty(targetKey)) {
+      return false;
+    }
+    let changed = false;
+    for (let index = 0; index < list.length; index += 1) {
+      if (Utility.getFavMatchKey(list[index]) !== targetKey) {
+        continue;
+      }
+      list[index].isFav = isFav;
+      changed = true;
+    }
+    return changed;
+  }
+
+  private syncFavoriteStateForVisibleList(list: Array<VideoItem> | undefined, dataSource: LazyDataSource<VideoItem>,
+    targetKey: string, isFav: number, removeOnCancel: boolean): boolean {
+    if (!list || ArrayUtil.isEmpty(list) || StrUtil.isEmpty(targetKey)) {
+      return false;
+    }
+    let changed = false;
+    for (let index = list.length - 1; index >= 0; index -= 1) {
+      if (Utility.getFavMatchKey(list[index]) !== targetKey) {
+        continue;
+      }
+      changed = true;
+      if (removeOnCancel) {
+        list.splice(index, 1);
+        if (index < dataSource.dataArray.length) {
+          dataSource.deleteData(index);
+        }
+      } else {
+        list[index].isFav = isFav;
+        if (index < dataSource.dataArray.length) {
+          dataSource.dataArray[index].isFav = isFav;
+          dataSource.notifyDataChange(index);
+        }
+      }
+    }
+    return changed;
+  }
+
+  private syncFavoriteStateInCaches(targetKey: string, isFav: number): void {
+    if (StrUtil.isEmpty(targetKey) || this.cache.size === 0) {
+      return;
+    }
+    let changed = false;
+    this.cache.forEach((items: Array<VideoItem>) => {
+      for (let index = 0; index < items.length; index += 1) {
+        if (Utility.getFavMatchKey(items[index]) !== targetKey) {
+          continue;
+        }
+        items[index].isFav = isFav;
+        changed = true;
+      }
+    });
+    if (changed) {
+      void this.saveCacheToStorage();
+    }
+  }
+
+  private syncFavoriteStateAcrossCollections(item: VideoItem, isFav: number): void {
+    const targetKey = Utility.getFavMatchKey(item);
+    if (StrUtil.isEmpty(targetKey)) {
+      return;
+    }
+    const removeFromCurrentView = this.isFavMusic && isFav === 0;
+    const currentViewChanged = this.syncFavoriteStateForVisibleList(this.videoLocalList, this.dataSource, targetKey, isFav,
+      removeFromCurrentView);
+    this.syncFavoriteStateForVisibleList(this.songList, this.sonDataSource, targetKey, isFav, false);
+    this.syncFavoriteStateForPlainList(this.currentSongList, targetKey, isFav);
+    this.syncFavoriteStateForPlainList(this.historyList, targetKey, isFav);
+    this.syncFavoriteStateForPlainList(this.mediaKuList, targetKey, isFav);
+    this.syncFavoriteStateForPlainList(this.artistList, targetKey, isFav);
+    this.syncFavoriteStateForPlainList(this.albumList, targetKey, isFav);
+    this.syncFavoriteStateForPlainList(this.filteredList, targetKey, isFav);
+    this.syncFavoriteStateInCaches(targetKey, isFav);
+    if (this.isFavMusic) {
+      this.currentTitleCover = ArrayUtil.isNotEmpty(this.favList) ? Utility.getFirstCoverFromList(this.favList) :
+        $r('app.media.alt');
+    }
+    if (currentViewChanged) {
+      this.setButtonStatus();
+      if (removeFromCurrentView) {
+        this.markAlphaBetDirty();
+      }
+    }
+  }
+
+  private syncRemoteFavoriteState(item: VideoItem, isFav: number): void {
+    if (!item || !isRemoteCloudType(item.type) || StrUtil.isEmpty(item.filePath)) {
+      return;
+    }
+    const remoteManager = RemoteDriveManager.getInstance();
+    const target = Utility.getItemByFilePath(remoteManager.webDavSongs, item.filePath);
+    if (target) {
+      target.isFav = isFav;
+    }
+  }
+
   doFav(item: VideoItem) {
     LogUtil.info('onecold doFav isFav=' + item.isFav)
 
@@ -5571,25 +5661,50 @@ export struct LocalMusic {
       isFav = 1
     }
 
-    this.table.updateIsFavByFilePath(item.filePath, isFav, async (result: boolean) => {
-      if (result) {
-        this.syncFavStateLocal(item, isFav);
-        if (isFav === 1) {
-          ToastUtil.showToast('收藏成功');
-        } else {
-          ToastUtil.showToast('取消收藏成功');
+    const targetFilePath = isRemoteCloudType(item.type) ? (this.resolveRemoteStoragePath(item) || item.filePath) :
+      item.filePath;
+
+    const onUpdateSuccess = (): void => {
+      this.syncFavStateLocal(item, isFav);
+      this.syncRemoteFavoriteState(item, isFav);
+      this.syncFavoriteStateAcrossCollections(item, isFav);
+      if (isFav === 1) {
+        ToastUtil.showToast('收藏成功');
+      } else {
+        ToastUtil.showToast('取消收藏成功');
+      }
+    };
+
+    const updateFavorite = (): void => {
+      this.table.updateIsFavByFilePath(targetFilePath, isFav, async (result: boolean, error?: string) => {
+        if (result) {
+          onUpdateSuccess();
+          return;
         }
-        this.deleteCache(this.currentPath)
-        this.getFavList(false)
-        if (this.modeType === 0 && !this.isFavMusic) {
-          LogUtil.info('onecold doFav currentPath =' + this.currentPath)
-          this.getSortedFiles(this.currentPath)
+        if (isRemoteCloudType(item.type)) {
+          Logger.warn(TAG, `远程歌曲收藏更新失败,准备补入库后重试: ${targetFilePath}, error=${error}`);
+          try {
+            await this.persistRemoteMetadataToDb(item);
+            this.table.updateIsFavByFilePath(targetFilePath, isFav, (retryResult: boolean, retryError?: string) => {
+              if (retryResult) {
+                onUpdateSuccess();
+              } else {
+                Logger.error(TAG, `远程歌曲收藏补入库后仍失败: ${targetFilePath}, error=${retryError}`);
+                ToastUtil.showToast(isFav === 1 ? '收藏失败' : '取消收藏失败');
+              }
+            });
+          } catch (persistError) {
+            Logger.error(TAG,
+              `远程歌曲收藏补入库失败: ${targetFilePath}, error=${(persistError as Error).message}`);
+            ToastUtil.showToast(isFav === 1 ? '收藏失败' : '取消收藏失败');
+          }
+          return;
         }
-        workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName });
-        workerInstance.postMessage({ code: 3, data: this.context });
-        workerInstance.postMessage({ code: 4, data: this.context });
-      }
-    })
+        ToastUtil.showToast(isFav === 1 ? '收藏失败' : '取消收藏失败');
+      })
+    };
+
+    updateFavorite()
   }
 
   //多选复制文件
@@ -16794,22 +16909,65 @@ export struct LocalMusic {
   }
 
   updateLastPlayTimeStr(filePath: string) {
-    // 检查是否为网络音频(WebDAV),如果是则不更新本地数据库
-    if (this.currentSong && isRemoteCloudType(this.currentSong.type)) {
-      Logger.info(`heanup 检测到网络音频播放,跳过数据库更新: ${filePath}`)
+    const activeSong = this.currentSong;
+    let targetFilePath = this.currentSong?.filePath || filePath;
+    if (activeSong && isRemoteCloudType(activeSong.type)) {
+      const remoteStoragePath = this.resolveRemoteStoragePath(activeSong);
+      if (remoteStoragePath) {
+        targetFilePath = remoteStoragePath;
+      }
+      Logger.info(`heanup 检测到网络音频播放,改为更新远程歌曲最近播放: ${targetFilePath}`)
+    }
+
+    if (!targetFilePath) {
+      Logger.warn(TAG, `更新最近播放时间失败,目标路径为空: ${filePath}`);
       return;
     }
 
-    const targetFilePath = this.currentSong?.filePath || filePath;
     let lastPlayTime = DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss')
-    this.table.updateLastPlayedStrByFilePath(targetFilePath, lastPlayTime, (success: boolean, error?: string) => {
-      if (success) {
-        this.getHistoryList(false)
-        console.log(" onecold 更新最近播放时间成功,数据库已同步");
-      } else {
+    const applyHistoryUpdate = (): void => {
+      this.table.updateLastPlayedStrByFilePath(targetFilePath, lastPlayTime, (success: boolean, error?: string) => {
+        if (success) {
+          if (activeSong && this.currentSong?.filePath === activeSong.filePath) {
+            this.currentSong.lastPlayedStr = lastPlayTime
+            this.currentSong.playCount = (this.currentSong.playCount || 0) + 1
+          }
+          this.getHistoryList(false)
+          console.log(" onecold 更新最近播放时间成功,数据库已同步");
+          return;
+        }
+
+        if (activeSong && isRemoteCloudType(activeSong.type)) {
+          Logger.warn(TAG, `远程歌曲最近播放更新失败,准备补入库后重试: ${targetFilePath}, error=${error}`);
+          this.persistRemoteMetadataToDb(activeSong)
+            .then(() => {
+              this.table.updateLastPlayedStrByFilePath(targetFilePath, lastPlayTime, (retrySuccess: boolean,
+                retryError?: string) => {
+                if (retrySuccess) {
+                  if (activeSong && this.currentSong?.filePath === activeSong.filePath) {
+                    this.currentSong.lastPlayedStr = lastPlayTime
+                    this.currentSong.playCount = (this.currentSong.playCount || 0) + 1
+                  }
+                  this.getHistoryList(false)
+                  Logger.info(TAG, `远程歌曲最近播放补入库后更新成功: ${targetFilePath}`);
+                } else {
+                  Logger.error(TAG,
+                    `远程歌曲最近播放补入库后仍更新失败: ${targetFilePath}, error=${retryError}`);
+                }
+              });
+            })
+            .catch((persistError: Error) => {
+              Logger.error(TAG,
+                `远程歌曲最近播放补入库失败: ${targetFilePath}, error=${persistError.message}`);
+            });
+          return;
+        }
+
         console.error(" onecold 更新最近播放时间数据库失败原因: " + error);
-      }
-    });
+      });
+    };
+
+    applyHistoryUpdate();
   }
 
   private completionNum(num: number): string | number {
@@ -16850,6 +17008,21 @@ export struct LocalMusic {
     return sanitized;
   }
 
+  private async dispatchPlaylistPlayRequest(playlistData: PlaylistPlayRequest): Promise<void> {
+    clearPendingPlaylistPlay();
+    if (playlistData.playType !== undefined) {
+      this.applyRequestedPlayType(playlistData.playType);
+    }
+    await this.handlePlaylistPlayRequest(
+      playlistData.playlistId,
+      playlistData.playlistName,
+      playlistData.songFilePaths,
+      playlistData.startIndex,
+      playlistData.isJump ?? false,
+      playlistData.songCount
+    );
+  }
+
   private async resolvePlaybackUrlForCurrentSong(source: string): Promise<string | null> {
     const song = this.currentSong;
     if (!song) {
@@ -17290,26 +17463,27 @@ export struct LocalMusic {
       this.searchCover(this.currentSong,  this.currentSong.name,  this.currentSong.artist||'')
     }
 
-    // 如果是WebDAV网络音频,使用webdav_account_id获取认证信息(等待完成后再设置一次性头部)
-    // SMB类型通过本地HTTP代理播放,不需要认证头,排除SMB和百度类型
-    if (this.currentSong && isRemoteCloudType(this.currentSong.type) && !isBaiduType(this.currentSong.type) && !isSmbType(this.currentSong.type)) {
-      Logger.info(`heanup WebDAV歌曲认证 - 歌曲名: ${this.currentSong.name}, webdav_account_id: ${this.currentSong.webdav_account_id || '未设置'}`);
+    // 仅对确实需要额外认证头的远程类型补充请求头。
+    // Navidrome/Plex/AudioStation/DaoLiYu 等类型的播放链接已自带认证参数,这里不要再误走 WebDAV 认证分支。
+    if (this.currentSong
+      && (isWebDavType(this.currentSong.type) || isJellyfinType(this.currentSong.type) || isEmbyType(this.currentSong.type))) {
+      Logger.info(`heanup 远程歌曲认证头构建 - 歌曲名: ${this.currentSong.name}, webdav_account_id: ${this.currentSong.webdav_account_id || '未设置'}`);
       if (this.currentSong.webdav_account_id) {
         try {
           const webdavManager = RemoteDriveManager.getInstance();
           const webDavHeaders = await webdavManager.buildHttpHeadersWithAccountId(this.currentSong, this.videoUrl);
           if (webDavHeaders && webDavHeaders.size > 0) {
             webDavHeaders.forEach((value, key) => headers.set(key, value));
-            Logger.info(`heanup 成功通过webdav_account_id获取并合并WebDAV认证头`);
+            Logger.info(`heanup 成功通过webdav_account_id获取并合并远程认证头`);
           } else {
-            Logger.warn(`heanup 通过webdav_account_id获取认证头失败,播放可能无法继续`);
+            Logger.warn(`heanup 通过webdav_account_id获取到额外认证头`);
           }
         } catch (error) {
           const err = error as Error;
-          Logger.error(`heanup 获取WebDAV认证头时出错: ${err.message}`);
+          Logger.error(`heanup 获取远程认证头时出错: ${err.message}`);
         }
       } else {
-        Logger.error(`heanup WebDAV歌曲 "${this.currentSong.name}" 缺少webdav_account_id,无法进行认证`);
+        Logger.error(`heanup 远程歌曲 "${this.currentSong.name}" 缺少webdav_account_id,无法构建认证头`);
       }
     } else if (this.currentSong && isSmbType(this.currentSong.type)) {
       Logger.info(`heanup SMB歌曲通过HTTP代理播放,无需认证头`);

+ 10 - 17
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -38,6 +38,7 @@ import { taskpool } from '@kit.ArkTS';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { PlayingIndicator } from './PlayingIndicator';
 import { hdsEffect } from '@kit.UIDesignKit';
+import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
 
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 const NAVIDROME_SEARCH_LIMIT = 500;
@@ -46,15 +47,6 @@ const ITEM_HEIGHT_SMALL: number = 48; // 列表项小高度
 const ITEM_HEIGHT: number = 65; // 列表项中高度
 const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度
 
-interface PlaylistEventData {
-  playlistId: string;
-  playlistName: string;
-  songCount: number;
-  startIndex: number;
-  isJump: boolean;
-  songFilePaths: string[];
-}
-
 enum NavFilterType {
   None = 0,
   Artist = 1,
@@ -392,7 +384,7 @@ export struct RemoteMusicPage {
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
     this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true)
     this.twoFingerType = PreferencesUtil.getNumberSync(SettingPage.TWO_FINGER_TYPE, 3)
-    this.isGridMusic = PreferencesUtil.getBooleanSync(SettingPage.IS_GRID_MUSIC, false)
+    this.isGridMusic = PreferencesUtil.getBooleanSync(SettingPage.IS_GRID_MUSIC, true)
   }
 
   aboutToAppear() {
@@ -3644,16 +3636,17 @@ export struct RemoteMusicPage {
         setNavidromeTotalCount(this.serverTotalSongCount);
       }
 
-      const playlistData: PlaylistEventData = {
-        playlistId: NAVIDROME_PLAYLIST_ID,
-        playlistName: `${getRemoteDriveDisplayLabel(account.webType)} - ${account.name ?? '未知账户'}`,
-        songCount: this.serverTotalSongCount > 0 ? this.serverTotalSongCount : playlistSource.length,
+      const playlistData = new PlaylistPlayRequest(
+        NAVIDROME_PLAYLIST_ID,
+        `${getRemoteDriveDisplayLabel(account.webType)} - ${account.name ?? '未知账户'}`,
+        this.serverTotalSongCount > 0 ? this.serverTotalSongCount : playlistSource.length,
         startIndex,
-        isJump: isJump,//设置true会弹出播放页
-        songFilePaths: playlistSource.map(item => item.filePath)
-      };
+        playlistSource.map(item => item.filePath),
+        isJump
+      );
 
       const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
+      savePendingPlaylistPlay(playlistData);
       emitter.emit(eventPlaylistPlay, { data: playlistData });
 
       void ServerLogUtil.info('StreamingPlay', `播放事件已发送`);