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

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	entry/src/main/ets/pages/ScanFilePage.ets
#	entry/src/main/ets/pages/SettingPage.ets
#	entry/src/main/ets/view/LocalMusic.ets
#	entry/src/main/resources/base/element/string.json
chendeben 1 год назад
Родитель
Сommit
2e9e234439

+ 2 - 2
AppScope/app.json5

@@ -2,8 +2,8 @@
   "app": {
     "bundleName": "com.xgplayer.ttmusic.hm",
     "vendor": "example",
-    "versionCode": 20250605,
-    "versionName": "1.3.9",
+    "versionCode": 20250616,
+    "versionName": "1.4.0",
     "icon": "$media:app_icon",
     "label": "$string:app_name",
     "multiAppMode": {

+ 4 - 0
entry/src/main/ets/common/constants/CommonConstants.ets

@@ -64,6 +64,8 @@ export class CommonConstants {
   static readonly TAG: string = '[UserAuth]';
   static readonly MILLISECONDS_TO_SECONDS: number = 1;
 
+  static readonly QQ_GROUP: string= "685109858"; // 替换为实际群号
+
   static readonly BREAKPOINT_TYPES: string[] = ['xs', 'sm', 'md', 'lg', 'xl'];
 
   static readonly VIDEO_FORMAT = ['.mp4','.mov','.m4v','.avi','.3gv','.wmv',
@@ -88,6 +90,8 @@ export class CommonConstants {
   static readonly SORT_ARRAY = ["按艺术家升序", "按艺术家降序", "按专辑升序", "按专辑降序", "按名称升序", "按名称降序", "按添加时间升序",
   "按添加时间降序"]
 
+  static readonly PIP_LYRIC_BG = ["音乐封面", "自定义"]
+
 
   //德本的接口调用微信支付下单接口返回的支付交易会话ID,该值有效期为2小时。
   static readonly WX_PAY_API: string = "https://pay.ss5.xyz/wechat/prepay"

+ 91 - 1
entry/src/main/ets/common/util/MediaTable.ets

@@ -223,7 +223,8 @@ export default class MediaTable {
       obj.lastPlayedStr  = resultSet.getString(resultSet.getColumnIndex('lastPlayedStr'));
       obj.playCount  = resultSet.getDouble(resultSet.getColumnIndex('playCount'));
       obj.lyricContent  = resultSet.getString(resultSet.getColumnIndex('lyricContent'));
-
+      obj.md5Str  = resultSet.getString(resultSet.getColumnIndex('md5Str'));
+      obj.extra_json  = resultSet.getString(resultSet.getColumnIndex('extra_json'));
       const valueBucket: relationalStore.ValuesBucket = obj
       // Step 4: 执行更新
       const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
@@ -422,6 +423,85 @@ export default class MediaTable {
     return items;
   }
 
+  // 根据filePath更新lastPlayedStr的值同时playCount值加1
+  public updateLastPlayedStrByFilePath(filePath: string, lastPlayedStr: string, callback: (success: boolean, error?: string) => void) {
+    const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
+    predicates.equalTo('filePath',   filePath);
+
+    this.accountTable.query(predicates,   (resultSet: relationalStore.ResultSet) => {
+      if (resultSet.rowCount   === 0) {
+        callback(false, 'Error: File not found');
+        resultSet.close();
+        return;
+      }
+
+      // 获取当前的 playCount 值
+      let currentPlayCount = 0;
+      if (resultSet.goToFirstRow())  {
+        currentPlayCount = resultSet.getLong(resultSet.getColumnIndex('playCount'));
+      }
+
+      resultSet.close();
+
+      // 计算新的 playCount 值
+      const newPlayCount = currentPlayCount + 1;
+
+      // 准备要更新的值
+      const valueBucket: relationalStore.ValuesBucket = {
+        lastPlayedStr: lastPlayedStr,
+        playCount: newPlayCount
+      };
+
+      // 更新数据
+      this.accountTable.updateData(predicates,   valueBucket, (success: boolean) => {
+        callback(success, success ? '' : 'Update failed');
+      });
+    });
+  }
+
+  // 根据最近播放时间查询指定数量的记录
+  public queryRecentPlayedRecords(count: number, callback: (result: VideoItem[]) => void) {
+    try {
+      // 1. 构建查询条件:按lastPlayedStr降序排列,限制返回条数,且lastPlayedStr不为空
+      const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
+      // 添加筛选条件,确保lastPlayedStr不为空
+      predicates.isNotNull('lastPlayedStr');
+      predicates.notEqualTo('lastPlayedStr',  ''); // 排除空字符串
+      // 使用 orderByDesc 方法进行降序排序
+      predicates.orderByDesc('lastPlayedStr');
+      // 使用 limit 方法限制返回的记录数量
+      predicates.limitAs(count);
+
+      // 2. 执行查询并处理结果
+      this.accountTable.query(predicates,  (resultSet: relationalStore.ResultSet) => {
+        // 3. 复用已有的解析逻辑
+        const result = this.parseResultSetToVideoItems(resultSet);
+        callback(result);
+      });
+    } catch (err) {
+      Logger.error(`queryRecentPlayedRecords   error: ${err.code}   - ${err.message}`);
+      callback([]);
+    }
+  }
+
+  // 清空播放历史记录
+  public clearPlayHistory(callback: (success: boolean, error?: string) => void) {
+    // 1. 构建查询条件:筛选lastPlayedStr非空的记录
+    const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
+    predicates.isNotNull('lastPlayedStr');
+
+    // 2. 准备更新数据:将lastPlayedStr设为空字符串
+    const valueBucket: relationalStore.ValuesBucket = {
+      lastPlayedStr: ''
+    };
+
+    // 3. 执行批量更新操作
+    this.accountTable.updateData(predicates,   valueBucket, (success: boolean) => {
+      // 将 null 替换为 undefined
+      callback(success, success ? 'Clear play history success' : 'Clear play history operation failed');
+    });
+  }
+
   private buildVideoItem(rs: relationalStore.ResultSet): VideoItem {
     // 添加空值保护
     const safeGet = (col: string) => {
@@ -459,6 +539,9 @@ export default class MediaTable {
     item.playCount = safeGetNumber(DB_COLUMNS.PLAY_COUNT);
     item.lyricContent = safeGet(DB_COLUMNS.LYRIC_CONTENT);
 
+    item.md5Str = safeGet('md5Str');
+    item.extra_json = safeGet('extra_json');
+
     return item;
   }
 
@@ -520,6 +603,13 @@ function generateBucket(item: VideoItem): relationalStore.ValuesBucket {
   if(item.lyricContent){
     obj.lyricContent = item.lyricContent;
   }
+  if(item.md5Str){
+    obj.md5Str = item.md5Str;
+  }
+  if(item.extra_json){
+    obj.extra_json = item.extra_json;
+  }
+
 
   return obj;
 }

+ 5 - 1
entry/src/main/ets/common/util/RdbUtils.ets

@@ -58,12 +58,14 @@ export default class RdbUtils {
       '        lastPlayedStr TEXT,\n' +
       '        trackCount TEXT,\n' +
       '        lyricContent TEXT,\n' +
+      '        md5Str TEXT,\n' +
+      '        extra_json TEXT,\n' +
       '        mimeType TEXT' +
       ')',
     columns: ['id', 'name', 'filePath','mtype', 'videoSize', 'cTime','size', 'artist', 'album',
       'fileName','parentPath','isFav','pixelMapPath','pixelMapToString',
       'duration',   'sampleRate','playCount',  'lastPlayedStr', 'trackCount',
-      'lyricContent','mimeType']
+      'lyricContent','md5Str','extra_json','mimeType']
   };
 
   constructor(tableName: string, sqlCreateTable: string, columns: Array<string>) {
@@ -133,6 +135,8 @@ export default class RdbUtils {
             'lastPlayedStr': 'TEXT',
             'trackCount': 'TEXT',
             'lyricContent': 'TEXT',
+            'md5Str': 'TEXT',
+            'extra_json': 'TEXT',
             'mimeType': 'TEXT'
           };
           

+ 5 - 2
entry/src/main/ets/pages/AboutPage.ets

@@ -184,8 +184,11 @@ export struct AboutPage{
               pressed: { opacity: 0.6 }
             })
             .onClick(()=>{
-              ToastUtil.showToast('复制群号成功!')
-              Utility.copyText('685109858')
+              // ToastUtil.showToast('复制群号成功!')
+              // Utility.copyText('685109858')
+              const qqUrl = `mqqapi://card/show_pslcard?src_type=internal&version=1&uin=${CommonConstants.QQ_GROUP}&card_type=group&source=external`;
+              let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
+              context.openLink(qqUrl, {})
             })
 
           Button('用户反馈', { type: ButtonType.Capsule, stateEffect: false })

+ 94 - 64
entry/src/main/ets/pages/ScanFilePage.ets

@@ -15,6 +15,7 @@ import { emitter } from '@kit.BasicServicesKit'
 import { BreakpointTypeEnum } from '../common/util/BreakpointSystem'
 import { resourceManager } from '@kit.LocalizationKit'
 import { ConfigurationConstant } from '@kit.AbilityKit'
+import { DialogHelper } from '@pura/harmony-dialog'
 
 
 @Preview
@@ -26,6 +27,7 @@ export struct ScanFilePage{
 
   @State strText:string = ''
   @State isStart:boolean = false
+  @State isStartCover:boolean = false
   @State appName:string = ''
   //lottie动画构建渲染上下文
   private mainRenderingSettings: RenderingContextSettings = new RenderingContextSettings(true)
@@ -77,12 +79,18 @@ export struct ScanFilePage{
   onColorModeChange() {
     this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
   }
-  endScan(){
+
+  endScan(isOnekey:boolean){
     const eventData: emitter.EventData = {};
     emitter.emit({ eventId: 101 }, eventData); // 发送视频打开广播事件
 
     ToastUtil.showToast('扫描文件入库成功!')
-    this.isStart = false
+    if(isOnekey){
+      this.isStartCover = false
+    }else{
+      this.isStart = false
+
+    }
     this.strText = ''
     //结束动画
     lottie.pause()
@@ -116,18 +124,22 @@ export struct ScanFilePage{
   initState(){
 
     this.initLottie(this.path,true)
-    this.isStart = true
+    if(isOnekey){
+      this.isStartCover = true
+    }else{
+      this.isStart = true
+    }
     this.strText = Utility.resourceToString(getContext(this),$r('app.string.scaning'))
     lottie.play()
   }
 
-  doOptimize(){
+  doOptimize(isOnekey:boolean){
 
-    this.initState()
+    this.initState(isOnekey)
     const task = new taskpool.Task(scanDirectoryTask, getContext(this), this.rootPath,
       this.lockPath,PreferencesUtil.getStringSync('COVER_API',''));
     taskpool.execute(task, taskpool.Priority.HIGH).then(()=>{
-      this.endScan()
+      this.endScan(isOnekey)
     }).catch((e:object)=>{
       console.info("task1 catch e: " + e);
     })
@@ -270,14 +282,34 @@ export struct ScanFilePage{
 
               .width('60%')
               .height(55)
-              .margin({ top: 30, bottom: 20 })
+              .margin({ top: 20, bottom: 10 })
               .linearGradient({
                 direction: GradientDirection.Right,
                 colors: [['#ff37a0fc', 0.0], ['#67e667', 0.5], ['#f5856e', 1.0]]
               })
               .enabled(this.isStart ?false:true)
               .onClick(() => {
-                this.doOptimize()
+                this.doOptimize(false)
+
+              })
+              .alignSelf(ItemAlign.Center)
+
+            Button(this.isStartCover?'正在扫描':$r('app.string.onekey_cover'), { type: ButtonType.Capsule, stateEffect: false })
+
+              .width('60%')
+              .height(55)
+              .margin({ top: 10, bottom: 20 })
+              .linearGradient({
+                direction: GradientDirection.Right,
+                colors: [['#ff37a0fc', 0.0], ['#67e667', 0.5], ['#f5856e', 1.0]]
+              })
+              .enabled(this.isStartCover ?false:true)
+              .onClick(() => {
+                if(PreferencesUtil.getStringSync('COVER_API','')===''){
+                  this.showTipsDialog()
+                  return
+                }
+                this.doOptimize(true)
 
               })
               .alignSelf(ItemAlign.Center)
@@ -296,65 +328,63 @@ export struct ScanFilePage{
     }
   }
 
-}
+  showTipsDialog() {
+    DialogHelper.showCustomContentDialog({
+      dialogId: 'tips',
+      title: "友情提示",
+      autoCancel: false, //点击遮障层时,不关闭弹窗
+      backCancel: true, //点击返回键,不关闭弹窗
+      contentBuilder: () => {
+        this.customTipsBuilder("请到设置界面配置封面服务器地址!")
+      },
+      buttons: [],
+    })
+  }
 
+  @Builder
+  customTipsBuilder(content: string) {
+    Column() {
+      Text(content)
+        .fontColor(Color.Gray)
+        .fontSize(16)
+        .alignSelf(ItemAlign.Start)
+        .margin({ bottom: 15 })
+        .fontSize(16)
+
+      Row() {
+        Button('取消')
+          .fontColor(Color.White)
+          .backgroundColor($r('app.color.title_bar_bg'))//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]})
+          .height(50)
+          .layoutWeight(1)
+          .stateEffect(true)
+          .margin({ right: 6 })
+          .onClick(() => {
+            DialogHelper.closeDialog('tips'); //关闭弹框
+          })
+        Button('跳转')
+          .fontColor(Color.White)//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]})
+          .layoutWeight(1)
+          .height(50)
+          .backgroundColor($r('app.color.title_bar_bg'))
+          .stateEffect(true)
+          .margin({ left: 6 })
+          .onClick(() => {
+            DialogHelper.closeDialog('tips'); //关闭弹框
+            router.pushUrl({
+              url: 'pages/SettingPage'
+            }, router.RouterMode.Single);
+          })
+      }
+
+    }
+    .width("100%")
+    .padding(10)
+  }
+
+}
 
 
-// async function scanDirectoryTask(context: Context, curPath: string, lockPath: string) {
-//   const table: MediaTable = new MediaTable(context);
-//
-//
-//   try {
-//     // 使用 Promise 包装数据库操作
-//     await new Promise<void>((resolve, reject) => {
-//       table.getRdbStore(context,  async (err:Error) => {
-//         if (err) {
-//           reject(err);
-//           return;
-//         }
-//
-//         const files = FileUtil.listFileSync(curPath);
-//         const pendingTasks: Promise<void>[] = [];
-//
-//         for (const file of files) {
-//           const fPath = `${curPath}/${file}`;
-//           if (fPath === lockPath) continue;
-//
-//           if (FileUtil.isDirectory(fPath))  {
-//             // 用立即执行函数处理异步递归
-//             pendingTasks.push((async  () => {
-//               await scanDirectoryTask(context, fPath, lockPath);
-//             })());
-//           } else {
-//             if(!fPath.endsWith('.lrc')&&Utility.isMeidaByExtension(fPath)&&!fPath.endsWith('.srt')){
-//               // 用 Promise 包装媒体处理逻辑
-//               pendingTasks.push((async  () => {
-//                 let mediaItem:VideoItem = await Utility.uriGetMusicAssetsFromFile(context, fPath, CommonConstants.TYPE_LOCAL, true);
-// ;
-//                 await new Promise<void>((resolve: (value: void) => void) => {
-//                   table.insert(mediaItem,  (id: number) => {
-//                     resolve(); // 明确调用 resolve 且无返回值
-//                   });
-//                 });
-//               })());
-//             }
-//           }
-//         }
-//
-//         // 关键点:等待所有异步操作完成
-//         await Promise.all(pendingTasks);
-//         resolve();
-//       });
-//     });
-//
-//     // Logger.info('onecold scanDirectory 全部扫描完成后执行 = ')
-//
-//   } catch (err) {
-//     Logger.error(`Scan  failed: ${err.code}  - ${err.message}`);
-//   }
-//
-//
-// }
 
 @Concurrent
 async function  scanDirectoryTask(context: Context, dirPath: string, lockPath: string,cover_api:string) {

+ 129 - 16
entry/src/main/ets/pages/SettingPage.ets

@@ -19,6 +19,7 @@ import {
   IBestRadio,
   IBestRadioGroup
 } from '@ibestservices/ibest-ui'
+import { Utility } from '../common/util/Utility'
 
 @Preview
 @Entry
@@ -73,6 +74,9 @@ export struct SettingPage {
   @State isMusicMemoryPlay: boolean = false //是否启用记忆播放
   @State isMusicBGCover: boolean = true //播放背景随封面
   @State sortType: number = 4 //默认排序方式
+  @State isStartAutoPlay: boolean = false //启动后自动播放
+  @State isMemoryLastPlay: boolean = false //是否启用应用退出记忆最后一首的播放进度
+
   @State isShowSimi: boolean = false //是否显示私密音频
   @State isShowFAV: boolean = true //是否显示我的收藏
   @State isShowHistory: boolean = true //是否显示最近播放
@@ -94,6 +98,29 @@ export struct SettingPage {
   @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
     ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
   @StorageLink('isDarkMode') isDarkMode: boolean = false // 是否启用深色模式
+  static readonly IS_BGPLAY_OPEN: string = 'isBgPlayOpen';
+  static readonly IS_AUTO_RATATE: string = 'isAutoRatate';
+  static readonly iS_MEMORY_PLAY: string = 'isMemoryPlay';
+  static readonly IS_MIDIACODEC_OPEN: string = 'isMediacodec';
+  static readonly iS_MUSIC_MEMORY_PLAY: string = 'isMusicMemoryPlay';
+  static readonly iS_MUSIC_BG_COVER: string = 'isMusicBGCover';
+  static readonly iS_SAVE_PLAY_MODE: string = 'isSavePlayMode';
+  static readonly IS_COVER_RECTANGLE: string = 'isCoverRectangle'; //封面圆形或者方形
+
+  static readonly IS_SHOW_SIMI: string = 'isShowSimi';
+  static readonly IS_SHOW_FAV: string = 'isShowFAV';
+  static readonly IS_SHOW_HISTORY: string = 'isShowHistory';
+  static readonly IS_CUSTOMIZE_BG: string = 'is_customize_bg';
+  static readonly IS_GRID_MUSIC: string = 'is_grid_music';
+  static readonly IS_CUSTOMIZE_BG_PATH: string = 'is_customize_bg_path';
+  static readonly IS_SCROLL_HIDE: string = 'isScrollHide';
+  static readonly IS_SAMETIME_PLAY: string = 'isSameTimePlay';
+  static readonly CUSTOMIZE_BG_BLUR: string = 'customize_bg_blur';
+  static readonly BG_BRIGHTNESS: string = 'bg_brightness';
+
+  static readonly iS_START_AUTO_PLAY: string = 'isStartAutoPlay';
+  static readonly iS_MEMORY_LAST_PLAY: string = 'isMemoryLastPlay';
+
   /** 当前断点类型(如大屏/小屏) */
   @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
   @State verName: string = ''
@@ -183,6 +210,8 @@ export struct SettingPage {
     this.isSavePlayMode = PreferencesUtil.getBooleanSync(SettingPage.iS_SAVE_PLAY_MODE, true)
     this.blurValue = PreferencesUtil.getNumberSync(SettingPage.CUSTOMIZE_BG_BLUR, 0)
     this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0)
+    this.isStartAutoPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_START_AUTO_PLAY, false)
+    this.isMemoryLastPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_MEMORY_LAST_PLAY, false)
     // 只用 themeMode 控制主题
     this.themeMode = PreferencesUtil.getNumberSync(SettingPage.THEME_MODE, 0)
     this.applyThemeMode(this.themeMode)
@@ -508,14 +537,15 @@ export struct SettingPage {
             })
 
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
-            Row() {
-              Text('自定义背景')
-                .margin({ left: 18 })
-                .fontSize(15)
-                .fontColor(Color.Gray)
-                .fontWeight(480)
-                .layoutWeight(1)
-                .bindSheet($$this.isCustomizeBgSheet, this.customizeBgSheet(), {
+            Button({ type: ButtonType.Normal, stateEffect: true }) {
+              Row() {
+                Text('自定义背景')
+                  .margin({ left: 18 })
+                  .fontSize(15)
+                  .fontColor(Color.Gray)
+                  .fontWeight(480)
+                  .layoutWeight(1)
+                  .bindSheet($$this.isCustomizeBgSheet, this.customizeBgSheet(), {
                   height: '95%',
                   dragBar: true,
                   preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM,
@@ -525,19 +555,20 @@ export struct SettingPage {
                   title: { title: '自定义背景' }
                 })
 
-              Blank()
-              // 右侧箭头
-              Image($r('app.media.arrow_right'))
-                .width(22)
-                .height(22)
-                .margin({ left: 20, right: 20 })
-                .align(Alignment.Center)
+                Blank()
+                // 右侧箭头
+                Image($r('app.media.arrow_right'))
+                  .width(22)
+                  .height(22)
+                  .margin({ left: 20, right: 20 })
+                  .align(Alignment.Center)
+              }
             }
             .height(55)
             .onClick(() => {
               this.isCustomizeBgSheet = !this.isCustomizeBgSheet;
             })
-
+            .backgroundColor(Color.Transparent)
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
 
             // 滚动隐藏'
@@ -677,6 +708,49 @@ export struct SettingPage {
             }
             .height(48)
 
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            // 启动时播放
+            Row() {
+              Text('启动时播放')
+                .margin({ left: 18 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Toggle({ type: ToggleType.Switch, isOn: this.isStartAutoPlay })
+                .selectedColor($r('app.color.tab_item_bg'))
+                .switchPointColor(Color.White)
+                .margin({ right: 18 })
+                .onChange((checked: boolean) => {
+                  this.isStartAutoPlay = checked;
+                  PreferencesUtil.put(SettingPage.iS_START_AUTO_PLAY, this.isStartAutoPlay)
+                })
+                .width(50)
+                .height(30);
+            }
+            .height(55)
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            // 记住播放进度
+            Row() {
+              Text('记住最后一首进度')
+                .margin({ left: 18 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Toggle({ type: ToggleType.Switch, isOn: this.isMemoryLastPlay })
+                .selectedColor($r('app.color.tab_item_bg'))
+                .switchPointColor(Color.White)
+                .margin({ right: 18 })
+                .onChange((checked: boolean) => {
+                  this.isMemoryLastPlay = checked;
+                  PreferencesUtil.put(SettingPage.iS_MEMORY_LAST_PLAY, this.isMemoryLastPlay)
+                  this.sendChangeEvent()
+                })
+                .width(50)
+                .height(30);
+            }
+            .height(55)
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
             // 记忆播放
             Row() {
@@ -693,6 +767,7 @@ export struct SettingPage {
                 .onChange((checked: boolean) => {
                   this.isMusicMemoryPlay = checked;
                   PreferencesUtil.put(SettingPage.iS_MUSIC_MEMORY_PLAY, this.isMusicMemoryPlay)
+                  this.sendChangeEvent()
                 })
                 .width(50)
                 .height(30);
@@ -869,6 +944,8 @@ export struct SettingPage {
             })
             .justifyContent(FlexAlign.Center)
             .height(55)
+
+
           }
           .backgroundColor($r('app.color.settings_background_main'))
           .borderRadius(20)
@@ -880,6 +957,42 @@ export struct SettingPage {
           })
           .padding(0)
 
+          Button({ type: ButtonType.Capsule, stateEffect: true }) {
+            Column() {
+              Row() {
+                Text('设置教程:')
+                  .margin({ left: 18 })
+                  .fontSize(15)
+                  .fontColor(Color.Gray)
+                  .fontWeight(480)
+                Text('点击进Q群讨论')
+                  .margin({ right: 10 })
+                  .fontSize(15)
+                  .fontColor(Color.Gray)
+                  .layoutWeight(1)
+                Blank()
+                // 右侧箭头
+                Image($r('app.media.arrow_right'))
+                  .width(22)
+                  .height(22)
+                  .margin({ left: 20, right: 20 })
+                  .align(Alignment.Center)
+              }
+              .height(55)
+
+            }
+          }
+          .onClick(() => {
+            const qqUrl = `mqqapi://card/show_pslcard?src_type=internal&version=1&uin=${CommonConstants.QQ_GROUP}&card_type=group&source=external`;
+            let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
+            context.openLink(qqUrl, {})
+          })
+          .backgroundColor($r('app.color.settings_background_main'))
+          .borderRadius(15)
+          .margin({  top: 0, bottom: 20 })
+          .padding(0)
+
+
           //空白
           Column() {
             Blank()

Разница между файлами не показана из-за своего большого размера
+ 503 - 167
entry/src/main/ets/view/LocalMusic.ets


+ 69 - 0
entry/src/main/ets/view/PipLyricTextBuilder.ets

@@ -0,0 +1,69 @@
+import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
+import { LyricController, LyricView2 } from "@seagazer/cclyric";
+
+class Params {
+  pipBg: string = '';
+  lyController:LyricController
+  constructor(pipBg: string,controller:LyricController) {
+    this.pipBg = pipBg;
+    this.lyController = controller;
+  }
+}
+
+//悬浮歌词功能的布局,画中画自定义布局的开始代码
+@Component
+struct PipLyricTextBuilder {
+  @Prop lyController: LyricController = new LyricController()//悬浮歌词控制器
+
+  build() {
+    Column() {
+
+    }
+  }
+}
+
+@Builder
+function buildLyricText(params: Params) {
+  Column() {
+    // PipLyricTextBuilder({lyController: params.lyController}) // 自定义组件
+    Column() {
+      LyricView2({ controller: params.lyController,
+        enableSeek: true,
+        seekUIColor: "#ff0000",// 滑动定位的按钮和文本颜色
+        seekLineColor: "#80ffffff",// 滑动定位线颜色
+        seekUIStyle: "listItem",// 滑动定位样式(seekLine传统样式,listItem类似抖音汽水音乐样式)
+      })
+    }
+    .margin({left:20,right:20})
+  }
+  .width('100%') // 宽度方向充满画中画窗口
+  .height('100%') // 高度方向充满画中画窗口
+  .backgroundColor(params.pipBg)
+  .backgroundBlurStyle(BlurStyle.BACKGROUND_THICK)
+}
+
+export  class TextNodeController extends NodeController {
+  private pipBg: string;
+  private lyController:LyricController;
+  private textNode: BuilderNode<[Params]> | null = null;
+  constructor(pipBg: string,lyController:LyricController) {
+    super();
+    this.pipBg = pipBg;
+    this.lyController = lyController;
+  }
+
+  // 通过BuilderNode加载自定义布局
+  makeNode(context: UIContext): FrameNode | null {
+    this.textNode = new BuilderNode(context);
+    this.textNode.build(wrapBuilder<[Params]>(buildLyricText), new Params(this.pipBg,this.lyController));
+    return this.textNode.getFrameNode();
+  }
+
+  // 开发者可自定义该方法实现布局更新
+  updateBgColor(color: string,lyController:LyricController) {
+    console.log(`onecold update message: ${color}`);
+    if (this.textNode !== null) {
+      this.textNode.update(new Params(color,lyController));
+    }
+  }
+}

+ 4 - 2
entry/src/main/ets/viewmodel/VideoItem.ets

@@ -45,7 +45,8 @@ export class VideoItem  {
   playCount?:number//播放次数
   lyricContent?:string//歌词内容
 
-
+  md5Str?:string
+  extra_json?:string
 
   constructor(name: string, id: string, filePath: string, type:number,videoSize:number,cTime: string,pixelMap?: image.PixelMap,
     size?: string,pixelMapPath?: string,artist?: string,album?: string,fileName?: string, lastPlayed?:Date) {
@@ -70,6 +71,7 @@ export class VideoItem  {
     this.fileName = fileName;
     this.lastPlayed = lastPlayed;
     this.playCount = 0
-
+    this.md5Str = ''
+    this.extra_json = ''
   }
 }

+ 4 - 0
entry/src/main/resources/base/element/string.json

@@ -431,6 +431,10 @@
     {
       "name": "user_center",
       "value": "用户中心"
+    },
+    {
+      "name": "onekey_cover",
+      "value": "一键获取封面"
     }
   ]
 }

+ 6 - 3
lib/src/main/ets/view/LyricView2.ets

@@ -271,7 +271,6 @@ export struct LyricView2 {
             .fontSize(this.textSize)
             .opacity(this.calculateOpacityFactor(index, this.currentIndex))
             .blur(this.calculateBlurFactor(index, this.currentIndex))
-            .copyOption(CopyOptions.InApp)
             .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
             .scale({
                 x: index == this.currentIndex ? this.controller.getHighlightScale() : 1,
@@ -299,7 +298,6 @@ export struct LyricView2 {
                     .margin(0)
                     .opacity(this.calculateOpacityFactor(index, this.currentIndex))
                     .blur(this.calculateBlurFactor(index, this.currentIndex))
-                    .copyOption(CopyOptions.InApp)
                     .animation({
                         // 动画播放速度
                         tempo: 0.8,
@@ -311,7 +309,12 @@ export struct LyricView2 {
             })
         }
         .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
-        .width(this.alignMode == 'center' ? '100%' : '76%')
+        .width(this.alignMode == 'center' ? '100%' : '95%')
+    }
+
+
+    isTopBottomLine(index:number){
+        return  index === 0 || index === this.listAdapter.totalCount()  - 1;
     }
 
     private handleSeekAction() {

Некоторые файлы не были показаны из-за большого количества измененных файлов