Переглянути джерело

优化 本地音乐 艺术家 专辑 点击 进入和然后返回 有闪现的问题
播放页 长按封面可以 替换封面 设为壁纸功能还没做好

onecold 5 місяців тому
батько
коміт
3c5a38000d

+ 5 - 4
entry/src/main/ets/entryability/EntryAbility.ets

@@ -40,8 +40,8 @@ export default class EntryAbility extends UIAbility {
     // UI上下文对象,用于获取窗口信息
     private uiContext?: UIContext;
     // private awareness: smartMobilityCommon.SmartMobilityAwareness = smartMobilityCommon.getSmartMobilityAwareness();
-    private awareness: smartMobilityCommon.SmartMobilityAwareness|undefined = canIUse("SystemCapability.CarService.DistributedEngine")?smartMobilityCommon.getSmartMobilityAwareness():undefined;
-
+    // private awareness: smartMobilityCommon.SmartMobilityAwareness|undefined = canIUse("SystemCapability.CarService.DistributedEngine")?smartMobilityCommon.getSmartMobilityAwareness():undefined;
+    private awareness?: smartMobilityCommon.SmartMobilityAwareness;//修复api23 报错问题
     /**
      * 窗口尺寸变化回调函数
      * @param windowSize 新的窗口尺寸对象
@@ -98,8 +98,6 @@ export default class EntryAbility extends UIAbility {
         AppStorage.setOrCreate('context', this.context);
         hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
 
-        // 初始化WebDAV管理器
-        this.initWebDAV();
 
         // 初始化歌单备份管理器
         PlaylistBackupManager.getInstance().setContext(this.context);
@@ -111,6 +109,9 @@ export default class EntryAbility extends UIAbility {
 
         this.handleWeChatCallIfNeed(want)
 
+        // 初始化WebDAV管理器
+        this.initWebDAV();
+
         this.getHiCarStatus()
 
     }

+ 0 - 5
entry/src/main/ets/pages/NewIndex.ets

@@ -1508,11 +1508,6 @@ struct NewIndex {
 
           Blank()
 
-          Image($r('app.media.arrow_right'))
-            .width(22)
-            .height(22)
-            .margin({ left: 20, right: 0 })
-            .align(Alignment.Center)
         }
         .width('100%')
         .height(55)

+ 131 - 0
entry/src/main/ets/pages/WallpaperPreviewPage.ets

@@ -0,0 +1,131 @@
+import { common } from '@kit.AbilityKit'
+import { promptAction, router } from '@kit.ArkUI'
+import { fileIo } from '@kit.CoreFileKit'
+import { image } from '@kit.ImageKit'
+import { photoAccessHelper } from '@kit.MediaLibraryKit'
+import { StrUtil } from '@pura/harmony-utils'
+import Logger from '../common/util/Logger'
+
+const TAG = 'WallpaperPreviewPage'
+
+@Entry
+@Component
+export struct WallpaperPreviewPage {
+  @State imageUri: string = '';
+  @State isSaving: boolean = false;
+  private readonly wallpaperRatio: number = 9 / 19.5;
+
+  aboutToAppear(): void {
+    const params = router.getParams() as Record<string, Object>;
+    const imageUri = params?.imageUri as string;
+    if (!imageUri || StrUtil.isEmpty(imageUri)) {
+      promptAction.showToast({ message: '未获取到图片' });
+      return;
+    }
+    this.imageUri = imageUri;
+  }
+
+  private async capturePreview(): Promise<image.PixelMap> {
+    return await new Promise<image.PixelMap>((resolve, reject) => {
+      this.getUIContext().getComponentSnapshot().get('wallpaper_preview', (error: Error, pixelMap: image.PixelMap) => {
+        if (error) {
+          reject(error);
+          return;
+        }
+        resolve(pixelMap);
+      });
+    });
+  }
+
+  private async saveWallpaper(): Promise<void> {
+    if (this.isSaving) {
+      return;
+    }
+    if (StrUtil.isEmpty(this.imageUri)) {
+      promptAction.showToast({ message: '图片不存在' });
+      return;
+    }
+    this.isSaving = true;
+    try {
+      const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
+      const pixelMap = await this.capturePreview();
+      const imagePacker = image.createImagePacker();
+      const packed = await imagePacker.packing(pixelMap, {
+        format: 'image/png',
+        quality: 96
+      });
+      const helper = photoAccessHelper.getPhotoAccessHelper(context);
+      const assetUri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');
+      const file = await fileIo.open(assetUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
+      await fileIo.write(file.fd, packed);
+      await fileIo.close(file.fd);
+      promptAction.showToast({ message: '已保存到相册' });
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `saveWallpaper failed: ${err.message}`);
+      promptAction.showToast({ message: '保存失败,请稍后重试' });
+    } finally {
+      this.isSaving = false;
+    }
+  }
+
+  build() {
+    Stack({ alignContent: Alignment.Bottom }) {
+      Column() {
+
+
+        Column() {
+          if (!StrUtil.isEmpty(this.imageUri)) {
+            Stack({ alignContent: Alignment.Center }) {
+              Image(this.imageUri)
+                .width('106%')
+                .height('66%')
+                .objectFit(ImageFit.Contain)
+                .blur(28)
+                .opacity(0.75)
+
+              Image(this.imageUri)
+                .width('100%')
+                .height('60%')
+                .objectFit(ImageFit.Contain)
+            }
+            .id('wallpaper_preview')
+            .width('100%')
+            .aspectRatio(this.wallpaperRatio)
+            .borderRadius(28)
+            .clip(true)
+          } else {
+            Text('未获取到图片')
+              .fontSize(16)
+              .fontColor('#99ffffff')
+          }
+        }
+        .width('100%')
+        .padding({ left: 18, right: 18, top: 12 })
+
+        Blank()
+      }
+      .width('100%')
+      .height('100%')
+
+      Row() {
+        SaveButton()
+          .onClick((event: ClickEvent, result: SaveButtonOnClickResult) => {
+            if (result === SaveButtonOnClickResult.SUCCESS) {
+              void this.saveWallpaper();
+              return;
+            }
+            promptAction.showToast({ message: '相册授权失败,请重试' });
+          })
+      }
+      .width('100%')
+      .justifyContent(FlexAlign.Center)
+      .padding({ left: 16, right: 16, bottom: 24 })
+    }
+    .width('100%')
+    .height('100%')
+    .backgroundImage(StrUtil.isNotEmpty(this.imageUri) ? this.imageUri : $r('app.media.alt'))
+    .backgroundImageSize(ImageSize.Cover)
+    .backgroundBlurStyle(BlurStyle.BACKGROUND_THIN)
+  }
+}

+ 439 - 124
entry/src/main/ets/view/LocalMusic.ets

@@ -252,6 +252,15 @@ interface WorkerMessageEditResult {
   data: WorkerEditMusicResult;
 }
 
+interface AlphaTaskItem {
+  type: number;
+  name?: string;
+  pyStr?: string;
+  fileName?: string;
+  artist?: string;
+  album?: string;
+}
+
 type LocalMusicWorkerMessage =
   WorkerMessageScanDone
     | WorkerMessageMediaList
@@ -373,6 +382,7 @@ export struct LocalMusic {
   private alphaBetRebuildTimer: number = 0
   private alphaBetDirty: boolean = false
   private alphaBetCacheKey: string = ''
+  private alphaBetBuildVersion: number = 0
   @State isShowPrecious: boolean = false //播控条显示上一首按钮
   //背景两侧流光控制器
   @State sceneBgController: HdsSceneController|undefined = deviceInfo.sdkApiVersion>=20
@@ -531,6 +541,11 @@ export struct LocalMusic {
   private waterScrollLastPagingEnd: number = -1
   private gridWaterScrollIdleTimer: number = 0
   private gridWaterLastActiveTs: number = 0
+  private listUpdateTimer: number = 0
+  private listUpdateVersion: number = 0
+  private detailBackTransitioning: boolean = false
+  private mediaLibraryEnterTs: number = 0
+  private mediaLibraryWarmupPrefetchTimer: number = 0
   private listVisibleStart: number = -1
   private listVisibleEnd: number = -1
   private listThumbWarmupCheckAt: number = 0
@@ -639,10 +654,14 @@ export struct LocalMusic {
     this.isRefreshing = false
   }
 
-  async onModeChange(): Promise<void> {
+  async onModeChange(preserveList: boolean = false): Promise<void> {
     this.isFavMusic = false
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
     this.refreshDirVisibilitySettings()
+    if (this.mediaLibraryWarmupPrefetchTimer) {
+      clearTimeout(this.mediaLibraryWarmupPrefetchTimer)
+      this.mediaLibraryWarmupPrefetchTimer = 0
+    }
 
     Logger.info('heanup LocalMusic', `onModeChange: 切换模式 modeType=${this.modeType}`);
 
@@ -653,18 +672,21 @@ export struct LocalMusic {
         // 切换模式时加载对应的排序类型
         this.titleName = Utility.resourceToString(this.context,$r('app.string.home'))
         this.sortType = this.loadSortTypeForCurrentMode()
+        this.mediaLibraryEnterTs = 0
         break
       case 1:
         this.rightTopImage = $r('sys.symbol.sort')
         this.titleName = Utility.resourceToString(this.context,$r('app.string.media_ku'))
         // 切换模式时加载对应的排序类型
         this.sortType = this.loadSortTypeForCurrentMode()
+        this.mediaLibraryEnterTs = Date.now()
         break
       case 2:
         this.rightTopImage = $r('sys.symbol.sort')
         this.titleName = Utility.resourceToString(this.context,$r('app.string.artist'))
         // 切换到艺术家模式时,加载艺术家模式的排序类型(默认按数量降序)
         this.sortType = this.loadSortTypeForCurrentMode()
+        this.mediaLibraryEnterTs = 0
         break
 
       case 3:
@@ -672,10 +694,11 @@ export struct LocalMusic {
         this.titleName = Utility.resourceToString(this.context,$r('app.string.album'))
         // 切换到专辑模式时,加载专辑模式的排序类型(默认按数量降序)
         this.sortType = this.loadSortTypeForCurrentMode()
+        this.mediaLibraryEnterTs = 0
         break
     }
     // 模式切换时重置分页状态并加载第一页
-    await this.resetAndLoadFirstPage();
+    await this.resetAndLoadFirstPage(!preserveList);
 
     Logger.info('heanup onModeChange', `doSortType: 切换排序方式 sortType=${this.sortType}, mode=${this.modeType}`);
   }
@@ -793,12 +816,27 @@ export struct LocalMusic {
     }
     this.isZero = false
     if (this.modeType !== 0 && this.isCanBack) {
+      if (this.detailBackTransitioning) {
+        return
+      }
+      this.detailBackTransitioning = true
       // 先设置 isCanBack=false,确保 onModeChange 读取的是列表页的排序设置
       this.isCanBack = false
-      this.onModeChange()
+      this.rightTopImage = $r('sys.symbol.sort')
+      if (this.modeType == 2) {
+        this.titleName = Utility.resourceToString(this.context,$r('app.string.artist'))
+        if (ArrayUtil.isNotEmpty(this.artistList)) {
+          this.updateListData(this.artistList, true, true)
+        }
+      } else if (this.modeType == 3) {
+        this.titleName = Utility.resourceToString(this.context,$r('app.string.album'))
+        if (ArrayUtil.isNotEmpty(this.albumList)) {
+          this.updateListData(this.albumList, true, true)
+        }
+      }
 
-      // 返回专辑列表时恢复滚动偏移量,支持列表和网格
-      setTimeout(() => {
+      // 返回列表页时保留当前列表,避免先清空导致闪现
+      void this.onModeChange(true).then(() => {
         if (this.modeType == 2) {
           if (this.isGridMusic||this.twoFingerType==4) {
             let lastOffset = this.mScrollMap.get('lastGridArtistScrollOffset') ?? 0
@@ -826,10 +864,14 @@ export struct LocalMusic {
             this.listScroller.scrollTo({ xOffset: 0, yOffset: lastOffset })
           }
         }
-
-      }, 200)
+        this.detailBackTransitioning = false
+      }).catch((error: Error) => {
+        Logger.error('heanup LocalMusic', `doSwipBack onModeChange error: ${error.message}`)
+        this.detailBackTransitioning = false
+      })
       return
     }
+    this.mediaLibraryEnterTs = 0
 
     if (this.isHistory || this.isFavMusic) {
       this.isHistory = false
@@ -866,8 +908,14 @@ export struct LocalMusic {
 
     this.currentPath = Utility.getParentDirectory(this.currentPath)
     Logger.info('this.currentPath2 = ' + this.currentPath)
-    this.getSortedFiles(this.currentPath)
+    this.getSortedFiles(this.currentPath, false, undefined, undefined, undefined, true)
+
+  }
 
+  private triggerSwipeBackWithAnimation(): void {
+    animateTo({ duration: 555 }, () => {
+      this.doSwipBack()
+    })
   }
   // 组件生命周期
   aboutToAppear() {
@@ -913,11 +961,7 @@ export struct LocalMusic {
     let eventUpdateSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_UPDATE }
     // 监听广播事件(手势返回处理)
     emitter.on(eventUpdateSwipeBack, (eventData: emitter.EventData) => {
-
-      animateTo({ duration: 555 }, () => {
-        this.doSwipBack()
-      })
-
+      this.triggerSwipeBackWithAnimation()
     });
 
     //ScanFilePage广播接收时间
@@ -1286,7 +1330,7 @@ export struct LocalMusic {
     this.bluetoothDisconnectPause = PreferencesUtil.getBooleanSync(SettingPage.BLUETOOTH_DISCONNECT_PAUSE, false)
     this.bluetoothLyricEnabled = PreferencesUtil.getBooleanSync(SettingPage.BLUETOOTH_LYRIC_ENABLED, false)
     this.bluetoothLyricMode = PreferencesUtil.getNumberSync(SettingPage.BLUETOOTH_LYRIC_MODE, 0)
-    this.isShowSpectrum  = PreferencesUtil.getBooleanSync('isShowSpectrum', false)
+    this.isShowSpectrum  = PreferencesUtil.getBooleanSync('isShowSpectrum', true)
     this.spectrumModeIndex = PreferencesUtil.getNumberSync('spectrumModeIndex', 0)
     this.enablePointLight = PreferencesUtil.getBooleanSync('enablePointLight', true)
     AppStorage.setOrCreate('EnablePointLight', this.enablePointLight);
@@ -2005,6 +2049,15 @@ export struct LocalMusic {
       clearTimeout(this.gridWaterScrollIdleTimer);
       this.gridWaterScrollIdleTimer = 0;
     }
+    if (this.listUpdateTimer) {
+      clearTimeout(this.listUpdateTimer);
+      this.listUpdateTimer = 0;
+    }
+    if (this.mediaLibraryWarmupPrefetchTimer) {
+      clearTimeout(this.mediaLibraryWarmupPrefetchTimer);
+      this.mediaLibraryWarmupPrefetchTimer = 0;
+    }
+    this.alphaBetBuildVersion++;
     this.gridWaterLastActiveTs = 0;
     this.coverThumbCache.dispose();
     this.coverThumbCacheReady = false;
@@ -2038,7 +2091,8 @@ export struct LocalMusic {
     this.getUIContext().getHostContext()!.eventHub.off('openPlayList');
   }
 
-  async getSortedFiles(curPath: string, isWorkerPost?: boolean, destPath?: string, isOpen?: boolean,openItem?:VideoItem) {
+  async getSortedFiles(curPath: string, isWorkerPost?: boolean, destPath?: string, isOpen?: boolean,
+    openItem?: VideoItem, immediateListUpdate?: boolean, scrollTopAfterUpdate?: boolean) {
     this.isFavMusic = false
     console.info('onecold getSortedFiles 1 curPath '+curPath)
     if (isWorkerPost) {
@@ -2083,7 +2137,10 @@ export struct LocalMusic {
         this.currentTitleCover = Utility.getFirstCoverFromList(cachedValue)
       }
       this.isRefreshing = false
-      this.updateListData(cachedValue)
+      this.updateListData(cachedValue, undefined, immediateListUpdate)
+      if (scrollTopAfterUpdate) {
+        this.scrollToTopForCurrentLayout()
+      }
     }
 
     if (cachedDirList !== undefined) {
@@ -2150,7 +2207,10 @@ export struct LocalMusic {
       // 合并文件夹和文件,文件夹在前,文件在后,广告在中间
       this.videoLocalList = directories.concat(files);
 
-      this.updateListData(this.videoLocalList)
+      this.updateListData(this.videoLocalList, undefined, immediateListUpdate)
+      if (scrollTopAfterUpdate) {
+        this.scrollToTopForCurrentLayout()
+      }
 
       if (ArrayUtil.isNotEmpty(files)) {
         if (this.currentPath != this.rootPath) {
@@ -2238,12 +2298,23 @@ export struct LocalMusic {
     }
     return mediaItems;
   }
-  updateListData(mList: Array<VideoItem>, noSort?: boolean) {
+  updateListData(mList: Array<VideoItem>, noSort?: boolean, immediate?: boolean) {
     this.isShowTitleBar = true
-    this.getUIContext().animateTo({ duration: 666 }, () => {
-      this.opacityItem = 0.5;
-    });
-    setTimeout(() => {
+    const shouldAnimate = !immediate
+    if (this.listUpdateTimer) {
+      clearTimeout(this.listUpdateTimer)
+      this.listUpdateTimer = 0
+    }
+    const updateVersion = ++this.listUpdateVersion
+    if (shouldAnimate) {
+      this.getUIContext().animateTo({ duration: 666 }, () => {
+        this.opacityItem = 0.5;
+      });
+    }
+    const applyListData = () => {
+      if (updateVersion !== this.listUpdateVersion) {
+        return
+      }
       this.videoLocalList = mList;
       if (!noSort) {
         if (this.modeType === 0 || this.modeType === 1) {
@@ -2252,12 +2323,24 @@ export struct LocalMusic {
         }
       }
       this.dataSource.pushArrayData(this.videoLocalList)
-      this.getUIContext().animateTo({ duration: 666 }, () => {
+      if (shouldAnimate) {
+        this.getUIContext().animateTo({ duration: 666 }, () => {
+          this.opacityItem = 1;
+        });
+      } else {
         this.opacityItem = 1;
-      });
+      }
       this.setButtonStatus()
       //添加到字母索引表
       this.markAlphaBetDirty()
+    };
+    if (immediate) {
+      applyListData();
+      return;
+    }
+    this.listUpdateTimer = setTimeout(() => {
+      this.listUpdateTimer = 0
+      applyListData();
     }, 200);
 
   }
@@ -2966,6 +3049,88 @@ export struct LocalMusic {
     }
   }
 
+  private async pickCoverImageToSandbox(): Promise<string | undefined> {
+    try {
+      const photoPicker = new photoAccessHelper.PhotoViewPicker();
+      const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
+      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
+      photoSelectOptions.maxSelectNumber = 1;
+      const photoSelectResult = await photoPicker.select(photoSelectOptions);
+      const selectUris = photoSelectResult.photoUris;
+      if (!selectUris || selectUris.length === 0) {
+        return undefined;
+      }
+
+      const coverDir = this.context.filesDir + FileUtil.separator + 'ttmusic_cover';
+      if (!FileUtil.accessSync(coverDir)) {
+        FileUtil.mkdirSync(coverDir);
+      }
+
+      const sourceUri = selectUris[0];
+      const sourceName = FileUtil.getFileName(sourceUri) || '';
+      let suffix = '.jpg';
+      const dotIndex = sourceName.lastIndexOf('.');
+      if (dotIndex >= 0) {
+        suffix = sourceName.substring(dotIndex);
+      }
+      const targetName = `${Date.now()}_${await MD5.digestSync(sourceUri)}${suffix}`;
+      const targetPath = coverDir + FileUtil.separator + targetName;
+
+      const sourceFile = fileIo.openSync(sourceUri, fileIo.OpenMode.READ_ONLY);
+      const targetFile = fileIo.openSync(targetPath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
+      fileIo.copyFileSync(sourceFile.fd, targetFile.fd);
+      fileIo.closeSync(sourceFile.fd);
+      fileIo.closeSync(targetFile.fd);
+
+      return fileUri.getUriFromPath(targetPath);
+    } catch (error) {
+      const err = error as BusinessError;
+      Logger.error(TAG, `pickCoverImageToSandbox failed, code: ${err.code}, message: ${err.message}`);
+      return undefined;
+    }
+  }
+
+  private async replaceCurrentSongCoverByAlbum(): Promise<void> {
+    if (!this.currentSong || StrUtil.isEmpty(this.currentSong.filePath)) {
+      ToastUtil.showToast('当前歌曲无效');
+      return;
+    }
+    if (this.currentSong.type !== CommonConstants.TYPE_LOCAL) {
+      ToastUtil.showToast('仅支持本地歌曲更换封面');
+      return;
+    }
+
+    const sandboxCoverUri = await this.pickCoverImageToSandbox();
+    if (StrUtil.isEmpty(sandboxCoverUri)) {
+      return;
+    }
+
+    this.loadingDialogId = DialogHelper.showLoadingDialog();
+    try {
+      const task = new taskpool.Task(
+        replaceLocalMusicCoverTask,
+        this.context,
+        this.currentSong.filePath,
+        FileUtil.getFilePath(sandboxCoverUri!)
+      );
+      const result = await taskpool.execute(task, taskpool.Priority.HIGH) as boolean;
+      if (!result) {
+        ToastUtil.showToast('更换封面失败');
+        return;
+      }
+
+      this.cover = sandboxCoverUri;
+      this.currentSong.pixelMapPath = sandboxCoverUri;
+      await this.doUpateEditedFields(this.currentSong);
+      ToastUtil.showToast('更换封面成功');
+    } catch (error) {
+      Logger.error(TAG, `replaceCurrentSongCoverByAlbum failed: ${JSON.stringify(error)}`);
+      ToastUtil.showToast('更换封面失败');
+    } finally {
+      DialogHelper.closeDialog(this.loadingDialogId);
+    }
+  }
+
   // 拉起picker选择文件管理器
   async callFilePickerSelectFile(): Promise<void> {
     try {
@@ -3593,7 +3758,7 @@ export struct LocalMusic {
     Column({space: 5}){
       if(this.showZMIndex&&this.enableShowAlphabet){
         if(this.isShowAlphaBet&&!(this.modeType==2&&!this.isCanBack)
-          &&!(this.modeType==3&&!this.isCanBack)){
+          &&!(this.modeType==3&&!this.isCanBack)&&(this.sortType==4||this.sortType==5)){
           AlphabetIndexer({arrayValue: this.alphaBet,selected: this.selectAlphaBetIndex})
             .attributeModifier(this.alphaBetStyle)
             .usingPopup(this.isTouchingAlphaBet)
@@ -3746,6 +3911,11 @@ export struct LocalMusic {
     if (!this.showZMIndex) {
       return;
     }
+    if (this.isMediaLibraryWarmupWindow()) {
+      this.alphaBetDirty = true;
+      this.scheduleAlphaBetRebuild(900);
+      return;
+    }
     this.alphaBetDirty = true;
     if (!this.isScrolling) {
       this.scheduleAlphaBetRebuild(80);
@@ -3837,6 +4007,34 @@ export struct LocalMusic {
     return 0
   }
 
+  private buildAlphaTaskItems(songs: Array<VideoItem>): Array<AlphaTaskItem> {
+    const items: Array<AlphaTaskItem> = []
+    for (const song of songs) {
+      items.push({
+        type: song.type,
+        name: song.name,
+        pyStr: song.pyStr,
+        fileName: song.fileName,
+        artist: song.artist,
+        album: song.album
+      })
+    }
+    return items
+  }
+
+  private async buildAlphaBetByTask(songs: Array<VideoItem>, version: number): Promise<void> {
+    const taskItems = this.buildAlphaTaskItems(songs)
+    const task = new taskpool.Task(buildAlphabetIndexTask, taskItems, this.sortType, this.isShowFileName)
+    const result = await taskpool.execute(task, taskpool.Priority.MEDIUM) as Array<string>
+    if (version !== this.alphaBetBuildVersion) {
+      return
+    }
+    this.alphaBet = result
+    if (this.videoLocalList.length > 0) {
+      this.selectAlphaBetIndex = 0
+    }
+  }
+
   async setAlphaBet(){
     if(!this.showZMIndex){
       return
@@ -3845,53 +4043,17 @@ export struct LocalMusic {
       await this.refreshAlphaBetFromMediaLibrary()
       return
     }
-    return new Promise<void>((resolve,reject) => {
-      try {
-        let songs = this.videoLocalList
-        if(songs.length > 40000){
-          this.alphaBet = []
-          resolve()
-          return
-        }
-        let alphabetSet = new Set<string>();
-        for (let song of songs) {
-          let text = this.isShowFileName?song.fileName:
-            (song.type!=CommonConstants.TYPE_IS_DIR&&song.pyStr)?song.pyStr:song.name
-          if(this.sortType === 0||this.sortType==1){
-            text = song.artist
-          }else if(this.sortType === 2||this.sortType==3){
-            text = song.album
-          }
-          if(text){
-            let firstPinyin = getFirstLetter(text)
-            if(!alphabetSet.has(firstPinyin)){
-              alphabetSet.add(firstPinyin)
-            }
-          }
-          // const pyStr = song.pyStr
-          // if(pyStr){
-          //   if(!alphabetSet.has(pyStr)){
-          //     alphabetSet.add(pyStr)
-          //   }
-          // }
-
-        }
-        this.alphaBet = Array.from(alphabetSet).sort((a,b)=>{
-          if(this.sortType ===4||this.sortType==0||this.sortType==2||this.sortType==6){
-            return a.localeCompare(b)
-          }else{
-            return b.localeCompare(a)
-          }
-        })
-        if(this.videoLocalList.length > 0){
-          this.selectAlphaBetIndex = 0
-        }
-        resolve()
-      }catch (e) {
-        console.error('testTag','排序字母表出错',JSON.stringify(e))
-        resolve()
+    try {
+      const songs = this.videoLocalList
+      if (songs.length > 40000) {
+        this.alphaBet = []
+        return
       }
-    })
+      const version = ++this.alphaBetBuildVersion
+      await this.buildAlphaBetByTask(songs, version)
+    } catch (e) {
+      console.error('testTag','排序字母表出错',JSON.stringify(e))
+    }
   }
 
   private async refreshAlphaBetFromMediaLibrary(): Promise<void> {
@@ -3914,29 +4076,8 @@ export struct LocalMusic {
       searchKeyword: this.isSearchMode ? this.searchText : undefined,
       isShowFileName: this.isShowFileName
     })
-    let alphabetSet = new Set<string>()
-    for (let song of result.items) {
-      let text = this.isShowFileName ? song.fileName :
-        (song.type != CommonConstants.TYPE_IS_DIR && song.pyStr) ? song.pyStr : song.name
-      if (this.sortType === 0 || this.sortType == 1) {
-        text = song.artist
-      } else if (this.sortType === 2 || this.sortType == 3) {
-        text = song.album
-      }
-      if (text) {
-        let firstPinyin = getFirstLetter(text)
-        if (!alphabetSet.has(firstPinyin)) {
-          alphabetSet.add(firstPinyin)
-        }
-      }
-    }
-    this.alphaBet = Array.from(alphabetSet).sort((a, b) => {
-      if (this.sortType === 4 || this.sortType == 0 || this.sortType == 2 || this.sortType == 6) {
-        return a.localeCompare(b)
-      } else {
-        return b.localeCompare(a)
-      }
-    })
+    const version = ++this.alphaBetBuildVersion
+    await this.buildAlphaBetByTask(result.items, version)
     this.alphaBetCacheKey = cacheKey
   }
 
@@ -3955,7 +4096,7 @@ export struct LocalMusic {
           .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
           .animation({ duration: 300, curve: Curve.Ease })
           .onClick(() => {
-            this.doSwipBack()
+            this.triggerSwipeBackWithAnimation()
           })
           .attributeModifier(new ShadowModifier())
           .zIndex(0)
@@ -4087,11 +4228,15 @@ export struct LocalMusic {
     }
     .padding({ top: this.topSafeHeight+10, left: 10, right: 10 })
     .width('100%')
-    .visibility(this.isShowTitleBar?Visibility.Visible:
-      this.autoHideTitle?Visibility.None:Visibility.Visible)
+    .visibility(Visibility.Visible)
+    .translate({
+      x: 0,
+      y: (this.isShowTitleBar || !this.autoHideTitle) ? 0 : -80
+    })
+    .opacity((this.isShowTitleBar || !this.autoHideTitle) ? 1 : 0)
     .animation({
-      duration: 500,
-      curve: Curve.Friction  // 可选动画曲线
+      duration: 240,
+      curve: Curve.EaseInOut
     })
   }
 
@@ -6622,10 +6767,13 @@ export struct LocalMusic {
       this.listVisibleStart = start
       this.listVisibleEnd = end
       this.lastVisibleEndIndex = end
-      this.warmupListVisibleThumbs(start, end)
+      if (!this.isMediaLibraryWarmupWindow()) {
+        this.warmupListVisibleThumbs(start, end)
+      }
       const now = Date.now()
       const alphaDelta = this.listScrollLastAlphaStart >= 0 ? Math.abs(start - this.listScrollLastAlphaStart) : 999
-      if (start >= 0 && start < this.videoLocalList.length &&
+      if (!this.isMediaLibraryWarmupWindow() &&
+        start >= 0 && start < this.videoLocalList.length &&
         (alphaDelta >= 2 || now - this.listScrollAlphaCheckAt >= 48)) {
         this.listScrollAlphaCheckAt = now
         this.listScrollLastAlphaStart = start
@@ -6803,6 +6951,7 @@ export struct LocalMusic {
     switch (item.type) {
       case CommonConstants.TYPE_IS_DIR:
         // 进入歌单前安全保存当前滚动偏移量,支持列表和网格
+        let shouldScrollTopNow = true
         let offsetB = 0
         if(this.twoFingerType==4){
           if (this.waterScroller && typeof this.waterScroller.currentOffset === 'function') {
@@ -6865,20 +7014,23 @@ export struct LocalMusic {
           this.currentTitleName = item.name
           this.currentPath = item.filePath
           this.currentTitleCover = $r('app.media.alt')
-          this.getSortedFiles(this.currentPath)
+          shouldScrollTopNow = false
+          this.getSortedFiles(this.currentPath, false, undefined, undefined, undefined, true, true)
 
         }
         //进入歌单的时候的滚动位置在顶部
-        if(this.twoFingerType==4){
-          setTimeout(() => {
-            this.waterScroller.scrollEdge(Edge.Top)
-            this.scroller.scrollEdge(Edge.Top)
-          }, 200)
-        }else{
-          if (this.isGridMusic) {
-            this.scroller.scrollToIndex(0,false)
+        if (shouldScrollTopNow) {
+          if(this.twoFingerType==4){
+            setTimeout(() => {
+              this.waterScroller.scrollEdge(Edge.Top)
+              this.scroller.scrollEdge(Edge.Top)
+            }, 200)
           }else{
-            this.listScroller.scrollToIndex(0,false)
+            if (this.isGridMusic) {
+              this.scroller.scrollToIndex(0,false)
+            }else{
+              this.listScroller.scrollToIndex(0,false)
+            }
           }
         }
         this.rightTopImage = $r('sys.symbol.chevron_left')
@@ -6961,7 +7113,7 @@ export struct LocalMusic {
           if (item.pixelMapPath) {
             this.currentTitleCover = item.pixelMapPath
           }
-          this.updateListData(mList, true) // 使用 true 跳过排序
+          this.updateListData(mList, true, true) // 详情页立即替换列表,避免旧列表顶部闪现
         } else {
           ToastUtil.showToast('未找到该艺术家的歌曲');
           return;
@@ -7060,7 +7212,7 @@ export struct LocalMusic {
           if (item.pixelMapPath) {
             this.currentTitleCover = item.pixelMapPath
           }
-          this.updateListData(albumList, true) // 使用 true 跳过排序
+          this.updateListData(albumList, true, true) // 详情页立即替换列表,避免旧列表顶部闪现
           this.mScrollMap.set(item.name, this.selectedIndex)
           this.rightTopImage = $r('sys.symbol.chevron_left')
         } else {
@@ -7527,6 +7679,7 @@ export struct LocalMusic {
       // 1. 检查缓存
       if (this.pageCache.has(pageIndex)) {
         Logger.info('heanup LocalMusic', `loadPage: 从缓存加载 page=${pageIndex}`);
+        const inMediaLibraryWarmup = this.modeType === 1 && pageIndex === 0 && !append && this.isMediaLibraryWarmupWindow();
         const cachedData = this.pageCache.get(pageIndex)!;
         const cachedHasMore = this.pageHasMoreCache.get(pageIndex);
         const cachedTotal = this.pageTotalCountCache.get(pageIndex);
@@ -7546,13 +7699,33 @@ export struct LocalMusic {
         } else {
           this.videoLocalList = cachedData;
           this.dataSource.pushArrayData(this.videoLocalList);
-          this.prefetchListThumbsFromItems(cachedData, 30);
-          this.prefetchGridWaterThumbsFromItems(cachedData, this.twoFingerType == 4 ? 56 : 42);
+          if (inMediaLibraryWarmup) {
+            this.prefetchListThumbsFromItems(cachedData, 8);
+          } else {
+            this.prefetchListThumbsFromItems(cachedData, 30);
+            this.prefetchGridWaterThumbsFromItems(cachedData, this.twoFingerType == 4 ? 56 : 42);
+          }
         }
         this.setButtonStatus();
-        this.markAlphaBetDirty();
+        if (inMediaLibraryWarmup) {
+          this.scheduleAlphaBetRebuild(900);
+        } else {
+          this.markAlphaBetDirty();
+        }
         if (this.hasMoreData) {
-          this.prefetchFollowingPages(pageIndex + 1);
+          if (inMediaLibraryWarmup) {
+            if (this.mediaLibraryWarmupPrefetchTimer) {
+              clearTimeout(this.mediaLibraryWarmupPrefetchTimer);
+            }
+            this.mediaLibraryWarmupPrefetchTimer = setTimeout(() => {
+              this.mediaLibraryWarmupPrefetchTimer = 0;
+              if (this.modeType === 1 && !this.isCanBack) {
+                this.prefetchFollowingPages(pageIndex + 1);
+              }
+            }, 900) as number;
+          } else {
+            this.prefetchFollowingPages(pageIndex + 1);
+          }
         }
         return;
       }
@@ -7636,6 +7809,7 @@ export struct LocalMusic {
       this.cleanOldCache(pageIndex);
 
       // 5. 更新显示列表
+      const inMediaLibraryWarmup = this.modeType === 1 && pageIndex === 0 && !append && this.isMediaLibraryWarmupWindow();
       if (append) {
         this.prefetchListThumbsFromItems(result.items, 26);
         this.prefetchGridWaterThumbsFromItems(result.items, this.twoFingerType == 4 ? 56 : 42);
@@ -7643,12 +7817,32 @@ export struct LocalMusic {
       } else {
         this.videoLocalList = result.items;
         this.dataSource.pushArrayData(this.videoLocalList);
-        this.prefetchListThumbsFromItems(result.items, 30);
-        this.prefetchGridWaterThumbsFromItems(result.items, this.twoFingerType == 4 ? 56 : 42);
+        if (inMediaLibraryWarmup) {
+          this.prefetchListThumbsFromItems(result.items, 8);
+        } else {
+          this.prefetchListThumbsFromItems(result.items, 30);
+          this.prefetchGridWaterThumbsFromItems(result.items, this.twoFingerType == 4 ? 56 : 42);
+        }
       }
       this.setButtonStatus();
-      this.markAlphaBetDirty();
-      this.prefetchFollowingPages(pageIndex + 1);
+      if (inMediaLibraryWarmup) {
+        this.scheduleAlphaBetRebuild(900);
+      } else {
+        this.markAlphaBetDirty();
+      }
+      if (inMediaLibraryWarmup) {
+        if (this.mediaLibraryWarmupPrefetchTimer) {
+          clearTimeout(this.mediaLibraryWarmupPrefetchTimer);
+        }
+        this.mediaLibraryWarmupPrefetchTimer = setTimeout(() => {
+          this.mediaLibraryWarmupPrefetchTimer = 0;
+          if (this.modeType === 1 && !this.isCanBack) {
+            this.prefetchFollowingPages(pageIndex + 1);
+          }
+        }, 900) as number;
+      } else {
+        this.prefetchFollowingPages(pageIndex + 1);
+      }
       this.isLoadingPage = false;
     } catch (err) {
       const error = err as Error;
@@ -7917,10 +8111,12 @@ export struct LocalMusic {
   /**
    * 重置分页状态并加载首页
    */
-  private async resetAndLoadFirstPage(): Promise<void> {
+  private async resetAndLoadFirstPage(clearExistingList: boolean = true): Promise<void> {
     Logger.info('heanup LocalMusic', 'resetAndLoadFirstPage: 重置分页状态');
     this.currentPage = 0;
-    this.videoLocalList = [];
+    if (clearExistingList) {
+      this.videoLocalList = [];
+    }
     this.pageCache.clear();
     this.pageHasMoreCache.clear();
     this.pageTotalCountCache.clear();
@@ -8032,6 +8228,16 @@ export struct LocalMusic {
     return this.modeType === 1 && !this.isGridMusic && this.twoFingerType != 4;
   }
 
+  private isMediaLibraryWarmupWindow(): boolean {
+    if (this.modeType !== 1 || this.isCanBack) {
+      return false;
+    }
+    if (this.mediaLibraryEnterTs <= 0) {
+      return false;
+    }
+    return Date.now() - this.mediaLibraryEnterTs < 1800;
+  }
+
   private getDynamicPreloadStartRatio(): number {
     if (this.twoFingerType == 4) {
       return 0.10;
@@ -8078,6 +8284,19 @@ export struct LocalMusic {
     }
   }
 
+  private scrollToTopForCurrentLayout(): void {
+    if (this.twoFingerType == 4) {
+      this.waterScroller.scrollEdge(Edge.Top);
+      this.scroller.scrollEdge(Edge.Top);
+      return;
+    }
+    if (this.isGridMusic) {
+      this.scroller.scrollEdge(Edge.Top);
+      return;
+    }
+    this.listScroller.scrollEdge(Edge.Top);
+  }
+
   private getListImageSource(item: VideoItem, index: number, coverVersion: number = 0): ResourceStr {
     const fallback = item.type == CommonConstants.TYPE_IS_DIR ? $r('app.media.dir_alt') : $r('app.media.alt');
     if (coverVersion < -1) {
@@ -12153,6 +12372,16 @@ export struct LocalMusic {
         .geometryTransition('cover') // 绑定标识符
         .opacity(this.opacityValueImage)
         .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.8 })
+        .bindContextMenu(this.MenuImageBuilder(this.cover), ResponseType.LongPress,
+          {
+            preview: MenuPreviewMode.IMAGE,
+            previewAnimationOptions: { scale: [0.8, 1.0] },
+          })
+        .bindContextMenu(this.MenuImageBuilder(this.cover), ResponseType.RightClick,
+          {
+            preview: MenuPreviewMode.IMAGE,
+            previewAnimationOptions: { scale: [0.8, 1.0] },
+          })
         .visibility(this.isPuraWP() ? Visibility.None : Visibility.Visible)
         .borderRadius(this.isCoverRectangle ? 20 : '100%')
         .clip(true)
@@ -12262,6 +12491,39 @@ export struct LocalMusic {
       centerY: '100%' })
   }
 
+  @Builder
+  MenuImageBuilder(cover: string|undefined) {
+    Menu(){
+      MenuItem({
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')),
+        content: '设为壁纸'
+      })
+        .onClick(async() => {
+          if (StrUtil.isEmpty(cover)) {
+            ToastUtil.showToast('当前没有可用封面')
+            return;
+          }
+          this.getUIContext().getRouter().pushUrl({
+            url: 'pages/WallpaperPreviewPage',
+            params: {
+              imageUri: cover
+            }
+          })
+        })
+
+      MenuItem({
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_up_to_line')),
+        content: '更换封面'
+      })
+        .onClick(async() => {
+          await this.replaceCurrentSongCoverByAlbum();
+        })
+
+
+    }
+
+  }
+
   @Builder
   private musicNameInfo(isHidden: boolean) {
     Row(){
@@ -15216,7 +15478,7 @@ export struct LocalMusic {
           let timeoutPlay = 0
           if (StrUtil.isNotEmpty(this.videoUrl) &&
             (this.videoUrl.toLowerCase().endsWith('.rmvb') || this.videoUrl.toLowerCase().endsWith('.flac'))) {
-            timeoutPlay = 300
+            timeoutPlay = 400
           }
 
           //跳过片头片尾关键代码,以下是跳过片头的关键代码
@@ -17819,6 +18081,33 @@ function cutPopupBuilder(dataBu: BubbleBean) {
 
 }
 
+@Concurrent
+function buildAlphabetIndexTask(items: Array<AlphaTaskItem>, sortType: number, isShowFileName: boolean): Array<string> {
+  const alphabetSet = new Set<string>()
+  for (const item of items) {
+    let text = isShowFileName ? (item.fileName || '') :
+      (item.type != CommonConstants.TYPE_IS_DIR && item.pyStr) ? item.pyStr : (item.name || '')
+    if (sortType === 0 || sortType == 1) {
+      text = item.artist || ''
+    } else if (sortType === 2 || sortType == 3) {
+      text = item.album || ''
+    }
+    if (text && text.length > 0) {
+      const first = getFirstLetter(text)
+      if (!alphabetSet.has(first)) {
+        alphabetSet.add(first)
+      }
+    }
+  }
+  const sorted = Array.from(alphabetSet).sort((a, b) => {
+    if (sortType === 4 || sortType == 0 || sortType == 2 || sortType == 6) {
+      return a.localeCompare(b)
+    }
+    return b.localeCompare(a)
+  })
+  return sorted
+}
+
 
 function getFileDirName(filePath: string,rootPath:string): string{
   if(filePath===rootPath){
@@ -17915,3 +18204,29 @@ async function updateInfoDb(
       });
   });
 }
+
+@Concurrent
+async function replaceLocalMusicCoverTask(
+  context: Context,
+  filePath: string,
+  sandboxCoverPath: string
+): Promise<boolean> {
+  const coverResult: boolean = await changeMusicCover(context, filePath, sandboxCoverPath, true, '');
+  if (!coverResult) {
+    return false;
+  }
+
+  const table: MediaTable = new MediaTable(context);
+  await new Promise<void>((resolve, reject) => {
+    table.getRdbStore(context, (err: Error) => {
+      err ? reject(err) : resolve();
+    });
+  });
+
+  return await new Promise<boolean>((resolve) => {
+    table.updatePixelMapPath(filePath, fileUri.getUriFromPath(sandboxCoverPath),
+      (success: boolean, _error?: string) => {
+        resolve(success);
+      });
+  });
+}

+ 2 - 1
entry/src/main/resources/base/profile/main_pages.json

@@ -8,6 +8,7 @@
     "pages/VipPage",
     "pages/Demo",
     "pages/PlaylistDetailPage",
-    "pages/SmbTestPage"
+    "pages/SmbTestPage",
+    "pages/WallpaperPreviewPage"
   ]
 }

+ 16 - 18
lib/src/main/ets/view/LyricView2.ets

@@ -216,8 +216,6 @@ export struct LyricView2 {
                 }
                 .padding(8)
                 .border({ radius: 12 })
-                // .backgroundColor(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
-                //     && this.enableSeek && this.isUserTouching ? '#80a9a9a9' : '#00000000')
                 .onClick(() => {
                     if (this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0) {
                         if (this.seekIndex != -1 && this.seekIndex != this.currentIndex) {
@@ -313,22 +311,22 @@ export struct LyricView2 {
             // 中文翻译(整行显示)
             if (item.translation)  {
 
-            Text(item.translation)
-                .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?
-                    this.textHighlightSize : this.textSize)
-                .fontColor(this.currentMediaPosition >= item.beginTime ?
-                    index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? this.textHighlightColor : this.textColor : this.textColor)
-                .fontWeight(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
-                .padding(this.isSingleLine?{ bottom:2 }:{ bottom:5 })
-                .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
-                .visibility(this.isSingleLine?
-                    (index >= this.currentIndex && index <= this.currentIndex + 1 ? Visibility.Visible : Visibility.None)
-                    : Visibility.Visible)
-                .blendMode(
-                    index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.DST_IN : undefined,
-                    index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
-                )
-                .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?'95%': '80%')
+                Text(item.translation)
+                    .fontSize(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?
+                        this.textHighlightSize : this.textSize)
+                    .fontColor(this.currentMediaPosition >= item.beginTime ?
+                        index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? this.textHighlightColor : this.textColor : this.textColor)
+                    .fontWeight(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
+                    .padding(this.isSingleLine?{ bottom:2 }:{ bottom:5 })
+                    .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
+                    .visibility(this.isSingleLine?
+                        (index >= this.currentIndex && index <= this.currentIndex + 1 ? Visibility.Visible : Visibility.None)
+                        : Visibility.Visible)
+                    .blendMode(
+                        index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.DST_IN : undefined,
+                        index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
+                    )
+                    .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?'95%': '80%')
 
             }
         }