Преглед изворни кода

Merge remote-tracking branch 'origin/feature/webdav' into feature/webdav

# Conflicts:
#	entry/src/main/ets/pages/PlaylistDetailPage.ets
chendeben пре 9 месеци
родитељ
комит
6c45819d0a

+ 3 - 0
entry/src/main/ets/common/constants/EventConstants.ets

@@ -47,4 +47,7 @@ export class EventConstants {
 
   // 播放状态变化事件
   static readonly EVENT_PLAYBACK_STATUS: number = 2003;
+
+  // 刷新播放列表的排序
+  static readonly EVENT_PLAYLIST_REFRESH_SORT: number = 2004;
 }

+ 75 - 0
entry/src/main/ets/common/util/PermissionUtil.ets

@@ -0,0 +1,75 @@
+import { fileShare, fileIo as fs, fileUri } from '@kit.CoreFileKit';
+import { FileUtil } from '@pura/harmony-utils';
+
+
+//权限类
+class PermissionUtil {
+  //激活权限
+  async activatePermission(uri: string | undefined): Promise<boolean> {
+    if (!uri) {
+      return false
+    }
+    try {
+      let fd = fs.openSync(uri);
+      fs.closeSync(fd);
+      console.log("onecold activatePermission 无需激活权限");
+      return true
+    } catch {
+      try {
+        if (canIUse('SystemCapability.FileManagement.AppFileService.FolderAuthorization')) {
+          uri = FileUtil.getUriFromPath(uri)
+          console.log('onecold 激活权限 activatePermission uri = '+uri);
+          let policyInfo: fileShare.PolicyInfo = {
+            uri: uri,
+            operationMode: fileShare.OperationMode.READ_MODE | fileShare.OperationMode.WRITE_MODE,
+          };
+          let policies: Array<fileShare.PolicyInfo> = [policyInfo];
+          let results = await fileShare.checkPersistentPermission(policies);
+          for (let i = 0; i < results.length; i++) {
+            console.log('onecold activatePermission 激活权限成功');
+            if (results[i]) {
+              let info: fileShare.PolicyInfo = {
+                uri: policies[i].uri,
+                operationMode: policies[i].operationMode,
+              };
+              let policy: Array<fileShare.PolicyInfo> = [info];
+              await fileShare.activatePermission(policy);
+            }
+          }
+          let fd = fs.openSync(uri);
+          fs.closeSync(fd);
+        }
+        return true
+      } catch (error) {
+        console.error('onecold activatePermission error = '+JSON.stringify(error));
+        if (error.code == 13900001 ) {
+          await this.persistPermission(uri)
+
+        }
+        return false
+      }
+    }
+  }
+
+  //持久化权限
+  async persistPermission(uri: string): Promise<boolean> {
+    try {
+      if (canIUse('SystemCapability.FileManagement.AppFileService.FolderAuthorization')) {
+        let policyInfo: fileShare.PolicyInfo = {
+          uri: uri,
+          operationMode: fileShare.OperationMode.READ_MODE | fileShare.OperationMode.WRITE_MODE,
+        };
+        let policies: Array<fileShare.PolicyInfo> = [policyInfo];
+        fileShare.persistPermission(policies).then(() => {
+        })
+        let fd = await fs.open(uri);
+        await fs.close(fd);
+      }
+    } catch (error) {
+      return true
+    }
+    return false
+  }
+}
+
+export default new PermissionUtil();

+ 1 - 0
entry/src/main/ets/common/util/PlaylistTable.ets

@@ -593,6 +593,7 @@ export default class PlaylistTable {
    */
   async updatePlaylistSongSortOrder(playlistId: string, songFilePath: string, sortOrder: number): Promise<boolean> {
     if (!this.rdbStore) {
+      Logger.info('heanup PlaylistTable', `rdbStore未初始化: ${songFilePath}`);
       return false;
     }
 

+ 52 - 0
entry/src/main/ets/common/util/ReqPermissionUtil.ets

@@ -0,0 +1,52 @@
+import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
+import { fileShare, fileIo as fs, fileUri } from '@kit.CoreFileKit';
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { BusinessError } from '@kit.BasicServicesKit';
+
+// 重新设置权限类,用于每次启动重新设置权限
+class ReqPermission {
+  public permissions: Permissions[] = ['ohos.permission.FILE_ACCESS_PERSIST'];
+
+  // 重新申请权限
+  reqPermissionsFromUser(permissions: Permissions[], context: common.UIAbilityContext): void {
+    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
+    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
+      let grantStatus: number[] = data.authResults;
+      let length: number = grantStatus.length;
+      for (let i = 0; i < length; i++) {
+        if (grantStatus[i] === 0) {
+        } else {
+          return;
+        }
+      }
+    })
+  }
+  //持久化权限
+  async persistPermission(uri: string): Promise<boolean> {
+    try {
+      if (canIUse('SystemCapability.FileManagement.AppFileService.FolderAuthorization')) {
+        if(uri.startsWith('file://media/Photo')){
+          uri = new fileUri.FileUri(uri).path
+        }
+        console.info('onecold 持久化权限persistPermission uri : ', uri);
+        let policyInfo: fileShare.PolicyInfo = {
+          uri: uri,
+          operationMode: fileShare.OperationMode.READ_MODE | fileShare.OperationMode.WRITE_MODE,
+        };
+        let policies: Array<fileShare.PolicyInfo> = [policyInfo];
+        fileShare.persistPermission(policies).then(() => {
+          console.log("onecold 持久化权限persistPermission success");
+        }).catch((err: BusinessError<Array<fileShare.PolicyErrorResult>>) => {
+          console.log("onecold persistPermission failed   err.message=" + JSON.stringify(err));
+        });
+        let fd = await fs.open(uri);
+        await fs.close(fd);
+      }
+    } catch (error) {
+      return true
+    }
+    return false
+  }
+}
+
+export default new ReqPermission()

+ 136 - 0
entry/src/main/ets/common/util/UserUtil.ets

@@ -8,6 +8,7 @@ import { hilog } from "@kit.PerformanceAnalysisKit";
 import { FileUtil } from "@pura/harmony-utils";
 import { fileUri, picker } from "@kit.CoreFileKit";
 import { VipPage } from "../../pages/VipPage";
+import { MD5 } from '@pura/harmony-utils';
 
 // 用户相关接口定义
 export interface WechatUserInfo {
@@ -137,6 +138,25 @@ export interface UserInfoApiResponse {
   data: UserInfoApiData;
 }
 
+// 用户注销接口响应数据
+export interface DeleteAccountData {
+  message: string;
+}
+
+// 用户注销接口响应
+export interface DeleteAccountResponse {
+  code: number;
+  msg: string;
+  data: DeleteAccountData;
+}
+
+// 用户注销请求参数
+export interface DeleteAccountRequest {
+  token: string;
+  timestamp: number;
+  sign: string;
+}
+
 export default class UserUtil {
   // 微信是否安装
   static async isHasWX(): Promise<boolean> {
@@ -457,4 +477,120 @@ export default class UserUtil {
       return false;
     }
   }
+
+  /**
+   * 用户注销
+   * 删除用户账户并进行匿名化处理
+   * @param secretKey 服务端提供的密钥
+   * @returns Promise<boolean> 注销是否成功
+   */
+  static async deleteAccount(): Promise<boolean> {
+    try {
+      const token = PreferencesUtil.getStringSync('userToken', '');
+      if (!token) {
+        ToastUtil.showToast('用户未登录');
+        return false;
+      }
+
+      // 获取当前时间戳(秒)
+      const timestamp = Math.floor(Date.now() / 1000);
+
+      // 签名密钥,与服务端保持一致
+      const SIGN_KEY = 'pay_ss5_xyz_delete_account_sign_key_2023';
+      
+      // 将token、时间戳和密钥拼接
+      const data = token + timestamp.toString() + SIGN_KEY;
+      
+      // 生成MD5签名
+      const sign = await MD5.digestSync(data);
+
+      LogUtil.debug("UserUtil", `注销请求参数: token=${token.substring(0, 10)}..., timestamp=${timestamp}, sign=${sign.substring(0, 10)}...`);
+
+      // 构造请求参数
+      const requestParams: DeleteAccountRequest = {
+        token: token,
+        timestamp: timestamp,
+        sign: sign
+      };
+
+      // 发送POST请求
+      const httpRequest = http.createHttp();
+      const options: http.HttpRequestOptions = {
+        method: http.RequestMethod.POST,
+        readTimeout: 10000,
+        connectTimeout: 10000,
+        header: {
+          'Content-Type': 'application/json'
+        },
+        extraData: JSON.stringify(requestParams)
+      };
+
+      const response: http.HttpResponse = await httpRequest.request('https://pay.ss5.xyz/user/delete_account', options);
+
+      if (response.responseCode === 200) {
+        const res = response.result as string;
+        LogUtil.debug("UserUtil", `注销响应: ${res}`);
+
+        const responseData: DeleteAccountResponse = JSON.parse(res) as DeleteAccountResponse;
+
+        if (responseData.code === 0) {
+          ToastUtil.showToast('账户注销成功');
+          LogUtil.info("UserUtil", '账户注销成功,开始清除本地数据');
+
+          // 清除本地登录状态和用户信息
+          UserUtil.logout();
+
+          // 额外清除一些可能的数据
+          PreferencesUtil.deleteSync('isNoble');
+          PreferencesUtil.deleteSync('nobleExpireDate');
+
+          return true;
+        } else {
+          // 处理具体的错误码
+          let errorMessage = '注销失败';
+          switch (responseData.code) {
+            case 40001:
+              errorMessage = 'token不能为空';
+              break;
+            case 40002:
+            case 40003:
+              errorMessage = 'token无效或已过期';
+              break;
+            case 40004:
+              errorMessage = '用户不存在';
+              break;
+            case 40005:
+              errorMessage = '时间戳不能为空';
+              break;
+            case 40006:
+              errorMessage = '请求已过期,请重新发起';
+              break;
+            case 40007:
+              errorMessage = '签名不能为空';
+              break;
+            case 40008:
+              errorMessage = '签名验证失败';
+              break;
+            case 50001:
+              errorMessage = '系统错误';
+              break;
+            default:
+              errorMessage = responseData.msg || '未知错误';
+          }
+          ToastUtil.showToast(errorMessage);
+          LogUtil.error("UserUtil", `账户注销失败: ${errorMessage}, 错误码: ${responseData.code}`);
+          return false;
+        }
+      } else {
+        ToastUtil.showToast('注销请求失败');
+        LogUtil.error("UserUtil", `注销请求失败,HTTP状态码: ${response.responseCode}`);
+        return false;
+      }
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '注销异常';
+      ToastUtil.showToast('注销异常:' + errorMessage);
+      LogUtil.error("UserUtil", `账户注销异常: ${errorMessage}`);
+      return false;
+    }
+  }
 }

+ 4 - 0
entry/src/main/ets/common/util/Utility.ets

@@ -24,6 +24,7 @@ import { pinyin4js } from '@ohos/pinyin4js';
 import { VipData } from '../../viewmodel/VipData';
 import { VipPage } from '../../pages/VipPage';
 import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
+import ReqPermissionUtil from './ReqPermissionUtil';
 
 
 export interface FFMpegTags {
@@ -649,6 +650,7 @@ export class Utility {
 
   //获取音乐资源的属性值,
   static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number,autoParseMusicName?:boolean): Promise<VideoItem> {
+    await ReqPermissionUtil.persistPermission(uri);
     if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
       return  new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
     }
@@ -832,6 +834,7 @@ export class Utility {
 
   static async  readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,autoParseMusicName?:boolean): Promise<VideoItem> {
     return new Promise((resolve, reject) => {
+      inputPath = FileUtil.getFilePath(inputPath)
       let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
       let outputJson = "";
 
@@ -1907,6 +1910,7 @@ async function extractLyricsContent(inputPath: string): Promise<string> {
  */
 async function parseAudioMetadata(inputPath: string): Promise<VideoItem> {
   return new Promise(async (resolve, reject) => {
+    inputPath = FileUtil.getFilePath(inputPath)
     try {
       // 1. 执行FFprobe命令
       const commands: string[] = [

+ 3 - 2
entry/src/main/ets/dialog/AddSongsToPlaylistDialog.ets

@@ -260,9 +260,10 @@ struct AddSongsToPlaylistDialogContent {
         Button('取消')
           .width('45%')
           .height(40)
-          .backgroundColor($r('app.color.cancel_button_background'))
           .borderRadius(8)
           .fontSize(14)
+          .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode)
+            :$r('app.color.cancel_button_background') )
           .fontColor($r('app.color.cancel_button_text'))
           .onClick(() => {
             this.onCancel?.()
@@ -272,7 +273,7 @@ struct AddSongsToPlaylistDialogContent {
         Button(`添加${this.selectedSongs.length > 0 ? `(${this.selectedSongs.length})` : ''}`)
           .width('45%')
           .height(40)
-          .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode) : this.themeColor)
+          .backgroundColor(this.isDarkMode ? $r('app.color.silvery'):this.themeColor)
           .borderRadius(8)
           .fontSize(14)
           .fontColor(Color.White)

+ 5 - 10
entry/src/main/ets/dialog/AddToPlaylistDialog.ets

@@ -47,18 +47,12 @@ struct AddToPlaylistDialogContent {
 
   build() {
     Column({ space: 16 }) {
-      // 标题
-      Text('添加到歌单')
-        .fontSize(18)
-        .fontWeight(FontWeight.Bold)
-        .fontColor($r('app.color.text_color'))
-        .margin({ top: 20 })
 
       // 歌曲信息
       if (this.currentSong) {
         Row({ space: 12 }) {
           // 封面
-          Image(this.currentSong.pixelMap || $r('app.media.icon'))
+          Image(this.currentSong.pixelMapPath || $r('app.media.icon'))
             .width(48)
             .height(48)
             .borderRadius(8)
@@ -196,7 +190,8 @@ struct AddToPlaylistDialogContent {
         Button('取消')
           .width('45%')
           .height(40)
-          .backgroundColor($r('app.color.cancel_button_background'))
+          .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode)
+            :$r('app.color.cancel_button_background') )
           .borderRadius(8)
           .fontSize(14)
           .fontColor($r('app.color.cancel_button_text'))
@@ -208,7 +203,7 @@ struct AddToPlaylistDialogContent {
         Button('添加')
           .width('45%')
           .height(40)
-          .backgroundColor(this.isDarkMode ? '#000000' : this.themeColor)
+          .backgroundColor(this.isDarkMode ? $r('app.color.silvery'):this.themeColor)
           .borderRadius(8)
           .fontSize(14)
           .fontColor(Color.White)
@@ -222,7 +217,7 @@ struct AddToPlaylistDialogContent {
       .justifyContent(FlexAlign.SpaceBetween)
       .margin({ top: 20, bottom: 20 })
     }
-    .width('90%')
+    .width('100%')
     .constraintSize({ maxWidth: 400 })
     .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.dialog_background'))
     .borderRadius(12)

+ 2 - 1
entry/src/main/ets/dialog/PlaylistDialog.ets

@@ -206,7 +206,8 @@ struct PlaylistDialogContent {
         Button('取消', { type: ButtonType.Capsule, stateEffect: true })
           .width('45%')
           .height(40)
-          .backgroundColor($r('app.color.cancel_button_background'))
+          .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode)
+            :$r('app.color.cancel_button_background') )
           .borderRadius(8)
           .fontSize(14)
           .fontColor($r('app.color.cancel_button_text'))

+ 3 - 3
entry/src/main/ets/dialog/WebDavAccountDialog.ets

@@ -26,11 +26,11 @@ export struct WebDavAccountDialog {
   onConfirm?: (account: WebDavAccount) => void;
   onCancel?: () => void;
   @State accountName: string = 'demo';
-  @State host: string = 'myhome.ss5.xyz';
+  @State host: string = '';
   @State port: number = 5005;
   @State filepath: string = '/';
-  @State username: string = 'chendeben';
-  @State password: string = 'chen384626WYT';
+  @State username: string = '';
+  @State password: string = '';
   @State enableHttps: boolean = false;
   @State coverPath: string = '';
   @State isDarkMode: boolean = false;

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

@@ -48,6 +48,7 @@ import { WebDavMainPage } from './WebDavMainPage';
 import { WebDavAccount } from '../viewmodel/WebDavAccount';
 import { WebdavManager } from '../common/util/WebdavManager';
 import { WebDavAccountDialog } from '../dialog/WebDavAccountDialog';
+import ReqPermissionUtil from '../common/util/ReqPermissionUtil';
 
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
@@ -245,6 +246,7 @@ struct NewIndex {
    */
 
   async aboutToAppear() {
+    ReqPermissionUtil.reqPermissionsFromUser(ReqPermissionUtil.permissions, this.context);
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
     this.idDefaultMediaKu = PreferencesUtil.getBooleanSync('idDefaultMediaKu', false)
     if(this.idDefaultMediaKu){
@@ -1680,4 +1682,3 @@ interface HiCarAspectRatio {
   playlistId: string;
 }
 
-

+ 34 - 6
entry/src/main/ets/pages/PlaylistDetailPage.ets

@@ -743,22 +743,22 @@ export struct PlaylistDetailPage {
               Row({ space: 6 }) {
                 Text('🗑️')
                   .fontSize(16)
-
+                  .fontColor(this.isDarkMode ? Color.White : this.themeColor)
                 Text('删除')
                   .fontSize(14)
-                  .fontColor(Color.Red)
+                  .fontColor(this.isDarkMode ? Color.White : this.themeColor)
                   .fontWeight(FontWeight.Medium)
               }
               .justifyContent(FlexAlign.Center)
             }
             .layoutWeight(1)
             .height(44)
-            .backgroundColor(Color.Transparent)
-            .borderRadius(22)
             .border({
               width: 1.5,
-              color: Color.Red
+              color: this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.6, this.isDarkMode) : this.themeColor
             })
+            .backgroundColor(Color.Transparent)
+            .borderRadius(22)
             .onClick(() => {
               this.deletePlaylist()
             })
@@ -1400,7 +1400,8 @@ export struct PlaylistDetailPage {
           LogUtil.error(`heanup 更新歌曲[${i}] ${song.name} sortOrder失败`)
         }
       }
-
+      // 发送刷新事件
+      emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH_SORT }, {})
       LogUtil.info('heanup 所有歌曲sortOrder更新完成')
     } catch (error) {
       LogUtil.error('heanup 更新歌曲sortOrder失败: ' + error)
@@ -1512,5 +1513,32 @@ export function emptyView(themeColor: string, isDarkMode: boolean = false) {
 }
 
 
+export  async function  updateAllSongsSortOrder(playlistTable:PlaylistTable,songList:VideoItem[], playlistId:string) {
+  try {
+    LogUtil.info('heanup 开始更新所有歌曲的sortOrder playlistId='+playlistId)
+    // const playlistTable: PlaylistTable = new PlaylistTable(context)
+    for (let i = 0; i < songList.length; i++) {
+      const song = songList[i]
+      const success = await playlistTable.updatePlaylistSongSortOrder(
+        playlistId,
+        song.filePath,
+        i
+      )
+
+      if (success) {
+        LogUtil.info(`heanup 更新歌曲[${i}] ${song.name} sortOrder成功`)
+      } else {
+        LogUtil.error(`heanup 更新歌曲[${i}] ${song.name} sortOrder失败`)
+      }
+    }
+    // 发送刷新事件
+    emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH_SORT }, {})
+    LogUtil.info('heanup 所有歌曲sortOrder更新完成')
+  } catch (error) {
+    LogUtil.error('heanup 更新歌曲sortOrder失败: ' + error)
+  }
+}
+
+
 
 

+ 34 - 8
entry/src/main/ets/pages/SplashIndex.ets

@@ -14,7 +14,7 @@ import { ConfigManager } from '../common/util/ConfigManager'
 import { UIUtil } from '../common/util/UIUtil'
 import { PrintBiddingTokenUtils } from '../common/util/PrintBiddintTokenUtils'
 import { DemoConstants } from '../entryability/DemoConstants'
-import { AppUtil, Base64Util, FileUtil, LogUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'
+import { AppUtil, Base64Util, FileUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'
 import { Utility } from '../common/util/Utility'
 import { VideoItem } from '../viewmodel/VideoItem'
 import { fileIo, fileUri, picker, ReadTextOptions } from '@kit.CoreFileKit'
@@ -131,9 +131,7 @@ struct  SplashIndex{
 
   @State rootPath:string = ''
   @State currentPath:string = ''
-  @State videoLocalList: Array<VideoItem> = []
-  @State  fileList: Array<String> = []
-  @State  dirList: Array<VideoItem> = []
+  @State videoLocalList: VideoItem[] = []
   @State expireDate:string = ''
   async mkDownLoadDir(){
 
@@ -142,9 +140,12 @@ struct  SplashIndex{
     let download_path = new fileUri.FileUri(documentSaveResult[0]).path
     this.rootPath = download_path
     let vPath = download_path + '/'+ STR_LOCK_VIDEO+ '/' + VIP_FILEPATH
+
     this.expireDate =  this.readDataFromFile(vPath)
     Utility.setNoble(this.expireDate)
-    this.doMain()
+    // 载入首页文件夹的缓存
+    this.loadCacheFromStorage(this.rootPath)
+
 
 
   }
@@ -209,6 +210,30 @@ struct  SplashIndex{
 
   }
 
+  // 缓存机制
+  private cache: Map<string, Array<VideoItem>> = new Map();
+  //查找缓存
+  private findCache(key: string): Array<VideoItem> | undefined {
+    return this.cache.get(key);
+  }
+  // 载入首页文件夹的缓存
+  async loadCacheFromStorage(curPath:string) {
+    const cacheString = await PreferencesUtil.getStringSync('music_cache', '');
+    if (cacheString) {
+      const cacheArray: [string, Array<VideoItem>][] = JSON.parse(cacheString);
+      this.cache = new Map(cacheArray);
+    }
+
+    const cachedValue = this.findCache(curPath);
+    // 合并文件夹和文件,文件夹在前,文件在后
+    if(cachedValue)
+      this.videoLocalList =  cachedValue;
+    console.log(" onecold 欢迎页面 this.videoLocalList.length = " + this.videoLocalList.length);
+    this.doMain()
+  }
+
+
+
   //初始化SDK
   async initCSJSDK(){
     await this.initAppConfig();
@@ -353,8 +378,8 @@ struct  SplashIndex{
 
 
   junpToMain(){
-    console.info('onecold SplashIndex junpToMain ' )
-    router.replace({
+    console.info('onecold SplashIndex junpToMain length= '+this.videoLocalList.length )
+    this.getUIContext().getRouter().replaceUrl({
       url: 'pages/NewIndex',
       params: {
         videoList: this.videoLocalList
@@ -423,4 +448,5 @@ struct  SplashIndex{
 
 
   }
-}
+}
+

+ 204 - 0
entry/src/main/ets/pages/UserCenter.ets

@@ -22,6 +22,7 @@ import json from '@ohos.util.json';
 import UserUtil, { VipFeature, VipPlanApi, VipPlanListApiResponse, WeChatPrepayId } from '../common/util/UserUtil';
 import { Utility } from '../common/util/Utility';
 import { pinyin4js } from '@ohos/pinyin4js';
+import { CustomContentDialog } from '@kit.ArkUI';
 
 
 // 微信支付相关工具方法
@@ -196,9 +197,36 @@ export struct UserCenter {
   @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
     ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
   @State showWxLogin: boolean = false;
+  @State appIdentifier: string = '';
   private wxApi = WXApi
   private wxEventHandler = WXEventHandler
 
+  // 注销功能相关
+  deleteAccountDialogController: CustomDialogController = new CustomDialogController({
+    builder: CustomContentDialog({
+      primaryTitle: '注销账户',
+      contentBuilder: () => {
+        this.deleteAccountDialogContent();
+      },
+      buttons: [
+        {
+          value: '取消',
+          action: () => {
+            this.deleteAccountDialogController.close();
+          }
+        },
+        {
+          value: '确认注销',
+          action: () => {
+            this.handleDeleteAccount();
+          }
+        }
+      ]
+    }),
+    autoCancel: true,
+    alignment: DialogAlignment.Center
+  })
+
   constructor() {
     super();
   }
@@ -252,6 +280,7 @@ export struct UserCenter {
     Utility.getAppName(getContext(this)).then((appName:string)=>{
       this.appName = appName
     })
+    this.getAppIdentifier(); // 获取应用标识符
     this.topRectHeight = px2vp(AppUtil.getStatusBarHeight());
     let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
     this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
@@ -451,6 +480,11 @@ export struct UserCenter {
           }
           this.buildVipFeatures()
 
+          // 注销功能区域(仅在登录时显示)
+          if (this.isLogin) {
+            this.buildDeleteAccountSection()
+          }
+
           // this.buildFunctionMenu()
         }
         .width('100%')
@@ -731,6 +765,74 @@ export struct UserCenter {
     })
   }
 
+  // 注销功能区域
+  @Builder
+  buildDeleteAccountSection(): void {
+    Column() {
+      Row() {
+        SymbolGlyph($r('sys.symbol.exclamationmark_triangle_fill'))
+          .fontSize(24)
+          .fontColor([Color.Red])
+          .margin({ left: 16 })
+        Text('危险操作')
+          .fontSize(18)
+          .fontWeight(FontWeight.Bold)
+          .fontColor(Color.Red)
+          .margin({ left: 12 })
+          .layoutWeight(1)
+      }
+      .width('100%')
+      .margin({ bottom: 16 })
+
+      Button() {
+        Row() {
+          SymbolGlyph($r('sys.symbol.trash'))
+            .fontSize(20)
+            .fontColor([Color.Red])
+            .alignSelf(ItemAlign.Center)
+            .margin({ left: 12 })
+          Text('注销账户')
+            .margin({ left: 8 })
+            .fontSize(15)
+            .fontColor(Color.Red)
+            .fontWeight(FontWeight.Medium)
+            .layoutWeight(1)
+          Image($r('app.media.arrow_right'))
+            .width(22)
+            .height(22)
+            .margin({ right: 16 })
+            .fillColor([Color.Red])
+        }
+      }
+      .backgroundColor(Color.Transparent)
+      .height(55)
+      .width('100%')
+      .clickEffect({ level: ClickEffectLevel.HEAVY })
+      .onClick(() => {
+        this.deleteAccountDialogController.open();
+      })
+    }
+    .width('100%')
+    .backgroundColor($r('app.color.user_center_card_background'))
+    .borderRadius(24)
+    .margin({
+      left: 16,
+      right: 16,
+      top: 10,
+      bottom: 20
+    })
+    .padding({
+      top: 20,
+      bottom: 20
+    })
+    .shadow({
+      radius: 8,
+      color: 0x11000000,
+      offsetX: 0,
+      offsetY: 2
+    })
+  }
+
   // 支付方式选择弹窗内容
   @Builder
   payDialogContentBuilder(): void {
@@ -1216,4 +1318,106 @@ export struct UserCenter {
       }
     }
   }
+
+  // 获取应用标识符
+  getAppIdentifier() {
+    let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION |
+    bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_SIGNATURE_INFO;
+    try {
+      bundleManager.getBundleInfoForSelf(bundleFlags).then((data) => {
+        hilog.info(0x0000, 'UserCenter', 'getBundleInfoForSelf successfully. Data: %{public}s', JSON.stringify(data));
+        let inde = JSON.stringify(data)
+        this.appIdentifier = inde.substring(inde.indexOf('appIdentifier'),inde.indexOf('"certificate'))
+        hilog.info(0x0000, 'UserCenter', 'appIdentifier   Data: %{public}s', this.appIdentifier);
+      }).catch((err: BusinessError) => {
+        hilog.error(0x0000, 'UserCenter', 'getBundleInfoForSelf failed. Cause: %{public}s', err.message);
+      });
+    } catch (err) {
+      let message = (err as BusinessError).message;
+      hilog.error(0x0000, 'UserCenter', 'getBundleInfoForSelf failed: %{public}s', message);
+    }
+  }
+
+  @Builder
+  deleteAccountDialogContent() {
+    Column() {
+      Text('注销账户后将无法恢复,所有相关数据将被永久删除。')
+        .fontSize(16)
+        .fontColor(Color.Red)
+        .textAlign(TextAlign.Start)
+        .width('100%')
+        .margin({ bottom: 20 })
+
+      // Text('包括但不限于:')
+      //   .fontSize(14)
+      //   .fontColor(Color.Gray)
+      //   .textAlign(TextAlign.Start)
+      //   .width('100%')
+      //   .margin({ bottom: 10 })
+      //
+      // Text('• 个人信息和设置(含订阅套餐)')
+      //   .fontSize(13)
+      //   .fontColor(Color.Gray)
+      //   .textAlign(TextAlign.Start)
+      //   .width('100%')
+      //   .margin({ left: 20, bottom: 5 })
+      //
+      // Text('• 收藏和播放列表')
+      //   .fontSize(13)
+      //   .fontColor(Color.Gray)
+      //   .textAlign(TextAlign.Start)
+      //   .width('100%')
+      //   .margin({ left: 20, bottom: 5 })
+      //
+      // Text('• 播放历史记录')
+      //   .fontSize(13)
+      //   .fontColor(Color.Gray)
+      //   .textAlign(TextAlign.Start)
+      //   .width('100%')
+      //   .margin({ left: 20, bottom: 15 })
+
+      Text('请确认您已了解此操作的后果。')
+        .fontSize(14)
+        .fontColor($r('app.color.text_color'))
+        .textAlign(TextAlign.Start)
+        .width('100%')
+    }
+    .width('100%')
+    .padding(20)
+  }
+
+  async handleDeleteAccount() {
+    try {
+
+      // 显示加载提示
+      ToastUtil.showToast('正在处理注销请求...');
+
+      // 调用注销API
+      const success = await UserUtil.deleteAccount();
+
+      if (success) {
+        // 更新页面状态
+        this.isLogin = false
+        this.userName = '未登录用户'
+        this.userAvatar = $r('app.media.icon_person2')
+        this.userAvatarUrl = ''
+        this.isVip = false
+        this.vipExpire = ''
+        this.hasActiveSubscription = false;
+        this.subscriptionName = '';
+        this.subscriptionEndDate = '';
+        this.isForever=false;
+
+
+        ToastUtil.showToast('账户已成功注销');
+        this.deleteAccountDialogController.close();
+
+        // 发送用户状态变更事件
+        emitter.emit({ eventId: EventConstants.EVENT_USER_STATE_CHANGE }, {})
+      }
+    } catch (error) {
+      hilog.error(0x0000, 'UserCenter', '注销账户失败: %{public}s', error);
+      ToastUtil.showToast('注销失败,请稍后重试');
+    }
+  }
 }

+ 1 - 1
entry/src/main/ets/view/DeleteComptent.ets

@@ -8,6 +8,7 @@ import { EventConstants } from '../common/constants/EventConstants';
 import { emitter } from '@kit.BasicServicesKit';
 import { taskpool } from '@kit.ArkTS';
 
+
 // 批量删除
 @Component
 export struct DeleteComptent {
@@ -172,7 +173,6 @@ export struct DeleteComptent {
       }else {
         this.onDeleteResult(false)
       }
-
     }).catch((error: Error) => {
       console.error('Subtitle sync failed:', error);
     });

+ 101 - 15
entry/src/main/ets/view/LocalMusic.ets

@@ -89,7 +89,7 @@ import { CueComptent } from '../view/CueComptent';
 import PlaylistTable from '../common/util/PlaylistTable';
 import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
 import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
-import { convertPlaylistSongsToVideoItems, emptyView } from '../pages/PlaylistDetailPage';
+import { convertPlaylistSongsToVideoItems, emptyView,updateAllSongsSortOrder } from '../pages/PlaylistDetailPage';
 import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
 import { WebdavManager, WebDavAuthItem} from '../common/util/WebdavManager';
 import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
@@ -204,6 +204,7 @@ const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度
 @Preview
 @Component
 export struct LocalMusic {
+  @State isCopyFileToDownLoad: boolean = false
   @State offHeight: number = 75
   @State currentSongList: Array<VideoItem> = []//当前歌单
   @Consume currentSongListName:string //当前歌单名称
@@ -247,7 +248,7 @@ export struct LocalMusic {
   @State isZero: boolean = false
   @State fileList: Array<string> = []
   @State dirList: Array<VideoItem> = []
-  @Consume videoLocalList: Array<VideoItem>
+  @Consume videoLocalList:VideoItem[]
   @State dataSource: LazyDataSource<VideoItem> = new LazyDataSource(this.videoLocalList)
   // @State mediaKuList: Array<VideoItem> = []; //媒体库文件
   @StorageProp('mediaKuList')  mediaKuList: Array<VideoItem> = []; //媒体库文件
@@ -407,9 +408,8 @@ export struct LocalMusic {
       } else {
         PreferencesUtil.putSync(this.currentSongListID+'currentSongList', JSON.stringify(this.currentSongList));
       }
-
-
     }
+    this.isRefreshing = false
   }
 
   onModeChange() {
@@ -640,6 +640,9 @@ export struct LocalMusic {
   }
   // 组件生命周期
   aboutToAppear() {
+    if(ArrayUtil.isNotEmpty(this.videoLocalList)&&this.modeType==0){
+      this.updateListData(this.videoLocalList)
+    }
     // 初始化 PlaylistTable
     this.playlistTable = new PlaylistTable(getContext(this))
 
@@ -723,6 +726,15 @@ export struct LocalMusic {
       Logger.error('heanup eventPlaylistPlay: 接收到无效的数据结构')
     });
 
+    let eventRefreshSort: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_REFRESH_SORT }
+    // 监听广播事件(通用设置配置更新)
+    emitter.on(eventRefreshSort, (eventData: emitter.EventData) => {
+      if(this.modeType ==4){
+        this.doSongListTask()//获取歌单信息
+      }
+
+    });
+
     let eventSetting: emitter.InnerEvent = { eventId: EventConstants.EVENT_SETTING_UPDATE }
     // 监听广播事件(通用设置配置更新)
     emitter.on(eventSetting, (eventData: emitter.EventData) => {
@@ -904,6 +916,7 @@ export struct LocalMusic {
     this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
     this.fastForwardSeconds  = PreferencesUtil.getStringSync('fastForwardSeconds', '10')
     this.isShowBackFast  = PreferencesUtil.getBooleanSync('isShowBackFast', false)
+    this.isCopyFileToDownLoad = PreferencesUtil.getBooleanSync(SettingPage.IS_COPYFILE_TO_DOWNLOAD, false)
     if(this.volumeSmall){
       this.volume = 0.5
     }else{
@@ -1405,7 +1418,7 @@ export struct LocalMusic {
   updateListData(mList: Array<VideoItem>, noSort?: boolean) {
 
     animateTo({ duration: 666 }, () => {
-      this.opacityItem = 0;
+      this.opacityItem = 0.5;
     });
     setTimeout(() => {
       this.videoLocalList = mList;
@@ -1844,8 +1857,76 @@ export struct LocalMusic {
   }
 
   @State progress: number = 0;
+  async saveVideoDatas(uris:string[],isOpen?:boolean){
+    if (ArrayUtil.isEmpty(uris))  {
+      return;
+    }
+
+    console.info('onecold this.isCopyFileToDownLoad = ' + this.isCopyFileToDownLoad);
+    if(this.isCopyFileToDownLoad||this.currentPath==this.lockPath){
+      this.saveVideoDatasToDownLoad(uris,isOpen)
+      return
+    }
+
+
+    // 初始化进度条
+    this.progress  = 0;
+    DialogHelper.showLoadingProgress({
+      progress: this.progress,
+      backCancel: false,
+      autoCancel: false,
+      loadColor: $r('app.color.title_bar_bg'),
+      fontColor: $r('app.color.title_bar_bg')
+    });
+
+    // 计算处理总数用于进度计算
+    const totalItems = uris.length;
+    let processedItems = 0;
+
+    for (let i = 0; i < uris.length;  i++) {
+      let filePath = uris[i];
+      try {
+        if (this.currentPath  !== this.lockPath)  { // 判断不是私密音乐,才入库
+          if (!filePath.endsWith('.lrc')  && Utility.isMeidaByExtension(filePath)  && !filePath.endsWith('.srt'))  {
+            let mediaItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context,
+              filePath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName);
+            console.info('onecold  saveVideoDatas filePath = ' + filePath)
+            if (!filePath.includes(this.rootPath))  {
+              console.info('onecold  saveVideoDatas mediaItem.parentPath111  = ' + mediaItem.parentPath)
+              mediaItem.parentPath  = this.currentPath;
+            }
+            console.info('onecold  saveVideoDatas mediaItem.parentPath  = ' + mediaItem.parentPath)
+            this.table.insert(mediaItem,  (id: number) => {
+              // 插入完成回调
+            });
+          }
+        }
+
+        // 更新进度
+        processedItems++;
+        this.progress  = Math.floor((processedItems  / totalItems) * 100);
+        DialogHelper.updateLoading(' 正在处理', this.progress);
+
+      } catch (error) {
+        Logger.error(TAG,  'saveVideoDatas failed with err: ' + JSON.stringify(error));
+        // 即使出错也更新进度
+        processedItems++;
+        this.progress  = Math.floor((processedItems  / totalItems) * 100);
+        DialogHelper.updateLoading(' 正在处理', this.progress);
+      }
+    }
+    // 关闭进度条
+    DialogHelper.closeLoading();
+    this.isZero = false
+    // 删除目标路径缓存
+    setTimeout(() => {
+      this.cache.delete(this.currentPath);
+      this.getSortedFiles(this.currentPath,false,uris[0],isOpen)
+      this.setButtonStatus()
+    }, 500);
 
-  async saveVideoDatas(uris: string[], isOpen?: boolean) {
+  }
+  async saveVideoDatasToDownLoad(uris: string[], isOpen?: boolean) {
     if (ArrayUtil.isEmpty(uris)) {
       return;
     }
@@ -3852,13 +3933,16 @@ export struct LocalMusic {
     }
   }
 
-  itemMove(index: number, newIndex: number): void {
+   itemMove(index: number, newIndex: number): void {
     if (newIndex < 0 || newIndex >= this.videoLocalList.length) {
       return;
     }
     let tmp = this.videoLocalList.splice(index, 1);
     this.videoLocalList.splice(newIndex, 0, tmp[0]);
     this.dataSource.pushArrayData(this.videoLocalList)
+    if(this.modeType==4&&this.playlistTable){
+       updateAllSongsSortOrder(this.playlistTable, this.videoLocalList,this.currentSongListID)
+    }
   }
 
 
@@ -7343,7 +7427,7 @@ export struct LocalMusic {
 
   }
 
-  itemMoveSon(index: number, newIndex: number): void {
+   itemMoveSon(index: number, newIndex: number): void {
     if (newIndex < 0 || newIndex >= this.songList.length) {
       return;
     }
@@ -7351,6 +7435,9 @@ export struct LocalMusic {
     this.songList.splice(newIndex, 0, tmp[0]);
     this.sonDataSource.pushArrayData(this.songList)
     this.curIndex = Utility.getIndexFromList(this.songList, this.videoUrl)
+    if(this.modeType==4&&this.playlistTable){
+       updateAllSongsSortOrder(this.playlistTable, this.songList,this.currentSongListID)
+    }
   }
   @Builder
   PlayList() {
@@ -8649,8 +8736,8 @@ export struct LocalMusic {
       Stack() {
 
         Image(StrUtil.isEmpty(this.cover) ? this.imageLabel : this.cover)
-          .height(62)
-          .width(62)
+          .height(58)
+          .width(58)
           .alt(this.imageLabel)
           .borderRadius(8)
           .clickEffect({ level: ClickEffectLevel.HEAVY })
@@ -8671,14 +8758,14 @@ export struct LocalMusic {
             .maxLines(1)
             .fontWeight(FontWeight.Bolder)
             .fontColor(this.currentLyricColor)
-            .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 })
+            .margin({ left: this.currentLyricAlignMode === 0 ? 24 : 0 })
           Row() {
             Text(this.currentSong?.artist !== undefined ? this.currentSong?.artist : '')
               .fontSize(13)
               .fontWeight(FontWeight.Bold)
               .padding({ top: 8 })
               .fontColor(this.currentLyricColor)
-              .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 })
+              .margin({ left: this.currentLyricAlignMode === 0 ? 24 : 0 })
             Blank()
 
           }
@@ -9488,7 +9575,7 @@ export struct LocalMusic {
         .width('99%')
         .textAlign(TextAlign.Center)
         .fontColor(Color.White)
-      Text(this.artist + '  ' + this.currentSong?.album)
+      Text(this.artist + '  ' + this.currentSong?.album||'')
         .fontSize(this.isCoverOpacity() ? 12 : 15)
         .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
         .fontColor(Color.White)
@@ -11011,8 +11098,7 @@ export struct LocalMusic {
         this.isShowMoreView = false
         break;
       case 20://AB循环
-        this.isOpenAB = !this.isOpenAB;
-        this.isABSheet = false
+        this.isABSheet =  !this.isABSheet
         break;
     }
 

+ 8 - 0
entry/src/main/ets/viewmodel/ParamInfo.ets

@@ -0,0 +1,8 @@
+import { VideoItem } from "./VideoItem"
+
+export  class ParamInfo {
+  videoList: VideoItem[]
+  constructor(videoList: VideoItem[]){
+    this.videoList = videoList
+  }
+}

+ 2 - 1
entry/src/main/module.json5

@@ -8,7 +8,8 @@
     "deviceTypes": [
       "phone",
       "tablet",
-      "2in1"
+      "2in1",
+      "tv"
     ],
     "querySchemes": [//支付宝配置
       "https",

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

@@ -13,11 +13,12 @@ export class LyricLine {
      * @param beginTime The begin timestamp of this lyric line.
      * @param nextTime The begin timestamp of the next lyric line.
      */
-    constructor(text: string, beginTime: number, nextTime: number,words?: LyricWord[] ) {
+    constructor(text: string, beginTime: number, nextTime: number,words?: LyricWord[],translation?: string ) {
         this.text = text
         this.beginTime = beginTime
         this.nextTime = nextTime
         this.words = words || []
+        this.translation = translation || ''
     }
 
     // 新增方法:判断是否有逐字歌词

+ 37 - 25
lib/src/main/ets/parse/LyricParser.ts

@@ -69,6 +69,7 @@ export class LyricParser implements IParser {
 
                 // 处理双语歌词的特殊情况(英文行+中文行交替)
                 if (this.isBilingualLyric(src,  i)) {
+                    console.info(`onecold isBilingualLyric 双语歌词处理中`)
                     const englishLine = src[i];
                     const chineseLine = src[i+1];
 
@@ -96,10 +97,17 @@ export class LyricParser implements IParser {
 
                     // 新增:逐字歌词[]检测方括号逐字歌词格式 [mm:ss.xxx] 文字
                 if (this.isSquareBracketWordByWordLyric(line))  {
-                    const { timeline, words } = this.parseSquareBracketWordLine(line,  offset);
+                    // console.info(`onecold 普通歌词处理中`)
+                    const { timeline, words,translation  } = this.parseSquareBracketWordLine(line,  offset,src[i+1]);
+                    // if (words.length  > 0) {
+                    //     lyricLines.push(new  LyricLine('', timeline, -1, words))
+                    // }
                     if (words.length  > 0) {
-                        lyricLines.push(new  LyricLine('', timeline, -1, words))
+                        const lyricLine = new LyricLine('', timeline, -1, words, translation);
+                        lyricLines.push(lyricLine);
+                        if (translation) i++; // 跳过已处理的中文行
                     }
+                    continue;
                 } else {
                     // 原逻辑处理,但支持逐字歌词
                     // [00:00.10]画心 - 张靓颖
@@ -164,33 +172,19 @@ export class LyricParser implements IParser {
         return /\[\d{2}:\d{2}\.\d{2,3}\]\S/.test(line);
     }
 
-    // 解析方括号格式的逐字歌词行
-    private parseSquareBracketWordLine(line: string, offset: number):
-        { timeline: number, words: LyricWord[] } {
 
+
+    private parseSquareBracketWordLine(
+        line: string,
+        offset: number,
+        nextLine?: string
+    ): { timeline: number, words: LyricWord[], translation?: string } {
         const words: LyricWord[] = [];
         let firstTimeline = -1;
 
-        // 正则匹配:[00:00.000]中文字
-        // const regex = /\[(\d{2}:\d{2}\.\d{2,3})\]([^\[]*)/g;
-        // let match;
-        //
-        // while ((match = regex.exec(line))  !== null) {
-        //     const timeStr = match[1];   // 时间部分 00:00.000
-        //     const word = match[2].trim(); // 歌词文本
-        //
-        //     if (!word) continue; // 跳过空词
-        //
-        //     const timeline = this.parseTimeline2(timeStr)  - offset;
-        //     if (firstTimeline < 0) firstTimeline = timeline;
-        //
-        //     words.push(new  LyricWord(word, timeline, 0));
-        // }
-
-        // 正则增强:支持高精度时间戳(如 [02:34.54001])
+        // 解析逐字歌词(原有逻辑)
         const regex = /\[(\d{2}:\d{2}[.:]\d{2,5})\]([^\[]*)/g;
         let match;
-
         while ((match = regex.exec(line))  !== null) {
             const timeStr = match[1];
             const word = match[2].trim();
@@ -201,12 +195,30 @@ export class LyricParser implements IParser {
             words.push(new  LyricWord(word, timeline, 0));
         }
 
-        // 计算每个持续时间
+        // 计算词持续时间
         for (let i = 0; i < words.length  - 1; i++) {
             words[i].duration = words[i + 1].startTime - words[i].startTime;
         }
         if (words.length  > 0 && words[words.length - 1].duration === 0) {
-            words[words.length - 1].duration = 200; // 默认200ms
+            words[words.length - 1].duration = 200;
+        }
+
+        // 双语支持
+        if (nextLine && /^\[\d{2}:\d{2}\.\d{2,3}\]/.test(nextLine)) {
+            // 提取两行时间戳(需完全一致)
+            const currentLineTime = line.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]/)?.[1];
+            const nextLineTime = nextLine.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]/)?.[1];
+
+            if (currentLineTime && nextLineTime && currentLineTime === nextLineTime) {
+                const chineseText = nextLine.replace(/^\[\d{2}:\d{2}\.\d{2,3}\]/,  '').trim();
+                if (chineseText) { // 非空文本才作为翻译
+                    return {
+                        timeline: firstTimeline,
+                        words,
+                        translation: chineseText
+                    };
+                }
+            }
         }
 
         return { timeline: firstTimeline, words };

+ 36 - 17
lib/src/main/ets/view/LyricView2.ets

@@ -280,23 +280,42 @@ export struct LyricView2 {
     // 普通歌词渲染(原有逻辑)
     @Builder
     NormalLyricLine(item: LyricLine, index: number) {
-        Text(this.getTransverterText(item.text))
-            .fontSize(this.textSize)
-            .opacity(this.calculateOpacityFactor(index, this.currentIndex))
-            .blur(this.calculateBlurFactor(index, this.currentIndex))
-            .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
-            .scale({
-                x: index == this.currentIndex ? this.controller.getHighlightScale() : 1,
-                y: index == this.currentIndex ? this.controller.getHighlightScale() : 1,
-                centerX: this.alignMode == 'center' ? '50%' : 0
-            })
-            .fontColor(index == this.currentIndex ? this.textHighlightColor : this.textColor)
-            .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
-            .animation({ duration: this.animDuration, curve: Curve.FastOutSlowIn })
-            .visibility(this.isSingleLine?
-                (index == this.currentIndex ?Visibility.Visible:Visibility.None)
-                :Visibility.Visible)
-            .width(this.alignMode == 'center' ? '100%' : '76%')
+        Column(){
+            Text(item.text)
+                .fontSize(this.textSize)
+                .opacity(this.calculateOpacityFactor(index, this.currentIndex))
+                .blur(this.calculateBlurFactor(index, this.currentIndex))
+                .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
+                .scale({
+                    x: index == this.currentIndex ? this.controller.getHighlightScale() : 1,
+                    y: index == this.currentIndex ? this.controller.getHighlightScale() : 1,
+                    centerX: this.alignMode == 'center' ? '50%' : 0
+                })
+                .fontColor(index == this.currentIndex ? this.textHighlightColor : this.textColor)
+                .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
+                .animation({ duration: this.animDuration, curve: Curve.FastOutSlowIn })
+                .visibility(this.isSingleLine?
+                    (index == this.currentIndex ?Visibility.Visible:Visibility.None)
+                    :Visibility.Visible)
+                .width(this.alignMode == 'center' ? '100%' : '76%')
+
+            // 中文翻译(整行显示)
+            if (item.translation)  {
+                Row({ space: 0 }) {
+                    Text(item.translation)
+                        .fontSize(index == this.currentIndex ?this.textSize*this.controller.getHighlightScale():this.textSize)
+                        .fontColor(this.currentMediaPosition >= item.beginTime ?
+                            index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
+                        .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
+                        .margin({ top: 4 })
+                        .opacity(this.calculateOpacityFactor(index, this.currentIndex))
+                        .blur(this.calculateBlurFactor(index, this.currentIndex))
+                }
+                .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
+                .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
+            }
+        }
+
     }
 
     @Builder

+ 0 - 0
oh_modules/@pura/spinkit/consumer-rules.txt