Przeglądaj źródła

抽离播放控制

onecold 4 miesięcy temu
rodzic
commit
91a4707649

+ 25 - 0
entry/src/main/ets/common/util/FindPlaylistStore.ets

@@ -58,6 +58,31 @@ export function getFindPlaylistMeta(): FindPlaylistMeta {
   return cloneFindPlaylistMeta(findPlaylistMeta);
   return cloneFindPlaylistMeta(findPlaylistMeta);
 }
 }
 
 
+function isSameFindQueue(left: VideoItem[], right: VideoItem[]): boolean {
+  if (left.length !== right.length) {
+    return false;
+  }
+  for (let index = 0; index < left.length; index++) {
+    if ((left[index]?.filePath ?? '') !== (right[index]?.filePath ?? '')) {
+      return false;
+    }
+  }
+  return true;
+}
+
+export function resolveFindPagedLocalQueueMetaForPlayback(queue: VideoItem[], startIndex: number): FindPlaylistMeta | undefined {
+  if (!findPlaylistMeta.isPagedLocalQueue) {
+    return undefined;
+  }
+  if (findCurrentPlayIndex !== startIndex) {
+    return undefined;
+  }
+  if (!isSameFindQueue(findVideoItems, queue)) {
+    return undefined;
+  }
+  return cloneFindPlaylistMeta(findPlaylistMeta);
+}
+
 export function clearFindPlaylist(): void {
 export function clearFindPlaylist(): void {
   findPlaylistId = '';
   findPlaylistId = '';
   findPlaylistName = '';
   findPlaylistName = '';

+ 3 - 0
entry/src/main/ets/common/util/RemoteMusicViewModeHelper.ets

@@ -0,0 +1,3 @@
+export function shouldRenderRemoteMusicSongView(selectedTab: number, isDetailView: boolean): boolean {
+  return selectedTab === 0 || isDetailView
+}

+ 5 - 4
entry/src/main/ets/controller/MusicPlaybackController.ets

@@ -286,12 +286,13 @@ export class MusicPlaybackController {
     }
     }
   }
   }
 
 
-  public async playSong(song: VideoItem, source: string = 'controller'): Promise<void> {
-    await this.playbackCoordinator.playSong(song, source)
+  public async playSong(song: VideoItem, source: string = 'controller', playType?: number): Promise<void> {
+    await this.playbackCoordinator.playSong(song, source, playType)
   }
   }
 
 
-  public async playQueue(queue: VideoItem[], startIndex: number, source: string = 'controller'): Promise<void> {
-    await this.playbackCoordinator.playQueue(queue, startIndex, source)
+  public async playQueue(queue: VideoItem[], startIndex: number, source: string = 'controller',
+    playType?: number): Promise<void> {
+    await this.playbackCoordinator.playQueue(queue, startIndex, source, playType)
   }
   }
 
 
   public async playOrPauseDirect(): Promise<void> {
   public async playOrPauseDirect(): Promise<void> {

+ 7 - 6
entry/src/main/ets/controller/PlaybackCoordinator.ets

@@ -2,7 +2,7 @@ import { PlaybackStateBridge } from './PlaybackStateBridge'
 import { VideoItem } from '../viewmodel/VideoItem'
 import { VideoItem } from '../viewmodel/VideoItem'
 
 
 export interface PlaybackRuntime {
 export interface PlaybackRuntime {
-  playQueue: (queue: VideoItem[], startIndex: number, source: string) => Promise<void>
+  playQueue: (queue: VideoItem[], startIndex: number, source: string, playType?: number) => Promise<void>
   playOrPause: () => Promise<void>
   playOrPause: () => Promise<void>
   playNext: () => Promise<void>
   playNext: () => Promise<void>
   playPrevious: () => Promise<void>
   playPrevious: () => Promise<void>
@@ -32,22 +32,23 @@ export class PlaybackCoordinator {
     }
     }
   }
   }
 
 
-  public async playQueue(queue: VideoItem[], startIndex: number, source: string): Promise<void> {
+  public async playQueue(queue: VideoItem[], startIndex: number, source: string, playType?: number): Promise<void> {
     if (!this.runtime || queue.length <= 0) {
     if (!this.runtime || queue.length <= 0) {
       return
       return
     }
     }
     const previousSnapshot = this.stateBridge.getSnapshot()
     const previousSnapshot = this.stateBridge.getSnapshot()
     this.stateBridge.replaceQueue(queue, startIndex)
     this.stateBridge.replaceQueue(queue, startIndex)
     try {
     try {
-      await this.runtime.playQueue(queue, this.stateBridge.getSnapshot().currentIndex, source)
+      await this.runtime.playQueue(queue, this.stateBridge.getSnapshot().currentIndex, source, playType)
     } catch (error) {
     } catch (error) {
       this.stateBridge.restoreSnapshot(previousSnapshot)
       this.stateBridge.restoreSnapshot(previousSnapshot)
-      throw error
+      const err = error as Error
+      throw new Error(err.message)
     }
     }
   }
   }
 
 
-  public async playSong(song: VideoItem, source: string): Promise<void> {
-    await this.playQueue([song], 0, source)
+  public async playSong(song: VideoItem, source: string, playType?: number): Promise<void> {
+    await this.playQueue([song], 0, source, playType)
   }
   }
 
 
   public async playOrPause(): Promise<void> {
   public async playOrPause(): Promise<void> {

+ 33 - 27
entry/src/main/ets/controller/PlaybackStateBridge.ets

@@ -1,4 +1,3 @@
-import { AppStorage } from '@kit.ArkUI'
 import { VideoItem } from '../viewmodel/VideoItem'
 import { VideoItem } from '../viewmodel/VideoItem'
 
 
 export interface PlaybackSnapshot {
 export interface PlaybackSnapshot {
@@ -32,14 +31,7 @@ export class PlaybackStateBridge {
   }
   }
 
 
   public reset(): void {
   public reset(): void {
-    this.snapshot = {
-      queue: [],
-      currentIndex: -1,
-      currentSong: undefined,
-      isPlaying: false,
-      positionMs: 0,
-      durationMs: 0
-    }
+    this.snapshot = this.buildSnapshot([], -1, undefined, false, 0, 0)
     AppStorage.setOrCreate('currentSong', undefined)
     AppStorage.setOrCreate('currentSong', undefined)
     AppStorage.setOrCreate('currentQueue', [])
     AppStorage.setOrCreate('currentQueue', [])
     AppStorage.setOrCreate('currentQueueIndex', -1)
     AppStorage.setOrCreate('currentQueueIndex', -1)
@@ -52,15 +44,7 @@ export class PlaybackStateBridge {
     const safeIndex = safeQueue.length <= 0 ? -1 : Math.max(0, Math.min(startIndex, maxIndex))
     const safeIndex = safeQueue.length <= 0 ? -1 : Math.max(0, Math.min(startIndex, maxIndex))
     const currentSong = safeIndex >= 0 ? safeQueue[safeIndex] : undefined
     const currentSong = safeIndex >= 0 ? safeQueue[safeIndex] : undefined
 
 
-    this.snapshot = {
-      ...this.snapshot,
-      queue: safeQueue,
-      currentIndex: safeIndex,
-      currentSong,
-      isPlaying: false,
-      positionMs: 0,
-      durationMs: 0
-    }
+    this.snapshot = this.buildSnapshot(safeQueue, safeIndex, currentSong, false, 0, 0)
     AppStorage.setOrCreate('currentSong', currentSong)
     AppStorage.setOrCreate('currentSong', currentSong)
     AppStorage.setOrCreate('currentQueue', safeQueue)
     AppStorage.setOrCreate('currentQueue', safeQueue)
     AppStorage.setOrCreate('currentQueueIndex', safeIndex)
     AppStorage.setOrCreate('currentQueueIndex', safeIndex)
@@ -68,21 +52,27 @@ export class PlaybackStateBridge {
   }
   }
 
 
   public updatePlaybackState(isPlaying: boolean, positionMs: number, durationMs: number): void {
   public updatePlaybackState(isPlaying: boolean, positionMs: number, durationMs: number): void {
-    this.snapshot = {
-      ...this.snapshot,
+    this.snapshot = this.buildSnapshot(
+      this.snapshot.queue,
+      this.snapshot.currentIndex,
+      this.snapshot.currentSong,
       isPlaying,
       isPlaying,
       positionMs,
       positionMs,
       durationMs
       durationMs
-    }
+    )
     AppStorage.setOrCreate('isPlaying', isPlaying)
     AppStorage.setOrCreate('isPlaying', isPlaying)
   }
   }
 
 
   public restoreSnapshot(snapshot: PlaybackSnapshot): void {
   public restoreSnapshot(snapshot: PlaybackSnapshot): void {
     const safeQueue = snapshot.queue.slice()
     const safeQueue = snapshot.queue.slice()
-    this.snapshot = {
-      ...snapshot,
-      queue: safeQueue
-    }
+    this.snapshot = this.buildSnapshot(
+      safeQueue,
+      snapshot.currentIndex,
+      snapshot.currentSong,
+      snapshot.isPlaying,
+      snapshot.positionMs,
+      snapshot.durationMs
+    )
     AppStorage.setOrCreate('currentSong', snapshot.currentSong)
     AppStorage.setOrCreate('currentSong', snapshot.currentSong)
     AppStorage.setOrCreate('currentQueue', safeQueue)
     AppStorage.setOrCreate('currentQueue', safeQueue)
     AppStorage.setOrCreate('currentQueueIndex', snapshot.currentIndex)
     AppStorage.setOrCreate('currentQueueIndex', snapshot.currentIndex)
@@ -90,9 +80,25 @@ export class PlaybackStateBridge {
   }
   }
 
 
   public getSnapshot(): PlaybackSnapshot {
   public getSnapshot(): PlaybackSnapshot {
+    return this.buildSnapshot(
+      this.snapshot.queue,
+      this.snapshot.currentIndex,
+      this.snapshot.currentSong,
+      this.snapshot.isPlaying,
+      this.snapshot.positionMs,
+      this.snapshot.durationMs
+    )
+  }
+
+  private buildSnapshot(queue: VideoItem[], currentIndex: number, currentSong: VideoItem | undefined,
+    isPlaying: boolean, positionMs: number, durationMs: number): PlaybackSnapshot {
     return {
     return {
-      ...this.snapshot,
-      queue: this.snapshot.queue.slice()
+      queue: queue.slice(),
+      currentIndex,
+      currentSong,
+      isPlaying,
+      positionMs,
+      durationMs
     }
     }
   }
   }
 }
 }

+ 21 - 3
entry/src/main/ets/pages/ChartsCount.ets

@@ -13,8 +13,9 @@ import MediaTable from '../common/util/MediaTable';
 import { McPieChart, Options } from '@mcui/mccharts'
 import { McPieChart, Options } from '@mcui/mccharts'
 import { ComponentContent } from '@kit.ArkUI';
 import { ComponentContent } from '@kit.ArkUI';
 import { TitleBarPointLightButton } from '../view/PointLight/TitleBarPointLightButton';
 import { TitleBarPointLightButton } from '../view/PointLight/TitleBarPointLightButton';
+import { MusicPlaybackController } from '../controller/MusicPlaybackController';
+
 
 
-// 批量编辑标签
 @Component
 @Component
 export struct ChartsCount {
 export struct ChartsCount {
   @StorageProp('topSafeHeight') topSafeHeight: number = 0;
   @StorageProp('topSafeHeight') topSafeHeight: number = 0;
@@ -47,6 +48,7 @@ export struct ChartsCount {
   @State maxRefreshingHeight: number = 100.0;
   @State maxRefreshingHeight: number = 100.0;
 
 
   private table: MediaTable = new MediaTable(this.context)
   private table: MediaTable = new MediaTable(this.context)
+  private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
   @State defOption: Options = new Options({
   @State defOption: Options = new Options({
     title: {
     title: {
       show: true,
       show: true,
@@ -377,6 +379,24 @@ export struct ChartsCount {
     .backgroundColor(Color.Transparent)
     .backgroundColor(Color.Transparent)
     .width('100%')
     .width('100%')
     .height(89)
     .height(89)
+    .onClick(() => {
+      this.playChartSong(item, index)
+    })
+  }
+
+  private playChartSong(item: VideoItem, index: number): void {
+    if (this.selectedFiles.length <= 0) {
+      return
+    }
+    const matchedIndex = this.selectedFiles.findIndex((song: VideoItem): boolean => song.filePath === item.filePath)
+    void this.playbackController.playQueue(
+      [...this.selectedFiles],
+      matchedIndex >= 0 ? matchedIndex : index,
+      'controller-open'
+    ).catch((error: Error) => {
+      LogUtil.error(`ChartsCount 播放排行榜歌曲失败: ${error}`)
+      ToastUtil.showToast('播放失败')
+    })
   }
   }
 }
 }
 
 
@@ -394,5 +414,3 @@ function customRefreshingContent() {
   .constraintSize({ minHeight: 32 })
   .constraintSize({ minHeight: 32 })
   .width("100%")
   .width("100%")
 }
 }
-
-

+ 19 - 63
entry/src/main/ets/pages/PlaylistDetailPage.ets

@@ -14,7 +14,7 @@ import { LazyDataSource } from '../common/util/LazyDataSource';
 import { ConfigurationConstant } from '@kit.AbilityKit';
 import { ConfigurationConstant } from '@kit.AbilityKit';
 import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
 import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
 import { PlayingIndicator } from '../view/PlayingIndicator';
 import { PlayingIndicator } from '../view/PlayingIndicator';
-import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
+import { MusicPlaybackController } from '../controller/MusicPlaybackController';
 
 
 // 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
 // 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
 function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
 function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
@@ -52,6 +52,7 @@ export struct PlaylistDetailPage {
 
 
   private playlistTable: PlaylistTable = new PlaylistTable(getContext(this))
   private playlistTable: PlaylistTable = new PlaylistTable(getContext(this))
   private mediaTable: MediaTable = new MediaTable(getContext(this))
   private mediaTable: MediaTable = new MediaTable(getContext(this))
+  private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
   private playlistId: string = ''
   private playlistId: string = ''
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
   @State isDarkMode: boolean = false
   @State isDarkMode: boolean = false
@@ -406,31 +407,13 @@ export struct PlaylistDetailPage {
       ToastUtil.showToast('歌单为空')
       ToastUtil.showToast('歌单为空')
       return
       return
     }
     }
-
-    // 发送播放歌单事件
-    const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
-
-    LogUtil.info(`heanup 准备发送播放事件,playlist: ${this.playlist ? '存在' : '不存在'}, songs: ${this.songList.length}首`)
-
-    // 构建歌单播放事件数据
-    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)}`)
-
-    const eventData: emitter.EventData = {
-      data: playlistData
-    };
-
-    savePendingPlaylistPlay(playlistData)
-    emitter.emit(eventPlaylistPlay, eventData)
-
-    ToastUtil.showToast('开始播放歌单')
+    LogUtil.info(`heanup 直连播放歌单,playlist: ${this.playlist ? '存在' : '不存在'}, songs: ${this.songList.length}首`)
+    void this.playbackController.playQueue([...this.songList], 0, 'controller-open').then(() => {
+      ToastUtil.showToast('开始播放歌单')
+    }).catch((error: Error) => {
+      LogUtil.error(`heanup 播放歌单失败: ${error}`)
+      ToastUtil.showToast('播放失败')
+    })
   }
   }
 
 
   /**
   /**
@@ -441,30 +424,11 @@ export struct PlaylistDetailPage {
     LogUtil.info(`heanup 播放指定歌曲: ${song.name}, 索引: ${index}`)
     LogUtil.info(`heanup 播放指定歌曲: ${song.name}, 索引: ${index}`)
     LogUtil.info(`heanup 歌曲列表长度: ${this.songList.length}`)
     LogUtil.info(`heanup 歌曲列表长度: ${this.songList.length}`)
     LogUtil.info(`heanup 歌单ID: ${this.playlist?.id}, 歌单名称: ${this.playlist?.name}`)
     LogUtil.info(`heanup 歌单ID: ${this.playlist?.id}, 歌单名称: ${this.playlist?.name}`)
-
-    // 检查歌曲文件路径
     LogUtil.info(`heanup 所有歌曲文件路径: ${JSON.stringify(this.songList.map(s => s.filePath))}`)
     LogUtil.info(`heanup 所有歌曲文件路径: ${JSON.stringify(this.songList.map(s => s.filePath))}`)
-
-    // 发送播放歌单事件
-    const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
-    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)}`)
-
-    const eventData: emitter.EventData = {
-      data: playlistData
-    };
-
-    LogUtil.info(`heanup eventData对象: ${JSON.stringify(eventData)}`)
-    savePendingPlaylistPlay(playlistData)
-    emitter.emit(eventPlaylistPlay, eventData)
+    void this.playbackController.playQueue([...this.songList], index, 'controller-open').catch((error: Error) => {
+      LogUtil.error(`heanup 播放指定歌曲失败: ${error}`)
+      ToastUtil.showToast('播放失败')
+    })
   }
   }
 
 
   /**
   /**
@@ -562,19 +526,12 @@ export struct PlaylistDetailPage {
           }
           }
 
 
           // 更新播放列表(重新发送歌单播放事件,保持当前播放位置)
           // 更新播放列表(重新发送歌单播放事件,保持当前播放位置)
-          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 已更新播放列表,移除了被删除的歌曲')
+          const nextIndex = this.curIndex >= this.songList.length ? Math.max(0, this.songList.length - 1) : this.curIndex
+          void this.playbackController.playQueue([...this.songList], nextIndex, 'playlist-detail-sync').then(() => {
+            LogUtil.info('heanup 已通过 controller 更新播放列表,移除了被删除的歌曲')
+          }).catch((error: Error) => {
+            LogUtil.error(`heanup 更新播放列表失败: ${error}`)
+          })
         }
         }
 
 
         ToastUtil.showToast('已从歌单移除')
         ToastUtil.showToast('已从歌单移除')
@@ -1754,4 +1711,3 @@ export  async function  updateAllSongsSortOrder(playlistTable:PlaylistTable,song
 
 
 
 
 
 
-

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

@@ -36,9 +36,9 @@ import { DownloadCenter } from '../view/DownloadCenter';
 import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
 import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { PlayingIndicator } from '../view/PlayingIndicator';
 import { PlayingIndicator } from '../view/PlayingIndicator';
-import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
 import { buildWebDavVisibleThumbPrefetchWindow, WebDavThumbPrefetchRange } from '../common/util/WebDavThumbPrefetchHelper';
 import { buildWebDavVisibleThumbPrefetchWindow, WebDavThumbPrefetchRange } from '../common/util/WebDavThumbPrefetchHelper';
 import { buildWebDavSongItemRenderKey } from '../common/util/WebDavListRenderHelper';
 import { buildWebDavSongItemRenderKey } from '../common/util/WebDavListRenderHelper';
+import { MusicPlaybackController } from '../controller/MusicPlaybackController';
 
 
 interface WebDavMetadataUpdatePayload {
 interface WebDavMetadataUpdatePayload {
   filePath: string;
   filePath: string;
@@ -175,6 +175,7 @@ export struct WebDavMainPage {
   private downloadCenterRefreshTimer: number = -1;
   private downloadCenterRefreshTimer: number = -1;
   private displayReloadToken: number = 0;
   private displayReloadToken: number = 0;
   private pendingDirectoryRefreshToken: number = 0;
   private pendingDirectoryRefreshToken: number = 0;
+  private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
 
 
   private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
   private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
     const nextTask: Promise<void> = globalRemoteThumbFfmpegQueue.then(async (): Promise<void> => {
     const nextTask: Promise<void> = globalRemoteThumbFfmpegQueue.then(async (): Promise<void> => {
@@ -1935,20 +1936,11 @@ export struct WebDavMainPage {
       return;
       return;
     }
     }
 
 
-    globalWebdavVideoItems = playlist;
-    globalWebdavCurrentPlayIndex = startIndex;
-
-    const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
-    const playlistData = new PlaylistPlayRequest(
-      'webdav-playlist',
-      '下载中心',
-      playlist.length,
-      startIndex,
-      playlist.map((item: VideoItem): string => item.filePath),
-      true
-    );
-    savePendingPlaylistPlay(playlistData);
-    emitter.emit(eventPlaylistPlay, { data: playlistData });
+    setWebdavPlaylist(playlist, startIndex);
+    void this.playbackController.playQueue(playlist.slice(), startIndex, 'controller-open').catch((error: Error) => {
+      Logger.error(TAG, `下载中心播放失败: ${error.message}`);
+      this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
+    });
 
 
 
 
   }
   }
@@ -2505,38 +2497,21 @@ export struct WebDavMainPage {
         });
         });
       }
       }
 
 
-      // 直接使用当前的VideoItem数组
       const videoItems: VideoItem[] = queue.slice();
       const videoItems: VideoItem[] = queue.slice();
-      const songFilePaths: string[] = [];
-
-      for (let i = 0; i < videoItems.length; i++) {
-        const item = videoItems[i];
-        songFilePaths.push(item.filePath); // 使用filePath作为文件路径
-      }
-
-      // 直接通过事件传递videoItems数据,不使用GlobalContext
-      const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
-
-      const playlistData = new PlaylistPlayRequest(
-        'webdav-playlist',
-        `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
-        videoItems.length,
-        playIndex,
-        songFilePaths,
-        isJump
-      );
-
-      // 保存videoItems到全局内存
-      globalWebdavVideoItems = videoItems;
-      globalWebdavCurrentPlayIndex = playIndex;
+      setWebdavPlaylist(videoItems, playIndex);
 
 
       Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${playIndex}`);
       Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${playIndex}`);
 
 
-      // 发送播放请求事件,只传递索引信息
-      savePendingPlaylistPlay(playlistData);
-      emitter.emit(eventPlaylistPlay, { data: playlistData });
-
-      Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${playIndex}`);
+      void this.playbackController.playQueue(
+        videoItems,
+        playIndex,
+        isJump ? 'controller-open' : 'webdav-main'
+      ).then(() => {
+        Logger.info(TAG, `heanup WebDAV 直连播放成功,索引: ${playIndex}`);
+      }).catch((error: Error) => {
+        Logger.error(TAG, `播放歌曲失败: ${error.message}`);
+        this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
+      });
 
 
 
 
     } catch (error) {
     } catch (error) {

+ 6 - 14
entry/src/main/ets/view/FindView.ets

@@ -29,7 +29,6 @@ import {
   getFindVideoItems,
   getFindVideoItems,
   setFindPlaylist
   setFindPlaylist
 } from '../common/util/FindPlaylistStore'
 } from '../common/util/FindPlaylistStore'
-import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore'
 import PlaylistTable from '../common/util/PlaylistTable'
 import PlaylistTable from '../common/util/PlaylistTable'
 import { convertPlaylistSongsToVideoItems } from '../pages/PlaylistDetailPage'
 import { convertPlaylistSongsToVideoItems } from '../pages/PlaylistDetailPage'
 import { getWebdavVideoItems, setWebdavPlaylist } from '../pages/WebDavMainPage'
 import { getWebdavVideoItems, setWebdavPlaylist } from '../pages/WebDavMainPage'
@@ -42,13 +41,13 @@ import {
   resolveQueueStartIndex,
   resolveQueueStartIndex,
   shouldWaitForIndexedRemotePlayback
   shouldWaitForIndexedRemotePlayback
 } from '../common/util/FindDiscoveryHelper'
 } from '../common/util/FindDiscoveryHelper'
-import { buildPlaylistDispatchFilePaths } from '../common/util/PlaylistPlayDispatchHelper'
 import { insertSongToNextPlayQueue, QueueInsertStatus } from '../common/util/PlayQueueHelper'
 import { insertSongToNextPlayQueue, QueueInsertStatus } from '../common/util/PlayQueueHelper'
 import { resolveRemoteCoverForSong } from '../common/util/RemoteCoverResolver'
 import { resolveRemoteCoverForSong } from '../common/util/RemoteCoverResolver'
 import {
 import {
   buildLocalRandomPagedQueueSeed,
   buildLocalRandomPagedQueueSeed,
   FIND_LOCAL_RANDOM_PAGED_PLAYLIST_ID
   FIND_LOCAL_RANDOM_PAGED_PLAYLIST_ID
 } from '../common/util/LocalRandomPagedQueueHelper'
 } from '../common/util/LocalRandomPagedQueueHelper'
+import { MusicPlaybackController } from '../controller/MusicPlaybackController'
 
 
 @Builder
 @Builder
 export function FindViewBuilder() {
 export function FindViewBuilder() {
@@ -228,6 +227,7 @@ export struct FindView {
   private remoteAccountMap: Map<string, WebDavAccount> = new Map<string, WebDavAccount>()
   private remoteAccountMap: Map<string, WebDavAccount> = new Map<string, WebDavAccount>()
   private remoteCoverRepairTicket: number = 0
   private remoteCoverRepairTicket: number = 0
   private remoteCoverRepairTimer: number = -1
   private remoteCoverRepairTimer: number = -1
+  private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
 
 
   aboutToAppear(): void {
   aboutToAppear(): void {
     this.initSetting()
     this.initSetting()
@@ -1170,18 +1170,10 @@ export struct FindView {
       // 发现页队列统一走内存仓库,避免事件负载里重复塞大数组。
       // 发现页队列统一走内存仓库,避免事件负载里重复塞大数组。
       setFindPlaylist(playlistId, playlistName, songs, safeIndex, meta)
       setFindPlaylist(playlistId, playlistName, songs, safeIndex, meta)
     }
     }
-    const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
-    const playlistData = new PlaylistPlayRequest(
-      playlistId,
-      playlistName,
-      dispatchCount,
-      safeIndex,
-      buildPlaylistDispatchFilePaths(playlistId, songs),
-      false,
-      playType
-    )
-    savePendingPlaylistPlay(playlistData)
-    emitter.emit(eventPlaylistPlay, { data: playlistData })
+    void this.playbackController.playQueue(songs.slice(), safeIndex, 'find-view', playType).catch((error: Error) => {
+      Logger.error(TAG, `发现页播放失败: ${error.message}`)
+      ToastUtil.showToast('播放失败')
+    })
   }
   }
 
 
   private getCurrentPlaybackQueue(): VideoItem[] {
   private getCurrentPlaybackQueue(): VideoItem[] {

+ 23 - 3
entry/src/main/ets/view/LocalMusic.ets

@@ -14,7 +14,13 @@ import {
   getNavidromeTotalCount,
   getNavidromeTotalCount,
   setNavidromeTotalCount
   setNavidromeTotalCount
 } from '../common/util/NavidromePlaylistStore';
 } from '../common/util/NavidromePlaylistStore';
-import { FindPlaylistMeta, getFindVideoItems, getFindCurrentPlayIndex, getFindPlaylistMeta } from '../common/util/FindPlaylistStore';
+import {
+  FindPlaylistMeta,
+  getFindVideoItems,
+  getFindCurrentPlayIndex,
+  getFindPlaylistMeta,
+  resolveFindPagedLocalQueueMetaForPlayback
+} from '../common/util/FindPlaylistStore';
 import { triggerLoadMoreSongs, hasMoreData } from '../common/util/NavidromeRandomLoader';
 import { triggerLoadMoreSongs, hasMoreData } from '../common/util/NavidromeRandomLoader';
 import {
 import {
   DEFAULT_LOCAL_RANDOM_QUEUE_PAGE_SIZE,
   DEFAULT_LOCAL_RANDOM_QUEUE_PAGE_SIZE,
@@ -489,18 +495,32 @@ export struct LocalMusic {
   private playbackCoordinator: PlaybackCoordinator = PlaybackCoordinator.getInstance()
   private playbackCoordinator: PlaybackCoordinator = PlaybackCoordinator.getInstance()
   private playbackStateBridge: PlaybackStateBridge = PlaybackStateBridge.getInstance()
   private playbackStateBridge: PlaybackStateBridge = PlaybackStateBridge.getInstance()
   private playbackRuntime: PlaybackRuntime = {
   private playbackRuntime: PlaybackRuntime = {
-    playQueue: async (queue: VideoItem[], startIndex: number, source: string) => {
+    playQueue: async (queue: VideoItem[], startIndex: number, source: string, playType?: number) => {
       const safeIndex = queue.length <= 0 ? -1 : Math.max(0, Math.min(startIndex, queue.length - 1))
       const safeIndex = queue.length <= 0 ? -1 : Math.max(0, Math.min(startIndex, queue.length - 1))
       if (safeIndex < 0) {
       if (safeIndex < 0) {
         return
         return
       }
       }
+      const findPagedLocalMeta = resolveFindPagedLocalQueueMetaForPlayback(queue, safeIndex)
+      if (playType !== undefined) {
+        this.applyRequestedPlayType(playType)
+      }
+      if (findPagedLocalMeta?.isPagedLocalQueue) {
+        this.playQueueScope = 'find_paged_local'
+        this.applyFindPagedLocalQueueState(findPagedLocalMeta, findPagedLocalMeta.totalCount)
+        this.currentPlaylistSongFilePaths = []
+      } else {
+        this.resetFindPagedLocalQueueState()
+      }
       this.songList = [...queue]
       this.songList = [...queue]
       this.currentSongList = [...queue]
       this.currentSongList = [...queue]
       this.sonDataSource.pushArrayData(this.songList)
       this.sonDataSource.pushArrayData(this.songList)
       this.curIndex = safeIndex
       this.curIndex = safeIndex
       this.currentSong = queue[safeIndex]
       this.currentSong = queue[safeIndex]
       AppStorage.setOrCreate('currentSong', this.currentSong)
       AppStorage.setOrCreate('currentSong', this.currentSong)
-      await this.doPlay(queue[safeIndex], safeIndex, false, source === 'controller-open')
+      if (source === 'controller-open') {
+        this.setShowPlayTrue()
+      }
+      await this.doPlay(queue[safeIndex], safeIndex, true)
       this.playbackStateBridge.replaceQueue(this.songList, this.curIndex)
       this.playbackStateBridge.replaceQueue(this.songList, this.curIndex)
       this.playbackStateBridge.updatePlaybackState(
       this.playbackStateBridge.updatePlaybackState(
         this.CONTROL_PlayStatus === PlayStatus.PLAY,
         this.CONTROL_PlayStatus === PlayStatus.PLAY,

+ 15 - 21
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -29,17 +29,16 @@ import { embyApi, EmbyAlbum, EmbyArtist, EmbyPlaylist, EmbySong } from '../commo
 import { audioStationApi, AudioStationAlbum, AudioStationArtist, AudioStationPlaylist, AudioStationSong } from '../common/network/AudioStationApi';
 import { audioStationApi, AudioStationAlbum, AudioStationArtist, AudioStationPlaylist, AudioStationSong } from '../common/network/AudioStationApi';
 import { plexApi, PlexAlbum, PlexArtist, PlexPlaylist, PlexSong } from '../common/network/PlexApi';
 import { plexApi, PlexAlbum, PlexArtist, PlexPlaylist, PlexSong } from '../common/network/PlexApi';
 import { daoLiYuApi, DaoLiYuAlbum, DaoLiYuArtist, DaoLiYuTrack, DaoLiYuPlaylist } from '../common/network/DaoLiYuApi';
 import { daoLiYuApi, DaoLiYuAlbum, DaoLiYuArtist, DaoLiYuTrack, DaoLiYuPlaylist } from '../common/network/DaoLiYuApi';
-import { getRemoteDriveDisplayLabel } from '../common/util/RemoteDriveLabel';
 import NavidromeListCache, { AllCacheData } from '../common/util/NavidromeListCache';
 import NavidromeListCache, { AllCacheData } from '../common/util/NavidromeListCache';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { taskpool } from '@kit.ArkTS';
 import { taskpool } from '@kit.ArkTS';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { PlayingIndicator } from './PlayingIndicator';
 import { PlayingIndicator } from './PlayingIndicator';
 import { hdsEffect } from '@kit.UIDesignKit';
 import { hdsEffect } from '@kit.UIDesignKit';
-import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
 import { filterAlbumsByKeyword, filterArtistsByKeyword, filterPlaylistsByKeyword } from '../common/util/RemoteMusicSearchHelper';
 import { filterAlbumsByKeyword, filterArtistsByKeyword, filterPlaylistsByKeyword } from '../common/util/RemoteMusicSearchHelper';
+import { MusicPlaybackController } from '../controller/MusicPlaybackController';
+import { shouldRenderRemoteMusicSongView } from '../common/util/RemoteMusicViewModeHelper';
 
 
-const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 const NAVIDROME_SEARCH_LIMIT = 500;
 const NAVIDROME_SEARCH_LIMIT = 500;
 
 
 const ITEM_HEIGHT_SMALL: number = 48; // 列表项小高度
 const ITEM_HEIGHT_SMALL: number = 48; // 列表项小高度
@@ -342,6 +341,7 @@ export struct RemoteMusicPage {
   @State sortType: number = 0; // 排序类型 0:名称升序 1:名称降序 2:时间升序 3:时间降序 4:大小升序 5:大小降序
   @State sortType: number = 0; // 排序类型 0:名称升序 1:名称降序 2:时间升序 3:时间降序 4:大小升序 5:大小降序
   @State isSearchLoading: boolean = false;
   @State isSearchLoading: boolean = false;
   private readonly REMOTE_SEARCH_LIMIT: number = 500;
   private readonly REMOTE_SEARCH_LIMIT: number = 500;
+  private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
 
 
   // SegmentButton选项
   // SegmentButton选项
   @State tabOptions: SegmentButtonOptions = this.createTabOptions();
   @State tabOptions: SegmentButtonOptions = this.createTabOptions();
@@ -3842,22 +3842,16 @@ export struct RemoteMusicPage {
       if (this.serverTotalSongCount > 0) {
       if (this.serverTotalSongCount > 0) {
         setNavidromeTotalCount(this.serverTotalSongCount);
         setNavidromeTotalCount(this.serverTotalSongCount);
       }
       }
-
-      const playlistData = new PlaylistPlayRequest(
-        NAVIDROME_PLAYLIST_ID,
-        `${getRemoteDriveDisplayLabel(account.webType)} - ${account.name ?? '未知账户'}`,
-        this.serverTotalSongCount > 0 ? this.serverTotalSongCount : playlistSource.length,
+      void this.playbackController.playQueue(
+        playlistSource.slice(),
         startIndex,
         startIndex,
-        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', `播放事件已发送`);
-      void ServerLogUtil.debug('StreamingPlay', `播放事件详情: 列表ID=${playlistData.playlistId}, 列表名称=${playlistData.playlistName}, 歌曲数=${playlistData.songCount}, 起始索引=${playlistData.startIndex}, 是否跳转=${playlistData.isJump}`);
+        isJump ? 'controller-open' : 'remote-music'
+      ).then(() => {
+        void ServerLogUtil.info('StreamingPlay', '播放请求已通过 controller 发送');
+      }).catch((error: Error) => {
+        void ServerLogUtil.error('StreamingPlay', `播放失败: ${error.message}`);
+        ToastUtil.showToast('播放失败');
+      });
 
 
 
 
     } catch (error) {
     } catch (error) {
@@ -4006,7 +4000,7 @@ export struct RemoteMusicPage {
       }
       }
 
 
 
 
-      if (this.selectedTab === 0||this.isDetailView) {
+      if (shouldRenderRemoteMusicSongView(this.selectedTab, this.isDetailView)) {
         LazyForEach(this.songDataSource, (item: VideoItem, index: number) => {
         LazyForEach(this.songDataSource, (item: VideoItem, index: number) => {
           GridItem() {
           GridItem() {
             this.buildSongItemGrid(item, index)
             this.buildSongItemGrid(item, index)
@@ -4579,7 +4573,7 @@ export struct RemoteMusicPage {
   getListView() {
   getListView() {
     List({scroller:this.scroller, space: 8 }) {
     List({scroller:this.scroller, space: 8 }) {
       // 详情视图模式下显示筛选后的歌曲,否则根据标签页显示对应内容
       // 详情视图模式下显示筛选后的歌曲,否则根据标签页显示对应内容
-      if (this.selectedTab === 0||this.isDetailView) {
+      if (shouldRenderRemoteMusicSongView(this.selectedTab, this.isDetailView)) {
         LazyForEach(this.songDataSource, (item: VideoItem, index: number) => {
         LazyForEach(this.songDataSource, (item: VideoItem, index: number) => {
           ListItem() {
           ListItem() {
             this.buildSongItem(item, index)
             this.buildSongItem(item, index)
@@ -4742,7 +4736,7 @@ export struct RemoteMusicPage {
           layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW
           layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW
         }) {
         }) {
 
 
-          if (this.selectedTab === 0) {
+          if (shouldRenderRemoteMusicSongView(this.selectedTab, this.isDetailView)) {
             // 歌曲瀑布流
             // 歌曲瀑布流
             LazyForEach(this.songDataSource, (item: VideoItem, index: number) => {
             LazyForEach(this.songDataSource, (item: VideoItem, index: number) => {
               FlowItem() {
               FlowItem() {

+ 26 - 0
entry/src/ohosTest/ets/test/MusicPlaybackController.test.ets

@@ -42,6 +42,32 @@ export default function musicPlaybackControllerTest() {
       coordinator.clearRuntime()
       coordinator.clearRuntime()
     })
     })
 
 
+    it('controllerPlayQueuePropagatesOptionalPlayType', 0, async () => {
+      const controller = MusicPlaybackController.getInstance()
+      const coordinator = PlaybackCoordinator.getInstance()
+      const calls: string[] = []
+      const songs: VideoItem[] = [
+        new VideoItem('Song A', '1', '/music/a.flac', 0, 0, '2026-04-02')
+      ]
+
+      controller.clearActions()
+      coordinator.clearRuntime()
+      coordinator.setRuntime({
+        playQueue: async (_queue, startIndex, source, playType) => {
+          calls.push(`${startIndex}:${source}:${playType ?? -1}`)
+        },
+        playOrPause: async () => {},
+        playNext: async () => {},
+        playPrevious: async () => {},
+        seekTo: async () => {}
+      })
+
+      await controller.playQueue(songs, 0, 'find-view', 3)
+
+      expect(calls.join(',')).assertEqual('0:find-view:3')
+      coordinator.clearRuntime()
+    })
+
     it('dispatchRegisteredActions', 0, async () => {
     it('dispatchRegisteredActions', 0, async () => {
       const controller = MusicPlaybackController.getInstance()
       const controller = MusicPlaybackController.getInstance()
       const calls: string[] = []
       const calls: string[] = []

+ 24 - 0
entry/src/ohosTest/ets/test/PlaybackCoordinator.test.ets

@@ -45,6 +45,30 @@ export default function playbackCoordinatorTest() {
       coordinator.clearRuntime()
       coordinator.clearRuntime()
     })
     })
 
 
+    it('playQueuePropagatesPlayTypeToRuntime', 0, async () => {
+      const coordinator = PlaybackCoordinator.getInstance()
+      const calls: string[] = []
+      const songs: VideoItem[] = [
+        new VideoItem('Song A', '1', '/music/a.flac', 0, 0, '2026-04-02')
+      ]
+
+      coordinator.clearRuntime()
+      coordinator.setRuntime({
+        playQueue: async (_queue, startIndex, source, playType) => {
+          calls.push(`${startIndex}:${source}:${playType ?? -1}`)
+        },
+        playOrPause: async () => {},
+        playNext: async () => {},
+        playPrevious: async () => {},
+        seekTo: async () => {}
+      })
+
+      await coordinator.playQueue(songs, 0, 'find-view', 3)
+
+      expect(calls.join(',')).assertEqual('0:find-view:3')
+      coordinator.clearRuntime()
+    })
+
     it('playSongPropagatesStartIndexAndSourceAndUpdatesBridge', 0, async () => {
     it('playSongPropagatesStartIndexAndSourceAndUpdatesBridge', 0, async () => {
       const coordinator = PlaybackCoordinator.getInstance()
       const coordinator = PlaybackCoordinator.getInstance()
       const bridge = PlaybackStateBridge.getInstance()
       const bridge = PlaybackStateBridge.getInstance()