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

Merge remote-tracking branch 'origin/master' into feature/桌面卡片-抽离ijkplayer播放控制

# Conflicts:
#	entry/src/main/ets/entryability/EntryAbility.ets
#	entry/src/main/ets/view/LocalMusic.ets
#	lib/src/main/ets/parse/LyricParser.ts
chendeben 11 месяцев назад
Родитель
Сommit
e251e62f84

+ 2 - 2
AppScope/app.json5

@@ -2,8 +2,8 @@
   "app": {
     "bundleName": "com.xgplayer.ttmusic.hm",
     "vendor": "example",
-    "versionCode": 20250819,
-    "versionName": "1.5.2",
+    "versionCode": 20250903,
+    "versionName": "1.5.5",
     "icon": "$media:app_icon",
     "label": "$string:app_name",
     "multiAppMode": {

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

@@ -3,7 +3,7 @@ import { LogUtil, StrUtil } from '@pura/harmony-utils';
 import { VideoItem } from '../../viewmodel/VideoItem';
 import Logger from './Logger';
 import RdbUtils from './RdbUtils';
-import { Utility } from './Utility';
+import { AudioQuality, Utility } from './Utility';
 
 /**
  * 数据库字段常量接口定义
@@ -624,6 +624,120 @@ export default class MediaTable {
   }
 
 
+  /**
+   * 查询播放次数大于0的记录,并按播放次数降序排列
+   * @param limitCount 限制返回的记录数量,0表示返回全部
+   * @param callback 回调函数,返回VideoItem数组
+   */
+  public queryByPlayCountDesc(limitCount: number = 0, callback: (result: VideoItem[]) => void): void {
+    try {
+      // 1. 构建查询条件:playCount > 0,按playCount降序排列
+      const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
+      predicates.greaterThan('playCount',  0);  // 播放次数大于0
+      predicates.orderByDesc('playCount');      // 按播放次数降序排列
+
+      // 只有当limitCount大于0时才设置限制
+      if (limitCount > 0) {
+        predicates.limitAs(limitCount);
+      }
+
+      // 2. 执行查询
+      this.accountTable.query(predicates,  (resultSet: relationalStore.ResultSet) => {
+        // 3. 解析结果集
+        const result = this.parseResultSetToVideoItems(resultSet);
+        callback(result);
+      });
+    } catch (err) {
+      Logger.error(`queryByPlayCountDesc  error: ${err.code}  - ${err.message}`);
+      callback([]);  // 发生错误时返回空数组
+    }
+  }
+
+
+  public countSongsByQualityOptimized(callback: (result: Record<AudioQuality, number>) => void): void {
+    const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
+    predicates.in('md5Str',  [AudioQuality.LQ, AudioQuality.SQ, AudioQuality.HQ, AudioQuality.HIRES, AudioQuality.HR]);
+
+    this.accountTable.query(predicates,  (resultSet: relationalStore.ResultSet) => {
+      const result: Record<AudioQuality, number> = {
+        [AudioQuality.LQ]: 0,
+        [AudioQuality.SQ]: 0,
+        [AudioQuality.HQ]: 0,
+        [AudioQuality.HIRES]: 0 , // 只保留HR,HIRES的统计将合并到这里
+        [AudioQuality.HR]: 0  // 只保留HR,HIRES的统计将合并到这里
+      };
+
+      if (resultSet.rowCount  > 0) {
+        resultSet.goToFirstRow();
+        do {
+          const quality = resultSet.getString(resultSet.getColumnIndex('md5Str'))  as AudioQuality;
+          if (quality === AudioQuality.LQ ||
+            quality === AudioQuality.SQ ||
+            quality === AudioQuality.HQ ||
+            quality === AudioQuality.HIRES ||  // HIRES的统计将被合并到HR
+            quality === AudioQuality.HR) {
+
+            // 如果质量是HIRES或HR,都统计到HR中
+            const targetKey = (quality === AudioQuality.HIRES || quality === AudioQuality.HR)
+              ? AudioQuality.HR
+              : quality;
+
+            result[targetKey]++;
+          }
+        } while (resultSet.goToNextRow());
+      }
+
+      resultSet.close();
+      callback(result);
+    });
+  }
+
+
+  /**
+   * Get lyric content by file path (Promise version)
+   * @param filePath The file path to query
+   * @returns Promise that resolves with the lyric content (string) or null if not found
+   */
+  public getLyricContentByFilePath(filePath: string): Promise<string | null> {
+    return new Promise((resolve, reject) => {
+      // Create query predicates
+      const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
+      predicates.equalTo(DB_COLUMNS.FILE_PATH,  filePath);
+
+      // Execute the query
+      this.accountTable.query(predicates,  (resultSet: relationalStore.ResultSet) => {
+        try {
+          if (resultSet.rowCount  === 0) {
+            Logger.info(RdbUtils.RDB_TAG,  `No record found for filePath: ${filePath}`);
+            resolve(null);
+            return;
+          }
+
+          // Get the first row
+          resultSet.goToFirstRow();
+
+          // Get the column index safely
+          const columnIndex = resultSet.getColumnIndex(DB_COLUMNS.LYRIC_CONTENT);
+          if (columnIndex < 0) {
+            Logger.error(RdbUtils.RDB_TAG,  `Column ${DB_COLUMNS.LYRIC_CONTENT} not found`);
+            resolve(null);
+            return;
+          }
+
+          // Get the lyric content
+          const lyricContent = resultSet.getString(columnIndex);
+          resolve(lyricContent || null);
+        } catch (err) {
+          Logger.error(RdbUtils.RDB_TAG,  `Error getting lyric content: ${err.message}`);
+          reject(err);
+        } finally {
+          // Ensure the result set is closed
+          resultSet.close();
+        }
+      });
+    });
+  }
+
 
 }
 

+ 133 - 16
entry/src/main/ets/common/util/MusicTagUtils.ets

@@ -18,8 +18,12 @@ import { FFMpegTags } from "./Utility";
 import { http } from "@kit.NetworkKit";
 import { BusinessError } from "@kit.BasicServicesKit";
 import ResponseCode from '@ohos.net.http';
-import { LogUtil, PreferencesUtil, StrUtil } from "@pura/harmony-utils";
+import { FileUtil, LogUtil, PreferencesUtil, StrUtil } from "@pura/harmony-utils";
 import { CommonConstants } from "../constants/CommonConstants";
+import { VideoItem } from "../../viewmodel/VideoItem";
+import MediaTable from "./MediaTable";
+import NetAxiosUtil from "./NetAxiosUtil";
+import { fileUri } from "@kit.CoreFileKit";
 
 /**
  * 修复音频文件的元数据标签
@@ -85,6 +89,7 @@ export async function repairAudioMetadata(
   //额外再次添加下lyrics-XXX的歌词参数,以便其他音乐播放器可以识别歌词
   // 额外添加歌词自定义标签(修复后的核心代码)
   if (StrUtil.isNotEmpty(lyrics)) { // 确保歌词内容存在时才添加
+
     // 多种歌词标签格式
     const lyricTags = [
       `LYRICS=${lyrics}`,
@@ -225,16 +230,17 @@ export async function changeMusicCover(
 
   // 如果是网络图片,先下载到临时文件
   let tempCoverPath = coverImagePath;
-  console.log(`onecold 开始下载 coverImagePath = [${coverImagePath}]`);
+  console.log(`onecold changeMusicCover coverImagePath = [${coverImagePath}]`);
   if (coverImagePath.startsWith('http')) {
     try {
       // 创建临时文件路径
       const tempDir = context.filesDir + '/'; // 默认缓存目录
-      const tempFileName = 'temp_cover.jpg';
+      const tempFileName = FileUtil.getFileName(inputPath)+'temp_cover.jpg';
       tempCoverPath = `${tempDir}${tempFileName}`;
 
       // 下载图片
       const result = await loadImageWithUrl(coverImagePath, tempCoverPath);
+      console.log(`onecold changeMusicCover下载的图片 tempCoverPath = [${tempCoverPath}]`);
       if (!result) {
         console.error('onecold 下载封面图片失败');
         // 清理可能已创建的临时文件
@@ -260,17 +266,29 @@ export async function changeMusicCover(
       return false;
     }
   }
-  console.log(`onecold 开始下载 tempCoverPath = [${tempCoverPath}]`);
+  console.log(`onecold 获取新的图片地址 tempCoverPath = [${tempCoverPath}]`);
   // 构建FFmpeg命令
+  // const commands: string[] = [
+  //   "ffmpeg",
+  //   "-i", inputPath,
+  //   "-i", tempCoverPath,
+  //   "-map", "0:0",           // 映射音频流
+  //   "-map", "1:0",           // 映射封面图片流
+  //   "-c", "copy",            // 复制音频流
+  //   "-id3v2_version", "3",   // ID3v2版本
+  //   "-y",                    // 覆盖输出文件
+  //   finalOutputPath
+  // ];
   const commands: string[] = [
     "ffmpeg",
     "-i", inputPath,
     "-i", tempCoverPath,
-    "-map", "0:0",           // 映射音频流
-    "-map", "1:0",           // 映射封面图片流
-    "-c", "copy",            // 复制音频流
-    "-id3v2_version", "3",   // ID3v2版本
-    "-y",                    // 覆盖输出文件
+    "-map", "0:0",
+    "-map", "1:0",
+    "-c", "copy",
+    "-id3v2_version", "3", // 设置ID3v2.3标签
+    "-disposition:v:0", "attached_pic",// 强制设置图片为封面
+    "-y",
     finalOutputPath
   ];
 
@@ -301,17 +319,27 @@ export async function changeMusicCover(
 
     // 验证文件是否存在
     try {
+
       const file = await fs.open(finalOutputPath, fs.OpenMode.READ_ONLY);
       await fs.close(file.fd);
+      const table: MediaTable = new MediaTable(context);
+      console.log("onecold musicTags 开始图片入库中2");
+      //图片入库
+      await new Promise<void>((resolve, reject) => {
+        table.getRdbStore(context,  (err:Error) => {
+          err ? reject(err) : resolve();
+        });
+      });
+      table.updatePixelMapPath(inputPath,  fileUri.getUriFromPath(tempCoverPath), (success: boolean, error?: string) => {
+        if (success) {
 
-      // 如果是下载的临时图片,清理临时文件
-      if (tempCoverPath !== coverImagePath) {
-        try {
-          fs.unlinkSync(tempCoverPath);
-        } catch (unlinkError) {
-          console.warn('onecold 清理临时文件失败:', unlinkError);
+          console.log("onecold musicTags 更新音乐封面成功,数据库已同步");
+        } else {
+          console.error("onecold  musicTags 更新音乐封面数据库失败原因: " + error);
         }
-      }
+        // Note: We don't resolve/reject here because we already returned res
+      });
+
 
       return true;
     } catch (e) {
@@ -457,4 +485,93 @@ interface lyricInfo{
   lyrics:string
   cover_url:string
   status:string
+}
+
+/**
+ *api搜索封面
+ *item,根据title和artist
+ */
+export function searchCover(context:Context,item: VideoItem, title: string, artist: string): Promise<string> {
+  const table: MediaTable = new MediaTable(context);
+  return NetAxiosUtil.getLyricCover(title,  artist, PreferencesUtil.getStringSync('COVER_API',  '')).then(async (res) => {
+    LogUtil.debug("onecold  res =" + res);
+
+    // if (StrUtil.isNotEmpty(res)  && res !== 'unknown' && res !== 'Timeout was reached') {
+    //
+    //
+    //   // Update pixel map path but always return res regardless of success
+    //   table.updatePixelMapPath(item.filePath,  res, (success: boolean, error?: string) => {
+    //     if (success) {
+    //
+    //       console.log("onecold  更新音乐封面成功,数据库已同步");
+    //     } else {
+    //       console.error("onecold  更新音乐封面数据库失败原因: " + error);
+    //     }
+    //     // Note: We don't resolve/reject here because we already returned res
+    //   });
+    //
+    // }
+
+    return res; // Always return res regardless of updatePixelMapPath result
+  });
+}
+
+/**
+ * 查找同名的图片文件(png或jpg)
+ * @param filePath 音频文件路径
+ * @returns 图片文件路径,如果未找到则返回空字符串
+ */
+export function findLocalCoverImage(filePath: string): string {
+  try {
+    // 获取文件名(不包含扩展名)
+    const lastDotIndex = filePath.lastIndexOf('.');
+    const basePath = lastDotIndex >= 0 ? filePath.substring(0, lastDotIndex) : filePath;
+
+    // 检查可能的图片文件扩展名
+    const imageExtensions = ['.png', '.jpg', '.jpeg'];
+
+    for (const ext of imageExtensions) {
+      const imagePath = basePath + ext;
+      if (fs.accessSync(imagePath)) {
+        return imagePath;
+      }
+    }
+
+    return ''; // 未找到同名图片文件
+  } catch (error) {
+    console.warn(`查找同名图片文件失败: ${JSON.stringify(error)}`);
+    return '';
+  }
+}
+
+/**
+ * 同步歌词到数据库
+ * @param filePath 音频文件路径
+ * @returns 图片文件路径,如果未找到则返回空字符串
+ */
+export async function syncLyricToDB(context:Context,filePath: string,lyric:string) {
+  try {
+    const table: MediaTable = new MediaTable(context);
+    //图片入库
+    await new Promise<void>((resolve, reject) => {
+      table.getRdbStore(context,  (err:Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+    table.updateMediaInfo(filePath, '', '', '',lyric,
+      '','','','','', '','','',
+      (success: boolean, error?: string) => {
+
+        if (success) {
+          console.info(" onecold  同步歌词成功: " );
+        } else {
+          // ToastUtil.showToast('保存失败' + error?.toString())
+          console.error(" onecold  编辑信息数据库失败原因: " + error);
+        }
+
+
+      });
+  } catch (error) {
+    console.warn(`查找同名图片文件失败: ${JSON.stringify(error)}`);
+  }
 }

+ 79 - 5
entry/src/main/ets/common/util/UpdateLogManager.ets

@@ -7,9 +7,83 @@ import { UpdateConstants } from '../constants/UpdateConstants';
 import Logger from './Logger';
 import { ConfigManager } from './ConfigManager';
 
+/**
+ * 更新日志项类型定义
+ */
+export interface UpdateLogItemType {
+  version: string;
+  date: string;
+  title?: string;
+  features?: string[];
+  improvements?: string[];
+  fixes?: string[];
+}
+
+/**
+ * 更新日志配置类型定义
+ */
+export interface UpdateLogConfigType {
+  title: string;
+  logs: UpdateLogItemType[];
+}
+
 export class UpdateLogManager {
   private static readonly TAG = 'UpdateLogManager';
 
+  /**
+   * 检查是否有有效的更新日志配置
+   * @returns boolean 是否有有效配置
+   */
+  private static hasValidUpdateLogConfig(): boolean {
+    try {
+      const updateLogConfig = ConfigManager.getConfig('update_log_config', null);
+      
+      if (!updateLogConfig) {
+        Logger.warn(UpdateLogManager.TAG, '更新日志配置为空');
+        return false;
+      }
+
+      // 检查配置结构是否有效
+      const config = updateLogConfig as UpdateLogConfigType;
+      if (!config.title || !config.logs) {
+        Logger.warn(UpdateLogManager.TAG, '更新日志配置结构无效,缺少title或logs字段');
+        return false;
+      }
+
+      const logs = config.logs as UpdateLogItemType[];
+      if (!Array.isArray(logs) || logs.length === 0) {
+        Logger.warn(UpdateLogManager.TAG, '更新日志配置中logs字段无效或为空');
+        return false;
+      }
+
+      // 检查至少有一个有效的日志项
+      const hasValidLog: boolean = logs.some((log: UpdateLogItemType): boolean => {
+        // 检查版本号和日期是否存在
+        if (!log.version || !log.date) {
+          return false;
+        }
+        
+        // 检查是否至少有一种更新内容
+        const hasFeatures: boolean = Boolean(log.features && Array.isArray(log.features) && log.features.length > 0);
+        const hasImprovements: boolean = Boolean(log.improvements && Array.isArray(log.improvements) && log.improvements.length > 0);
+        const hasFixes: boolean = Boolean(log.fixes && Array.isArray(log.fixes) && log.fixes.length > 0);
+        
+        return hasFeatures || hasImprovements || hasFixes;
+      });
+
+      if (!hasValidLog) {
+        Logger.warn(UpdateLogManager.TAG, '更新日志配置中没有有效的日志项');
+        return false;
+      }
+
+      Logger.info(UpdateLogManager.TAG, `更新日志配置有效,包含${logs.length}个日志项`);
+      return true;
+    } catch (error) {
+      Logger.error(UpdateLogManager.TAG, '检查更新日志配置有效性失败:' + error);
+      return false;
+    }
+  }
+
   /**
    * 检查并显示更新日志
    * @returns Promise<boolean> 是否显示了更新日志
@@ -24,10 +98,11 @@ export class UpdateLogManager {
       }
       Logger.info(UpdateLogManager.TAG, '更新日志功能已通过API配置启用');
 
-      // if (!UpdateConstants.ENABLE_UPDATE_LOG) {
-      //   Logger.info(UpdateLogManager.TAG, '更新日志功能已禁用');
-      //   return false;
-      // }
+      // 检查更新日志配置是否存在且有效
+      if (!UpdateLogManager.hasValidUpdateLogConfig()) {
+        Logger.info(UpdateLogManager.TAG, '更新日志配置无效或不存在');
+        return false;
+      }
 
       const shouldShow = UpdateLogManager.shouldShowUpdateLog();
       if (!shouldShow) {
@@ -109,5 +184,4 @@ export class UpdateLogManager {
     }
   }
 
-
 }

+ 38 - 30
entry/src/main/ets/common/util/Utility.ets

@@ -51,6 +51,7 @@ export interface FFMpegTags {
   LYRICIST?:string//作词家
   COMMENT?:string//注释
   albumartist?:string//专辑艺术家
+  album_artist?:string//专辑艺术家
   TPE2?:string//专辑艺术家
   composer?:string//作曲家
   lyricist?:string//作词家
@@ -866,8 +867,10 @@ export class Utility {
             let artist = tags.artist ||tags.ARTIST || '';
             let title = tags.title ||tags.TITLE || '';
             const album = tags.album ||tags.ALBUM|| '';
-
-
+            console.log(`onecold tags.tags:${JSON.stringify(tags)}`);
+            // console.log(`onecold tags.LYRICS:${tags.LYRICS}`);
+            // console.log(`onecold tags.lyrics:${tags.lyrics}`);
+            // console.log(`onecold tags.UNSYNCEDLYRICS:${tags.UNSYNCEDLYRICS}`);
             if(StrUtil.isEmpty(title)){
               //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
               console.log(`onecold musicName为空:${file.name}`);
@@ -918,25 +921,28 @@ export class Utility {
             // 如果标准字段没有歌词,则尝试解析 `lyrics-` 字段
             // tags 必须是 Record<string, string | undefined>(或 any)
             // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX)
-            const tagsRecord = tags as Record<string, string>;
-            const possibleKeys = Object.keys(tagsRecord);
-            for (let i = 0; i < possibleKeys.length;  i++) {
-              const key = possibleKeys[i];
-              if (key && key.toLowerCase().startsWith('lyrics-'))  {
-                //console.info('readMetaInfoFFmpeg  videoItem.key:  ', key);
-                // 通过转换后的 Record 类型安全访问属性
-                videoItem.lyricContent  = tagsRecord[key];
-                //console.info('readMetaInfoFFmpeg  videoItem.lyricContent2:  ', videoItem.lyricContent);
-                if (videoItem.lyricContent)  {
-                  break;
+            if(StrUtil.isEmpty( videoItem.lyricContent)){
+              const tagsRecord = tags as Record<string, string>;
+              const possibleKeys = Object.keys(tagsRecord);
+              for (let i = 0; i < possibleKeys.length;  i++) {
+                const key = possibleKeys[i];
+                if (key && key.toLowerCase().startsWith('lyrics-'))  {
+                  //console.info('readMetaInfoFFmpeg  videoItem.key:  ', key);
+                  // 通过转换后的 Record 类型安全访问属性
+                  videoItem.lyricContent  = tagsRecord[key];
+                  //console.info('readMetaInfoFFmpeg  videoItem.lyricContent2:  ', videoItem.lyricContent);
+                  if (videoItem.lyricContent)  {
+                    break;
+                  }
                 }
               }
             }
 
+
             videoItem.genre  = tags.genre||tags.GENRE|| '';
             videoItem.track  = tags.track||tags.TRACK|| '';
 
-            videoItem.ALBUMARTIST  = tags.ALBUMARTIST||tags.albumartist||tags.TPE2|| '';
+            videoItem.ALBUMARTIST  = tags.ALBUMARTIST||tags.album_artist||tags.albumartist||tags.TPE2|| '';
             videoItem.COMPOSER  = tags.COMPOSER||tags.composer|| '';
             videoItem.LYRICIST  = tags.LYRICIST||tags.lyricist||tags.TEXT ||'';
             videoItem.COMMENT  = tags.COMMENT||tags.comment||tags.COMM|| '';
@@ -1466,21 +1472,26 @@ async function isDirectory(filePath: string): Promise<boolean> {
 interface VideoNameParts {
   nonNumeric: string;
   numeric: number;
+  suffix:string;
 }
 
-// 提取文件名中的非数字和数字部分
 function extractParts(name: string): VideoNameParts {
-  // Adjust the regex to handle names that start with numbers
-  const match = name.match(/^(\D*?)(\d+)(\.\w+)?$/);
+  // 更完善的正则表达式,处理各种情况
+  // 匹配:非数字前缀 + 数字部分 + 可选的剩余部分
+  const match = name.match(/^(\D*)(\d+)(.*)$/);
   if (match) {
     return {
-      nonNumeric: match[1] || '', // Ensure nonNumeric is not undefined
-      numeric: parseInt(match[2], 10),
+      nonNumeric: match[1] || '', // 非数字前缀
+      numeric: parseInt(match[2], 10), // 数字部分
+      suffix: match[3] || '' // 剩余部分(可能包含扩展名等)
     };
   }
+
+  // 如果没有找到数字模式,将整个名称作为非数字部分
   return {
     nonNumeric: name,
     numeric: 0,
+    suffix: ''
   };
 }
 
@@ -1908,12 +1919,12 @@ function formatBitrateToKbps(bitRate: string, decimalPlaces: number = 0): string
 
 
 // 定义音质等级
-enum AudioQuality {
+export enum AudioQuality {
   LQ = "LQ",             // 低音质
-  SQ = "SQ",             // 标准音质
   HQ = "HQ",             // 高音质
-  LOSSLESS = "Lossless", // 无损
-  HIRES = "Hi-Res"       // 高解析度
+  SQ = "SQ",             // 无损
+  HIRES = "Hi-Res",      // 高解析度
+  HR = "HR"       // 高解析度
 }
 
 // 明确定义支持的音频格式类型
@@ -1939,7 +1950,7 @@ const SUPPORTED_FORMATS: SupportedFormats = {
 function determineAudioQuality(
   format: string,
   bitrate: number,
-  sampleRate: number
+  sampleRate: number,
 ): AudioQuality {
   // 统一转为小写便于比较
   const normalizedFormat = format.toLowerCase();
@@ -1948,12 +1959,12 @@ function determineAudioQuality(
   const isLossless = SUPPORTED_FORMATS.LOSSLESS.has(normalizedFormat);
   if (isLossless) {
     // Hi-Res标准:采样率≥96kHz且位深≥24bit
-    if (sampleRate >= 96000 ) {
+    if (sampleRate >= 48000 ) {
       return AudioQuality.HIRES;
     }
     // CD级无损标准:采样率≥44.1kHz且位深≥16bit
     if (sampleRate >= 44100) {
-      return AudioQuality.LOSSLESS;
+      return AudioQuality.SQ;
     }
   }
 
@@ -1962,10 +1973,7 @@ function determineAudioQuality(
   if (bitrate >= 256000 && sampleRate >= 44100) {
     return AudioQuality.HQ;
   }
-  // 标准音质:比特率≥128kbps且采样率≥22.05kHz
-  else if (bitrate >= 128000 && sampleRate >= 22050) {
-    return AudioQuality.SQ;
-  }
+
   // 其余情况为低音质
   else {
     return AudioQuality.LQ;

+ 286 - 132
entry/src/main/ets/dialog/OnlineUpdateLog.ets

@@ -1,13 +1,12 @@
 /**
  * 在线更新日志弹窗组件
- * 参考WebIndex.ets的成功实现,确保WebView正常工作
+ * 通过ConfigManager获取json配置并渲染页面,避免WebView加载loading
  */
-import { webview } from '@kit.ArkWeb';
-import { UpdateConstants } from '../common/constants/UpdateConstants';
-import { UpdateLogManager } from '../common/util/UpdateLogManager';
+import { UpdateLogManager, UpdateLogItemType, UpdateLogConfigType } from '../common/util/UpdateLogManager';
 import { ConfigurationConstant } from '@kit.AbilityKit';
 import { AppUtil, PreferencesUtil } from '@pura/harmony-utils';
 import { CommonConstants } from '../common/constants/CommonConstants';
+import { ConfigManager } from '../common/util/ConfigManager';
 
 @Component
 export default struct OnlineUpdateLog {
@@ -15,20 +14,14 @@ export default struct OnlineUpdateLog {
   controller?: CustomDialogController;
   /** 关闭回调 */
   onClose?: () => void;
-  /** 更新日志URL */
-  @State updateLogUrl: string = '';
   /** 当前应用版本 */
   @State currentVersion: string = '';
-  /** WebView控制器 */
-  private webViewController: webview.WebviewController = new webview.WebviewController();
+  /** 更新日志配置数据 */
+  @State updateLogConfig: UpdateLogConfigType | null = null;
   /** 是否加载完成 */
   @State isLoading: boolean = true;
   /** 加载错误信息 */
   @State errorMessage: string = '';
-  /** 加载进度 */
-  @State progressValue: number = 0;
-  /** 进度条是否可见 */
-  @State progressVisible: boolean = true;
   /** 深色模式 */
   @State isDarkMode: boolean = false;
   @StorageProp('currentColorMode') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
@@ -38,20 +31,16 @@ export default struct OnlineUpdateLog {
     try {
       // 获取当前应用版本
       this.getCurrentVersion();
-      // 设置更新日志URL - 直接使用在线URL
-      this.updateLogUrl = UpdateConstants.UPDATE_LOG_URL;
       // 检查深色模式
       this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
       let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
       AppStorage.setOrCreate('themeColor', themeColor);
       this.themeColor = themeColor
-      console.info('OnlineUpdateLogDialog URL:', this.updateLogUrl);
-      console.info('OnlineUpdateLogDialog 开始加载在线更新日志');
       
-      // 确保WebView控制器已初始化
-      if (!this.webViewController) {
-        this.webViewController = new webview.WebviewController();
-      }
+      // 从ConfigManager获取更新日志配置
+      this.loadUpdateLogConfig();
+      
+      console.info('OnlineUpdateLogDialog 开始加载更新日志配置');
     } catch (error) {
       console.error('OnlineUpdateLogDialog aboutToAppear error:', error);
       this.errorMessage = '初始化失败';
@@ -64,34 +53,46 @@ export default struct OnlineUpdateLog {
    */
   private getCurrentVersion() {
     try {
-      this.currentVersion = AppUtil.getVersionName()//UpdateConstants.APP_VERSION;
+      this.currentVersion = AppUtil.getVersionName();
     } catch (error) {
       this.currentVersion = '1.0.0';
     }
   }
 
   /**
-   * 记录已显示的版本,避免重复显示
+   * 从ConfigManager加载更新日志配置
    */
-  private markVersionShown() {
+  private loadUpdateLogConfig() {
     try {
-      UpdateLogManager.markCurrentVersionShown();
+      this.isLoading = true;
+      this.errorMessage = '';
+      
+      // 从ConfigManager获取更新日志配置
+      const configData = ConfigManager.getConfig('update_log_config', null);
+      
+      if (configData) {
+        this.updateLogConfig = configData as UpdateLogConfigType;
+        this.isLoading = false;
+        console.info('OnlineUpdateLogDialog 成功加载更新日志配置', JSON.stringify(this.updateLogConfig));
+      } else {
+        this.errorMessage = '未找到更新日志配置';
+        this.isLoading = false;
+        console.warn('OnlineUpdateLogDialog 未找到更新日志配置');
+      }
     } catch (error) {
-      console.error('OnlineUpdateLogDialog markVersionShown error:', error);
+      console.error('OnlineUpdateLogDialog 加载更新日志配置失败:', error);
+      this.errorMessage = '加载配置失败';
+      this.isLoading = false;
     }
   }
 
-
   /**
    * 组件销毁时清理资源
    */
   aboutToDisappear() {
     try {
       console.info('OnlineUpdateLogDialog aboutToDisappear');
-      // 清理WebView控制器
-      if (this.webViewController) {
-        // 这里可以添加WebView的清理逻辑,如果需要的话
-      }
+      // 清理资源
     } catch (error) {
       console.error('OnlineUpdateLogDialog aboutToDisappear error:', error);
     }
@@ -99,40 +100,7 @@ export default struct OnlineUpdateLog {
 
   build() {
     Column() {
-
-
-      // 版本信息和进度条
-      Column() {
-        // 进度条 - 使用主题色
-        if (this.progressVisible && this.progressValue < 100) {
-          Column() {
-            Progress({ value: this.progressValue, total: 100, type: ProgressType.Linear })
-              .width('100%')
-              .height(4)
-              .color(this.themeColor)
-              .backgroundColor(this.isDarkMode ? '#4A4A4A' : '#E0E0E0')
-              .borderRadius(2)
-            
-            Text(`加载中... ${this.progressValue}%`)
-              .fontSize(10)
-              .fontColor(this.isDarkMode ? '#E0E0E0' : Color.Gray)
-              .margin({ top: 4 })
-              .alignSelf(ItemAlign.End)
-          }
-          .width('100%')
-        }
-      }
-      .width('100%')
-      .padding({ left: 20, right: 20, top: 12, bottom: 8 })
-      .backgroundColor(this.isDarkMode ? '#3A3A3A' : '#F8F9FA')
-      .borderRadius({
-        topLeft: 0,
-        topRight: 0,
-        bottomLeft: 8,
-        bottomRight: 8
-      })
-
-      // WebView内容区域 - 完全参考WebIndex.ets的实现
+      // 内容区域
       if (this.errorMessage) {
         // 错误状态
         Column() {
@@ -151,20 +119,20 @@ export default struct OnlineUpdateLog {
             width: 1,
             color: this.isDarkMode ? '#666666' : '#FED7D7'
           })
-          
+
           Text('加载失败')
             .fontSize(16)
             .fontColor(this.isDarkMode ? Color.White : Color.Black)
             .fontWeight(FontWeight.Medium)
             .margin({ top: 16 })
-          
+
           Text(this.errorMessage)
             .fontSize(12)
             .fontColor(this.isDarkMode ? '#E0E0E0' : Color.Gray)
             .margin({ top: 8, left: 20, right: 20 })
             .textAlign(TextAlign.Center)
             .maxLines(3)
-          
+
           Button('重试')
             .fontSize(14)
             .backgroundColor(this.themeColor)
@@ -175,14 +143,8 @@ export default struct OnlineUpdateLog {
             .margin({ top: 20 })
             .onClick(() => {
               try {
-                this.errorMessage = '';
-                this.isLoading = true;
-                this.progressValue = 0;
-                this.progressVisible = true;
-                // 重新加载
-                if (this.webViewController) {
-                  this.webViewController.refresh();
-                }
+                // 重新加载配置
+                this.loadUpdateLogConfig();
               } catch (error) {
                 console.error('OnlineUpdateLogDialog 重试失败:', error);
                 this.errorMessage = '重试失败,请稍后再试';
@@ -190,75 +152,267 @@ export default struct OnlineUpdateLog {
             })
         }
         .width('100%')
-        .height(350)
+        .height('100%')
         .justifyContent(FlexAlign.Center)
         .alignItems(HorizontalAlign.Center)
         .padding(20)
         .backgroundColor(this.isDarkMode ? '#2C2C2C' : Color.White)
+        .borderRadius(16)
+      } else if (this.isLoading) {
+        // 加载状态 - 优化设计
+        Column() {
+          // 加载动画
+          LoadingProgress()
+            .width(48)
+            .height(48)
+            .color(this.themeColor)
+            .margin({ bottom: 20 })
+          
+          Text('正在加载更新日志...')
+            .fontSize(16)
+            .fontColor(this.isDarkMode ? '#F0F0F0' : '#2A2A2A')
+            .fontWeight(FontWeight.Medium)
+            .margin({ bottom: 8 })
+          
+          Text('请稍候')
+            .fontSize(13)
+            .fontColor(this.isDarkMode ? '#B0B0B0' : '#888888')
+        }
+        .width('100%')
+        .height('100%')
+        .justifyContent(FlexAlign.Center)
+        .alignItems(HorizontalAlign.Center)
+        .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#F5F7FA')
+        .borderRadius(16)
       } else {
-        // WebView内容 - 完全按照WebIndex.ets的方式实现
-        Web({
-          src: this.updateLogUrl,
-          controller: this.webViewController
-        })
-          .width('100%')
-          .height(350)
-          .borderRadius(12)
-          .layoutWeight(1)
-          .backgroundColor(this.isDarkMode ? '#2C2C2C' : Color.White)
-          // .border({
-          //   width: 1,
-          //   color: this.isDarkMode ? '#333333' : '#E0E0E0'
-          // })
-          .margin({ left: 12, right: 12, bottom: 8 })
-          .darkMode(this.isDarkMode ? WebDarkMode.On : WebDarkMode.Off)
-          .forceDarkAccess(this.isDarkMode)
-          .onProgressChange((event) => {
-            if (event) {
-              console.info('OnlineUpdateLogDialog WebView进度:', event.newProgress);
-              this.progressValue = event.newProgress;
+        // 更新日志内容
+        if (!this.updateLogConfig || !this.updateLogConfig.logs || this.updateLogConfig.logs.length === 0) {
+                  // 无数据状态 - 优化设计
+        Column() {
+          // 空状态图标
+          Column() {
+            Text('📋')
+              .fontSize(36)
+          }
+          .width(80)
+          .height(80)
+          .backgroundColor(this.isDarkMode ? '#3A3A3A' : '#F5F7FA')
+          .borderRadius(40)
+          .justifyContent(FlexAlign.Center)
+          .alignItems(HorizontalAlign.Center)
+          .margin({ bottom: 20 })
+          .shadow({
+            radius: 8,
+            color: this.isDarkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.06)',
+            offsetX: 0,
+            offsetY: 4
+          })
+          
+          Text('暂无更新日志')
+            .fontSize(18)
+            .fontColor(this.isDarkMode ? '#F0F0F0' : '#2A2A2A')
+            .fontWeight(FontWeight.Medium)
+            .margin({ bottom: 8 })
+          
+          Text('当前版本已是最新内容')
+            .fontSize(13)
+            .fontColor(this.isDarkMode ? '#B0B0B0' : '#888888')
+        }
+        .width('100%')
+        .height('100%')
+        .justifyContent(FlexAlign.Center)
+        .alignItems(HorizontalAlign.Center)
+        .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#F5F7FA')
+        .borderRadius(16)
+        } else {
+          // 更新日志列表 - 扩大页面占比
+          Scroll() {
+            Column({ space: 20 }) {
+              ForEach(this.updateLogConfig.logs, (logItem: UpdateLogItemType, index: number) => {
+                this.buildLogItemCard(logItem, index)
+              })
               
-              // 进度完成时隐藏进度条
-              if (event.newProgress >= 100) {
-                setTimeout(() => {
-                  this.progressVisible = false;
-                  this.isLoading = false;
-                }, 500);
-              }
+              // 底部间距
+              Column()
+                .height(20)
             }
-          })
-          .onPageBegin(() => {
-            console.info('OnlineUpdateLogDialog WebView开始加载:', this.updateLogUrl);
-            this.isLoading = true;
-            this.errorMessage = '';
-            this.progressValue = 0;
-            this.progressVisible = true;
-          })
-          .onPageEnd(() => {
-            console.info('OnlineUpdateLogDialog WebView加载完成');
-            this.isLoading = false;
-            setTimeout(() => {
-              this.progressVisible = false;
-            }, 1000);
-          })
-          .onErrorReceive((event) => {
-            const errorInfo = event?.error?.getErrorInfo() || '网络错误';
-            console.error('OnlineUpdateLogDialog WebView加载错误:', errorInfo);
-            this.isLoading = false;
-            this.progressVisible = false;
-            this.errorMessage = `加载失败:${errorInfo}`;
-          })
+            .padding({ left: 20, right: 20, top: 20, bottom: 20 })
+          }
+          .width('100%')
+          .height('100%')
+          .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#F5F7FA')
+          .borderRadius(16)
+          .scrollable(ScrollDirection.Vertical)
+          .scrollBar(BarState.Auto)
+          .edgeEffect(EdgeEffect.Spring)
+        }
       }
     }
-    .backgroundColor(this.isDarkMode ? '#2C2C2C' : Color.White)
-    .borderRadius(12)
-    .width('92%')
-    .constraintSize({ maxHeight: '85%' })
+    .backgroundColor('transparent')
+    .borderRadius(16)
+    .width('95%')
+    .height('85%')
+    .shadow({
+      radius: 24,
+      color: this.isDarkMode ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.12)',
+      offsetX: 0,
+      offsetY: 8
+    })
+    .clip(true)
+  }
+
+
+
+  /**
+   * 构建单个更新日志卡片
+   */
+  @Builder
+  buildLogItemCard(logItem: UpdateLogItemType, index: number) {
+    Column() {
+      // 版本号和日期头部 - 重新设计
+      Row() {
+        // 版本号标签
+        Row() {
+          Text('v')
+            .fontSize(12)
+            .fontColor(Color.White)
+            .fontWeight(FontWeight.Medium)
+          Text(logItem.version)
+            .fontSize(16)
+            .fontColor(Color.White)
+            .fontWeight(FontWeight.Bold)
+        }
+        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
+        .backgroundColor(this.themeColor)
+        .borderRadius(16)
+        .shadow({
+          radius: 4,
+          color: this.themeColor + '40',
+          offsetX: 0,
+          offsetY: 2
+        })
+        
+        Blank()
+        
+        // 日期标签
+        Text(logItem.date)
+          .fontSize(11)
+          .fontColor(this.isDarkMode ? '#B0B0B0' : '#888888')
+          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
+          .backgroundColor(this.isDarkMode ? '#3A3A3A' : '#F0F0F0')
+          .borderRadius(8)
+      }
+      .width('100%')
+      .margin({ bottom: 16 })
+      
+      // 更新标题
+      if (logItem.title) {
+        Text(logItem.title)
+          .fontSize(15)
+          .fontColor(this.isDarkMode ? '#F0F0F0' : '#1A1A1A')
+          .fontWeight(FontWeight.Medium)
+          .textAlign(TextAlign.Start)
+          .width('100%')
+          .margin({ bottom: 16 })
+          .lineHeight(22)
+      }
+      
+      // 更新内容区域
+      Column({ space: 12 }) {
+        // 新功能
+        if (logItem.features && logItem.features.length > 0) {
+          this.buildUpdateSection('✨ 新功能', logItem.features, '#FF6B6B')
+        }
+        
+        // 优化改进  
+        if (logItem.improvements && logItem.improvements.length > 0) {
+          this.buildUpdateSection('🔧 优化改进', logItem.improvements, '#4ECDC4')
+        }
+        
+        // 问题修复
+        if (logItem.fixes && logItem.fixes.length > 0) {
+          this.buildUpdateSection('🐛 问题修复', logItem.fixes, '#45B7D1')
+        }
+      }
+      .width('100%')
+    }
+    .width('100%')
+    .padding(20)
+    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
+    .borderRadius(16)
     .shadow({
-      radius: 16,
-      color: this.isDarkMode ? 'rgba(0,0,0,0.8)' : 'rgba(0,0,0,0.15)',
+      radius: 12,
+      color: this.isDarkMode ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.08)',
       offsetX: 0,
       offsetY: 4
     })
+    .border({
+      width: 1,
+      color: this.isDarkMode ? '#3A3A3A' : '#F0F0F0'
+    })
+    .transition(TransitionEffect.OPACITY.animation({ duration: 300, delay: index * 100 }))
+  }
+
+  /**
+   * 构建更新内容分区
+   */
+  @Builder
+  buildUpdateSection(title: string, items: string[], accentColor: string = this.themeColor) {
+    Column() {
+      // 分区标题 - 重新设计
+      Row() {
+        // 左侧装饰线
+        Column()
+          .width(3)
+          .height(20)
+          .backgroundColor(accentColor)
+          .borderRadius(2)
+          .margin({ right: 8 })
+        
+        Text(title)
+          .fontSize(14)
+          .fontWeight(FontWeight.Bold)
+          .fontColor(this.isDarkMode ? '#F0F0F0' : '#2A2A2A')
+          .textAlign(TextAlign.Start)
+      }
+      .width('100%')
+      .margin({ bottom: 10 })
+      
+      // 分区内容列表 - 优化设计
+      Column({ space: 8 }) {
+        ForEach(items, (item: string, index: number) => {
+          Row() {
+            // 圆点装饰
+            Column()
+              .width(6)
+              .height(6)
+              .backgroundColor(accentColor)
+              .borderRadius(3)
+              .margin({ right: 12, top: 8 })
+            
+            Text(item)
+              .fontSize(13)
+              .fontColor(this.isDarkMode ? '#E0E0E0' : '#4A4A4A')
+              .layoutWeight(1)
+              .textAlign(TextAlign.Start)
+              .lineHeight(20)
+          }
+          .width('100%')
+          .alignItems(VerticalAlign.Top)
+          .padding({ left: 8, right: 4, top: 2, bottom: 2 })
+          .backgroundColor(this.isDarkMode ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)')
+          .borderRadius(8)
+        })
+      }
+      .width('100%')
+    }
+    .width('100%')
+    .padding({ left: 8, right: 8, top: 8, bottom: 8 })
+    .backgroundColor(this.isDarkMode ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)')
+    .borderRadius(12)
+    .border({
+      width: 1,
+      color: this.isDarkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.06)'
+    })
   }
 }

+ 59 - 48
entry/src/main/ets/entryability/EntryAbility.ets

@@ -110,7 +110,7 @@ export default class EntryAbility extends UIAbility {
   private awareness: smartMobilityCommon.SmartMobilityAwareness | undefined =
     canIUse("SystemCapability.SmartOptimizer.SmartMobility") ? smartMobilityCommon.getSmartMobilityAwareness() :
       undefined;
-  
+
   // 服务就绪状态缓存,避免在 widget 控制事件中重复检查
   private isServiceFullyReady: boolean = false;
   private lastServiceReadyCheckTime: number = 0;
@@ -274,6 +274,10 @@ export default class EntryAbility extends UIAbility {
       };
       // 解注册智慧出行连接状态的监听 示例2
       this.awareness.off('smartMobilityStatus', types, callBack);
+            }
+        }catch (error) {
+            console.error(`Failed to getHiCarStatus: ${error.code}, message: ${error.message}`);
+
     }
   }
 
@@ -386,42 +390,49 @@ export default class EntryAbility extends UIAbility {
   }
 
     getHiCarStatus(){
-        if (!this.awareness){
-            this.awareness=canIUse("SystemCapability.CarService.DistributedEngine")?smartMobilityCommon.getSmartMobilityAwareness():undefined;
-        }
-        console.log('enter getHiCarStatus,awareness:'+JSON.stringify(this.awareness));
-        if(this.awareness){
-            // this.awareness = smartMobilityCommon.getSmartMobilityAwareness();
-            console.log('enter awareness');
-            // 业务类型
-            let types: smartMobilityCommon.SmartMobilityType[] = [smartMobilityCommon.SmartMobilityType.HICAR];
-            // 获取出行业务连接状态
-            let info = this.awareness.getSmartMobilityStatus(types[0]);
-
-            hilog.info(0x0000, 'getHiCarStatus  info: ', JSON.stringify(info));
-            if(info&&info.status==1){
-                AppStorage.setOrCreate('isHiCarStatus', true);
-            }else{
-                AppStorage.setOrCreate('isHiCarStatus', false);
+        try {
+            if (!this.awareness){
+                this.awareness=canIUse("SystemCapability.CarService.DistributedEngine")?smartMobilityCommon.getSmartMobilityAwareness():undefined;
             }
+            console.log('enter getHiCarStatus,awareness:'+JSON.stringify(this.awareness));
+            if(this.awareness){
+
+
+                console.log('enter awareness');
+                // 业务类型
+                let types: smartMobilityCommon.SmartMobilityType[] = [smartMobilityCommon.SmartMobilityType.HICAR];
+                // 获取出行业务连接状态
+                let info = this.awareness.getSmartMobilityStatus(types[0]);
 
-            // 出行连接状态回调函数
-            const callBack = (info: smartMobilityCommon.SmartMobilityInfo) => {
-                hilog.info(0x0000, 'getHiCarStatus Received smart mobility info: ', JSON.stringify(info));
+                hilog.info(0x0000, 'getHiCarStatus  info: ', JSON.stringify(info));
                 if(info&&info.status==1){
                     AppStorage.setOrCreate('isHiCarStatus', true);
                 }else{
                     AppStorage.setOrCreate('isHiCarStatus', false);
                 }
-                this.sendChangeEvent()
-            };
-            // 注册智慧出行连接状态的监听
-            this.awareness.on('smartMobilityStatus', types, callBack);
-        }else{
-            AppStorage.setOrCreate('isHiCarStatus', false);
-        }
+
+                // 出行连接状态回调函数
+                const callBack = (info: smartMobilityCommon.SmartMobilityInfo) => {
+                    hilog.info(0x0000, 'getHiCarStatus Received smart mobility info: ', JSON.stringify(info));
+                    if(info&&info.status==1){
+                        AppStorage.setOrCreate('isHiCarStatus', true);
+                    }else{
+                        AppStorage.setOrCreate('isHiCarStatus', false);
+                    }
+                    this.sendChangeEvent()
+                };
+                // 注册智慧出行连接状态的监听
+                this.awareness.on('smartMobilityStatus', types, callBack);
 
 
+            }else{
+                AppStorage.setOrCreate('isHiCarStatus', false);
+            }
+        }catch (error) {
+            console.error(`Failed to getHiCarStatus: ${error.code}, message: ${error.message}`);
+
+        }
+
 
     }
 
@@ -444,7 +455,7 @@ export default class EntryAbility extends UIAbility {
       this.callee.on('playPause', (data: rpc.MessageSequence) => {
         try {
           hilog.info(0x0000, 'Heanup2', `🎵 Widget call: playPause received`);
-          
+
           // 优化:简化参数解析,减少JSON序列化开销
           let params: Record<string, Object> = {};
           const dataString = data.readString();
@@ -456,7 +467,7 @@ export default class EntryAbility extends UIAbility {
             }
           }
 
-          // 优化:立即返回成功状态,使用异步微任务优先处理业务逻辑  
+          // 优化:立即返回成功状态,使用异步微任务优先处理业务逻辑
           Promise.resolve().then(() => {
             this.sendWidgetControlEvent('PLAY_PAUSE', params).catch((error: Error) => {
               hilog.error(0x0000, 'Heanup2', `❌ playPause async error: ${error}`);
@@ -474,7 +485,7 @@ export default class EntryAbility extends UIAbility {
       this.callee.on('nextSong', (data: rpc.MessageSequence) => {
         try {
           hilog.info(0x0000, 'Heanup2', `🎵 Widget call: nextSong received`);
-          
+
           // 优化:简化参数解析,减少JSON序列化开销
           let params: Record<string, Object> = {};
           const dataString = data.readString();
@@ -505,7 +516,7 @@ export default class EntryAbility extends UIAbility {
       this.callee.on('prevSong', (data: rpc.MessageSequence) => {
         try {
           hilog.info(0x0000, 'Heanup2', `🎵 Widget call: prevSong received`);
-          
+
           // 优化:简化参数解析,减少JSON序列化开销
           let params: Record<string, Object> = {};
           const dataString = data.readString();
@@ -592,37 +603,37 @@ export default class EntryAbility extends UIAbility {
   private async sendWidgetControlEvent(command: string, params: Record<string, Object>): Promise<void> {
     try {
       hilog.info(0x0000, 'Heanup2', `🎵 开始处理Widget控制事件: ${command}`);
-      
+
       // 冷启动优化:如果服务未就绪,进行等待和重试
       if (!this.isServiceFullyReady) {
         hilog.info(0x0000, 'Heanup2', '🔄 服务未就绪,开始等待服务初始化...');
-        
+
         // 等待服务就绪,最多等待5秒
         const maxWaitTime = 5000;
         const startTime = Date.now();
         let retryCount = 0;
-        
+
         while (!this.isServiceFullyReady && (Date.now() - startTime) < maxWaitTime) {
           retryCount++;
           hilog.info(0x0000, 'Heanup2', `🔄 等待服务就绪中... (第${retryCount}次检查)`);
-          
+
           // 尝试强制检查服务状态
           const serviceReady = await this.validateAndCacheServiceReadiness();
           if (serviceReady) {
             hilog.info(0x0000, 'Heanup2', '✅ 服务就绪检查成功');
             break;
           }
-          
+
           // 如果服务仍未就绪,尝试强制数据恢复
           if (!this.unifiedService.isDataRestorationCompleted()) {
             hilog.info(0x0000, 'Heanup2', '🔄 尝试强制数据恢复...');
             await this.unifiedService.forceDataRestoration();
           }
-          
+
           // 短暂等待后继续检查
           await new Promise<void>(resolve => setTimeout(resolve, 200));
         }
-        
+
         // 最终检查服务状态
         if (!this.isServiceFullyReady) {
           hilog.error(0x0000, 'Heanup2', `❌ 服务等待超时,无法执行Widget控制事件: ${command}`);
@@ -630,10 +641,10 @@ export default class EntryAbility extends UIAbility {
         }
         hilog.info(0x0000, 'Heanup2', `✅ 服务就绪,开始执行Widget控制事件: ${command}`);
       }
-      
+
       // 优化:立即执行命令,减少状态检查开销
       let commandPromise: Promise<void>;
-      
+
       switch (command) {
         case 'PLAY_PAUSE':
           // 优化:直接基于参数判断,避免重复状态查询
@@ -676,7 +687,7 @@ export default class EntryAbility extends UIAbility {
 
       // 执行命令
       await commandPromise;
-      
+
       // 优化:使用低优先级异步广播,避免阻塞主线程
       Promise.resolve().then(() => {
         setTimeout(() => {
@@ -685,9 +696,9 @@ export default class EntryAbility extends UIAbility {
           });
         }, 50); // 小延迟确保命令执行完成
       });
-      
+
       hilog.info(0x0000, 'Heanup2', `✅ Widget控制事件执行成功: ${command}`);
-      
+
     } catch (error) {
       hilog.error(0x0000, 'Heanup2', `❌ Widget控制事件执行失败: ${command}, error: ${error}`);
       throw new Error(`Widget控制事件执行失败: ${command}`);
@@ -700,9 +711,9 @@ export default class EntryAbility extends UIAbility {
   private async validateAndCacheServiceReadiness(): Promise<boolean> {
     try {
       const currentTime = Date.now();
-      
+
       // 如果缓存仍然有效,直接返回缓存结果
-      if (this.isServiceFullyReady && 
+      if (this.isServiceFullyReady &&
           (currentTime - this.lastServiceReadyCheckTime) < this.SERVICE_READY_CACHE_DURATION) {
         hilog.info(0x0000, 'Heanup2', '✅ 使用服务就绪状态缓存');
         return true;
@@ -718,7 +729,7 @@ export default class EntryAbility extends UIAbility {
 
       // 执行完整的服务就绪检查
       hilog.info(0x0000, 'Heanup2', '🔄 执行服务就绪状态检查...');
-      
+
       // 检查基础服务就绪状态
       if (!unifiedService.isAllServicesReady()) {
         this.isServiceFullyReady = false;
@@ -743,7 +754,7 @@ export default class EntryAbility extends UIAbility {
       this.isServiceFullyReady = true;
       this.lastServiceReadyCheckTime = currentTime;
       hilog.info(0x0000, 'Heanup2', '✅ 服务就绪状态检查完成并已缓存');
-      
+
       return true;
     } catch (error) {
       hilog.error(0x0000, 'Heanup2', `❌ 服务就绪状态检查失败: ${error}`);

+ 11 - 0
entry/src/main/ets/pages/AboutPage.ets

@@ -191,6 +191,17 @@ export struct AboutPage{
               });
             })
 
+          Button('音频标签', { type: ButtonType.Capsule, stateEffect: false })
+            .width(this.currentBreakpoint !== BreakpointTypeEnum.SM?'25%':'60%')
+            .height(55)
+            .margin({top:10,bottom:10})
+            .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7})
+            .stateEffect(true)
+            .backgroundColor(this.themeColor)
+            .onClick(()=>{
+              Utility.gotoMarket(getContext(this) as common.UIAbilityContext, 'com.xgplayer.ttmusic.tags')
+            })
+
           Button('粉丝QQ群', { type: ButtonType.Capsule, stateEffect: false })
             .width(this.currentBreakpoint !== BreakpointTypeEnum.SM?'25%':'60%')
             .height(55)

+ 405 - 0
entry/src/main/ets/pages/ChartsCount.ets

@@ -0,0 +1,405 @@
+import { common, ConfigurationConstant } from '@kit.AbilityKit';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { VideoItem } from '../viewmodel/VideoItem';
+import { LazyDataSource } from '../common/util/LazyDataSource';
+import { LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
+import fs from '@ohos.file.fs';
+import { changeMusicCover, findLocalCoverImage, getApiLyric, repairAudioMetadata,
+  searchCover,
+  syncLyricToDB} from '../common/util/MusicTagUtils';
+import { util } from '@kit.ArkTS';
+import { AudioQuality, FFMpegTags } from '../common/util/Utility';
+import MediaTable from '../common/util/MediaTable';
+import { McPieChart, Options } from '@mcui/mccharts'
+import { ComponentContent } from '@kit.ArkUI';
+
+// 批量编辑标签
+@Component
+export struct ChartsCount {
+  private listScroller: ListScroller = new ListScroller()
+  @State isLyric:boolean = true
+  @State isCover:boolean = true
+  @Link isShowDrawer: boolean;
+  @Link offsetX: number;
+  @State dataSource: LazyDataSource<VideoItem> = new LazyDataSource([])
+  @State selectedFiles: Array<VideoItem> = []
+  @StorageProp('topRectHeight') topRectHeight: number = 0;
+  @State mediaKuCount: number = 0;
+  @State albumCount: number = 0;
+  @State artistCount: number = 0;
+
+  @State lqCount: number = 0;
+  @State sqCount: number = 0;
+  @State hqCount: number = 0;
+  @State losslessCount: number = 0;
+  @State hrCount: number = 0;
+  @State isDarkMode: boolean = false
+  @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
+    ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  context =  this.getUIContext().getHostContext() as common.UIAbilityContext
+  private contentNode?: ComponentContent<Object> = undefined;
+  @State isRefreshing: boolean = false;
+  @State ratio: number = 1;
+  @State maxRefreshingHeight: number = 100.0;
+
+  private table: MediaTable = new MediaTable(this.context)
+  @State defOption: Options = new Options({
+    title: {
+      show: true,
+      text: '曲库统计',
+      left: 40,
+      top: 20
+    },
+    series:[
+      {
+        data:[
+          {value:this.mediaKuCount, name:'媒体库'},
+          {value:this.artistCount, name:'艺术家'},
+          {value:this.albumCount, name:'专辑'},
+        ]
+      }
+    ]
+  })
+
+  @State HrOption: Options = new Options({
+    title: {
+      show: true,
+      text: '音质统计',
+      left: 40,
+      top: 20
+    },
+    series:[
+      {
+        data:[
+          {value:this.lqCount, name:'LQ'},
+          {value:this.sqCount, name:'SQ'},
+          {value:this.hqCount, name:'HQ'},
+          {value:this.losslessCount, name:'Lossless'},
+          {value:this.hrCount, name:'Hi-Res'},
+        ]
+      }
+    ]
+  })
+
+
+  onColorModeChange() {
+    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
+  }
+
+  async aboutToAppear() {
+    let uiContext = this.getUIContext();
+    this.contentNode = new ComponentContent(uiContext, wrapBuilder(customRefreshingContent));
+    this.centerChart()
+
+
+  }
+
+  async centerChart(){
+    this.mediaKuCount = PreferencesUtil.getNumberSync('mediaKuCount', 0)
+    this.artistCount = PreferencesUtil.getNumberSync('artistCount', 0)
+    this.albumCount = PreferencesUtil.getNumberSync('albumCount', 0)
+    setTimeout(() => {
+      // 使用Option实例对象的setVal方法来实现,修改什么属性就传什么
+      this.defOption.setVal({
+        animation:true,
+        series: [
+          {
+            data:[
+              {value:this.mediaKuCount, name:'媒体库'},
+              {value:this.artistCount, name:'艺术家'},
+              {value:this.albumCount, name:'专辑'},
+            ]
+          }
+        ]
+      })
+    }, 1000)
+
+
+    await new Promise<void>((resolve, reject) => {
+      this.table.getRdbStore(this.context,  (err:Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+    this.table.queryByPlayCountDesc(1000, async (result: VideoItem[]) => {
+      this.selectedFiles = result
+      // 确保 selectedFiles 有默认值后再初始化 dataSource
+      if (this.selectedFiles && this.selectedFiles.length > 0) {
+        // this.dataSource = new LazyDataSource(this.selectedFiles)
+        this.dataSource.pushArrayData(this.selectedFiles)
+      } else {
+        this.dataSource = new LazyDataSource([])
+      }
+
+    })
+
+    this.table.countSongsByQualityOptimized(async (result:Record<AudioQuality, number>) => {
+      this.lqCount = result.LQ
+      this.hqCount = result.HQ
+      this.hrCount = result.HR
+      this.sqCount = result.SQ
+      setTimeout(() => {
+        // 使用Option实例对象的setVal方法来实现,修改什么属性就传什么
+        this.HrOption.setVal({
+          animation:true,
+          series:[
+            {
+              data:[
+                {value:this.lqCount, name:'LQ'},
+                {value:this.hqCount, name:'HQ'},
+                {value:this.sqCount, name:'SQ'},
+                {value:this.hrCount, name:'Hi-Res'},
+              ]
+            }
+          ]
+        })
+      }, 1000)
+
+    })
+
+  }
+
+
+
+  build() {
+    Column(){
+      this.topTitleBar()
+      this.listView()
+
+    }
+    .height('100%')
+    .width('100%')
+    .backgroundColor($r('app.color.index_background'))
+
+  }
+
+  @Builder
+  centerCharts() {
+    Row() {
+      Swiper() {
+        McPieChart({
+          options: this.HrOption
+        })
+        McPieChart({
+          options: this.defOption
+        })
+
+      }
+
+    }
+    .borderRadius(14)
+    .backgroundColor($r('app.color.start_window_background'))
+    .height('35%')
+    .margin({bottom:10})
+  }
+
+  @Builder
+  listView() {
+    Column(){
+      Refresh({ refreshing: $$this.isRefreshing, refreshingContent: this.contentNode }) {
+        List({ scroller: this.listScroller }) {
+          ListItemGroup({ header: this.centerCharts() }) {
+            ListItem() {
+              Column() {
+                Text('听歌次数排行榜')
+                  .fontSize(15)
+                  .maxLines(1)
+                  .fontWeight(FontWeight.Bold)
+                  .textAlign(TextAlign.Start)
+                  .padding({ left: 10 })
+                  .fontColor($r('app.color.text_color'))
+                  .margin({bottom:6,top:4,left: 1 })
+              }
+            }
+            LazyForEach(this.dataSource, (item: VideoItem, index: number) => {
+              ListItem() {
+                Column() {
+                  this.MusicItem(item, index)
+                }
+              }
+              .borderRadius(14)
+              .backgroundColor($r('app.color.start_window_background'))
+              .margin({top:7,bottom:7})
+              .transition(TransitionEffect.asymmetric(
+                TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
+                TransitionEffect.scale({ x: 0, y: 0 })
+              ))
+              .clickEffect({ level: ClickEffectLevel.LIGHT })
+            }, (item: VideoItem) => item.filePath)
+          }
+
+        }
+        .cachedCount(2)
+        // .layoutWeight(1)
+        // .height('100%')
+        .transition(TransitionEffect.asymmetric(
+          TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
+          TransitionEffect.scale({ x: 0, y: 0 })
+        ))
+      }
+      .pullDownRatio(this.ratio)
+      .pullToRefresh(true)
+      .refreshOffset(64)
+      .onOffsetChange((offset: number) => {
+        // 越接近最大距离,下拉跟手系数越小。
+        this.ratio = 1 - Math.pow((offset / this.maxRefreshingHeight), 3);
+      })
+      .onStateChange((refreshStatus: RefreshStatus) => {
+        console.info('onecold Refresh onStatueChange state is ' + refreshStatus);
+      })
+      .onRefreshing(async () => {
+        await this.centerChart()
+        setTimeout(() => {
+          this.isRefreshing = false;
+        }, 2000)
+        console.log('onRefreshing test');
+      })
+
+    }
+    .borderRadius(20)
+    .padding({top:10,bottom:6})
+    .height('auto')
+    .margin({top:10,right:20,left:20})
+    .layoutWeight(1)
+    .height('100%')
+  }
+
+  @Builder
+  listHeader() {
+    Stack({alignContent:Alignment.Start}){
+      Text(`共${this.selectedFiles.length}首歌`)
+        .fontSize(14)
+        .maxLines(1)
+        .textAlign(TextAlign.Start)
+        .padding({ left: 25 })
+        .fontColor($r('app.color.text_color'))
+        .margin({bottom:6,top:4,left: 6 })
+    }
+    .height('auto').width('100%')
+  }
+
+
+
+
+
+  @Builder
+  topTitleBar() {
+    // 顶部安全区和自定义标题栏
+    Column() {
+      // 顶部安全区
+      Blank()
+        .height(this.topRectHeight)
+        .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
+      // 自定义标题栏(Stack实现绝对居中)
+      Stack() {
+        // 居中标题
+        Text('歌曲统计')
+          .fontSize(18)
+          .fontColor(Color.White)
+          .align(Alignment.Center)
+        // 左右按钮
+        Row() {
+          Image($r('app.media.menu'))
+            .width(26)
+            .height(26)
+            .margin({ left: 12, right: 8 })
+            .onClick(() => {
+              this.getUIContext().animateTo({ duration: 666 }, () => {
+                // 动画闭包内控制Image组件的出现和消失
+                this.isShowDrawer = !this.isShowDrawer
+                this.offsetX = 0
+              })
+            })
+          Blank().flexGrow(1)
+          Blank().width(32)
+        }
+        .height(48)
+        .width('100%')
+        .alignItems(VerticalAlign.Center)
+      }
+      .height(48)
+      .width('100%')
+      .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+    }
+  }
+
+  @Builder
+  private MusicItem(item: VideoItem, index: number) {
+    Button({ type: ButtonType.Normal, stateEffect: true }) {
+        Column() {
+          Row() {
+            Stack({ alignContent: Alignment.TopStart }) {
+              Image(item.pixelMapPath)
+                .width(75)
+                .height(70)
+                .borderRadius(8)
+                .alt($r('app.media.music_red'))
+                .alignSelf(ItemAlign.Center)
+                .interpolation(ImageInterpolation.Medium)// 用于重采样后的抗锯齿
+                .autoResize(true) // 重采样,可减少内存占用
+                .padding({ left: 10,top:3,bottom:3 })
+            };
+
+            Column(){
+              Text(item.name)
+                .fontSize(17)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
+                .fontColor($r('app.color.text_color'))
+                .padding({ left: 10 })
+                .textAlign(TextAlign.Start)
+                .margin({ left: 5 })
+              Text(StrUtil.isEmpty(item.artist) ? item.cTime : item.artist )
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
+                .textAlign(TextAlign.Start)
+                .padding({  top: 5,left: 10 })
+                .margin({ left: 5 })
+            }
+            .alignItems(HorizontalAlign.Start)
+
+            Blank()
+            // 根据嵌入状态显示勾选图标
+            Text(item.playCount+'')
+              .fontSize(index < 3?18:16)
+              .maxLines(1)
+              .padding({ right: 5 })
+              .textOverflow({ overflow: TextOverflow.MARQUEE })
+              .animation({
+                duration: 555,
+                curve: 'Linear',
+              })
+              .fontColor(index < 3?this.themeColor:$r('app.color.text_color'))
+              .margin({ right: 18 })
+
+          }
+          .layoutWeight(1)
+          .height('100%')
+          .width('100%')
+
+        }
+    }
+    .backgroundColor(Color.Transparent)
+    .width('100%')
+    .height(89)
+  }
+}
+
+@Builder
+function customRefreshingContent() {
+  Stack() {
+    Row() {
+      LoadingProgress().height(32)
+    }
+    .alignItems(VerticalAlign.Center)
+  }
+  .align(Alignment.Center)
+  .clip(true)
+  // 设置最小高度约束保证自定义组件高度随刷新区域高度变化时自定义组件高度不会低于minHeight。
+  .constraintSize({ minHeight: 32 })
+  .width("100%")
+}
+
+

+ 25 - 13
entry/src/main/ets/pages/NewIndex.ets

@@ -38,6 +38,7 @@ import { UpdateLogManager } from '../common/util/UpdateLogManager';
 import OnlineUpdateLogDialog from '../dialog/OnlineUpdateLog';
 import OnlineUpdateLog from '../dialog/OnlineUpdateLog';
 import { LocalMusic } from '../view/LocalMusic';
+import { ChartsCount } from './ChartsCount';
 
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
@@ -84,7 +85,10 @@ struct NewIndex {
   @StorageProp('windowHeight') windowHeight: number = 0;
   @StorageProp('isHiCarStatus') isHiCarStatus: boolean = false;
   @StorageProp('curDisplayIsHiCar') curDisplayIsHiCar: boolean = false;
-  @State isShowUpdateDialog: boolean = false //是否显示更新日志开关
+  /**
+   * 是否显示更新日志开关
+   */
+  @State isShowUpdateDialog: boolean = false
   /** 标题栏配置模型 */
   @State titleBarModel: TitleBar.Model = new TitleBar.Model()
     .setTitleTextStyle(FontStyle.Normal)
@@ -258,6 +262,7 @@ struct NewIndex {
           this.isShowUpdateDialog = !this.isShowUpdateDialog
           UpdateLogManager.markCurrentVersionShown()
       }
+      // this.isShowUpdateDialog = true;//调试期间显示更新日志,测试完成后请删除
     } catch (error) {
       console.error('NewIndex checkAndShowUpdateLog error:', error);
     }
@@ -294,10 +299,17 @@ struct NewIndex {
             .visibility(this.mType === 1 ? Visibility.Visible : Visibility.None)
           ScanFilePage()
             .visibility(this.mType === 2 ? Visibility.Visible : Visibility.None)
-          SettingPage()
+
+          ChartsCount({
+            isShowDrawer:this.isShowDrawer,
+            offsetX:this.offsetX
+          })
             .visibility(this.mType === 3 ? Visibility.Visible : Visibility.None)
-          AboutPage()
+
+          SettingPage()
             .visibility(this.mType === 4 ? Visibility.Visible : Visibility.None)
+          AboutPage()
+            .visibility(this.mType === 5 ? Visibility.Visible : Visibility.None)
           
           // 抽屉打开时的遮罩层,用于拦截点击事件  这个只能正常尺寸的手机竖屏的才能生效
           if (this.isShowDrawer&&this.isPhonePortrait()) {
@@ -583,6 +595,7 @@ struct NewIndex {
                   SymbolGlyph((item.img as sysResource).value as Resource)// .size({ width: 22, height: 22 })
                     .fontSize(22)
                     .fontColor([this.themeColor])
+                    .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
                     .alignSelf(ItemAlign.Center)
                     .margin({ left: 25 })
                 } else {
@@ -624,13 +637,11 @@ struct NewIndex {
                   this.mType = 2
                   this.doShowDrawer()
                   break
-                // case MainViewModel.MENU_HOME:
-                //   this.mType = 0
-                //   this.modeType = 0
-                //   this.getUIContext()?.animateTo({ duration: 555 }, () => {
-                //     this.isShowDrawer = false
-                //   })
-                //   break
+                case MainViewModel.MENU_CHARTS:
+                  this.mType = 3
+                  this.modeType = 0
+                  this.doShowDrawer()
+                  break
                 case MainViewModel.MENU_MIEDIA_KU:
                   this.modeType = 1
                   this.mType = 0
@@ -647,7 +658,7 @@ struct NewIndex {
                   this.doShowDrawer()
                   break
                 case MainViewModel.MENU_SETTING:
-                  this.mType = 3
+                  this.mType = 4
                   this.doShowDrawer()
 
                   break
@@ -681,7 +692,7 @@ struct NewIndex {
                   break
 
                 case MainViewModel.MENU_ABOUT:
-                  this.mType = 4
+                  this.mType = 5
                   this.doShowDrawer()
                   break
                 case MainViewModel.MENU_UPDATE:
@@ -732,7 +743,8 @@ struct NewIndex {
     })
     .alignListItem(ListItemAlign.Center)
     .layoutWeight(1)
-    .backgroundColor($r('app.color.silvery'))
+    .backgroundColor($r('app.color.left_draw_bg'))
+
     .edgeEffect(EdgeEffect.None) // 必须设置列表为滑动到边缘无效果
   }
 

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

@@ -94,7 +94,7 @@ export struct SettingPage {
   @State isShowFAV: boolean = true //是否显示我的收藏
   @State isShowHistory: boolean = true //是否显示最近播放
   @State isShowPlayPageBack: boolean = false //是否显示播放页返回键
-  @State isShowSingleLineLyric: boolean = false //是否显示播放页返回键
+  @State isShowSingleLineLyric: boolean = false //是否显示单行歌词
   @State isShowHeader: boolean = true
   @State isCustomizeBg: boolean = false //自定义背景界面
   @State isCustomizeBgSheet: boolean = false //自定义背景界面
@@ -110,6 +110,7 @@ export struct SettingPage {
   @State isSameTimePlay: boolean = false //是否和其他app同时播放
   @State isCoverRectangle: boolean = false
   @State isSavePlayMode: boolean = true
+  @State volumeSmall: boolean = false
   @State isCoverTop: boolean = true
   @State isCoverTopBig: boolean = false//顶部大封面部分手机显示会和播放控制页重叠
   @State isSwipe: boolean = false //listItem的左滑开关
@@ -238,6 +239,7 @@ export struct SettingPage {
     this.isSwipe = PreferencesUtil.getBooleanSync(SettingPage.IS_SWIPE, true)
     this.is_auto_hide_progress = PreferencesUtil.getBooleanSync(SettingPage.IS_AUTO_HIDE_PROGRESS, false)
     this.openSkipSongAnimate = PreferencesUtil.getBooleanSync(SettingPage.OPEN_SKIPSONG_ANIMATE, true)
+    this.volumeSmall = PreferencesUtil.getBooleanSync('volumeSmall', false)
     // 只用 themeMode 控制主题
     this.themeMode = PreferencesUtil.getNumberSync(SettingPage.THEME_MODE, 0)
     this.applyThemeMode(this.themeMode)
@@ -560,6 +562,14 @@ export struct SettingPage {
 
       Scroll() {
         Column() {
+
+
+          // API设置分组
+          this.apiBuilder()
+          //设置教程
+          this.jcBuilder()
+
+
           // 主题设置分组
           Column() {
             Row() {
@@ -952,7 +962,7 @@ export struct SettingPage {
 
           }
           .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
-            .animation({ duration: 500, curve: Curve.Ease, delay: 200 }))
+            .animation({ duration: 500, curve: Curve.Ease, delay: 260 }))
           .backgroundColor($r('app.color.settings_background_main'))
           .borderRadius(20)
           .margin({
@@ -1177,6 +1187,38 @@ export struct SettingPage {
             .height(55)
             .clickEffect({ level: ClickEffectLevel.HEAVY })
 
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            // 保存播放模式
+            Row() {
+              SymbolGlyph($r('sys.symbol.speaker'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .margin({ left: 15 })
+              Text('音量优化')
+                .margin({ left: 8 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Toggle({ type: ToggleType.Switch, isOn: this.volumeSmall })
+                .selectedColor(this.themeColor)
+                .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
+                .switchPointColor(Color.White)
+                .margin({ right: 18 })
+                .onChange((checked: boolean) => {
+                  this.volumeSmall = checked;
+                  if(this.volumeSmall){
+                    ToastUtil.showToast('打开音量优化会比平时音量声音低些')
+                  }
+                  PreferencesUtil.put('volumeSmall', this.volumeSmall)
+                })
+                .width(50)
+                .height(30);
+            }
+            .height(55)
+            .clickEffect({ level: ClickEffectLevel.HEAVY })
+
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
             // 保存播放模式
             Row() {
@@ -1277,10 +1319,6 @@ export struct SettingPage {
           })
           .padding(0)
 
-          // API设置分组
-          this.apiBuilder()
-          //设置教程
-          this.jcBuilder()
           //显示与隐藏设置
           this.xsBuilder()
 
@@ -1657,6 +1695,8 @@ export struct SettingPage {
     .borderRadius(15)
     .margin({ top: 0, bottom: 20 })
     .padding(0)
+    .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
+      .animation({ duration: 500, curve: Curve.Ease, delay: 220 }))
   }
 
   @Builder
@@ -1664,14 +1704,19 @@ export struct SettingPage {
     // API设置分组
     Column() {
       Row() {
-        Text('API设置')
+        Text(){
+          Span('API设置')
+          Span('\n(请注意:使用此功能您需拥有相关著作人的授权,否则可能导致版权法律纠纷,本软件对此不负任何责任。)')
+            .fontColor(this.themeColor).fontSize(14)
+            // .fontStyle(FontStyle.Italic)
+        }
           .margin({ left: 18, right: 20 })
           .fontSize(16)
           .fontColor(Color.Gray)
           .fontWeight(480)
           .layoutWeight(1)
       }
-      .height(48)
+      .height(82)
 
       Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
       // 歌词API
@@ -1743,7 +1788,7 @@ export struct SettingPage {
 
     }
     .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
-      .animation({ duration: 500, curve: Curve.Ease, delay: 600 }))
+      .animation({ duration: 500, curve: Curve.Ease, delay: 200 }))
     .backgroundColor($r('app.color.settings_background_main'))
     .borderRadius(20)
     .margin({
@@ -1816,15 +1861,20 @@ export struct SettingPage {
     Column() {
       Image(this.customizeBgPath)
         .height('42%')
-        .alt($r('app.color.white'))
+        .alt($r('app.media.add_image2'))
         .objectFit(ImageFit.Contain)
-        .borderRadius(20)
+        .borderRadius(15)
         .clip(true)
         .clickEffect({ level: ClickEffectLevel.HEAVY })
         .blur(this.blurValue)
+        .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.6})
         .brightness(this.bgBrightness + 0.8)
         .margin({ left: 30, right: 30, bottom: 20 })
+        .onClick(async () => {
 
+          this.goSelectImage()
+
+        })
       Row() {
 
         Row() {

+ 9 - 2
entry/src/main/ets/pages/UserCenter.ets

@@ -155,6 +155,13 @@ export struct UserCenter {
       isVipOnly: true,
       hasCome: true
     },
+    {
+      icon: $r('app.media.search'),
+      title: '批量标签内嵌',
+      description: '一键内嵌音乐封面和歌词',
+      isVipOnly: true,
+      hasCome: true
+    },
     {
       icon: $r('app.media.skip'),
       title: '跳过头尾',
@@ -680,7 +687,7 @@ export struct UserCenter {
                     top: 2,
                     bottom: 2
                   })
-                  .margin({ top: 2, bottom: 6 })
+                  .margin({ top: 2, bottom: 8 })
               }
             }
             .backgroundColor($r('app.color.user_center_card_background'))
@@ -698,7 +705,7 @@ export struct UserCenter {
             })
             .padding(12)
             .width('100%')
-            .height(145)
+            .height(158)
           }
 
         })

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


+ 539 - 0
entry/src/main/ets/view/TagsContentCover.ets

@@ -0,0 +1,539 @@
+import { common, ConfigurationConstant } from '@kit.AbilityKit';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { VideoItem } from '../viewmodel/VideoItem';
+import { LazyDataSource } from '../common/util/LazyDataSource';
+import { LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
+import fs from '@ohos.file.fs';
+import { changeMusicCover, findLocalCoverImage, getApiLyric, repairAudioMetadata,
+  searchCover,
+  syncLyricToDB} from '../common/util/MusicTagUtils';
+import { util } from '@kit.ArkTS';
+import { FFMpegTags, Utility } from '../common/util/Utility';
+import { DialogHelper } from '@pura/harmony-dialog';
+
+//批量编辑标签
+// 批量编辑标签
+@Component
+export struct TagsContentCover {
+  private listScroller: ListScroller = new ListScroller()
+  onTagsResult = (_result: boolean) => {
+  }
+  @State isLyric:boolean = true
+  @State isCover:boolean = true
+  @Link isShowDrawer: boolean;
+  @State dataSource: LazyDataSource<VideoItem> = new LazyDataSource([])
+  @Prop selectedFiles: Array<VideoItem>
+  @StorageProp('topRectHeight') topRectHeight: number = 0;
+  @State isDarkMode: boolean = false
+  @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
+    ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  context =  this.getUIContext().getHostContext() as common.UIAbilityContext
+  // 新增状态用于进度显示
+  @State currentProgress: number = 0;
+  @State totalFiles: number = 0;
+  @State currentFileIndex: number = 0;
+  @State currentFileName: string = '';
+  @State isEmbedding: boolean = false;
+  @State embedSuccessCount: number = 0;
+  @State embedFailedCount: number = 0;
+
+  // 新增状态用于跟踪每个文件的嵌入状态
+  @State embedStatusMap: Map<string, boolean> = new Map<string, boolean>();
+
+
+  onColorModeChange() {
+    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
+  }
+
+  async aboutToAppear() {
+    // 确保 selectedFiles 有默认值后再初始化 dataSource
+    if (this.selectedFiles && this.selectedFiles.length > 0) {
+      this.dataSource = new LazyDataSource(this.selectedFiles)
+    } else {
+      this.dataSource = new LazyDataSource([])
+    }
+  }
+
+
+
+  build() {
+    Column(){
+      this.topTitleBar()
+      this.TagSetting()
+      this.startButton()
+      this.listView()
+
+    }
+    .height('100%')
+    .width('100%')
+    .backgroundColor($r('app.color.index_background'))
+  }
+
+  @Builder
+  listView() {
+    Column(){
+      List({ scroller: this.listScroller }) {
+        ListItemGroup({ header: this.listHeader() }) {
+          LazyForEach(this.dataSource, (item: VideoItem, index: number) => {
+            ListItem() {
+              Column() {
+                this.MusicItem(item, index)
+              }
+            }
+            .transition(TransitionEffect.asymmetric(
+              TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
+              TransitionEffect.scale({ x: 0, y: 0 })
+            ))
+            .clickEffect({ level: ClickEffectLevel.LIGHT })
+          }, (item: VideoItem) => item.filePath)
+        }
+
+      }
+      .cachedCount(2)
+      // .layoutWeight(1)
+      // .height('100%')
+      .transition(TransitionEffect.asymmetric(
+        TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
+        TransitionEffect.scale({ x: 0, y: 0 })
+      ))
+
+    }
+    .borderRadius(20)
+    .padding({top:10,bottom:6})
+    .height('auto')
+    .backgroundColor($r('app.color.start_window_background'))
+    .margin({top:20,right:20,left:20})
+    .layoutWeight(1)
+    .height('100%')
+  }
+
+  @Builder
+  listHeader() {
+    Stack({alignContent:Alignment.Start}){
+      Text(`共${this.selectedFiles.length}首歌`)
+        .fontSize(14)
+        .maxLines(1)
+        .textAlign(TextAlign.Start)
+        .padding({ left: 25 })
+        .fontColor($r('app.color.text_color'))
+        .margin({bottom:6,top:4,left: 6 })
+    }
+    .height('auto').width('100%')
+  }
+
+
+  @Builder
+  TagSetting() {
+    Column(){
+
+      Row() {
+        SymbolGlyph($r('sys.symbol.doc_text'))
+          .fontSize(20)
+          .fontColor([this.themeColor])
+          .alignSelf(ItemAlign.Center)
+          .margin({ left: 15 })
+        Text('内嵌歌词')
+          .margin({ left: 8 })
+          .fontSize(15)
+          .fontColor($r('app.color.text_color'))
+          .fontWeight(480)
+          .layoutWeight(1)
+        Toggle({ type: ToggleType.Switch, isOn: this.isLyric })
+          .selectedColor(this.themeColor)
+          .switchPointColor(Color.White)
+          .margin({ right: 18 })
+          .onChange((checked: boolean) => {
+            this.isLyric = checked;
+          })
+          .width(50)
+          .height(30);
+      }
+      .height(55)
+      .clickEffect({level:ClickEffectLevel.HEAVY,scale:0.6})
+
+      Row() {
+        SymbolGlyph($r('sys.symbol.picture'))
+          .fontSize(20)
+          .fontColor([this.themeColor])
+          .alignSelf(ItemAlign.Center)
+          .margin({ left: 15 })
+        Text('内嵌封面')
+          .margin({ left: 8 })
+          .fontSize(15)
+          .fontColor($r('app.color.text_color'))
+          .fontWeight(480)
+          .layoutWeight(1)
+        Toggle({ type: ToggleType.Switch, isOn: this.isCover })
+          .selectedColor(this.themeColor)
+          .switchPointColor(Color.White)
+          .margin({ right: 18 })
+          .onChange((checked: boolean) => {
+            this.isCover = checked;
+          })
+          .width(50)
+          .height(30);
+      }
+      .height(55)
+      .clickEffect({level:ClickEffectLevel.HEAVY,scale:0.6})
+
+    }
+    .borderRadius(20)
+    .backgroundColor($r('app.color.start_window_background'))
+    .margin({top:20,right:20,left:20})
+  }
+
+  @Builder
+  startButton(){
+    Stack(){
+      Progress({ value: this.currentProgress, total: this.totalFiles,
+        type: ProgressType.Capsule }).height(55)
+        .margin({top:20,right:20,left:20})
+        .backgroundColor($r('app.color.index_background'))
+      Button({ type: ButtonType.Capsule, stateEffect: true }) {
+        Row() {
+          // 菜单图标
+          SymbolGlyph($r('sys.symbol.star_trophy'))// .size({ width: 22, height: 22 })
+            .fontSize(22)
+            .fontColor([this.themeColor])
+            .alignSelf(ItemAlign.Center)
+            .margin({ left: 25 })
+
+          // 菜单标题
+          Text('开始内嵌')
+            .margin({ left: 10, right: 20 })
+            .fontSize(15)
+            .fontWeight(480)
+          Blank()
+          // 右侧箭头
+          Image($r('app.media.arrow_right'))
+            .width(22)
+            .height(22)
+            .margin({ left: 0, right: 20 })
+            .align(Alignment.Center)
+        }
+        .width('100%')
+      }
+      .margin({top:20,right:20,left:20})
+      .height(55)
+      .enabled(!this.isEmbedding)
+      .backgroundColor($r('app.color.start_window_background'))
+      .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
+      .onClick(()=>{
+        this.startEmbed()
+      })
+
+
+    }
+
+  }
+
+  // 会员功能弹窗
+  showVipDialog() {
+    DialogHelper.showCustomContentDialog({
+      dialogId: 'vip',
+      title: "友情提示",
+      autoCancel: false,
+      backCancel: true,
+      contentBuilder: () => {
+        this.customVipBuilder("该功能需开通会员,非常感谢您的支持!")
+      },
+      buttons: [],
+    })
+  }
+
+  @Builder
+  customVipBuilder(content: string) {
+    Column() {
+      Text(content)
+        .fontColor(Color.Gray)
+        .fontSize(16)
+        .alignSelf(ItemAlign.Start)
+        .margin({ bottom: 15 })
+      Row() {
+        Button('知道了')
+          .fontColor(Color.White)
+          .layoutWeight(1)
+          .height(50)
+          .backgroundColor(this.themeColor)
+          .stateEffect(true)
+          .margin({ left: 6 })
+          .onClick(() => {
+            DialogHelper.closeDialog('vip');
+          })
+      }
+    }
+    .width("100%")
+    .padding(10)
+  }
+
+
+  /**
+   * 开始嵌入封面和歌词
+   */
+  async startEmbed() {
+    if (this.isEmbedding || !this.selectedFiles || this.selectedFiles.length === 0) {
+      return;
+    }
+
+    if (this.isCover&&PreferencesUtil.getStringSync('COVER_API',  '') === '') {
+      ToastUtil.showToast('请到设置界面配置封面Api')
+      return; // Return empty string when no API is configured
+    }
+
+    //判断有没有设置api
+    let apiUrl = PreferencesUtil.getStringSync('LRC_API', '')
+    if (this.isLyric&&StrUtil.isEmpty(apiUrl)) {
+      ToastUtil.showToast('请到设置界面配置歌词Api')
+      return
+    }
+    if (!Utility.isNoble()) {
+      this.showVipDialog();
+      return;
+    }
+
+    this.isEmbedding = true;
+    this.currentProgress = 0;
+    this.currentFileIndex = 0;
+    this.embedSuccessCount = 0;
+    this.embedFailedCount = 0;
+    this.totalFiles = this.selectedFiles.length;
+
+    // 清空之前的嵌入状态
+    this.embedStatusMap = new Map<string, boolean>();
+
+    // 创建异步任务数组
+    const embedTasks = this.selectedFiles.map(async (item: VideoItem, index: number) => {
+      try {
+        this.currentFileIndex = index + 1;
+        this.currentFileName = item.name;
+
+
+        let success = true;
+
+        // 1. 处理封面 (如果启用且需要处理)
+        if(this.isCover){
+          let  defaultCover = ''
+          // 先查找同名的png或jpg文件
+          if (StrUtil.isEmpty(item.pixelMapPath) && item.filePath) {
+            defaultCover = findLocalCoverImage(item.filePath);
+
+            // 如果没有找到同名图片文件,则进行在线查询
+            if (StrUtil.isEmpty(defaultCover)) {
+              const imagePath: string | undefined = await searchCover(
+                this.context,
+                item,
+                item.name,
+                item?.artist || ''
+              );
+              defaultCover = imagePath || '';
+            }
+          }else if(item.pixelMapPath&&item.pixelMapPath.startsWith('http')){
+            defaultCover = item.pixelMapPath
+          }
+          if (StrUtil.isNotEmpty(defaultCover)) {
+            // 确保 filePath 存在
+            if (item.filePath) {
+              const result = await changeMusicCover(
+                this.context,
+                item.filePath,
+                defaultCover,
+                true // 覆盖原文件
+              );
+              if (!result) {
+                success = false;
+              }
+            } else {
+              success = false;
+            }
+          }
+        }
+
+
+        // 2. 处理歌词 (如果启用)
+        if (this.isLyric) {
+          let lyricContent = item.lyricContent || ''; // 默认为空字符串
+
+          // 如果歌词为空,尝试读取同名.lrc文件
+          if (StrUtil.isEmpty(lyricContent) && item.filePath) {
+            const lrcPath = item.filePath.substring(0, item.filePath.lastIndexOf('.')) + '.lrc';
+            try {
+              if (fs.accessSync(lrcPath)) {
+                const file = fs.openSync(lrcPath, fs.OpenMode.READ_ONLY);
+                const buf = new ArrayBuffer(102400); // 100KB缓冲区
+                const len = fs.readSync(file.fd, buf);
+                const textDecoder = new util.TextDecoder('utf-8');
+                // 修复错误3: 使用 Uint8Array 包装 ArrayBuffer
+                lyricContent = textDecoder.decode(new Uint8Array(buf.slice(0, len)));
+                fs.closeSync(file);
+              }
+            } catch (error) {
+              console.warn(`读取LRC文件失败: ${JSON.stringify(error)}`);
+            }
+          }
+
+          // 如果仍然为空,尝试从API获取歌词
+          if (StrUtil.isEmpty(lyricContent) && item.filePath) {
+            try {
+              const apiUrl = PreferencesUtil.getStringSync('LRC_API', '');
+              // 修复错误1: 添加非空检查
+              if (StrUtil.isNotEmpty(apiUrl)) {
+                lyricContent = await getApiLyric(
+                  apiUrl,
+                  item.name || '', // 修复可能为undefined的情况
+                  item.artist || '', // 修复可能为undefined的情况
+                  apiUrl.includes(CommonConstants.LRC_API_2)
+                );
+              }
+            } catch (error) {
+              console.warn(`获取API歌词失败: ${JSON.stringify(error)}`);
+            }
+          }
+
+          // 如果有歌词内容,嵌入到音频文件
+          if (StrUtil.isNotEmpty(lyricContent) && item.filePath) {
+            const metadata: FFMpegTags = {};
+            // 修复错误2: 添加非空检查
+            const result = await repairAudioMetadata(
+              item.filePath,
+              lyricContent,
+              metadata,
+              true // 覆盖原文件
+            );
+            if (!result) {
+              success = false;
+            }else{
+              //内嵌成功后同步歌词到数据库
+              syncLyricToDB(this.context,item.filePath,lyricContent)
+            }
+          }
+        }
+
+        // 更新嵌入状态
+        if (item.filePath) {
+          this.embedStatusMap.set(item.filePath, success);
+        }
+
+        if (success) {
+          this.embedSuccessCount++;
+        } else {
+          this.embedFailedCount++;
+        }
+        // 滚动到当前处理的项目位置
+        // this.listScroller.scrollToIndex(index);
+        this.currentProgress = Math.floor(((index + 1) / this.totalFiles) * 100);
+        return success;
+      } catch (error) {
+        console.error(`处理文件 ${item.name} 失败: ${JSON.stringify(error)}`);
+        this.embedFailedCount++;
+        return false;
+      }
+    });
+
+    try {
+      // 并发执行所有嵌入任务
+      await Promise.all(embedTasks);
+    } catch (error) {
+      this.onTagsResult(false)
+      console.error(`批量嵌入过程出错: ${JSON.stringify(error)}`);
+    } finally {
+      ToastUtil.showToast(`嵌入完成: 成功 ${this.embedSuccessCount}, 失败 ${this.embedFailedCount}`)
+      this.isEmbedding = false;
+      this.currentProgress = 0;
+      this.totalFiles = this.selectedFiles.length;
+      this.onTagsResult(true)
+      // 可以在这里添加完成后的提示或回调
+      console.info(`嵌入完成: 成功 ${this.embedSuccessCount}, 失败 ${this.embedFailedCount}`);
+    }
+  }
+
+
+  @Builder
+  topTitleBar() {
+    // 顶部安全区和自定义标题栏
+    Column() {
+      // 顶部安全区
+      Blank()
+        .height(this.topRectHeight)
+        .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
+      // 自定义标题栏(Stack实现绝对居中)
+      Stack() {
+        // 居中标题
+        Text('批量内嵌标签')
+          .fontSize(18)
+          .fontColor(Color.White)
+          .align(Alignment.Center)
+        // 左右按钮
+        Row() {
+          Image($r('app.media.left_back_white'))
+            .width(26)
+            .height(26)
+            .margin({ left: 12, right: 8 })
+            .onClick(() => {
+              this.getUIContext().animateTo({ duration: 666 }, () => {
+                // 动画闭包内控制Image组件的出现和消失
+                this.isShowDrawer = !this.isShowDrawer
+              })
+            })
+          Blank().flexGrow(1)
+          Blank().width(32)
+        }
+        .height(48)
+        .width('100%')
+        .alignItems(VerticalAlign.Center)
+      }
+      .height(48)
+      .width('100%')
+      .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+    }
+  }
+
+  @Builder
+  private MusicItem(item: VideoItem, index?: number) {
+    Button({ type: ButtonType.Normal, stateEffect: true }) {
+      Column() {
+        Row() {
+          SymbolGlyph($r('sys.symbol.music'))
+            .fontSize(20)
+            .fontColor([this.themeColor])
+            .alignSelf(ItemAlign.Center)
+            .padding({ left: 25 })
+          Text(item.name)
+            .fontSize(14)
+            .maxLines(1)
+            .padding({ left: 5 })
+            .textOverflow({ overflow: TextOverflow.MARQUEE })
+            .animation({
+              duration: 555,
+              curve: 'Linear',
+            })
+            .fontColor(Color.Gray)
+            .margin({ left: 8 })
+          Blank()
+          // 根据嵌入状态显示勾选图标
+          SymbolGlyph($r('sys.symbol.checkmark_circle'))
+            .fontSize(20)
+            .fontColor([this.themeColor])
+            .alignSelf(ItemAlign.Center)
+            .padding({ right: 25 })
+            .animation({
+              duration: 666,
+              curve: 'ease-in-out' // 可选动画曲线
+            })
+            .visibility(this.embedStatusMap.get(item.filePath)?Visibility.Visible:Visibility.None)
+
+        }
+        .layoutWeight(1)
+        .height('100%')
+        .width('100%')
+
+      }
+    }
+    .backgroundColor(Color.Transparent)
+    .width('100%')
+    .height(40)
+  }
+}
+
+

+ 6 - 0
entry/src/main/ets/viewmodel/MainViewModel.ets

@@ -43,6 +43,7 @@ export  class  MainViewModel{
   static readonly MENU_MIEDIA_ALBUM: number = 230;
   static readonly MENU_FILE_SCAN: number = 231;
   static readonly MENU_SHARE: number = 232;
+  static readonly MENU_CHARTS: number = 233;
   //测滑菜单的数据
   getDrawerData(): Array<ItemData> {
     let drawerGridData: ItemData[] = [
@@ -53,6 +54,9 @@ export  class  MainViewModel{
       new ItemData($r('app.string.album'), $r('app.media.llq'),MainViewModel.MENU_MIEDIA_ALBUM,false),
       new ItemData($r('app.string.user_center'), { type: 'symbol', value: $r('sys.symbol.person') }, MainViewModel.MENU_USER, false),
       new ItemData($r('app.string.file_scan'), { type: 'symbol', value: $r('sys.symbol.doc_text_badge_magnifyingglass') },MainViewModel.MENU_FILE_SCAN,false),
+
+      new ItemData($r('app.string.music_charts'), { type: 'symbol', value: $r('sys.symbol.doc_plaintext') },MainViewModel.MENU_CHARTS,false),
+
       new ItemData($r('app.string.setting'),  { type: 'symbol', value: $r('sys.symbol.gearshape') },MainViewModel.MENU_SETTING,false),
       new ItemData($r('app.string.about_me'), { type: 'symbol', value: $r('sys.symbol.info_shield') },MainViewModel.MENU_ABOUT,false),
       new ItemData($r('app.string.haoping'), { type: 'symbol', value: $r('sys.symbol.flower') },MainViewModel.MENU_HAOPING,false),
@@ -69,6 +73,8 @@ export  class  MainViewModel{
       new ItemData($r('app.string.local_music'), { type: 'symbol', value: $r('sys.symbol.music') }, MainViewModel.MENU_MUSIC, false),
       new ItemData($r('app.string.user_center'), { type: 'symbol', value: $r('sys.symbol.person') }, MainViewModel.MENU_USER, false),
       new ItemData($r('app.string.file_scan'), { type: 'symbol', value: $r('sys.symbol.doc_text_badge_magnifyingglass') },MainViewModel.MENU_FILE_SCAN,false),
+      new ItemData($r('app.string.music_charts'), { type: 'symbol', value: $r('sys.symbol.doc_plaintext') },MainViewModel.MENU_CHARTS,false),
+
       new ItemData($r('app.string.setting'),  { type: 'symbol', value: $r('sys.symbol.gearshape') },MainViewModel.MENU_SETTING,false),
       new ItemData($r('app.string.about_me'), { type: 'symbol', value: $r('sys.symbol.info_shield') },MainViewModel.MENU_ABOUT,false),
       // new ItemData($r('app.string.vip'), { type: 'symbol', value: $r('sys.symbol.vip_hand') }, MainViewModel.MENU_VIP, false),

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

@@ -175,6 +175,10 @@
     {
       "name": "user_center_card_background",
       "value": "#FFFFFF"
+    },
+    {
+      "name": "left_draw_bg",
+      "value": "#FEFEFE"
     }
   ]
 }

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

@@ -471,6 +471,10 @@
     {
       "name": "select_all",
       "value": "多选"
+    },
+    {
+      "name": "music_charts",
+      "value": "歌曲统计"
     }
   ]
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
entry/src/main/resources/base/media/add_image2.svg


+ 5 - 1
entry/src/main/resources/dark/element/color.json

@@ -43,7 +43,7 @@
     },
     {
       "name": "index_background",
-      "value": "#FF373737"
+      "value": "#000000"
     },
     {
       "name": "tab_bar_sel",
@@ -192,6 +192,10 @@
     {
       "name": "user_center_button_background",
       "value": "#1E1E1E"
+    },
+    {
+      "name": "left_draw_bg",
+      "value": "#000000"
     }
   ]
 }

+ 18 - 12
lib/src/main/ets/parse/LyricParser.ts

@@ -29,7 +29,7 @@ export class LyricParser implements IParser {
         let album = ""
         let by = ""
         let offset = 0
-        const ignoredTags = [ 'hash', 'sign', 'qq', 'total','Outro']; // 定义需要忽略的标签
+        const ignoredTags = [ 'id', 'hash', 'sign', 'qq', 'total','Outro','Intro']; // 定义需要忽略的标签
         for (let i = 0; i < src.length; i++) {
             let line = src[i]
             console.info(`content The line of file:line ${line}`);
@@ -45,17 +45,23 @@ export class LyricParser implements IParser {
                 printW(`the lyric line contains ignored tag, line index= ${i}`);
                 continue;
             }
-            if (line.indexOf("ti") > 0) {
-                title = this.parseIdTag(line)
-            } else if (line.indexOf("ar") > 0) {
-                artist = this.parseIdTag(line)
-            } else if (line.indexOf("al") > 0) {
-                album = this.parseIdTag(line)
-            } else if (line.indexOf("by") > 0) {
-                by = this.parseIdTag(line)
-            } else if (line.indexOf("offset") > 0) {
-                offset = Number.parseInt(this.parseIdTag(line))
-            } else {
+            // 修改后的标签检测逻辑,修复英文歌词的时候,部分歌词没有显示出来。
+            if (line.startsWith("[ti:"))  {
+                title = this.parseIdTag(line);
+            }
+            else if (line.startsWith("[ar:"))  {
+                artist = this.parseIdTag(line);
+            }
+            else if (line.startsWith("[al:"))  {
+                album = this.parseIdTag(line);
+            }
+            else if (line.startsWith("[by:"))  {
+                by = this.parseIdTag(line);
+            }
+            else if (line.startsWith("[offset:"))  {
+                offset = Number.parseInt(this.parseIdTag(line));
+            }
+            else {
 
                 // 新增逐字歌词解析逻辑[mm:ss.xx] <mm:ss.xx>
                 if (this.isWordByWordLyric(line)) {

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