Просмотр исходного кода

Merge branch 'refs/heads/master' into feature/歌单

# Conflicts:
#	entry/src/main/ets/view/LocalMusic.ets
chendeben 10 месяцев назад
Родитель
Сommit
bf60b30070

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

@@ -145,8 +145,8 @@ struct NewIndex {
   @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.tab({
     buttons: [{ text: '分类' }, { text: '歌单' }],
     direction: Direction.Ltr,
-    backgroundColor: $r('app.color.start_window_background'),
-    selectedBackgroundColor:$r('app.color.index_background'),
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor:$r('app.color.start_window_background'),
     selectedFontColor: $r('app.color.text_color'),
     localizedTextPadding: {
       end: LengthMetrics.vp(10),

+ 118 - 24
entry/src/main/ets/view/LocalMusic.ets

@@ -1,6 +1,7 @@
 import TitleBar from './TitleBar'
 import { curves, display, PiPWindow, promptAction, router, SymbolGlyphModifier, window } from '@kit.ArkUI';
 import { VideoItem } from '../viewmodel/VideoItem';
+import {  stringForTime } from '../common/util/CommUtils';
 import { Playlist } from '../viewmodel/Playlist';
 import {  LengthMetrics, SegmentButton,borderRadiuses, SegmentButtonOptions } from '@kit.ArkUI';
 import {
@@ -161,6 +162,10 @@ const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度
 @Preview
 @Component
 export struct LocalMusic {
+  @State isRefreshing: boolean = false;
+  @State maxRefreshingHeight: number = 100.0;
+  private contentNode?: ComponentContent<Object> = undefined;
+  @State ratioL: number = 1;
   @State mediaKuCount: number = 0;
   @State albumCount: number = 0;
   @State artistCount: number = 0;
@@ -891,6 +896,7 @@ export struct LocalMusic {
           this.doUpdateData()
           break;
         case 102: //查询媒体库列表
+          this.isRefreshing = false
           this.mediaKuList = e.data.data
           Logger.info('onecold 查询媒体库列表 this.mediaKuList length= ' + this.mediaKuList.length)
           Utility.doSortListAscending(this.mediaKuList)
@@ -909,6 +915,7 @@ export struct LocalMusic {
           }
           break;
         case 103: //收到查询艺术家列表
+          this.isRefreshing = false
           this.artistMap = e.data.data1;
           this.artistList = e.data.data2;
           PreferencesUtil.putSync('artistCount', this.artistList.length)
@@ -926,6 +933,7 @@ export struct LocalMusic {
           break;
 
         case 104: //收到查询专辑列表
+          this.isRefreshing = false
           this.albumMap = e.data.data1;
 
           this.albumList = e.data.data2
@@ -2247,7 +2255,22 @@ export struct LocalMusic {
                 if(this.isGridMusic){
                   this.getGridView()
                 }else {
-                  this.getListView()
+                  Refresh({ refreshing: $$this.isRefreshing, refreshingContent: this.contentNode }) {
+                    this.getListView()
+                  }
+                  .pullDownRatio(this.ratioL)
+                  .pullToRefresh(true)
+                  .refreshOffset(0)
+                  .onOffsetChange((offset: number) => {
+                    // 越接近最大距离,下拉跟手系数越小。
+                    this.ratioL = 1 - Math.pow((offset / this.maxRefreshingHeight), 3);
+                  })
+                  .onStateChange((refreshStatus: RefreshStatus) => {
+                    console.info('onecold Refresh onStatueChange state is ' + refreshStatus);
+                  })
+                  .onRefreshing(async () => {
+                    this.refreshCurrent()
+                  })
                 }
               }
               if (this.modeType == 0) {
@@ -3346,6 +3369,19 @@ export struct LocalMusic {
     return this.currentTitleCover
   }
 
+  refreshCurrent() {
+    if(this.modeType==0){
+      this.getHistoryList(false)
+      this.asyncCurrentPathOnlyFile()
+    }else if(this.modeType == 1){
+      workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName });
+    }else if(this.modeType == 2){
+      workerInstance.postMessage({ code: 3, data: this.context });
+    }else if(this.modeType == 3){
+      workerInstance.postMessage({ code: 4, data: this.context });
+    }
+  }
+
   @Builder
   listViewTitle() {
     Column() {
@@ -3561,26 +3597,18 @@ export struct LocalMusic {
             this.isSearchMode = true
           })
 
-        Image($r("app.media.refresh"))
-          .fillColor(this.themeColor)
-          .width(25)
-          .visibility(this.isHistory || this.isCanBack || this.isSearchMode ? Visibility.None : Visibility.Visible)
-          .animation({
-            duration: 666,
-            curve: 'ease-in-out' // 可选动画曲线
-          })
-          .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-          .onClick(() => {
-            if (this.isFavMusic) {
-              this.getFavList(true)
-            } else {
-              if(this.modeType == 0){
-                this.asyncCurrentPathData()
-              }else if(this.modeType ==1){
-                workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName });
-              }
-            }
-          })
+        // Image($r("app.media.refresh"))
+        //   .fillColor(this.themeColor)
+        //   .width(25)
+        //   .visibility(this.isHistory || this.isCanBack || this.isSearchMode ? Visibility.None : Visibility.Visible)
+        //   .animation({
+        //     duration: 666,
+        //     curve: 'ease-in-out' // 可选动画曲线
+        //   })
+        //   .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
+        //   .onClick(() => {
+        //     this.refreshCurrent()
+        //   })
 
         Image($r("app.media.top_rank"))
           .width(25)
@@ -4766,6 +4794,25 @@ export struct LocalMusic {
     this.pinchValue = 1;
     this.scaleValue = 1;
   }
+
+  // 扫描当前目录下的文件,不扫描子目录
+  asyncCurrentPathOnlyFile(){
+    if(this.modeType==0){
+      const task = new taskpool.Task(scanCurrentDirectoryTask, getContext(this), this.currentPath,
+        this.lockPath,PreferencesUtil.getStringSync('COVER_API',''));
+      taskpool.execute(task, taskpool.Priority.HIGH).then(()=>{
+        if(this.modeType==0){
+          this.getSortedFiles(this.currentPath)
+        }
+        setTimeout(() => {
+          this.isRefreshing = false;
+        }, 100)
+      }).catch((e:object)=>{
+        console.info("task1 catch e: " + e);
+      })
+    }
+  }
+
   @Builder
   getListView() {
 
@@ -10794,8 +10841,8 @@ export struct LocalMusic {
       onClickBItem: async ()=>{
         if(this.mIjkMediaPlayer){
           this.jumpBTime = await this.mIjkMediaPlayer.getCurrentPosition()
-          if(this.jumpBTime<=this.jumpATime){
-            ToastUtil.showToast('设置B点时间不能小于A点时间')
+          if(this.jumpBTime<=this.jumpATime||stringForTime(this.jumpATime)==stringForTime(this.jumpBTime)){
+            ToastUtil.showToast('设置B点时间不能小于或等于A点时间')
             this.jumpBTime = 0
           }
         }
@@ -13297,4 +13344,51 @@ function getFileDirName(filePath: string,rootPath:string): string{
   if(result.startsWith('.'))
     result = result.replace(/\./g, '')
   return result
-}
+}
+
+
+//只扫描当前目录下文件(不扫描子文件夹)的方法
+@Concurrent
+async function scanCurrentDirectoryTask(context: Context, dirPath: string, lockPath: string, cover_api: string) {
+  const table: MediaTable = new MediaTable(context);
+
+  try {
+    // 直接获取当前目录下的文件列表
+    const files = FileUtil.listFileSync(dirPath);
+
+    await Promise.all(files.map(async  (file) => {
+      const fPath = `${dirPath}/${file}`;
+
+      // 跳过目录,只处理文件
+      if (FileUtil.isDirectory(fPath))  {
+        return;
+      }
+
+      // 跳过特定格式文件
+      if (fPath.endsWith('.lrc')  || fPath.endsWith('.srt'))  {
+        return;
+      }
+
+      // 检查是否为媒体文件
+      if (Utility.isMeidaByExtension(fPath))  {
+        let mType = CommonConstants.TYPE_LOCAL;
+        if (fPath.includes(lockPath))  {
+          mType = CommonConstants.TYPE_LOCK;
+        }
+
+        const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+          context, fPath, mType, true
+        );
+
+        table.insert(mediaItem,  (id: number) => {
+          // 插入回调函数
+        }, cover_api);
+      }
+    }));
+  } catch (error) {
+    console.error(' 扫描当前目录失败:', error);
+  }
+}
+
+
+

+ 1 - 0
lib/src/main/ets/bean/LyricLine.ts

@@ -7,6 +7,7 @@ export class LyricLine {
     readonly beginTime: number
     nextTime: number
     words: LyricWord[] = []
+    translation?: string; // 存储翻译文本
     /**
      * @param text The text of lyric line.
      * @param beginTime The begin timestamp of this lyric line.

+ 38 - 0
lib/src/main/ets/parse/LyricParser.ts

@@ -67,6 +67,26 @@ export class LyricParser implements IParser {
             }
             else {
 
+                // 处理双语歌词的特殊情况(英文行+中文行交替)
+                if (this.isBilingualLyric(src,  i)) {
+                    const englishLine = src[i];
+                    const chineseLine = src[i+1];
+
+                    // 解析英文逐字歌词
+                    const { timeline, words } = this.parseWordByWordLine(englishLine,  offset);
+
+                    // 获取中文文本(去掉时间戳)
+                    const chineseText = chineseLine.replace(/^\[\d{2}:\d{2}\.\d{2}\]/,  '').trim();
+
+                    // 创建双语歌词行(将中文作为附加文本)
+                    const bilingualLine = new LyricLine('', timeline, -1, words);
+                    bilingualLine.translation  = chineseText; // 新增translation字段存储翻译
+
+                    lyricLines.push(bilingualLine);
+                    i++; // 跳过已处理的中文行
+                    continue;
+                }
+
                 // 新增逐字歌词解析逻辑[mm:ss.xx] <mm:ss.xx>
                 if (this.isWordByWordLyric(line)) {
                     const { timeline, words } = this.parseWordByWordLine(line, offset);
@@ -120,6 +140,24 @@ export class LyricParser implements IParser {
         return result
     }
 
+    // 新增判断是否为双语歌词的方法
+    private isBilingualLyric(lines: string[], currentIndex: number): boolean {
+        if (currentIndex + 1 >= lines.length)  return false;
+
+        const currentLine = lines[currentIndex];
+        const nextLine = lines[currentIndex + 1];
+
+        // 检查当前行是否是英文逐字歌词
+        const isEnglishLine = this.isWordByWordLyric(currentLine)  &&
+        /[a-zA-Z]/.test(currentLine);
+
+        // 检查下一行是否是纯中文歌词(带时间戳但不含逐字标记)
+        const isChineseLine = /^\[\d{2}:\d{2}\.\d{2}\]/.test(nextLine) &&
+            !this.isWordByWordLyric(nextLine)  &&
+        /[\u4e00-\u9fa5]/.test(nextLine);
+
+        return isEnglishLine && isChineseLine;
+    }
 
     // 检测方括号格式的逐字歌词 [00:00.000]文[00:01.000]字
     private isSquareBracketWordByWordLyric(line: string): boolean {

+ 76 - 27
lib/src/main/ets/view/LyricView2.ets

@@ -300,38 +300,87 @@ export struct LyricView2 {
     }
     // 逐字歌词渲染
     @Builder
+    WordByWordLyric2(item: LyricLine, index: number) {
+        Column() {
+            // 英文逐字歌词
+            Row({ space: 0 }) {
+                ForEach(item.words,  (word: LyricWord, wordIndex: number) => {
+                    Text(word.word)
+                        .fontSize(index == this.currentIndex  ?
+                            this.textSize*this.controller.getHighlightScale()  : this.textSize)
+                        .fontColor(this.currentMediaPosition  >= word.startTime  ?
+                            index == this.currentIndex  ? this.textHighlightColor  : this.textColor  : this.textColor)
+                        .margin({ right: 2 })
+                })
+            }
+            .justifyContent(this.alignMode  === 'center' ? FlexAlign.Center : FlexAlign.Start)
+
+            // 中文翻译(整行显示)
+            if (item.translation)  {
+                Text(item.translation)
+                    .fontSize(this.textSize)
+                    .fontColor(index == this.currentIndex  ?
+                    this.textHighlightColor  : this.textColor)
+                    .margin({ top: 4 })
+            }
+        }
+        .width(this.alignMode  == 'center' ? '100%' : '95%')
+        .opacity(this.calculateOpacityFactor(index,  this.currentIndex))
+    }
+    @Builder
     WordByWordLyric(item: LyricLine, index: number) {
-        Row({ space: 0 }) {
-            ForEach(item.words, (word: LyricWord, wordIndex: number) => {
-
-
-                Text(this.getTransverterText(word.word))
-                    .fontSize(index == this.currentIndex ?this.textSize*this.controller.getHighlightScale():this.textSize)
-                    .fontColor(this.currentMediaPosition >= word.startTime ?
-                        index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
-                    .fontWeight(this.currentMediaPosition >= word.startTime && this.isHighlightBold ?
-                        index == this.currentIndex? FontWeight.Bold : this.textWeight: this.textWeight)
-                    .margin(0)
-                    .opacity(this.calculateOpacityFactor(index, this.currentIndex))
-                    .blur(this.calculateBlurFactor(index, this.currentIndex))
-                    .visibility(this.isSingleLine?
-                        (index == this.currentIndex ?Visibility.Visible:Visibility.None)
-                        :Visibility.Visible)
-                    .animation({
-                        // 动画播放速度
-                        tempo: 0.8,
-                        // 动画持续时间,单位是毫秒
-                        duration: 777,
-                        // 动画缓动函数
-                        curve: Curve.FastOutSlowIn
-                    })
-            })
+        Column() {
+            Row({ space: 0 }) {
+                ForEach(item.words, (word: LyricWord, wordIndex: number) => {
+
+
+                    Text(word.word)
+                        .fontSize(index == this.currentIndex ?this.textSize*this.controller.getHighlightScale():this.textSize)
+                        .fontColor(this.currentMediaPosition >= word.startTime ?
+                            index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
+                        .fontWeight(this.currentMediaPosition >= word.startTime && this.isHighlightBold ?
+                            index == this.currentIndex? FontWeight.Bold : this.textWeight: this.textWeight)
+                        .margin({ right: 4 })
+                        .opacity(this.calculateOpacityFactor(index, this.currentIndex))
+                        .blur(this.calculateBlurFactor(index, this.currentIndex))
+                        .visibility(this.isSingleLine?
+                            (index == this.currentIndex ?Visibility.Visible:Visibility.None)
+                            :Visibility.Visible)
+                        .animation({
+                            // 动画播放速度
+                            tempo: 0.8,
+                            // 动画持续时间,单位是毫秒
+                            duration: 777,
+                            // 动画缓动函数
+                            curve: Curve.FastOutSlowIn
+                        })
+                })
+            }
+            .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
+            .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
+
+            // 中文翻译(整行显示)
+            if (item.translation)  {
+                Row({ space: 0 }) {
+                    Text(item.translation)
+                        .fontSize(index == this.currentIndex ?this.textSize*this.controller.getHighlightScale():this.textSize)
+                        .fontColor(this.currentMediaPosition >= item.beginTime ?
+                            index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
+                        .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
+                        .margin({ top: 4 })
+                        .opacity(this.calculateOpacityFactor(index, this.currentIndex))
+                        .blur(this.calculateBlurFactor(index, this.currentIndex))
+                }
+                .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
+                .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
+            }
         }
-        .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
-        .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
+
+
     }
 
 
+
     isTopBottomLine(index:number){
         return  index === 0 || index === this.listAdapter.totalCount()  - 1;
     }