ソースを参照

优化发光效果

onecold 4 ヶ月 前
コミット
027c89714f

+ 2 - 2
AppScope/app.json5

@@ -2,8 +2,8 @@
   "app": {
     "bundleName": "com.xgplayer.ttmusic.hm",
     "vendor": "example",
-    "versionCode": 20251224,
-    "versionName": "1.8.0",
+    "versionCode": 20260328,
+    "versionName": "2.0.6",
     "icon": "$media:app_icon",
     "label": "$string:app_name",
     "multiAppMode": {

+ 128 - 0
entry/src/main/ets/common/util/FindDiscoveryHelper.ets

@@ -0,0 +1,128 @@
+import { StrUtil } from '@pura/harmony-utils'
+import { VideoItem } from '../../viewmodel/VideoItem'
+
+class IndexedDiscoverySong {
+  song: VideoItem
+  index: number
+
+  constructor(song: VideoItem, index: number) {
+    this.song = song
+    this.index = index
+  }
+}
+
+export enum FindCollectionSortType {
+  TRACK_ASC = 0,
+  TRACK_DESC = 1,
+  NAME_ASC = 2,
+  NAME_DESC = 3,
+  TIME_ASC = 4,
+  TIME_DESC = 5,
+  RECENT_DESC = 6
+}
+
+function parseTrackNumber(track?: string): number {
+  if (StrUtil.isEmpty(track)) {
+    return Number.MAX_SAFE_INTEGER
+  }
+  const normalizedTrack = (track as string).split('/')[0].trim()
+  const parsedTrack = Number.parseInt(normalizedTrack, 10)
+  return Number.isNaN(parsedTrack) || parsedTrack <= 0 ? Number.MAX_SAFE_INTEGER : parsedTrack
+}
+
+function parseDateValue(value?: string): number {
+  if (StrUtil.isEmpty(value)) {
+    return 0
+  }
+  const parsed = Date.parse(value as string)
+  return Number.isNaN(parsed) ? 0 : parsed
+}
+
+function compareTrackNumber(leftTrack: number, rightTrack: number, descending: boolean): number {
+  const leftMissing = leftTrack === Number.MAX_SAFE_INTEGER
+  const rightMissing = rightTrack === Number.MAX_SAFE_INTEGER
+  if (leftMissing && rightMissing) {
+    return 0
+  }
+  if (leftMissing) {
+    return 1
+  }
+  if (rightMissing) {
+    return -1
+  }
+  return descending ? rightTrack - leftTrack : leftTrack - rightTrack
+}
+
+export function getDiscoverySongTitle(item: VideoItem): string {
+  if (StrUtil.isNotEmpty(item.name)) {
+    return item.name
+  }
+  if (StrUtil.isNotEmpty(item.fileName)) {
+    return item.fileName as string
+  }
+  return ''
+}
+
+function compareSongsBySortType(left: VideoItem, right: VideoItem, sortType: FindCollectionSortType): number {
+  switch (sortType) {
+    case FindCollectionSortType.TRACK_ASC:
+      return compareTrackNumber(parseTrackNumber(left.track), parseTrackNumber(right.track), false)
+    case FindCollectionSortType.TRACK_DESC:
+      return compareTrackNumber(parseTrackNumber(left.track), parseTrackNumber(right.track), true)
+    case FindCollectionSortType.NAME_ASC:
+      return getDiscoverySongTitle(left).localeCompare(getDiscoverySongTitle(right))
+    case FindCollectionSortType.NAME_DESC:
+      return getDiscoverySongTitle(right).localeCompare(getDiscoverySongTitle(left))
+    case FindCollectionSortType.TIME_ASC:
+      return parseDateValue(left.cTime) - parseDateValue(right.cTime)
+    case FindCollectionSortType.TIME_DESC:
+      return parseDateValue(right.cTime) - parseDateValue(left.cTime)
+    case FindCollectionSortType.RECENT_DESC:
+      return parseDateValue(right.lastPlayedStr) - parseDateValue(left.lastPlayedStr)
+    default:
+      return 0
+  }
+}
+
+export function buildSortedDiscoverySongs(items: VideoItem[], sortType: FindCollectionSortType): VideoItem[] {
+  const indexedItems: IndexedDiscoverySong[] = items.map((song: VideoItem, index: number) =>
+    new IndexedDiscoverySong(song, index))
+  indexedItems.sort((left, right) => {
+    const compareResult = compareSongsBySortType(left.song, right.song, sortType)
+    if (compareResult !== 0) {
+      return compareResult
+    }
+    return left.index - right.index
+  })
+  return indexedItems.map((item) => item.song)
+}
+
+function filterUniquePlaybackSongs(items: VideoItem[]): VideoItem[] {
+  const result: VideoItem[] = []
+  const seen: Set<string> = new Set<string>()
+  for (let index = 0; index < items.length; index += 1) {
+    const item = items[index]
+    const filePath = item.filePath || ''
+    if (StrUtil.isEmpty(filePath) || seen.has(filePath)) {
+      continue
+    }
+    seen.add(filePath)
+    result.push(item)
+  }
+  return result
+}
+
+export function buildPreferredRemotePlaybackPool(indexedSongs: VideoItem[], fallbackSongs: VideoItem[]): VideoItem[] {
+  const indexedPool = filterUniquePlaybackSongs(indexedSongs)
+  if (indexedPool.length > 0) {
+    return indexedPool
+  }
+  return filterUniquePlaybackSongs(fallbackSongs)
+}
+
+export function resolveQueueStartIndex(queue: VideoItem[], filePath: string): number {
+  if (StrUtil.isEmpty(filePath)) {
+    return -1
+  }
+  return queue.findIndex((item: VideoItem) => item.filePath === filePath)
+}

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

@@ -3233,6 +3233,36 @@ export class RemoteDriveManager {
     };
   }
 
+  public isGlobalSearchIndexReady(account: WebDavAccount): boolean {
+    if (!account || !this.supportsGlobalSearchIndex(account)) {
+      return false;
+    }
+    const index = this.getOrCreateGlobalSearchIndex(account);
+    return index.lastBuiltAt > 0;
+  }
+
+  public supportsGlobalSearchIndexForAccount(account: WebDavAccount): boolean {
+    return !!account && this.supportsGlobalSearchIndex(account);
+  }
+
+  public async getGlobalSearchIndexSongs(account: WebDavAccount): Promise<VideoItem[]> {
+    if (!account || !this.supportsGlobalSearchIndex(account)) {
+      return [];
+    }
+    const index = this.getOrCreateGlobalSearchIndex(account);
+    if (this.currentAccount && this.currentAccount.id === account.id) {
+      this.seedCurrentDirectoryIntoGlobalSearchIndex(account);
+    }
+    if (index.isBuilding && index.buildPromise) {
+      await index.buildPromise;
+      return index.songs.slice();
+    }
+    if (index.lastBuiltAt === 0 && !index.lastError) {
+      await this.ensureGlobalSearchIndex(account);
+    }
+    return index.songs.slice();
+  }
+
   public async ensureGlobalSearchIndex(account: WebDavAccount): Promise<void> {
     if (!account || !this.supportsGlobalSearchIndex(account)) {
       return;

+ 104 - 28
entry/src/main/ets/pages/NewIndex.ets

@@ -58,6 +58,7 @@ import { RemoteMusicPage } from '../view/RemoteMusicPage';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { PlayStatus } from '../common/PlayStatus';
 import { PointLightDefaultButton } from '../view/PointLight/PointLightDeFaultButton';
+import { PointLightContentButton } from '../view/PointLight/PointLightContentButton';
 import { FindView } from '../view/FindView';
 import { hdsEffect } from '@kit.UIDesignKit';
 
@@ -846,68 +847,143 @@ struct NewIndex {
 
 
   @Builder
-  playConRigth() {
+  private buildPlayPreviousControlContent() {
     Row() {
-
       SymbolGlyph($r('sys.symbol.backward_end_fill'))
         .fontSize(28)
         .fontColor([Color.White])
-        .alignSelf(ItemAlign.Center)
-        .visibility(this.isShowPrecious? Visibility.Visible:Visibility.None)
-        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-        .margin({ left: 8, right: 14 })
-        .displayPriority(2)
-        .onClick(() => {
-          this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
-        })
-      //播放进度条
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Center)
+    .alignItems(VerticalAlign.Center)
+  }
+
+  @Builder
+  private buildPlayToggleControlContent() {
+    Row() {
       Stack() {
         Progress({
           value: Math.floor(this.progressValue),
           type: ProgressType.Ring,
-
         })
-          .color(this.themeColor)// 进度条前景色为灰色
+          .color(this.themeColor)
           .height(38)
           .aspectRatio(CommonConstants.ASPECT_RATIO)
         Image(this.CONTROL_PlayStatus === PlayStatus.PLAY ? $r('app.media.hm_pause') : $r('app.media.hm_play2'))
           .height(36)
           .width(36)
-          .displayPriority(3)
-          .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
           .fillColor(Color.White)
-          .onClick(() => {
-            this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
-          })
       }
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Center)
+    .alignItems(VerticalAlign.Center)
+  }
 
-
+  @Builder
+  private buildPlayNextControlContent() {
+    Row() {
       SymbolGlyph($r('sys.symbol.forward_end_fill'))
         .fontSize(28)
         .fontColor([Color.White])
-        .alignSelf(ItemAlign.Center)
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Center)
+    .alignItems(VerticalAlign.Center)
+  }
+
+  @Builder
+  private buildPlayListControlContent() {
+    Row() {
+      SymbolGlyph($r('sys.symbol.music_note_list'))
+        .fontSize(28)
+        .fontColor([Color.White])
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Center)
+    .alignItems(VerticalAlign.Center)
+  }
+
+  @Builder
+  playConRigth() {
+    Row() {
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 22,
+        pointLightHeight: 56,
+        pressScale: 0.88,
+        builder: () => {
+          this.buildPlayPreviousControlContent()
+        }
+      })
+        .width(44)
+        .height(44)
+        .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
+        .margin({ left: 5, right: 5 })
+        .displayPriority(2)
+        .onClick(() => {
+          this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
+        })
+      // 播放进度条与播放键共用一个点光点击区域
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 23,
+        pointLightHeight: 62,
+        pressScale: 0.9,
+        builder: () => {
+          this.buildPlayToggleControlContent()
+        }
+      })
+        .width(46)
+        .height(46)
+        .displayPriority(3)
+        .onClick(() => {
+          this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
+        })
+
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 22,
+        pointLightHeight: 56,
+        pressScale: 0.88,
+        builder: () => {
+          this.buildPlayNextControlContent()
+        }
+      })
+        .width(44)
+        .height(44)
         .margin({
-          right: 14,
-          left: 14
+          right: 5,
+          left: 5
         })
-        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
         .displayPriority(2)
         .onClick(() => {
           this.getUIContext().getHostContext()!.eventHub.emit('playNext');
         })
 
-      Image($r('app.media.hm_playlist'))
-        .height(26)
-        .width(26)
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 21,
+        pointLightHeight: 52,
+        pressScale: 0.88,
+        builder: () => {
+          this.buildPlayListControlContent()
+        }
+      })
+        .width(42)
+        .height(42)
         .displayPriority(1)
-        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-        .fillColor(Color.White)
         .onClick(() => {
           this.getUIContext().getHostContext()!.eventHub.emit('openPlayList');
         })
     }
     .margin({left:this.curDisplayIsHiCar?20:5})
     .justifyContent(this.curDisplayIsHiCar?FlexAlign.Start:FlexAlign.End)
+    .alignItems(VerticalAlign.Center)
   }
 
 

+ 8 - 11
entry/src/main/ets/pages/SettingPage.ets

@@ -18,7 +18,6 @@ import { bundleManager, common as AbilityCommon } from '@kit.AbilityKit'
 import { hilog } from '@kit.PerformanceAnalysisKit'
 import { SelectItem } from './SelectItem'
 import { FastForwardSecondInterface } from './FastForwardSecondInterface'
-import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'
 import { CustomizeICON, Icon } from '../view/CustomizeICON'
 import { appInfoManager } from '@kit.StoreKit'
 import { clearAllRemoteCaches } from '../common/network/RemoteSongCache';
@@ -30,6 +29,7 @@ import { BackupManageView } from '../view/BackupManageView';
 import { audio } from '@kit.AudioKit'
 import cacheManageService, { CacheDirectoryInfo } from '../common/util/CacheManageService'
 import { CacheManageSheet } from '../view/CacheManageSheet'
+import { TitleBarPointLightButton } from '../view/PointLight/TitleBarPointLightButton'
 
 @Preview
 // @Entry
@@ -876,19 +876,16 @@ export struct SettingPage {
       Row({ space: 15 }) {
 
         //左侧滑动按钮
-        Button({ type: ButtonType.Circle, stateEffect: true }) {
-          SymbolGlyph($r('sys.symbol.chevron_left'))
-            .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
-        }
-        .attributeModifier(new ButtonFancyModifier(40, 40))
-        .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
-        .animation({ duration: 300, curve: Curve.Ease })
-        .onClick(() => {
+        TitleBarPointLightButton({
+          iconResource: $r('sys.symbol.chevron_left'),
+          iconSize: 25,
+          pointColor: this.themeColor,
+          clickScale: 0.6,
+          clickHandler: () => {
             const eventData: emitter.EventData = {};
             emitter.emit({ eventId: EventConstants.EVENT_SETTING_BACK_TO_HOME }, eventData);
+          }
         })
-        .attributeModifier(new ShadowModifier())
-        .zIndex(0)
 
         Text($r('app.string.setting'))
           .margin({left:3,right:10})

+ 8 - 6
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -2990,17 +2990,21 @@ export struct WebDavMainPage {
             iconResource: $r('sys.symbol.list_number'),
             iconSize: 25,
             pointColor: this.themeColor,
-            clickScale: 0.6
+            clickScale: 0.6,
+            menuBuilder: () => {
+              this.SortMenuBuilder()
+            }
           })
-          .bindMenu(this.SortMenuBuilder)
           //添加/上传综合按钮
           TitleBarPointLightButton({
             iconResource: $r('sys.symbol.plus'),
             iconSize: 25,
             pointColor: this.themeColor,
-            clickScale: 0.6
+            clickScale: 0.6,
+            menuBuilder: () => {
+              this.MoreMenuBuilder()
+            }
           })
-          .bindMenu(this.MoreMenuBuilder)
           .bindContentCover($$this.isShowUploadFile, this.UploadFielBuilder(), {
             transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
           })
@@ -4244,5 +4248,3 @@ export struct WebDavMainPage {
     }
   }
 }
-
-

+ 36 - 97
entry/src/main/ets/view/FindAlbumDetail.ets

@@ -1,20 +1,12 @@
 import { SymbolGlyphModifier } from '@kit.ArkUI'
 import { StrUtil } from '@pura/harmony-utils'
 import { CommonConstants } from '../common/constants/CommonConstants'
-import { ButtonFancyModifier, MenuModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'
+import { MenuModifier } from '../common/util/AttributeModifierUtil'
 import { VideoItem } from '../viewmodel/VideoItem'
 import { PlayingIndicator } from './PlayingIndicator'
 import { PointLightContentButton } from './PointLight/PointLightContentButton'
-
-class AlbumSortSongItem {
-  song: VideoItem
-  index: number
-
-  constructor(song: VideoItem, index: number) {
-    this.song = song
-    this.index = index
-  }
-}
+import { TitleBarPointLightButton } from './PointLight/TitleBarPointLightButton'
+import { buildSortedDiscoverySongs, FindCollectionSortType } from '../common/util/FindDiscoveryHelper'
 
 function getFindAlbumSongKey(item: VideoItem, index: number): string {
   if (StrUtil.isNotEmpty(item.filePath)) {
@@ -39,9 +31,11 @@ export struct FindAlbumDetail {
   @Prop topSafeHeight: number = 0
   @Prop bottomSafeHeight: number = 0
   @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
+  @Prop defaultSortType: number = FindCollectionSortType.TRACK_ASC
+  @Prop supportRecentPlayedSort: boolean = false
   @StorageProp('isDarkMode') isDarkMode: boolean = false
   @State private displaySongs: VideoItem[] = []
-  @State private sortType: number = 0
+  @State private sortType: number = FindCollectionSortType.TRACK_ASC
   @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined
   @StorageLink('isPlaying') isPlaying: boolean = false
   @Prop allowDelete: boolean = false
@@ -53,7 +47,7 @@ export struct FindAlbumDetail {
   onDeleteSong: (song: VideoItem) => void = (_song: VideoItem) => {}
 
   aboutToAppear(): void {
-    this.applySortType(this.sortType)
+    this.applySortType(this.defaultSortType)
   }
 
   private onSongsChange(): void {
@@ -71,71 +65,9 @@ export struct FindAlbumDetail {
     return this.songs.slice()
   }
 
-  private parseTrackNumber(item: VideoItem): number {
-    if (StrUtil.isEmpty(item.track)) {
-      return Number.MAX_SAFE_INTEGER
-    }
-    const normalizedTrack = (item.track as string).split('/')[0].trim()
-    const parsedTrack = Number.parseInt(normalizedTrack, 10)
-    return Number.isNaN(parsedTrack) ? Number.MAX_SAFE_INTEGER : parsedTrack
-  }
-
-  private parseSongTime(item: VideoItem): number {
-    if (StrUtil.isEmpty(item.cTime)) {
-      return 0
-    }
-    const parsedTime = Date.parse(item.cTime as string)
-    return Number.isNaN(parsedTime) ? 0 : parsedTime
-  }
-
-  private compareTrackNumber(left: VideoItem, right: VideoItem, descending: boolean): number {
-    const leftTrack = this.parseTrackNumber(left)
-    const rightTrack = this.parseTrackNumber(right)
-    const leftMissing = leftTrack === Number.MAX_SAFE_INTEGER
-    const rightMissing = rightTrack === Number.MAX_SAFE_INTEGER
-    if (leftMissing && rightMissing) {
-      return 0
-    }
-    if (leftMissing) {
-      return 1
-    }
-    if (rightMissing) {
-      return -1
-    }
-    return descending ? rightTrack - leftTrack : leftTrack - rightTrack
-  }
-
-  private compareSongsBySortType(left: VideoItem, right: VideoItem, sortType: number): number {
-    switch (sortType) {
-      case 0:
-        return this.compareTrackNumber(left, right, false)
-      case 1:
-        return this.compareTrackNumber(left, right, true)
-      case 2:
-        return this.getSongTitle(left).localeCompare(this.getSongTitle(right))
-      case 3:
-        return this.getSongTitle(right).localeCompare(this.getSongTitle(left))
-      case 4:
-        return this.parseSongTime(left) - this.parseSongTime(right)
-      case 5:
-        return this.parseSongTime(right) - this.parseSongTime(left)
-      default:
-        return 0
-    }
-  }
-
   private applySortType(sortType: number): void {
     this.sortType = sortType
-    const indexedSongs: AlbumSortSongItem[] = this.songs.map((song: VideoItem, index: number) =>
-      new AlbumSortSongItem(song, index))
-    indexedSongs.sort((left, right) => {
-      const compareResult = this.compareSongsBySortType(left.song, right.song, sortType)
-      if (compareResult !== 0) {
-        return compareResult
-      }
-      return left.index - right.index
-    })
-    this.displaySongs = indexedSongs.map((item) => item.song)
+    this.displaySongs = buildSortedDiscoverySongs(this.songs, sortType as FindCollectionSortType)
   }
 
   private getSongTitle(item: VideoItem): string {
@@ -348,6 +280,17 @@ export struct FindAlbumDetail {
         .onClick(() => {
           this.applySortType(5)
         })
+      MenuItem({
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')),
+        content: '按最近播放时间',
+        symbolEndIcon: this.sortType === FindCollectionSortType.RECENT_DESC ?
+          new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
+      })
+        .visibility(this.supportRecentPlayedSort ? Visibility.Visible : Visibility.None)
+        .backgroundColor(this.getSortItemBackground(FindCollectionSortType.RECENT_DESC))
+        .onClick(() => {
+          this.applySortType(FindCollectionSortType.RECENT_DESC)
+        })
     }
     .attributeModifier(new MenuModifier())
   }
@@ -633,29 +576,25 @@ export struct FindAlbumDetail {
   private topTitleBar() {
     Column() {
       Row() {
-        Button({ type: ButtonType.Circle, stateEffect: true }) {
-          SymbolGlyph($r('sys.symbol.chevron_left'))
-            .attributeModifier(new SymbolGlyphFancyModifier(24, '', ''))
-        }
-        .attributeModifier(new ButtonFancyModifier(40, 40))
-        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-        .animation({ duration: 300, curve: Curve.Ease })
-        .onClick(() => {
-           this.onBack()
+        TitleBarPointLightButton({
+          iconResource: $r('sys.symbol.chevron_left'),
+          iconSize: 24,
+          pointColor: this.themeColor,
+          clickScale: 0.6,
+          clickHandler: () => {
+            this.onBack()
+          }
         })
-        .attributeModifier(new ShadowModifier())
-        .zIndex(0)
         Blank()
-        Button({ type: ButtonType.Circle, stateEffect: true }) {
-          SymbolGlyph($r('sys.symbol.list_number'))
-            .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
-        }
-        .attributeModifier(new ButtonFancyModifier(40, 40))
-        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-        .animation({ duration: 300, curve: Curve.Ease })
-        .bindMenu(this.SortMenuBuilder)
-        .attributeModifier(new ShadowModifier())
-        .zIndex(0)
+        TitleBarPointLightButton({
+          iconResource: $r('sys.symbol.list_number'),
+          iconSize: 25,
+          pointColor: this.themeColor,
+          clickScale: 0.6,
+          menuBuilder: () => {
+            this.SortMenuBuilder()
+          }
+        })
     }
     .padding({ top: this.topSafeHeight + 10, left: 10, right: 10, bottom: 6 })
     .width('100%')

+ 127 - 45
entry/src/main/ets/view/FindView.ets

@@ -25,6 +25,12 @@ import { setFindPlaylist } from '../common/util/FindPlaylistStore'
 import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore'
 import PlaylistTable from '../common/util/PlaylistTable'
 import { convertPlaylistSongsToVideoItems } from '../pages/PlaylistDetailPage'
+import {
+  buildPreferredRemotePlaybackPool,
+  buildSortedDiscoverySongs,
+  FindCollectionSortType,
+  resolveQueueStartIndex
+} from '../common/util/FindDiscoveryHelper'
 
 @Builder
 export function FindViewBuilder() {
@@ -143,6 +149,8 @@ export struct FindView {
   @State private currentAlbumCoverPath: string = ''
   @State private currentAlbumSourceLabel: string = ''
   @State private currentAlbumSongs: VideoItem[] = []
+  @State private currentAlbumDefaultSortType: number = FindCollectionSortType.TRACK_ASC
+  @State private currentAlbumSupportRecentPlayedSort: boolean = false
   @State private searchText: string = ''
   @State private searchResults: VideoItem[] = []
   @State private searchHistoryItems: string[] = []
@@ -183,6 +191,7 @@ export struct FindView {
   private featuredAlbumsPool: FindAlbumGroup[] = []
   private cloudAlbumsPool: FindAlbumGroup[] = []
   private searchRemoteSongsPool: VideoItem[] = []
+  private remotePlaybackPool: VideoItem[] = []
   private readonly searchHistoryScope: string = 'find_music'
   private searchTicket: number = 0
   private cloudSectionPageVersion: number = 0
@@ -284,22 +293,25 @@ export struct FindView {
       const uniqueRecentSongs = this.filterUniqueSongs(recentSongs)
       const uniqueTopPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
       const uniqueFavoriteSongs = this.filterUniqueSongs(favoriteSongs)
+      const sortedRecentSongs = buildSortedDiscoverySongs(uniqueRecentSongs, FindCollectionSortType.RECENT_DESC)
+      const sortedFavoriteSongs = buildSortedDiscoverySongs(uniqueFavoriteSongs, FindCollectionSortType.NAME_ASC)
 
       this.playlistSongsCache.clear()
       this.localSongsPool = uniqueLocalSongs
       this.coveredSongsPool = coveredSongs
+      this.remotePlaybackPool = []
       this.applyRemoteDiscoverySongs(uniqueRemoteSongs)
-      this.recentSongsPool = uniqueRecentSongs
+      this.recentSongsPool = sortedRecentSongs
       this.topPlayedSongsPool = uniqueTopPlayedSongs
-      this.favoriteSongsPool = uniqueFavoriteSongs
+      this.favoriteSongsPool = sortedFavoriteSongs
       this.heartPlaylists = playlists
       this.featuredAlbumsPool = this.buildAlbumGroups(this.localSongsPool, false)
       this.searchRemoteSongsPool = []
       this.swiperSongs = this.pickRandomSongs(this.coveredSongsPool, SWIPER_MAX_COUNT)
       this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
-      this.recentSongs = this.pickPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT)
+      this.recentSongs = this.recentSongsPool.slice(0, Math.min(RECENT_SECTION_COUNT, this.recentSongsPool.length))
       this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
-      this.favoriteSongs = this.pickPreferredSongs(this.favoriteSongsPool, FAVORITE_SECTION_COUNT)
+      this.favoriteSongs = this.favoriteSongsPool.slice(0, Math.min(FAVORITE_SECTION_COUNT, this.favoriteSongsPool.length))
       this.featuredAlbums = this.pickAlbumGroups(this.featuredAlbumsPool, FEATURED_ALBUM_COUNT)
       this.swiperIndex = 0
       this.resetPagedSectionIndices()
@@ -337,6 +349,7 @@ export struct FindView {
       this.featuredAlbumsPool = []
       this.cloudAlbumsPool = []
       this.searchRemoteSongsPool = []
+      this.remotePlaybackPool = []
       this.featuredAlbums = []
       this.featuredAlbumPages = []
       this.cloudAlbums = []
@@ -363,7 +376,6 @@ export struct FindView {
 
   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)
@@ -380,7 +392,7 @@ export struct FindView {
     if (!this.mediaTable) {
       return
     }
-    if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.searchRemoteSongsPool.length > 0) {
+    if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0) {
       return
     }
     const remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
@@ -391,11 +403,7 @@ export struct FindView {
     await this.bootstrapRemoteDiscoverySongsIfNeeded()
   }
 
-  private async bootstrapRemoteDiscoverySongsIfNeeded(): Promise<void> {
-    if (this.isRemoteBootstrapLoading || !this.mediaTable || this.remoteSongsPool.length > 0) {
-      return
-    }
-    this.isRemoteBootstrapLoading = true
+  private async prepareActiveRemoteAccount(): Promise<WebDavAccount | undefined> {
     try {
       const context = getContext(this) as common.Context
       this.remoteDriveManager.setContext(context)
@@ -403,9 +411,39 @@ export struct FindView {
       await this.remoteDriveManager.queryWebDavAccountsFromDB()
       const accounts: WebDavAccount[] = this.remoteDriveManager.getAllWebDavAccounts()
       if (accounts.length === 0) {
-        return
+        return undefined
       }
-      const account = this.remoteDriveManager.getActivatedWebDavAccount() || accounts[0]
+      return this.remoteDriveManager.getActivatedWebDavAccount() || accounts[0]
+    } catch (error) {
+      Logger.warn(TAG, `发现页加载远程账号失败: ${this.toErrorMessage(error)}`)
+      return undefined
+    }
+  }
+
+  private async queryIndexedRemotePlaybackSongs(): Promise<VideoItem[]> {
+    const account = await this.prepareActiveRemoteAccount()
+    if (!account) {
+      return []
+    }
+    const supportsGlobalIndex = this.remoteDriveManager.supportsGlobalSearchIndexForAccount(account)
+    const isIndexReady = supportsGlobalIndex && this.remoteDriveManager.isGlobalSearchIndexReady(account)
+    if (supportsGlobalIndex && !isIndexReady) {
+      ToastUtil.showToast('首次云端漫游正在建立全量歌曲索引,请稍候')
+    }
+    const indexedSongs = await this.remoteDriveManager.getGlobalSearchIndexSongs(account)
+    const clonedSongs = indexedSongs.map((item: VideoItem) => cloneVideoItem(item))
+    Logger.info(TAG,
+      `发现页全量云端索引池加载完成: account=${account.name}, type=${account.webType}, indexed=${clonedSongs.length}`)
+    return this.filterUniqueSongs(clonedSongs)
+  }
+
+  private async bootstrapRemoteDiscoverySongsIfNeeded(): Promise<void> {
+    if (this.isRemoteBootstrapLoading || !this.mediaTable || this.remoteSongsPool.length > 0) {
+      return
+    }
+    this.isRemoteBootstrapLoading = true
+    try {
+      const account = await this.prepareActiveRemoteAccount()
       if (!account) {
         return
       }
@@ -809,28 +847,8 @@ export struct FindView {
     return result
   }
 
-  private parseTrackNumber(track?: string): number {
-    if (!track) {
-      return 999999
-    }
-    const parsed = Number.parseInt(track, 10)
-    if (Number.isNaN(parsed) || parsed <= 0) {
-      return 999999
-    }
-    return parsed
-  }
-
   private sortAlbumSongs(items: VideoItem[]): VideoItem[] {
-    const copy = items.slice()
-    copy.sort((left: VideoItem, right: VideoItem) => {
-      const leftTrack = this.parseTrackNumber(left.track)
-      const rightTrack = this.parseTrackNumber(right.track)
-      if (leftTrack !== rightTrack) {
-        return leftTrack - rightTrack
-      }
-      return this.getSongTitle(left).localeCompare(this.getSongTitle(right))
-    })
-    return copy
+    return buildSortedDiscoverySongs(items, FindCollectionSortType.TRACK_ASC)
   }
 
   private pickAlbumCoverSong(items: VideoItem[]): VideoItem | undefined {
@@ -905,16 +923,67 @@ export struct FindView {
   }
 
   private async getFullRemoteSongsPool(): Promise<VideoItem[]> {
-    if (this.searchRemoteSongsPool.length > 0) {
-      return this.searchRemoteSongsPool
+    if (this.remotePlaybackPool.length > 0) {
+      return this.remotePlaybackPool
     }
     if (!this.mediaTable) {
       return []
     }
-    await this.ensureRemoteDiscoverySongsAvailable()
-    const remoteSongs = await this.mediaTable.queryRemoteSongsAsync()
-    this.searchRemoteSongsPool = this.filterUniqueSongs(remoteSongs)
-    return this.searchRemoteSongsPool
+    const indexedSongs = await this.queryIndexedRemotePlaybackSongs()
+    let remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync())
+    if (indexedSongs.length === 0 && remoteSongs.length === 0) {
+      await this.ensureRemoteDiscoverySongsAvailable()
+      remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync())
+    }
+    this.remotePlaybackPool = buildPreferredRemotePlaybackPool(indexedSongs, remoteSongs)
+    Logger.info(TAG, `发现页全量云端播放池已就绪: indexed=${indexedSongs.length}, db=${remoteSongs.length}, selected=${this.remotePlaybackPool.length}`)
+    return this.remotePlaybackPool
+  }
+
+  private async playFromFullRemoteQueue(playlistId: string, playlistName: string, fallbackSongs: VideoItem[],
+    targetSong?: VideoItem, randomStart: boolean = false): Promise<void> {
+    const fullQueue = await this.getFullRemoteSongsPool()
+    if (randomStart) {
+      const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
+      if (queue.length === 0) {
+        ToastUtil.showToast('需要先配置网盘并加载歌曲后才能播放')
+        return
+      }
+      const startIndex = Math.floor(Math.random() * queue.length)
+      this.emitPlaylistPlay(playlistId, playlistName, queue, startIndex, 3)
+      return
+    }
+
+    if (!targetSong) {
+      const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
+      if (queue.length === 0) {
+        ToastUtil.showToast('暂无可播放歌曲')
+        return
+      }
+      this.emitPlaylistPlay(playlistId, playlistName, queue, 0)
+      return
+    }
+
+    const targetFilePath = targetSong.filePath || ''
+    const fullIndex = resolveQueueStartIndex(fullQueue, targetFilePath)
+    if (fullIndex >= 0) {
+      this.emitPlaylistPlay(playlistId, playlistName, fullQueue, fullIndex)
+      return
+    }
+
+    const fallbackIndex = resolveQueueStartIndex(fallbackSongs, targetFilePath)
+    if (fallbackIndex >= 0) {
+      Logger.warn(TAG, `全量云端队列未命中当前歌曲,回退到当前卡片队列: ${targetFilePath}`)
+      this.emitPlaylistPlay(playlistId, playlistName, fallbackSongs, fallbackIndex)
+      return
+    }
+
+    const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
+    if (queue.length === 0) {
+      ToastUtil.showToast('暂无可播放歌曲')
+      return
+    }
+    this.emitPlaylistPlay(playlistId, playlistName, queue, 0)
   }
 
   private async handleActionButtonTap(type: string): Promise<void> {
@@ -937,8 +1006,7 @@ export struct FindView {
       ToastUtil.showToast('需要先配置网盘并加载歌曲后才能播放')
       return
     }
-    const startIndex = Math.floor(Math.random() * allRemoteSongs.length)
-    this.emitPlaylistPlay('find-random-cloud', '云端漫游', allRemoteSongs, startIndex, 3)
+    await this.playFromFullRemoteQueue('find-random-cloud', '云端漫游', this.remoteSongsPool, undefined, true)
   }
 
   private handleSwiperTap(index: number): void {
@@ -946,7 +1014,8 @@ export struct FindView {
   }
 
   private handleRemoteSongTap(index: number): void {
-    this.emitPlaylistPlay('find-cloud', '漫步云端', this.remoteSongs, index)
+    const targetSong = this.remoteSongs[index]
+    void this.playFromFullRemoteQueue('find-cloud', '漫步云端', this.remoteSongs, targetSong)
   }
 
   private handleHotSongTap(index: number): void {
@@ -954,7 +1023,8 @@ export struct FindView {
   }
 
   private handleCloudMoodSongTap(index: number): void {
-    this.emitPlaylistPlay('find-cloud-mood', '云卷云舒', this.cloudMoodSongs, index)
+    const targetSong = this.cloudMoodSongs[index]
+    void this.playFromFullRemoteQueue('find-cloud-mood', '云卷云舒', this.cloudMoodSongs, targetSong)
   }
 
   private handleRecentSongTap(index: number): void {
@@ -991,7 +1061,7 @@ export struct FindView {
   }
 
   private refreshRecentSongs(): void {
-    this.recentSongs = this.pickDifferentPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT, this.recentSongs)
+    this.recentSongs = this.recentSongsPool.slice(0, Math.min(RECENT_SECTION_COUNT, this.recentSongsPool.length))
     this.recentSectionPageIndex = 0
   }
 
@@ -1036,10 +1106,14 @@ export struct FindView {
 
   private openLocalSpecialList(target: string): void {
     if (target === 'recent') {
+      this.currentAlbumDefaultSortType = FindCollectionSortType.RECENT_DESC
+      this.currentAlbumSupportRecentPlayedSort = true
       this.openSongCollectionDetail('find-recent-collection', '最近播放', '', this.recentSongsPool)
       return
     }
     if (target === 'favorite') {
+      this.currentAlbumDefaultSortType = FindCollectionSortType.NAME_ASC
+      this.currentAlbumSupportRecentPlayedSort = false
       this.openSongCollectionDetail('find-favorite-collection', '我的收藏', '', this.favoriteSongsPool)
       return
     }
@@ -1065,6 +1139,8 @@ export struct FindView {
   }
 
   private openAlbumDetail(album: FindAlbumGroup): void {
+    this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
+    this.currentAlbumSupportRecentPlayedSort = false
     this.currentAlbumId = album.id
     this.currentAlbumTitle = album.title
     this.currentAlbumArtist = album.artist
@@ -1099,6 +1175,8 @@ export struct FindView {
       ToastUtil.showToast('歌单里还没有可展示歌曲')
       return
     }
+    this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
+    this.currentAlbumSupportRecentPlayedSort = false
     const coverSong = this.pickAlbumCoverSong(songs)
     this.currentAlbumId = `find-playlist-${playlist.id}`
     this.currentAlbumTitle = playlist.name
@@ -1194,6 +1272,8 @@ export struct FindView {
     this.currentAlbumCoverPath = ''
     this.currentAlbumSourceLabel = ''
     this.currentAlbumSongs = []
+    this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
+    this.currentAlbumSupportRecentPlayedSort = false
   }
 
   private canDeleteCurrentAlbumSongs(): boolean {
@@ -3113,6 +3193,8 @@ export struct FindView {
           albumCoverPath: this.currentAlbumCoverPath,
           albumSourceLabel: this.currentAlbumSourceLabel,
           songs: this.currentAlbumSongs,
+          defaultSortType: this.currentAlbumDefaultSortType,
+          supportRecentPlayedSort: this.currentAlbumSupportRecentPlayedSort,
           allowDelete: this.canDeleteCurrentAlbumSongs(),
           onBack: () => {
             this.exitAlbumMode()

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

@@ -4567,10 +4567,15 @@ export struct LocalMusic {
             iconResource: $r('sys.symbol.list_number'),
             iconSize: 25,
             pointColor: this.themeColor,
-            clickScale: 0.6
+            clickScale: 0.6,
+            menuBuilder: () => {
+              if ((this.modeType==2&&!this.isCanBack)||(this.modeType==3&&!this.isCanBack)) {
+                this.SortMenuForArtistAblumBuilder()
+                return
+              }
+              this.SortMenuBuilder()
+            }
           })
-          .bindMenu((this.modeType==2&&!this.isCanBack)||(this.modeType==3&&!this.isCanBack)
-            ?this.SortMenuForArtistAblumBuilder:this.SortMenuBuilder)
           //添加按钮
           TitleBarPointLightButton({
             iconResource: $r('sys.symbol.plus'),
@@ -19491,15 +19496,23 @@ export struct LocalMusic {
     // 通过检查当前歌曲类型判断是否为网盘音乐(而非modeType,因为不同网盘类型modeType不同)
     const isCloudSong = this.currentSong && isRemoteCloudType(this.currentSong.type);
     if (isCloudSong) {
+      const findRemoteQueue = getFindVideoItems();
+      const isSameFindQueue = this.songList.length === findRemoteQueue.length &&
+        this.songList.every((item: VideoItem, index: number) => item.filePath === findRemoteQueue[index]?.filePath);
+      const useFindRemoteQueue = this.playQueueScope === 'playlist' &&
+        findRemoteQueue.length > 0 &&
+        isSameFindQueue &&
+        findRemoteQueue.every((item: VideoItem) => isRemoteCloudType(item.type)) &&
+        findRemoteQueue.some((item: VideoItem) => item.filePath === this.currentSong?.filePath);
       // 阈值常量
       const UNPLAYED_THRESHOLD = 10; // 剩余未播放歌曲阈值
       const LOAD_MORE_PAGES = 3;     // 每次加载页数
 
       // 获取网盘播放列表
-      const navidromeList = getNavidromeVideoItems();
-      const webdavList = getWebdavVideoItems();
-      const isNavidrome = navidromeList.length > 0;
-      const remoteList = isNavidrome ? navidromeList : webdavList;
+      const navidromeList = useFindRemoteQueue ? [] : getNavidromeVideoItems();
+      const webdavList = useFindRemoteQueue ? [] : getWebdavVideoItems();
+      const isNavidrome = !useFindRemoteQueue && navidromeList.length > 0;
+      const remoteList = useFindRemoteQueue ? findRemoteQueue : (isNavidrome ? navidromeList : webdavList);
 
       if (remoteList.length > 0) {
         // 将当前歌加入历史栈
@@ -19518,7 +19531,9 @@ export struct LocalMusic {
         const totalCount = remoteList.length;
         const playedCount = isNavidrome ? getPlayedCount() : 0;
 
-        Logger.info(TAG, `randomPlay(网盘模式): 总数${totalCount}, 已播放${playedCount}, 未播放${unplayedCount}`);
+        Logger.info(TAG,
+          `randomPlay(网盘模式): source=${useFindRemoteQueue ? 'find-playlist' : (isNavidrome ? 'navidrome' : 'webdav')}, ` +
+          `总数${totalCount}, 已播放${playedCount}, 未播放${unplayedCount}`);
 
         // 如果未播放数量不足阈值,且还有更多数据,触发加载更多页
         if (isNavidrome && unplayedCount < UNPLAYED_THRESHOLD && hasMoreData()) {

+ 4 - 2
entry/src/main/ets/view/PointLight/PointLightButton.ets

@@ -1,9 +1,11 @@
 import { hdsEffect } from "@kit.UIDesignKit"
 import { deviceInfo } from "@kit.BasicServicesKit"
 import { PreferencesUtil } from "@pura/harmony-utils"
-
+import { CommonConstants } from "../../common/constants/CommonConstants"
 @Component
 export struct PointLightButton{
+  @StorageProp('themeColor') themeColor: string =
+    PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
   @BuilderParam builder:() => void
   @Require isPx: boolean
   @Require@Prop@Watch('setButtonSize') builderHeight: number
@@ -55,7 +57,7 @@ export struct PointLightButton{
       .onTouch((event: TouchEvent) => {
         if (event.type === TouchType.Down) {
           this.pointLightOptions = {
-            color: Color.White,
+            color: this.themeColor,
             intensity: 1,
             height: this.pointLightHeight
           }

+ 5 - 2
entry/src/main/ets/view/PointLight/PointLightDeFaultButton.ets

@@ -1,5 +1,7 @@
 import { hdsEffect } from "@kit.UIDesignKit"
 import { deviceInfo } from "@kit.BasicServicesKit"
+import { PreferencesUtil } from "@pura/harmony-utils"
+import { CommonConstants } from "../../common/constants/CommonConstants"
 
 @Component
 export struct PointLightDefaultButton{
@@ -15,7 +17,8 @@ export struct PointLightDefaultButton{
   public canShadow: boolean = true
   public canPointLight: boolean = true
 
-
+  @StorageProp('themeColor') themeColor: string =
+    PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
   @StorageProp("EnablePointLight") enablePointLight: boolean = true
   @StorageProp("EnableShadow") enableShadow: boolean = true
 
@@ -57,7 +60,7 @@ export struct PointLightDefaultButton{
       .onTouch((event: TouchEvent) => {
         if (event.type === TouchType.Down) {
           this.pointLightOptions = {
-            color: Color.White,
+            color: this.themeColor,
             intensity: 1,
             height: this.pointLightHeight
           }

+ 68 - 32
entry/src/main/ets/view/PointLight/TitleBarPointLightButton.ets

@@ -12,6 +12,7 @@ export struct TitleBarPointLightButton {
   @Prop pointLightHeight: number = 100
   @Prop clickScale: number = 0.8
   @Prop useShadow: boolean = true
+  @BuilderParam menuBuilder?: CustomBuilder
 
   clickHandler: () => void = () => {}
 
@@ -26,39 +27,74 @@ export struct TitleBarPointLightButton {
   }
 
   build() {
-    Button({ type: ButtonType.Circle, stateEffect: true }) {
-      SymbolGlyph(this.iconResource)
-        .fontSize(this.iconSize)
-        .fontColor([this.iconColor])
-    }
-    .attributeModifier(new ButtonFancyModifier(this.buttonSize, this.buttonSize))
-    .backdropBlur(150)
-    .opacity(0.9)
-    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: this.clickScale })
-    .animation({ duration: 300, curve: Curve.Ease })
-    .onTouch((event: TouchEvent) => {
-      if (event.type === TouchType.Down) {
-        this.pointLightOptions = {
-          color: this.pointColor,
-          intensity: 1,
-          height: this.pointLightHeight
+    if (this.menuBuilder) {
+      Button({ type: ButtonType.Circle, stateEffect: true }) {
+        SymbolGlyph(this.iconResource)
+          .fontSize(this.iconSize)
+          .fontColor([this.iconColor])
+      }
+      .attributeModifier(new ButtonFancyModifier(this.buttonSize, this.buttonSize))
+      .backdropBlur(150)
+      .opacity(0.9)
+      .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: this.clickScale })
+      .animation({ duration: 300, curve: Curve.Ease })
+      .onTouch((event: TouchEvent) => {
+        if (event.type === TouchType.Down) {
+          this.pointLightOptions = {
+            color: this.pointColor,
+            intensity: 1,
+            height: this.pointLightHeight
+          }
+        } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
+          this.pointLightOptions = undefined
         }
-      } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
-        this.pointLightOptions = undefined
+      })
+      .shadow(this.useShadow && this.enableShadow ? { radius: 26, color: $r('app.color.shadow_color') } : undefined)
+      .visualEffect(this.enablePointLight && this.sdkApiVersion >= 20
+        ? new hdsEffect.HdsEffectBuilder()
+          .pointLight({
+            options: this.pointLightOptions,
+            illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+          })
+          .buildEffect()
+        : undefined)
+      .zIndex(0)
+      .bindMenu(this.menuBuilder)
+    } else {
+      Button({ type: ButtonType.Circle, stateEffect: true }) {
+        SymbolGlyph(this.iconResource)
+          .fontSize(this.iconSize)
+          .fontColor([this.iconColor])
       }
-    })
-    .shadow(this.useShadow && this.enableShadow ? { radius: 26, color: $r('app.color.shadow_color') } : undefined)
-    .visualEffect(this.enablePointLight && this.sdkApiVersion >= 20
-      ? new hdsEffect.HdsEffectBuilder()
-        .pointLight({
-          options: this.pointLightOptions,
-          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-        })
-        .buildEffect()
-      : undefined)
-    .zIndex(0)
-    .onClick(() => {
-      this.clickHandler()
-    })
+      .attributeModifier(new ButtonFancyModifier(this.buttonSize, this.buttonSize))
+      .backdropBlur(150)
+      .opacity(0.9)
+      .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: this.clickScale })
+      .animation({ duration: 300, curve: Curve.Ease })
+      .onTouch((event: TouchEvent) => {
+        if (event.type === TouchType.Down) {
+          this.pointLightOptions = {
+            color: this.pointColor,
+            intensity: 1,
+            height: this.pointLightHeight
+          }
+        } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
+          this.pointLightOptions = undefined
+        }
+      })
+      .shadow(this.useShadow && this.enableShadow ? { radius: 26, color: $r('app.color.shadow_color') } : undefined)
+      .visualEffect(this.enablePointLight && this.sdkApiVersion >= 20
+        ? new hdsEffect.HdsEffectBuilder()
+          .pointLight({
+            options: this.pointLightOptions,
+            illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+          })
+          .buildEffect()
+        : undefined)
+      .zIndex(0)
+      .onClick(() => {
+        this.clickHandler()
+      })
+    }
   }
 }

+ 4 - 3
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -2709,9 +2709,11 @@ export struct RemoteMusicPage {
             iconResource: $r('sys.symbol.list_number'),
             iconSize: 25,
             pointColor: this.themeColor,
-            clickScale: 0.6
+            clickScale: 0.6,
+            menuBuilder: () => {
+              this.SortMenuBuilder()
+            }
           })
-          .bindMenu(this.SortMenuBuilder)
         }
       }
       .width('100%')
@@ -5310,4 +5312,3 @@ async function loadNavidromeDataTask(accountData: AccountData, ticket: number):
 
   return result;
 }
-

+ 93 - 0
entry/src/ohosTest/ets/test/FindDiscoveryHelper.test.ets

@@ -0,0 +1,93 @@
+import { describe, it, expect } from '@ohos/hypium'
+import { VideoItem } from '../../../main/ets/viewmodel/VideoItem'
+import {
+  buildPreferredRemotePlaybackPool,
+  buildSortedDiscoverySongs,
+  FindCollectionSortType,
+  resolveQueueStartIndex
+} from '../../../main/ets/common/util/FindDiscoveryHelper'
+
+function createSong(name: string, filePath: string, options?: {
+  track?: string,
+  cTime?: string,
+  lastPlayedStr?: string
+}): VideoItem {
+  const song = new VideoItem(name, filePath, filePath, 0, 0, options?.cTime ?? '2026-03-01 00:00:00')
+  song.track = options?.track ?? ''
+  song.lastPlayedStr = options?.lastPlayedStr ?? ''
+  song.fileName = `${name}.flac`
+  return song
+}
+
+export default function findDiscoveryHelperTest() {
+  describe('FindDiscoveryHelperTest', () => {
+    it('sortRecentSongsByLastPlayedDesc', 0, () => {
+      const songs = [
+        createSong('B', '/music/b.flac', { lastPlayedStr: '2026-03-01 12:00:00' }),
+        createSong('A', '/music/a.flac', { lastPlayedStr: '2026-03-03 12:00:00' }),
+        createSong('C', '/music/c.flac', { lastPlayedStr: '2026-03-02 12:00:00' })
+      ]
+
+      const sorted = buildSortedDiscoverySongs(songs, FindCollectionSortType.RECENT_DESC)
+      expect(sorted.map(item => item.filePath).join(',')).assertEqual('/music/a.flac,/music/c.flac,/music/b.flac')
+    })
+
+    it('sortFavoriteSongsByNameAsc', 0, () => {
+      const songs = [
+        createSong('周三', '/music/c.flac'),
+        createSong('周一', '/music/a.flac'),
+        createSong('周二', '/music/b.flac')
+      ]
+
+      const sorted = buildSortedDiscoverySongs(songs, FindCollectionSortType.NAME_ASC)
+      expect(sorted.map(item => item.name).join(',')).assertEqual('周一,周二,周三')
+    })
+
+    it('sortAlbumSongsByTrackAsc', 0, () => {
+      const songs = [
+        createSong('Track 10', '/music/10.flac', { track: '10/12' }),
+        createSong('Track 02', '/music/02.flac', { track: '2/12' }),
+        createSong('Track X', '/music/x.flac')
+      ]
+
+      const sorted = buildSortedDiscoverySongs(songs, FindCollectionSortType.TRACK_ASC)
+      expect(sorted.map(item => item.filePath).join(',')).assertEqual('/music/02.flac,/music/10.flac,/music/x.flac')
+    })
+
+    it('resolveQueueStartIndexByFilePath', 0, () => {
+      const queue = [
+        createSong('A', '/music/a.flac'),
+        createSong('B', '/music/b.flac'),
+        createSong('C', '/music/c.flac')
+      ]
+
+      expect(resolveQueueStartIndex(queue, '/music/b.flac')).assertEqual(1)
+      expect(resolveQueueStartIndex(queue, '/music/missing.flac')).assertEqual(-1)
+    })
+
+    it('preferIndexedRemoteSongsForPlaybackPool', 0, () => {
+      const indexedSongs = [
+        createSong('Indexed B', '/cloud/b.flac'),
+        createSong('Indexed A', '/cloud/a.flac'),
+        createSong('Indexed A Dup', '/cloud/a.flac')
+      ]
+      const dbSongs = [
+        createSong('Db Only', '/db/only.flac')
+      ]
+
+      const queue = buildPreferredRemotePlaybackPool(indexedSongs, dbSongs)
+      expect(queue.map(item => item.filePath).join(',')).assertEqual('/cloud/b.flac,/cloud/a.flac')
+    })
+
+    it('fallbackToDatabaseRemoteSongsWhenIndexedPoolEmpty', 0, () => {
+      const dbSongs = [
+        createSong('Db B', '/db/b.flac'),
+        createSong('Db A', '/db/a.flac'),
+        createSong('Db A Dup', '/db/a.flac')
+      ]
+
+      const queue = buildPreferredRemotePlaybackPool([], dbSongs)
+      expect(queue.map(item => item.filePath).join(',')).assertEqual('/db/b.flac,/db/a.flac')
+    })
+  })
+}