Browse Source

桌面卡片支持网络封面图片

chendeben 1 year ago
parent
commit
81979a9106

+ 17 - 5
entry/src/main/ets/common/widget/FormLayoutManager.ets

@@ -84,7 +84,11 @@ export class FormLayoutManager {
       widgetSize: data.config.size as string,
       
       // 时间戳用于强制更新
-      timestamp: Date.now()
+      timestamp: Date.now(),
+      
+      // 图片相关字段(初始为空,会在EntryFormAbility中设置)
+      imgName: undefined,
+      formImages: undefined
     };
   }
 
@@ -108,7 +112,9 @@ export class FormLayoutManager {
       showProgress: false,
       showCover: false,
       widgetSize: baseData.widgetSize,
-      timestamp: baseData.timestamp
+      timestamp: baseData.timestamp,
+      imgName: baseData.imgName,
+      formImages: baseData.formImages
     };
   }
 
@@ -134,7 +140,9 @@ export class FormLayoutManager {
       showProgress: false,
       showCover: true,
       widgetSize: baseData.widgetSize,
-      timestamp: baseData.timestamp
+      timestamp: baseData.timestamp,
+      imgName: baseData.imgName,
+      formImages: baseData.formImages
     };
   }
 
@@ -160,7 +168,9 @@ export class FormLayoutManager {
       showProgress: false,
       showCover: true,
       widgetSize: baseData.widgetSize,
-      timestamp: baseData.timestamp
+      timestamp: baseData.timestamp,
+      imgName: baseData.imgName,
+      formImages: baseData.formImages
     };
   }
 
@@ -184,7 +194,9 @@ export class FormLayoutManager {
       showProgress: true,
       showCover: true,
       widgetSize: baseData.widgetSize,
-      timestamp: baseData.timestamp
+      timestamp: baseData.timestamp,
+      imgName: baseData.imgName,
+      formImages: baseData.formImages
     };
   }
 

+ 0 - 1
entry/src/main/ets/common/widget/PlayerControlService.ets

@@ -3,7 +3,6 @@ import Want from '@ohos.app.ability.Want';
 import common from '@ohos.app.ability.common';
 import { hilog } from '@kit.PerformanceAnalysisKit';
 import { WidgetData, WidgetCommand, EventData, RequestData, WidgetControlParams } from './WidgetTypes';
-import { ObjectUtils } from './ObjectUtils';
 import { WidgetTypeHelpers } from './WidgetTypeHelpers';
 import { 
   WIDGET_CONTROL_EVENT,

+ 0 - 11
entry/src/main/ets/common/widget/PlayerStateBroadcastData.ets

@@ -1,11 +0,0 @@
-import { PlayState, SongInfo, PlayProgress, PlaylistState } from './WidgetTypes';
-
-/**
- * 播放器状态广播数据接口
- */
-export interface PlayerStateBroadcastData {
-  playState: PlayState;
-  currentSong: SongInfo;
-  progress: PlayProgress;
-  playlist: PlaylistState;
-}

+ 3 - 0
entry/src/main/ets/common/widget/WidgetTypes.ets

@@ -181,6 +181,9 @@ export interface FormattedWidgetData {
   showCover: boolean;
   widgetSize: string;
   timestamp: number;
+  // 新增字段用于支持网络图片
+  imgName?: string;  // 图片文件名,用于memory://协议
+  formImages?: Record<string, number>;  // 图片文件描述符映射
 }
 
 /**

+ 243 - 17
entry/src/main/ets/entryformability/EntryFormAbility.ets

@@ -2,11 +2,13 @@ import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@
 import { Configuration, Want } from '@kit.AbilityKit';
 import { BusinessError } from '@kit.BasicServicesKit';
 import { hilog } from '@kit.PerformanceAnalysisKit';
+import { fileIo } from '@kit.CoreFileKit';
+import { http } from '@kit.NetworkKit';
 
 import { WidgetDataManager } from '../common/widget/WidgetDataManager';
 import { PlayerControlService } from '../common/widget/PlayerControlService';
 import { AvSessionWidgetListener } from '../common/widget/AvSessionWidgetListener';
-import { WidgetCommand, WidgetControlParams, WidgetData, WidgetSize } from '../common/widget/WidgetTypes';
+import { WidgetCommand, WidgetControlParams, WidgetData, WidgetSize, FormattedWidgetData } from '../common/widget/WidgetTypes';
 import { FormLayoutManager } from '../common/widget/FormLayoutManager';
 import { GlobalWidgetManager } from '../common/widget/GlobalWidgetManager';
 import { WidgetSizeAdapter, SizeChangeListener } from '../common/widget/WidgetSizeAdapter';
@@ -53,6 +55,39 @@ interface WidgetConfigParams extends Record<string, Object> {
   configValue: string;
 }
 
+/**
+ * 扩展的卡片数据接口,支持图片传递
+ */
+interface ExtendedWidgetData extends FormattedWidgetData {
+  formImages?: Record<string, number>;
+}
+
+/**
+ * 自定义对象复制函数,替代Object.assign
+ */
+function copyWidgetData(target: ExtendedWidgetData, overrides: Partial<ExtendedWidgetData>): ExtendedWidgetData {
+  return {
+    isPlaying: overrides.isPlaying !== undefined ? overrides.isPlaying : target.isPlaying,
+    isPaused: overrides.isPaused !== undefined ? overrides.isPaused : target.isPaused,
+    isLoading: overrides.isLoading !== undefined ? overrides.isLoading : target.isLoading,
+    songTitle: overrides.songTitle !== undefined ? overrides.songTitle : target.songTitle,
+    songArtist: overrides.songArtist !== undefined ? overrides.songArtist : target.songArtist,
+    songAlbum: overrides.songAlbum !== undefined ? overrides.songAlbum : target.songAlbum,
+    coverImage: overrides.coverImage !== undefined ? overrides.coverImage : target.coverImage,
+    currentTime: overrides.currentTime !== undefined ? overrides.currentTime : target.currentTime,
+    totalTime: overrides.totalTime !== undefined ? overrides.totalTime : target.totalTime,
+    progressPercentage: overrides.progressPercentage !== undefined ? overrides.progressPercentage : target.progressPercentage,
+    hasNext: overrides.hasNext !== undefined ? overrides.hasNext : target.hasNext,
+    hasPrevious: overrides.hasPrevious !== undefined ? overrides.hasPrevious : target.hasPrevious,
+    showProgress: overrides.showProgress !== undefined ? overrides.showProgress : target.showProgress,
+    showCover: overrides.showCover !== undefined ? overrides.showCover : target.showCover,
+    widgetSize: overrides.widgetSize !== undefined ? overrides.widgetSize : target.widgetSize,
+    timestamp: overrides.timestamp !== undefined ? overrides.timestamp : target.timestamp,
+    imgName: overrides.imgName !== undefined ? overrides.imgName : target.imgName,
+    formImages: overrides.formImages !== undefined ? overrides.formImages : target.formImages
+  };
+}
+
 /**
  * 桌面播放器卡片扩展能力
  * 负责处理卡片的生命周期管理和用户交互事件
@@ -79,6 +114,10 @@ implements SizeChangeListener, ThemeChangeListener, PreferencesChangeListener {
   // 进程启动时间,用于检测进程重启
   private processStartTime: number = Date.now();
   private lastHealthCheck: number = 0;
+  
+  // 图片下载缓存
+  private downloadingImages: Map<string, Promise<string | null>> = new Map();
+  private imageCache: Map<string, string> = new Map(); // url -> fileName映射
 
   /**
    * 初始化服务
@@ -196,29 +235,44 @@ implements SizeChangeListener, ThemeChangeListener, PreferencesChangeListener {
   }
 
   /**
-   * 更新单个卡片,带重试机制
+   * 更新单个卡片,带重试机制和网络图片支持
    */
   private updateSingleWidget(formId: string, size: WidgetSize, data: WidgetData, retryCount: number = 0): void {
     try {
       // 根据卡片尺寸适配数据
-      const adaptedData = this.layoutManager.adaptDataForSize(data, size);
+      const formattedData = this.layoutManager.adaptDataForSize(data, size);
+      
+      // 转换为ExtendedWidgetData
+      const adaptedData: ExtendedWidgetData = {
+        isPlaying: formattedData.isPlaying,
+        isPaused: formattedData.isPaused,
+        isLoading: formattedData.isLoading,
+        songTitle: formattedData.songTitle,
+        songArtist: formattedData.songArtist,
+        songAlbum: formattedData.songAlbum,
+        coverImage: formattedData.coverImage,
+        currentTime: formattedData.currentTime,
+        totalTime: formattedData.totalTime,
+        progressPercentage: formattedData.progressPercentage,
+        hasNext: formattedData.hasNext,
+        hasPrevious: formattedData.hasPrevious,
+        showProgress: formattedData.showProgress,
+        showCover: formattedData.showCover,
+        widgetSize: formattedData.widgetSize,
+        timestamp: formattedData.timestamp,
+        imgName: formattedData.imgName,
+        formImages: formattedData.formImages
+      };
       
       hilog.info(0x0000, TAG, `Heanup widget ${formId} calling formProvider.updateForm with isPlaying=${adaptedData.isPlaying}, hasNext=${adaptedData.hasNext}, hasPrevious=${adaptedData.hasPrevious}, coverImage=${adaptedData.coverImage || 'empty'}`);
       
-      // 直接使用系统API更新卡片
-      const formData = formBindingData.createFormBindingData(adaptedData);
-      formProvider.updateForm(formId, formData).then(() => {
-        hilog.info(0x0000, TAG, `Heanup widget ${formId} updated successfully: isPlaying=${adaptedData.isPlaying}, title=${adaptedData.songTitle}, hasNext=${adaptedData.hasNext}, hasPrevious=${adaptedData.hasPrevious}`);
-      }).catch(() => {
-        hilog.error(0x0000, TAG, `Heanup widget ${formId} update failed, retry count: ${retryCount}`);
-        
-        // 重试机制:最多重试2次
-        if (retryCount < 2) {
-          setTimeout(() => {
-            this.updateSingleWidget(formId, size, data, retryCount + 1);
-          }, 1000 * (retryCount + 1)); // 递增延迟
-        }
-      });
+      // 检查是否有网络图片需要下载
+      if (adaptedData.coverImage && this.isNetworkUrl(adaptedData.coverImage)) {
+        this.updateWidgetWithNetworkImage(formId, adaptedData, retryCount);
+      } else {
+        // 没有网络图片,直接更新
+        this.updateWidgetDirectly(formId, adaptedData, retryCount);
+      }
     } catch (error) {
       hilog.error(0x0000, TAG, `Heanup widget ${formId} process error: ${error}`);
       
@@ -231,6 +285,178 @@ implements SizeChangeListener, ThemeChangeListener, PreferencesChangeListener {
     }
   }
 
+  /**
+   * 直接更新卡片(无网络图片)
+   */
+  private updateWidgetDirectly(formId: string, adaptedData: ExtendedWidgetData, retryCount: number = 0): void {
+    const formData = formBindingData.createFormBindingData(adaptedData);
+    formProvider.updateForm(formId, formData).then(() => {
+      hilog.info(0x0000, TAG, `Heanup widget ${formId} updated successfully: isPlaying=${adaptedData.isPlaying}, title=${adaptedData.songTitle}, hasNext=${adaptedData.hasNext}, hasPrevious=${adaptedData.hasPrevious}`);
+    }).catch(() => {
+      hilog.error(0x0000, TAG, `Heanup widget ${formId} update failed, retry count: ${retryCount}`);
+      
+      // 重试机制:最多重试2次
+      if (retryCount < 2) {
+        setTimeout(() => {
+          this.updateWidgetDirectly(formId, adaptedData, retryCount + 1);
+        }, 1000 * (retryCount + 1)); // 递增延迟
+      }
+    });
+  }
+
+  /**
+   * 使用网络图片更新卡片
+   */
+  private async updateWidgetWithNetworkImage(formId: string, adaptedData: ExtendedWidgetData, retryCount: number = 0): Promise<void> {
+    try {
+      const imageUrl: string = adaptedData.coverImage;
+      hilog.info(0x0000, TAG, `Heanup widget ${formId} downloading network image: ${imageUrl}`);
+      
+      // 先用无图片的数据快速更新一次
+      const dataWithoutImage: ExtendedWidgetData = copyWidgetData(adaptedData, { coverImage: '' });
+      this.updateWidgetDirectly(formId, dataWithoutImage, 0);
+      
+      // 下载图片
+      const fileName = await this.downloadNetworkImage(imageUrl);
+      
+      if (fileName) {
+        // 准备包含图片的数据
+        const imageMap: Record<string, number> = {};
+        imageMap[fileName] = await this.getImageFileDescriptor(fileName);
+        
+        const dataWithImage: ExtendedWidgetData = copyWidgetData(adaptedData, {
+          coverImage: '',  // 清空URL
+          imgName: fileName,  // 设置图片名称用于memory://协议
+          formImages: imageMap
+        });
+        
+        const formDataWithImage = formBindingData.createFormBindingData(dataWithImage);
+        await formProvider.updateForm(formId, formDataWithImage);
+        
+        hilog.info(0x0000, TAG, `Heanup widget ${formId} updated with network image: ${fileName}`);
+      } else {
+        hilog.warn(0x0000, TAG, `Heanup widget ${formId} failed to download image, using default`);
+        // 图片下载失败,使用默认图片
+        this.updateWidgetDirectly(formId, dataWithoutImage, 0);
+      }
+    } catch (error) {
+      hilog.error(0x0000, TAG, `Heanup widget ${formId} network image update error: ${error}`);
+      
+      // 失败后回退到无图片模式
+      const dataWithoutImage: ExtendedWidgetData = copyWidgetData(adaptedData, { coverImage: '' });
+      this.updateWidgetDirectly(formId, dataWithoutImage, retryCount);
+    }
+  }
+
+  /**
+   * 检查是否为网络URL
+   */
+  private isNetworkUrl(url: string): boolean {
+    return url.startsWith('http://') || url.startsWith('https://');
+  }
+
+  /**
+   * 下载网络图片
+   */
+  private async downloadNetworkImage(imageUrl: string): Promise<string | null> {
+    try {
+      // 检查缓存
+      if (this.imageCache.has(imageUrl)) {
+        const fileName = this.imageCache.get(imageUrl)!;
+        hilog.info(0x0000, TAG, `Using cached image: ${fileName} for ${imageUrl}`);
+        return fileName;
+      }
+      
+      // 检查是否正在下载
+      if (this.downloadingImages.has(imageUrl)) {
+        hilog.info(0x0000, TAG, `Image already downloading, waiting: ${imageUrl}`);
+        const existingPromise = this.downloadingImages.get(imageUrl);
+        if (existingPromise) {
+          return await existingPromise;
+        }
+      }
+      
+      // 开始下载
+      const downloadPromise = this.performImageDownload(imageUrl);
+      this.downloadingImages.set(imageUrl, downloadPromise);
+      
+      const fileName = await downloadPromise;
+      
+      // 清理下载状态
+      this.downloadingImages.delete(imageUrl);
+      
+      if (fileName) {
+        this.imageCache.set(imageUrl, fileName);
+      }
+      
+      return fileName;
+    } catch (error) {
+      hilog.error(0x0000, TAG, `Failed to download image ${imageUrl}: ${error}`);
+      this.downloadingImages.delete(imageUrl);
+      return null;
+    }
+  }
+
+  /**
+   * 执行图片下载
+   */
+  private async performImageDownload(imageUrl: string): Promise<string | null> {
+    try {
+      // 创建HTTP请求
+      const httpRequest = http.createHttp();
+      
+      // 设置超时时间为3秒,适合卡片的快速响应需求
+      const response = await httpRequest.request(imageUrl, {
+        method: http.RequestMethod.GET,
+        expectDataType: http.HttpDataType.ARRAY_BUFFER,
+        connectTimeout: 3000,
+        readTimeout: 3000
+      });
+      
+      if (response.responseCode === http.ResponseCode.OK && response.result) {
+        // 生成文件名
+        const fileName = 'cover_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
+        const tempDir = this.context.getApplicationContext().tempDir;
+        const filePath = tempDir + '/' + fileName;
+        
+        // 保存文件
+        const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
+        await fileIo.write(file.fd, response.result as ArrayBuffer);
+        fileIo.closeSync(file);
+        
+        hilog.info(0x0000, TAG, `Image downloaded successfully: ${fileName}`);
+        httpRequest.destroy();
+        return fileName;
+      } else {
+        hilog.error(0x0000, TAG, `HTTP request failed: ${response.responseCode}`);
+        httpRequest.destroy();
+        return null;
+      }
+    } catch (error) {
+      hilog.error(0x0000, TAG, `Download error: ${error}`);
+      return null;
+    }
+  }
+
+  /**
+   * 获取图片文件描述符
+   */
+  private async getImageFileDescriptor(fileName: string): Promise<number> {
+    try {
+      const tempDir = this.context.getApplicationContext().tempDir;
+      const filePath = tempDir + '/' + fileName;
+      const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
+      
+      // 注意:文件描述符会被系统自动管理,不需要手动关闭
+      // 系统会在卡片更新完成后自动关闭文件描述符
+      hilog.info(0x0000, TAG, `Got file descriptor for ${fileName}: ${file.fd}`);
+      return file.fd;
+    } catch (error) {
+      hilog.error(0x0000, TAG, `Failed to get file descriptor for ${fileName}: ${error}`);
+      throw new Error(`Failed to get file descriptor for ${fileName}: ${error}`);
+    }
+  }
+
   /**
    * 卡片创建时调用
    */

+ 20 - 7
entry/src/main/ets/pages/ScanFilePage.ets

@@ -1,9 +1,8 @@
-import TitleBar from '../view/TitleBar'
-import {  router, window } from '@kit.ArkUI'
-import { AppUtil, DeviceUtil, DisplayUtil, FileUtil, LogUtil, PreferencesUtil, ToastUtil } from '@pura/harmony-utils'
+import { window } from '@kit.ArkUI'
+import { AppUtil, FileUtil, LogUtil, PreferencesUtil } from '@pura/harmony-utils'
 import lottie from '@ohos/lottie'
 import { AnimationItem } from '@ohos/lottie'
-import { MessageEvents, taskpool, worker } from '@kit.ArkTS'
+import { taskpool } from '@kit.ArkTS'
 import { fileUri, picker } from '@kit.CoreFileKit'
 import { LocalMusic } from '../view/LocalMusic'
 import { Utility } from '../common/util/Utility'
@@ -13,8 +12,7 @@ import Logger from '../common/util/Logger'
 import { CommonConstants } from '../common/constants/CommonConstants'
 import { emitter } from '@kit.BasicServicesKit'
 import { BreakpointTypeEnum } from '../common/util/BreakpointSystem'
-import { resourceManager } from '@kit.LocalizationKit'
-import { ConfigurationConstant } from '@kit.AbilityKit'
+import { ConfigurationConstant, Context } from '@kit.AbilityKit'
 import { DialogHelper } from '@pura/harmony-dialog'
 
 
@@ -122,7 +120,22 @@ export struct ScanFilePage{
 
   //扫描过程中屏幕要保存常亮
   watchStatus(isKeep:boolean) {
-      this.windowClass.setWindowKeepScreenOn(isKeep);
+    try {
+      if (!this.windowClass) {
+        window.getLastWindow(getContext(this)).then((windowClass) => {
+          this.windowClass = windowClass;
+          if (this.windowClass) {
+            this.windowClass.setWindowKeepScreenOn(isKeep);
+          }
+        }).catch(() => {
+          LogUtil.error('ScanFilePage', `Failed to get window`);
+        });
+      } else {
+        this.windowClass.setWindowKeepScreenOn(isKeep);
+      }
+    } catch (error) {
+      LogUtil.error('ScanFilePage', `Failed to set screen keep on: ${error}`);
+    }
   }
   // 组件生命周期
   aboutToDisappear() {

+ 24 - 4
entry/src/main/ets/widget/pages/PlayerWidgetMedium.ets

@@ -40,6 +40,7 @@ struct PlayerWidgetMedium {
   @LocalStorageProp('hasPrevious') hasPrevious: boolean = false;
   @LocalStorageProp('isLoading') isLoading: boolean = false;
   @LocalStorageProp('imageColorHex') imageColorHex: string = '2A2A2A';
+  @LocalStorageProp('imgName') imgName: string = ''; // 图片文件名,用于memory://协议
 
   /**
    * 获取按钮透明度
@@ -84,15 +85,34 @@ struct PlayerWidgetMedium {
     }
     return this.songArtist;
   }
+  /**
+   * 检查是否为网络URL
+   */
+  private isNetworkUrl(url: string): boolean {
+    return url.startsWith('http://') || url.startsWith('https://');
+  }
 
   /**
    * 获取专辑封面
    */
-  private getCoverImage(): Resource {
-    // 如果有封面图片,使用封面图片,否则使用默认图片
-    if (this.coverImage && this.coverImage.trim() !== '') {
-      return $rawfile(this.coverImage);
+  private getCoverImage(): Resource | string {
+    console.info(`Heanup PlayerWidgetSquare: getCoverImage called, coverImage='${this.coverImage}', imgName='${this.imgName}'`);
+
+    // 优先使用通过formImages传递的本地图片(支持网络图片下载后的显示)
+    if (this.imgName && this.imgName.trim() !== '') {
+      const memoryUrl = 'memory://' + this.imgName;
+      console.info(`Heanup PlayerWidgetSquare: Using memory image: ${memoryUrl}`);
+      return memoryUrl;
     }
+
+    // 如果有coverImage且不是网络URL,使用本地路径
+    if (this.coverImage && this.coverImage.trim() !== '' && !this.isNetworkUrl(this.coverImage)) {
+      console.info(`Heanup PlayerWidgetSquare: Using local cover image: ${this.coverImage}`);
+      return this.coverImage;
+    }
+
+    // 默认使用内置图片
+    console.info(`Heanup PlayerWidgetSquare: Using default cover image`);
     return $r('app.media.ic_avatar4'); // 使用默认专辑封面
   }
 

+ 23 - 5
entry/src/main/ets/widget/pages/PlayerWidgetSquare.ets

@@ -31,6 +31,7 @@ struct PlayerWidgetSquare {
   @LocalStorageProp('coverImage') coverImage: string = '';
   @LocalStorageProp('isLoading') isLoading: boolean = false;
   @LocalStorageProp('imageColorHex') imageColorHex: string = '2A2A2A';
+  @LocalStorageProp('imgName') imgName: string = ''; // 图片文件名,用于memory://协议
 
   /**
    * 格式化歌曲标题显示
@@ -56,16 +57,33 @@ struct PlayerWidgetSquare {
    * 获取专辑封面
    */
   private getCoverImage(): Resource | string {
-    console.info(`Heanup PlayerWidgetSquare: getCoverImage called, coverImage='${this.coverImage}'`);
-    // 如果有封面图片,使用封面图片,否则使用默认图片
-    if (this.coverImage && this.coverImage.trim() !== '') {
-      console.info(`Heanup PlayerWidgetSquare: Using actual cover image: ${this.coverImage}`);
-      return this.coverImage; // 直接返回文件路径
+    console.info(`Heanup PlayerWidgetSquare: getCoverImage called, coverImage='${this.coverImage}', imgName='${this.imgName}'`);
+    
+    // 优先使用通过formImages传递的本地图片(支持网络图片下载后的显示)
+    if (this.imgName && this.imgName.trim() !== '') {
+      const memoryUrl = 'memory://' + this.imgName;
+      console.info(`Heanup PlayerWidgetSquare: Using memory image: ${memoryUrl}`);
+      return memoryUrl;
     }
+    
+    // 如果有coverImage且不是网络URL,使用本地路径
+    if (this.coverImage && this.coverImage.trim() !== '' && !this.isNetworkUrl(this.coverImage)) {
+      console.info(`Heanup PlayerWidgetSquare: Using local cover image: ${this.coverImage}`);
+      return this.coverImage;
+    }
+    
+    // 默认使用内置图片
     console.info(`Heanup PlayerWidgetSquare: Using default cover image`);
     return $r('app.media.ic_avatar4'); // 使用默认专辑封面
   }
 
+  /**
+   * 检查是否为网络URL
+   */
+  private isNetworkUrl(url: string): boolean {
+    return url.startsWith('http://') || url.startsWith('https://');
+  }
+
   build() {
     RelativeContainer() {
       // 歌曲标题 - 参考华为官方样式