Ver Fonte

新增封面

chendeben há 9 meses atrás
pai
commit
b552616cad

+ 38 - 3
entry/src/main/ets/common/util/ImagePickerUtil.ets

@@ -70,17 +70,18 @@ export class ImagePickerUtil {
    * 将图片复制到应用私有目录
    * @param context 应用上下文
    * @param imageUri 图片URI
+   * @param type 图片类型,默认为playlist,可选webdav
    * @returns 返回复制后的图片路径
    */
-  private static async copyImageToAppDir(context: Context, imageUri: string): Promise<string> {
+  private static async copyImageToAppDir(context: Context, imageUri: string, type: string = 'playlist'): Promise<string> {
     try {
       // 生成唯一文件名
       const timestamp = Date.now();
       const randomSuffix = Math.random().toString(36).substring(2, 8);
-      const fileName = `playlist_cover_${timestamp}_${randomSuffix}.jpg`;
+      const fileName = `${type}_cover_${timestamp}_${randomSuffix}.jpg`;
 
       // 目标路径
-      const targetDir = context.filesDir + FileUtil.separator + 'playlist_covers';
+      const targetDir = context.filesDir + FileUtil.separator + `${type}_covers`;
       const targetPath = targetDir + FileUtil.separator + fileName;
 
       // 确保目录存在
@@ -120,6 +121,40 @@ export class ImagePickerUtil {
     }
   }
 
+  /**
+   * 选择单张图片(WebDAV账户封面)
+   * @param context 应用上下文
+   * @returns 返回选择的图片路径,如果取消选择返回null
+   */
+  static async selectSingleWebDavCover(context: Context): Promise<string | null> {
+    try {
+      Logger.info(TAG, '开始选择WebDAV账户封面');
+
+      const photoSelectOptions: photoAccessHelper.PhotoSelectOptions = {
+        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
+        maxSelectNumber: 1
+      };
+
+      const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
+      const photoSelectResult = await photoViewPicker.select(photoSelectOptions);
+
+      Logger.info(TAG, `选择了 ${photoSelectResult.photoUris.length} 张图片`);
+
+      if (photoSelectResult.photoUris.length === 0) {
+        return null;
+      }
+
+      // 将选中的图片复制到应用私有目录
+      const uri = photoSelectResult.photoUris[0];
+      const copiedPath = await ImagePickerUtil.copyImageToAppDir(context, uri, 'webdav');
+
+      return copiedPath;
+    } catch (error) {
+      Logger.error(TAG, `选择WebDAV账户封面失败: ${(error as Error).message}`);
+      return null;
+    }
+  }
+
   /**
    * 删除歌单封面图片
    * @param imagePath 图片路径

+ 64 - 8
entry/src/main/ets/common/util/WebdavManager.ets

@@ -124,7 +124,7 @@ export class WebdavManager {
 
   // 创建WebDAV账户表
   public createWebDavTableInDB(): Promise<void> {
-    const sql = `CREATE TABLE IF NOT EXISTS ${this.webDavTable} (
+    const createTableSql = `CREATE TABLE IF NOT EXISTS ${this.webDavTable} (
       id INTEGER PRIMARY KEY AUTOINCREMENT,
       name TEXT,
       isActivate INTEGER DEFAULT 1,
@@ -138,12 +138,16 @@ export class WebdavManager {
       uploadFilePath TEXT,
       account TEXT,
       password TEXT,
-      enableHttps INTEGER DEFAULT 0
+      enableHttps INTEGER DEFAULT 0,
+      coverPath TEXT
     )`;
 
-    return this.dataBaseUtil.executeSql(sql)
+    return this.dataBaseUtil.executeSql(createTableSql)
       .then(() => {
         Logger.info(TAG, 'WebDAV账户表创建成功');
+
+        // 检查并添加新字段(用于数据库升级)
+        return this.upgradeWebDavTable();
       })
       .catch((err: Error) => {
         Logger.error(TAG, '创建WebDAV账户表失败:', err.message);
@@ -151,15 +155,34 @@ export class WebdavManager {
       });
   }
 
+  // 升级WebDAV表结构
+  private async upgradeWebDavTable(): Promise<void> {
+    try {
+      // 直接尝试添加coverPath字段,如果字段已存在会失败但不影响应用运行
+      Logger.info(TAG, '检查数据库表结构,尝试添加coverPath字段...');
+      const addColumnSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN coverPath TEXT`;
+
+      await this.dataBaseUtil.executeSql(addColumnSql);
+      Logger.info(TAG, 'coverPath字段添加成功,数据库升级完成');
+    } catch (error) {
+      // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
+      Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
+    }
+  }
+
   // 从数据库查询所有账户
   public async queryWebDavAccountsFromDB(): Promise<void> {
     try {
+      // 确保表结构是最新的
+      await this.upgradeWebDavTable();
+
       const predicates = new relationalStore.RdbPredicates(this.webDavTable);
-      const columns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
+      // 先查询基础字段(确保这些字段在旧版本中存在)
+      const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
         'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
         'account', 'password', 'enableHttps'];
 
-      const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, columns, predicates);
+      const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
 
       this.webDavAccounts = [];
       while (resultSet.goToNextRow()) {
@@ -178,11 +201,41 @@ export class WebdavManager {
         account.account = resultSet.getString(resultSet.getColumnIndex('account'));
         account.password = resultSet.getString(resultSet.getColumnIndex('password'));
         account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
+        // 设置coverPath为默认值undefined,稍后会尝试更新
+        account.coverPath = undefined;
 
         this.webDavAccounts.push(account);
       }
       resultSet.close();
 
+      // 尝试查询coverPath字段(如果升级成功)
+      try {
+        const coverPathResultSet = await this.dataBaseUtil.queryData(this.webDavTable, ['id', 'coverPath'], predicates);
+        if (coverPathResultSet.goToFirstRow()) {
+          // 创建一个映射来存储coverPath
+          const coverPathMap = new Map<number, string>();
+          do {
+            const accountId = coverPathResultSet.getLong(coverPathResultSet.getColumnIndex('id'));
+            const coverPathIndex = coverPathResultSet.getColumnIndex('coverPath');
+            const coverPath = coverPathIndex >= 0 ? coverPathResultSet.getString(coverPathIndex) : undefined;
+            if (coverPath) {
+              coverPathMap.set(accountId, coverPath);
+            }
+          } while (coverPathResultSet.goToNextRow());
+
+          // 将coverPath值赋给对应的账户
+          for (const account of this.webDavAccounts) {
+            if (coverPathMap.has(account.id)) {
+              account.coverPath = coverPathMap.get(account.id);
+            }
+          }
+        }
+        coverPathResultSet.close();
+      } catch (error) {
+        // 如果查询coverPath失败,说明字段可能不存在,忽略错误
+        Logger.info(TAG, 'coverPath字段不存在或查询失败,使用默认值');
+      }
+
       Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
       this.notifyObservers(WebdavManagerStates.QueryAccountsSucceed);
     } catch (err) {
@@ -206,7 +259,8 @@ export class WebdavManager {
     imageFilePath: string,
     account: string,
     password: string,
-    enableHttps: boolean
+    enableHttps: boolean,
+    coverPath?: string
   ): Promise<void> {
     try {
       const values: relationalStore.ValuesBucket = {
@@ -222,7 +276,8 @@ export class WebdavManager {
         'uploadFilePath': uploadFilePath,
         'account': account,
         'password': password,
-        'enableHttps': enableHttps ? 1 : 0
+        'enableHttps': enableHttps ? 1 : 0,
+        'coverPath': coverPath || null
       };
 
       await this.dataBaseUtil.insertData(this.webDavTable, values);
@@ -254,7 +309,8 @@ export class WebdavManager {
         'uploadFilePath': account.uploadFilePath,
         'account': account.account,
         'password': account.password,
-        'enableHttps': account.enableHttps ? 1 : 0
+        'enableHttps': account.enableHttps ? 1 : 0,
+        'coverPath': account.coverPath || null
       };
 
       const predicates = new relationalStore.RdbPredicates(this.webDavTable);

+ 143 - 1
entry/src/main/ets/dialog/WebDavAccountDialog.ets

@@ -1,6 +1,22 @@
 import { ToastUtil } from '@pura/harmony-utils';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { WebDavAccount } from '../viewmodel/WebDavAccount';
+import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
+import Logger from '../common/util/Logger';
+import { ConfigurationConstant, Context } from '@kit.AbilityKit';
+
+// 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
+function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
+  if (isDarkMode) {
+    // 深色模式下返回更深的灰色或半透明黑色
+    return `rgba(34,34,34,${alpha + 0.2 > 1 ? 1 : alpha + 0.2})`;
+  }
+  const color = themeColor.replace('#', '');
+  const r = parseInt(color.substring(0, 2), 16);
+  const g = parseInt(color.substring(2, 4), 16);
+  const b = parseInt(color.substring(4, 6), 16);
+  return `rgba(${r},${g},${b},${alpha})`;
+}
 
 // WebDAV账户对话框
 @Component
@@ -16,9 +32,20 @@ export struct WebDavAccountDialog {
   @State username: string = 'chendeben';
   @State password: string = 'chen384626WYT';
   @State enableHttps: boolean = false;
+  @State coverPath: string = '';
+  @State isDarkMode: boolean = false;
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
+    ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
+
+  onColorModeChange() {
+    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
+  }
 
   aboutToAppear(): void {
+    // 初始化深色模式状态
+    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
+
     if(this.isEditMode){
       this.accountName = this.account.name;
       this.host = this.account.host;
@@ -27,8 +54,12 @@ export struct WebDavAccountDialog {
       this.username = this.account.account;
       this.password = this.account.password;
       this.enableHttps = this.account.enableHttps;
+      this.coverPath = this.account.coverPath || '';
+      Logger.info('heanup WebDavAccountDialog', `编辑模式加载账户: ${this.accountName}, 封面路径: ${this.coverPath}`);
+      if (this.coverPath) {
+        Logger.info('heanup WebDavAccountDialog', `封面文件是否存在: ${ImagePickerUtil.imageExists(this.coverPath)}`);
+      }
     }
-
   }
 
   build() {
@@ -39,6 +70,75 @@ export struct WebDavAccountDialog {
         .fontWeight(FontWeight.Bold)
         .fontColor($r('app.color.index_tab_font_color'))
 
+      // 账户封面选择
+      Column({ space: 8 }) {
+        Text('账户封面(可选)')
+          .fontSize(14)
+          .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666')
+          .fontWeight(FontWeight.Medium)
+          .alignSelf(ItemAlign.Start)
+
+        // 封面选择区域
+        Row({ space: 12 }) {
+          // 封面预览
+          Stack() {
+            if (this.coverPath) {
+              Image(this.coverPath)
+                .width(80)
+                .height(80)
+                .borderRadius(8)
+                .objectFit(ImageFit.Cover)
+            } else {
+              // 默认封面图标
+              Column() {
+                Image($r('app.media.cloudDisk'))
+                  .width(40)
+                  .height(40)
+                  .fillColor(this.isDarkMode ? Color.White : this.themeColor)
+              }
+              .width(80)
+              .height(80)
+              .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
+              .borderRadius(8)
+              .justifyContent(FlexAlign.Center)
+            }
+          }
+          .onClick(() => {
+            this.handleSelectCover()
+          })
+
+          // 操作按钮
+          Column({ space: 8 }) {
+            Button('选择封面')
+              .width(120)
+              .height(36)
+              .fontSize(12)
+              .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
+              .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
+              .onClick(() => {
+                this.handleSelectCover()
+              })
+
+            if (this.coverPath) {
+              Button('移除封面')
+                .width(120)
+                .height(36)
+                .fontSize(12)
+                .fontColor(this.isDarkMode ? '#FF453A' : Color.Red)
+                .backgroundColor(this.isDarkMode ? 'rgba(255,69,58,0.2)' : '#FFE5E5')
+                .onClick(() => {
+                  this.handleRemoveCover()
+                })
+            }
+          }
+          .alignItems(HorizontalAlign.Start)
+        }
+        .width('100%')
+        .justifyContent(FlexAlign.Start)
+      }
+      .width('100%')
+      .alignItems(HorizontalAlign.Start)
+
       // 账户名称
       Row({ space: 8 }) {
         Text('名称')
@@ -170,6 +270,7 @@ export struct WebDavAccountDialog {
             updatedAccount.account = this.username;
             updatedAccount.password = this.password;
             updatedAccount.enableHttps = this.enableHttps;
+            updatedAccount.coverPath = this.coverPath;
             updatedAccount.isActivate = true;
             updatedAccount.localHost = '';
             updatedAccount.isUseLocalHost = false;
@@ -187,4 +288,45 @@ export struct WebDavAccountDialog {
     .width('90%')
     .borderRadius(16)
   }
+
+  /**
+   * 处理选择封面
+   */
+  private async handleSelectCover() {
+    try {
+      // 需要获取上下文,这里通过全局上下文获取
+      const context = getContext(this) as Context
+      if (!context) {
+        ToastUtil.showToast('获取应用上下文失败')
+        return
+      }
+
+      const selectedPath = await ImagePickerUtil.selectSingleWebDavCover(context)
+      if (selectedPath) {
+        // 如果之前有封面,删除旧封面
+        if (this.coverPath && this.coverPath !== this.account.coverPath) {
+          ImagePickerUtil.deleteImage(this.coverPath)
+        }
+        this.coverPath = selectedPath
+        Logger.info('heanup WebDavAccountDialog', `选择封面成功: ${selectedPath}`)
+      }
+    } catch (error) {
+        Logger.error('heanup WebDavAccountDialog', `选择封面失败: ${(error as Error).message}`)
+        ToastUtil.showToast('选择封面失败')
+    }
+  }
+
+  /**
+   * 处理移除封面
+   */
+  private handleRemoveCover() {
+    if (this.coverPath) {
+      // 只有当封面不是原来的封面时才删除文件
+      if (this.coverPath !== this.account.coverPath) {
+        ImagePickerUtil.deleteImage(this.coverPath)
+      }
+      this.coverPath = ''
+      Logger.info('heanup WebDavAccountDialog', '移除封面成功')
+    }
+  }
 }

+ 28 - 11
entry/src/main/ets/pages/NewIndex.ets

@@ -1210,17 +1210,33 @@ struct NewIndex {
       ListItem() {
         Button({ type: ButtonType.Capsule, stateEffect: true }) {
           Row() {
-            // 网盘类型图标(目前只有WebDAV,将来可扩展其他类型)
-            Image($r('app.media.cloudDisk'))
-              .width(22)
-              .height(22)
-              .margin({ left: 25 })
-              .fillColor(this.themeColor)
+            // 账户封面或默认图标
+            Stack() {
+              if (account.coverPath) {
+                Image(account.coverPath)
+                  .width(32)
+                  .height(32)
+                  .borderRadius(16)
+                  .objectFit(ImageFit.Cover)
+                  .border({ width: 1.5, color: this.themeColor })
+              } else {
+                // 默认账户图标
+                Circle({ width: 32, height: 32 })
+                  .fill(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
+                  .border({ width: 1.5, color: this.themeColor })
+
+                Image($r('app.media.cloudDisk'))
+                  .width(16)
+                  .height(16)
+                  .fillColor(this.themeColor)
+              }
+            }
+            .margin({ left: 20 })
 
             Column() {
               // 账户名称
               Text(account.name)
-                .margin({ left: 8, right: 20 })
+                .margin({ left: 12, right: 20 })
                 .fontSize(15)
                 .fontColor($r('app.color.text_color'))
                 .fontWeight(480)
@@ -1233,7 +1249,7 @@ struct NewIndex {
                   .fontSize(10)
                   .fontColor($r('app.color.index_tab_font_color'))
                   .opacity(0.8)
-                  .padding({ left: 8,top: 2, bottom: 2 })
+                  .padding({ left: 8, top: 2, bottom: 2 })
                   .borderRadius(3)
 
                 // 服务器地址
@@ -1242,7 +1258,7 @@ struct NewIndex {
                   .fontColor($r('app.color.index_tab_font_color'))
                   .opacity(0.7)
                   .maxLines(1)
-                  .padding({  right: 8, top: 2, bottom: 2 })
+                  .padding({ right: 8, top: 2, bottom: 2 })
                   .textOverflow({ overflow: TextOverflow.Ellipsis })
               }
             }
@@ -1257,7 +1273,7 @@ struct NewIndex {
               .align(Alignment.Center)
           }
           .width('100%')
-          .height(55)
+          .height(60)
         }
         .backgroundColor(Color.Transparent)
         .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
@@ -1636,7 +1652,8 @@ struct NewIndex {
              account.imageFilePath,
              account.account,
              account.password,
-             account.enableHttps
+             account.enableHttps,
+             account.coverPath
            ).then(() => {
              ToastUtil.showToast('添加成功')
              // 重新加载WebDAV账户列表 观察着模式去更新了,所以这里不需要更新,先注释掉

+ 60 - 15
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -305,26 +305,11 @@ export struct WebDavMainPage {
         songFilePaths.push(item.filePath); // 使用filePath作为文件路径
       }
 
-      Logger.info(TAG, 'heanup 所有WebDAV歌曲文件路径: ' + JSON.stringify(songFilePaths));
-
       // 保存WebDAV歌曲数据到全局上下文
       globalContext.setObject('videoItems', videoItems);
       globalContext.setObject('currentPlayIndex', index);
       Logger.info(TAG, 'heanup 已将WebDAV歌曲列表保存到全局上下文,长度:' + videoItems.length);
 
-      // 验证保存是否成功
-      const savedVideoItems = globalContext.getObject('videoItems') as VideoItem[];
-      const savedIndex = globalContext.getObject('currentPlayIndex') as number;
-      Logger.info(TAG, 'heanup 验证保存结果 - videoItems长度: ' + (savedVideoItems?.length || 0) + ', currentPlayIndex: ' + savedIndex);
-
-      // 检查认证信息是否还在
-      const savedAuthInfo = globalContext.getObject('webDavAuthInfo') as WebDavAuthInfo;
-      if (savedAuthInfo) {
-        Logger.info(TAG, 'heanup 认证信息验证成功,账户ID: ' + savedAuthInfo.accountId);
-      } else {
-        Logger.error(TAG, 'heanup 认证信息验证失败,webDavAuthInfo为空');
-      }
-
       // 发送播放事件,类似歌单播放的方式
       const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
 
@@ -474,6 +459,66 @@ export struct WebDavMainPage {
       // 加载按钮和面包屑导航
       Column({ space: 8 }) {
 
+        // 账户信息显示
+        if (this.selectedAccount) {
+          Row({ space: 12 }) {
+            // 账户封面
+            Stack() {
+              if (this.selectedAccount.coverPath) {
+                Image(this.selectedAccount.coverPath)
+                  .width(40)
+                  .height(40)
+                  .borderRadius(20)
+                  .objectFit(ImageFit.Cover)
+                  .border({ width: 2, color: this.themeColor })
+              } else {
+                // 默认账户图标
+                Circle({ width: 40, height: 40 })
+                  .fill(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
+                  .border({ width: 2, color: this.themeColor })
+
+                Image($r('app.media.cloudDisk'))
+                  .width(20)
+                  .height(20)
+                  .fillColor(this.themeColor)
+              }
+            }
+            .onClick(() => {
+              // 点击账户封面可以查看账户详情或编辑账户
+              Logger.info(TAG, '点击账户封面');
+            })
+
+            // 账户信息
+            Column({ space: 4 }) {
+              Text(this.selectedAccount.name || '未知账户')
+                .fontSize(16)
+                .fontWeight(FontWeight.Medium)
+                .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+              Text(`${this.selectedAccount.host}:${this.selectedAccount.port}`)
+                .fontSize(12)
+                .fontColor(this.isDarkMode ? '#8E8E93' : $r('app.color.index_tab_font_color'))
+                .opacity(0.7)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+            }
+            .alignItems(HorizontalAlign.Start)
+            .layoutWeight(1)
+
+            // 在线状态指示器
+            Circle({ width: 8, height: 8 })
+              .fill(Color.Green)
+              .border({ width: 1, color: Color.White })
+          }
+          .width('100%')
+          .padding({ left: 4, right: 4, top: 8, bottom: 8 })
+          .backgroundColor(this.isDarkMode ? 'rgba(44,44,46,0.8)' : 'rgba(248,248,248,0.8)')
+          .borderRadius(8)
+          .margin({ bottom: 8 })
+        }
+
         // 面包屑导航
         if (this.webdavManager.currentPath !== '') {
           Row({ space: 8 }) {

+ 1 - 13
entry/src/main/ets/view/LocalMusic.ets

@@ -12223,21 +12223,9 @@ export struct LocalMusic {
             const webDavAuthInfo = globalContext.getObject('webDavAuthInfo') as WebDavAuthInfo;
             if (webDavAuthInfo) {
               isWebDavError = true;
-            } else {
-              // 通过URL特征判断
-              const filePath = this.currentSong.filePath;
-              if (filePath.includes('/webdav') || filePath.includes(':5005') || filePath.includes(':5000') ||
-                  filePath.includes('/remote.php') || filePath.includes('/dav/')) {
-                isWebDavError = true;
-              }
             }
           } catch (error) {
-            // 通过URL特征判断
-            const filePath = this.currentSong.filePath;
-            if (filePath.includes('/webdav') || filePath.includes(':8080') || filePath.includes(':5000') ||
-                filePath.includes('/remote.php') || filePath.includes('/dav/')) {
-              isWebDavError = true;
-            }
+            console.error('OnErrorListener-->go: 获取WebDAV认证信息失败');
           }
         }
 

+ 2 - 0
entry/src/main/ets/viewmodel/WebDavAccount.ets

@@ -21,6 +21,8 @@ export class WebDavAccount{
   public enableHttps: boolean = false
   public lyricFilePaths: string[] = []
   public imageFilePaths: string[] = []
+  // 自定义封面路径
+  public coverPath?: string
 
   public setIsUseLocalHost(isuse: boolean){
     this.isUseLocalHost = isuse