Forráskód Böngészése

长按歌词可以弹出复制歌词窗口
播放页UI调整可直接在播放页切换沉浸式

onecold 5 hónapja
szülő
commit
bf5eedd9b9

+ 4 - 4
entry/src/main/ets/pages/NewIndex.ets

@@ -691,7 +691,7 @@ struct NewIndex {
 
       SymbolGlyph($r('sys.symbol.backward_end_fill'))
         .fontSize(28)
-        .fontColor([this.themeColor])
+        .fontColor([Color.White])
         .alignSelf(ItemAlign.Center)
         .visibility(this.isShowPrecious? Visibility.Visible:Visibility.None)
         .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
@@ -715,7 +715,7 @@ struct NewIndex {
           .width(36)
           .displayPriority(3)
           .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
-          .fillColor(this.themeColor)
+          .fillColor(Color.White)
           .onClick(() => {
             this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
           })
@@ -724,7 +724,7 @@ struct NewIndex {
 
       SymbolGlyph($r('sys.symbol.forward_end_fill'))
         .fontSize(28)
-        .fontColor([this.themeColor])
+        .fontColor([Color.White])
         .alignSelf(ItemAlign.Center)
         .margin({
           right: 14,
@@ -741,7 +741,7 @@ struct NewIndex {
         .width(26)
         .displayPriority(1)
         .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-        .fillColor(this.themeColor)
+        .fillColor(Color.White)
         .onClick(() => {
           this.getUIContext().getHostContext()!.eventHub.emit('openPlayList');
         })

+ 5 - 8
entry/src/main/ets/pages/WallpaperPreviewPage.ets

@@ -77,19 +77,16 @@ export struct WallpaperPreviewPage {
         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)
+                .shadow({
+                  radius: 55,
+                  type: ShadowType.BLUR,
+                  color: 'on_primary'
+                })
             }
-            .id('wallpaper_preview')
             .width('100%')
             .aspectRatio(this.wallpaperRatio)
             .borderRadius(28)

+ 162 - 26
entry/src/main/ets/view/LocalMusic.ets

@@ -176,6 +176,7 @@ import { getFirstLetter } from '../Constants';
 import { audio } from '@kit.AudioKit';
 import { PointLightDefaultButton } from './PointLight/PointLightDeFaultButton';
 import { CoverThumbCache } from '../common/util/CoverThumbCache';
+import { LyricCopySheet } from './LyricCopySheet';
 
 const TAG = 'LocalMusic';
 const HICAR_LOG_TAG = 'HiCarWindow';
@@ -724,6 +725,9 @@ export struct LocalMusic {
   @State translateY: number = 0;
   @State isShowSheet: boolean = false;
   @State isShowSheetView: boolean = false;
+  @State isShowLyricCopySheet: boolean = false;
+  @State lyricCopyLines: string[] = [];
+  @State lyricSelectedIndexes: number[] = [];
   @State isShowTimeCloseView: boolean = false;
   private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Vertical });
   @StorageLink('currIndex') curIndex: number = 0;
@@ -5142,6 +5146,28 @@ export struct LocalMusic {
     )
   }
 
+  private syncFavStateLocal(item: VideoItem, isFav: number): void {
+    const nextFavList = [...this.favList];
+    const favIndex = nextFavList.findIndex((favItem: VideoItem) => favItem.filePath === item.filePath);
+    if (isFav === 1) {
+      if (favIndex >= 0) {
+        nextFavList[favIndex].isFav = 1;
+      } else {
+        const favItem = cloneVideoItem(item);
+        favItem.isFav = 1;
+        nextFavList.push(favItem);
+      }
+    } else if (favIndex >= 0) {
+      nextFavList.splice(favIndex, 1);
+    }
+    this.favList = nextFavList;
+    item.isFav = isFav;
+    if (this.currentSong && this.currentSong.filePath === item.filePath) {
+      this.currentSong.isFav = isFav;
+      this.isFac = isFav === 1;
+    }
+  }
+
   doFav(item: VideoItem) {
     LogUtil.info('onecold doFav isFav=' + item.isFav)
 
@@ -5154,6 +5180,7 @@ export struct LocalMusic {
 
     this.table.updateIsFavByFilePath(item.filePath, isFav, async (result: boolean) => {
       if (result) {
+        this.syncFavStateLocal(item, isFav);
         if (isFav === 1) {
           ToastUtil.showToast('收藏成功');
         } else {
@@ -11348,6 +11375,83 @@ export struct LocalMusic {
     this.lyricControllerSingle.setLyric(null);
   }
 
+  private extractLyricCopyLines(lyricText: string): string[] {
+    if (StrUtil.isEmpty(lyricText)) {
+      return [];
+    }
+    const normalizedLyric = LyricUtil.convertLyricToSimpleLrc(lyricText);
+    const source = StrUtil.isNotEmpty(normalizedLyric) ? normalizedLyric : lyricText;
+    const lines = source.split('\n');
+    const result: string[] = [];
+    const metaTagPattern = /^\[(ti|ar|al|by|offset|hash|sign|qq|total|tool|re|ve|length|au):?.*]$/i;
+
+    for (let i = 0; i < lines.length; i++) {
+      const line = lines[i].trim();
+      if (!line || metaTagPattern.test(line)) {
+        continue;
+      }
+      const pureLine = line
+        .replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?]/g, '')
+        .replace(/<\d{1,2}:\d{2}(?:\.\d{1,3})?>/g, '')
+        .trim();
+      if (pureLine.length > 0) {
+        result.push(pureLine);
+      }
+    }
+    return result;
+  }
+
+  private openLyricCopySheet(): void {
+    const lines = this.extractLyricCopyLines(this.lyricContent);
+    if (ArrayUtil.isEmpty(lines)) {
+      ToastUtil.showToast('暂无可复制歌词');
+      return;
+    }
+    this.lyricCopyLines = lines;
+    this.lyricSelectedIndexes = [];
+    this.isShowLyricCopySheet = true;
+  }
+
+  private toggleLyricLineSelection(index: number): void {
+    const selectedIndex = this.lyricSelectedIndexes.indexOf(index);
+    if (selectedIndex >= 0) {
+      const nextSelectedIndexes = [...this.lyricSelectedIndexes];
+      nextSelectedIndexes.splice(selectedIndex, 1);
+      this.lyricSelectedIndexes = nextSelectedIndexes;
+      return;
+    }
+    this.lyricSelectedIndexes = [...this.lyricSelectedIndexes, index];
+  }
+
+  private toggleLyricSelectAll(): void {
+    if (this.lyricSelectedIndexes.length === this.lyricCopyLines.length) {
+      this.lyricSelectedIndexes = [];
+      return;
+    }
+    this.lyricSelectedIndexes = this.lyricCopyLines.map((_: string, index: number) => index);
+  }
+
+  private copySelectedLyricLines(): void {
+    if (ArrayUtil.isEmpty(this.lyricSelectedIndexes)) {
+      ToastUtil.showToast('请先选择歌词');
+      return;
+    }
+    const selectedIndexes = [...this.lyricSelectedIndexes].sort((a: number, b: number) => a - b);
+    const selectedLines: string[] = [];
+    for (let i = 0; i < selectedIndexes.length; i++) {
+      const index = selectedIndexes[i];
+      if (index >= 0 && index < this.lyricCopyLines.length) {
+        selectedLines.push(this.lyricCopyLines[index]);
+      }
+    }
+    if (ArrayUtil.isEmpty(selectedLines)) {
+      ToastUtil.showToast('暂无可复制歌词');
+      return;
+    }
+    Utility.copyText(selectedLines.join('\n'));
+    ToastUtil.showToast(`已复制${selectedLines.length}行歌词`);
+  }
+
   /**
    * 从在线API获取歌词
    */
@@ -11704,6 +11808,18 @@ export struct LocalMusic {
           .width("100%")
           .layoutWeight(1)
           .margin({ left: this.currentLyricAlignMode === 0 ? 0 : 50, top: 2, bottom: this.isPhoneLan()||this.isHiCarKuanBianPing() ? 40 : 10 })
+          .gesture(LongPressGesture()
+            .onAction(() => {
+              this.openLyricCopySheet();
+            }))
+          .bindSheet($$this.isShowLyricCopySheet, this.LyricCopySheetBuilder(), {
+            height: '99%',
+            dragBar: true,
+            showClose: true,
+            preferType: SheetType.CENTER,
+            blurStyle: BlurStyle.Thin,
+            title: { title: '歌词' }
+          })
 
 
         if (this.isPhoneLan()) {
@@ -11725,6 +11841,26 @@ export struct LocalMusic {
     }
   }
 
+  @Builder
+  LyricCopySheetBuilder() {
+    LyricCopySheet({
+      lyricCopyLines: this.lyricCopyLines,
+      lyricSelectedIndexes: this.lyricSelectedIndexes,
+      isDarkMode: this.isDarkMode,
+      themeColor: this.themeColor,
+      bottomSafeHeight: this.bottomSafeHeight,
+      onToggleLyricLine: (index: number) => {
+        this.toggleLyricLineSelection(index);
+      },
+      onToggleSelectAll: () => {
+        this.toggleLyricSelectAll();
+      },
+      onCopySelectedLines: () => {
+        this.copySelectedLyricLines();
+      }
+    })
+  }
+
   @Builder
   SingleLyricView() {
     Column() {
@@ -11736,6 +11872,10 @@ export struct LocalMusic {
           seekLineColor: "#80ffffff", // 滑动定位线颜色
           seekUIStyle: "listItem", // 滑动定位样式(seekLine传统样式,listItem类似抖音汽水音乐样式)
         })
+          .gesture(LongPressGesture()
+            .onAction(() => {
+              this.openLyricCopySheet();
+            }))
       }
       .height(82)
       .margin({ bottom: 20 })
@@ -12094,20 +12234,24 @@ export struct LocalMusic {
 
         })
 
-
       PointLightDefaultButton({
-        isSysBol:false,
+        isSysBol:true,
         pointColor:Color.White,
-        imageResource:$r('app.media.lyric'),
+        imageResource:Utility.getIsFav(this.favList,this.currentSong)?
+          $r('sys.symbol.heart_fill'):$r('sys.symbol.heart'),
         isPx: false,
         builderHeight: 26,
         builderWidth: 26,
       })
         .onClick(async () => {
-          this.isLyricSetting = !this.isLyricSetting;
+          if(this.currentSong){
+            this.doFav(this.currentSong)
+          }
 
         })
 
+
+
       this.playListBuilder()
 
     }
@@ -12565,29 +12709,21 @@ export struct LocalMusic {
       .margin({ left: 15 })
       Blank()
       Row(){
-
-        Button({type:ButtonType.Circle,stateEffect:true}){
-          //添加或取消收藏
-          SymbolGlyph(Utility.getIsFav(this.favList,this.currentSong)?
-            $r('sys.symbol.heart_fill'):$r('sys.symbol.heart'))
-            .fontSize(22)
-            .fontColor([Color.White])
-            .alignSelf(ItemAlign.Center)
-            .clickEffect({level:ClickEffectLevel.MIDDLE})
-            .aspectRatio(CommonConstants.ASPECT_RATIO)
-            .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
-            .onClick(async () => {
-              if(this.currentSong){
-                this.doFav(this.currentSong)
-              }
-            })
-        }
-        .margin({ left: 8,right:8 })
-        .backgroundColor(Color.Transparent)
-        .width(45)
-        .height(45)
-
+        PointLightDefaultButton({
+          isSysBol:true,
+          pointColor:Color.White,
+          imageResource:this.showSimple?
+            $r('sys.symbol.sun_max'):$r('sys.symbol.sun_max_fill'),
+          isPx: false,
+          builderHeight: 26,
+          builderWidth: 26,
+        })
+          .onClick(async () => {
+            this.showSimple =  !this.showSimple
+            PreferencesUtil.put('showSimple', this.showSimple)
+          })
       }
+      .margin({ left: 8,right:8 })
 
 
     }

+ 114 - 0
entry/src/main/ets/view/LyricCopySheet.ets

@@ -0,0 +1,114 @@
+import { CommonConstants } from '../common/constants/CommonConstants';
+
+@Component
+export struct LyricCopySheet {
+  @Prop lyricCopyLines: string[] = [];
+  @Prop lyricSelectedIndexes: number[] = [];
+  @Prop isDarkMode: boolean = false;
+  @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @Prop bottomSafeHeight: number = 0;
+
+  onToggleLyricLine = (_index: number) => {
+  }
+  onToggleSelectAll = () => {
+  }
+  onCopySelectedLines = () => {
+  }
+
+  private isLyricLineSelected(index: number): boolean {
+    return this.lyricSelectedIndexes.indexOf(index) >= 0;
+  }
+
+  build() {
+    Column() {
+      Row() {
+        Text(`已选 ${this.lyricSelectedIndexes.length} 行`)
+          .fontSize(13)
+          .fontColor(this.isDarkMode ? '#B3B3B3' : '#7A7A7A')
+          .margin({right:10})
+        Blank()
+        Text(this.lyricSelectedIndexes.length === this.lyricCopyLines.length ? '清空' : '全选')
+          .fontSize(13)
+          .margin({left:15})
+          .fontColor(this.themeColor)
+          .onClick(() => {
+            this.onToggleSelectAll();
+          })
+      }
+      .padding({ left: 16, right: 16, top: 12, bottom: 8 })
+
+      List({ space: 8 }) {
+        ForEach(this.lyricCopyLines, (line: string, index: number) => {
+          ListItem() {
+            Row() {
+              Text(line)
+                .layoutWeight(1)
+                .fontSize(16)
+                .fontColor($r('app.color.text_color'))
+                .maxLines(3)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+              if (this.isLyricLineSelected(index)) {
+
+              }
+            }
+            .width('100%')
+            .padding({ left: 16, right: 16, top: 12, bottom: 12 })
+            .backgroundColor(this.isLyricLineSelected(index) ?
+              (this.isDarkMode ? '#243242' : '#E9F3FF') :
+              (this.isDarkMode ? '#1E1E1E' : '#F7F8FA'))
+            .border({
+              width: 1,
+              color: this.isLyricLineSelected(index) ? this.themeColor : Color.Transparent
+            })
+            .borderRadius(10)
+            .onClick(() => {
+              this.onToggleLyricLine(index);
+            })
+          }
+        }, (line: string, index: number) => `${index}-${line}`)
+      }
+      .layoutWeight(1)
+      .padding({ left: 12, right: 12, top: 4, bottom: 12 })
+      .scrollBar(BarState.Auto)
+
+      Row() {
+        Button(this.lyricSelectedIndexes.length === this.lyricCopyLines.length ? '清空' : '全选', {
+          type: ButtonType.Capsule,
+          stateEffect: true
+        })
+          .layoutWeight(1)
+          .height(42)
+          .fontSize(14)
+          .fontColor(this.themeColor)
+          .backgroundColor(Color.Transparent)
+          .border({
+            width: 1,
+            color: this.themeColor
+          })
+          .margin({left:50,right:25})
+          .onClick(() => {
+            this.onToggleSelectAll();
+          })
+
+        Button(`复制(${this.lyricSelectedIndexes.length})`, {
+          type: ButtonType.Capsule,
+          stateEffect: true
+        })
+          .layoutWeight(1)
+          .height(42)
+          .fontSize(14)
+          .margin({left:25,right:50})
+          .enabled(this.lyricSelectedIndexes.length > 0)
+          .opacity(this.lyricSelectedIndexes.length > 0 ? 1 : 0.55)
+          .backgroundColor(this.themeColor)
+          .onClick(() => {
+            this.onCopySelectedLines();
+          })
+      }
+      .padding({ left: 16, right: 16, top: 8, bottom: this.bottomSafeHeight + 16 })
+      .justifyContent(FlexAlign.SpaceBetween)
+    }
+    .width('100%')
+    .height('100%')
+  }
+}

+ 3 - 3
entry/src/main/ets/view/PointLight/PointLightDeFaultButton.ets

@@ -5,8 +5,8 @@ import { deviceInfo } from "@kit.BasicServicesKit"
 export struct PointLightDefaultButton{
   @Require isPx: boolean
   @Require isSysBol: boolean
-  @Require pointColor: ResourceColor
-  @Require imageResource: ResourceStr|Resource
+  @Require@Prop pointColor: ResourceColor
+  @Require@Prop imageResource: ResourceStr|Resource
   @Require@Prop@Watch('setButtonSize') builderHeight: number
   @Require@Prop@Watch('setButtonSize') builderWidth: number
   // 可选
@@ -96,4 +96,4 @@ export struct PointLightDefaultButton{
         .aspectRatio(1)
     }
   }
-}
+}

+ 50 - 5
lib/src/main/ets/view/LyricView2.ets

@@ -139,11 +139,50 @@ export struct LyricView2 {
             }
         }
         this.isLyricEmpty = this.listAdapter.isEmpty();
-        this.currentIndex = 0;
-        this.animateToIndex(0);
+        this.lastPositionUpdateTs = 0
+        this.lastPositionForRender = -1
+        if (this.isLyricEmpty) {
+            this.currentIndex = 0
+            this.scrollToIndexImmediately(0)
+            return
+        }
+        const initialIndex = this.resolveInitialIndex()
+        this.currentIndex = initialIndex
+        this.scrollToIndexImmediately(initialIndex)
 
     }
 
+    private resolveInitialIndex(): number {
+        const count = this.listAdapter.totalCount()
+        if (count <= 0) {
+            return 0
+        }
+        if (this.currentLyric && this.currentLyric.isPlainText) {
+            return 0
+        }
+        const firstLine = this.listAdapter.getData(0)
+        const lastLine = this.listAdapter.getData(count - 1)
+        const lastEndTime = lastLine.nextTime > lastLine.beginTime ? lastLine.nextTime : lastLine.beginTime
+        if (this.currentMediaPosition <= firstLine.beginTime - 2000 || this.currentMediaPosition > lastEndTime + 2000) {
+            return 0
+        }
+        return this.getIndex(this.currentMediaPosition)
+    }
+
+    private scrollToIndexImmediately(index: number): void {
+        const count = this.listAdapter.totalCount()
+        if (count <= 0) {
+            return
+        }
+        const safeIndex = Math.max(0, Math.min(count - 1, index))
+        if(this.isHightLightCenter){
+            this.scroller.scrollToIndex(safeIndex, false, ScrollAlign.CENTER)
+        }else{
+            const targetIndex = Math.max(0, safeIndex - 2)
+            this.scroller.scrollToIndex(targetIndex, false, ScrollAlign.START)
+        }
+    }
+
     private getAttrFromController() {
         this.currentLyric = this.controller.getLyric()
         this.textSize = this.controller.getTextSize()
@@ -691,15 +730,21 @@ export struct LyricView2 {
 
     private animateToIndex(index: number) {
         // printD('animate to index= ' + index)
-        this.currentIndex = index
+        const count = this.listAdapter.totalCount()
+        if (count <= 0) {
+            this.currentIndex = 0
+            return
+        }
+        const safeIndex = Math.max(0, Math.min(count - 1, index))
+        this.currentIndex = safeIndex
         if (this.isUserTouching) {
             return
         }
         if(this.isHightLightCenter){
-            this.scroller.scrollToIndex(index, true, ScrollAlign.CENTER)
+            this.scroller.scrollToIndex(safeIndex, true, ScrollAlign.CENTER)
         }else{
             // 计算目标索引,使其在居中位置上方有两条歌词
-            const targetIndex = Math.max(0, index - 2);
+            const targetIndex = Math.max(0, safeIndex - 2);
             this.scroller.scrollToIndex(targetIndex, true, ScrollAlign.START);
         }