chendeben 1 tahun lalu
induk
melakukan
9990f27a0a

+ 6 - 6
build-profile.json5

@@ -28,13 +28,13 @@
         "name": "default",
         "type": "HarmonyOS",
         "material": {
-          "certpath": "C:\\Users\\Admin\\.ohos\\config\\default_TTMusic-new_gDcMTvfBBEwzdIYOYKAqM86VO4gb0Q1MpV1IYbfPK60=.cer",
-          "keyAlias": "debugKey",
-          "keyPassword": "0000001B535CB6CC1761559F1F1BE7484BC8F8ED913AA276A3382B606D63D7908491C37F3B3D813728713D",
-          "profile": "C:\\Users\\Admin\\.ohos\\config\\default_TTMusic-new_gDcMTvfBBEwzdIYOYKAqM86VO4gb0Q1MpV1IYbfPK60=.p7b",
+          "storeFile": "/Users/chendeben/Documents/certs/release/qimeng.p12",
+          "storePassword": "00000018C51C5283F09C8BCF083F7B3D7FD05EEB100350491A708BFDF0CDEBC7DDCBD1026F8A6BF4",
+          "keyAlias": "qimeng",
+          "keyPassword": "0000001805C230F9D99D7B02755274A3F909A23506DB2B112262DD5F9F372DBC881FDBCA1635EB29",
           "signAlg": "SHA256withECDSA",
-          "storeFile": "C:\\Users\\Admin\\.ohos\\config\\default_TTMusic-new_gDcMTvfBBEwzdIYOYKAqM86VO4gb0Q1MpV1IYbfPK60=.p12",
-          "storePassword": "0000001BBB2B40CC0DCF997B480478393C697411E4F454E69F1749EE5FB4759F365E31022109A5B4DCA4D5"
+          "profile": "/Users/chendeben/Documents/certs/release/内部测试Release.p7b",
+          "certpath": "/Users/chendeben/Documents/certs/release/qimeng.cer"
         }
       }
     ]

+ 2 - 0
entry/src/main/ets/common/service/DataPersistenceService.ets

@@ -43,6 +43,7 @@ export interface PlayerStateData {
   isPaused: boolean;
   currentIndex: number;
   playMode: PlayMode;
+  isFavorite: boolean;
   volume: number;
   speed: number;
   timestamp: number;
@@ -560,6 +561,7 @@ export class DataPersistenceService implements IDataPersistenceService {
         isPaused: state.isPaused,
         currentIndex: state.currentIndex,
         playMode: state.playMode,
+        isFavorite: state.isFavorite,
         volume: state.volume,
         speed: state.speed,
         timestamp: Date.now()

+ 5 - 2
entry/src/main/ets/common/service/PlayerStateModel.ets

@@ -25,6 +25,7 @@ export interface PlayerState {
   playMode: PlayMode;
   volume: number;
   speed: number;
+  isFavorite: boolean; // 是否收藏
   // 播放列表相关状态
   hasNext?: boolean;
   hasPrevious?: boolean;
@@ -98,7 +99,8 @@ export class PlayerStateModel {
       hasNext: false,
       hasPrevious: false,
       totalCount: 0,
-      currentSong: undefined
+      currentSong: undefined,
+      isFavorite: false
     };
   }
 
@@ -119,7 +121,8 @@ export class PlayerStateModel {
       hasNext: this.state.hasNext,
       hasPrevious: this.state.hasPrevious,
       totalCount: this.state.totalCount,
-      currentSong: this.state.currentSong
+      currentSong: this.state.currentSong,
+      isFavorite: this.state.isFavorite
     };
   }
 

+ 31 - 1
entry/src/main/ets/common/service/UnifiedPlayerService.ets

@@ -246,6 +246,11 @@ export interface IPlayerService {
    * 强制刷新收藏状态(用于收藏/取消收藏操作后)
    */
   refreshFavoriteStatus(): Promise<void>;
+
+  /**
+   * 切换收藏状态
+   */
+  toggleFavorite(): Promise<void>;
 }
 
 /**
@@ -900,7 +905,7 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
   /**
    * 获取当前歌曲的收藏状态
    */
-  private getCurrentSongFavoriteState(): boolean {
+  public  getCurrentSongFavoriteState(): boolean {
     try {
       let currentSong = this.playlistModel.getCurrentSong();
       if (!currentSong) {
@@ -983,6 +988,31 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
       LogUtils.getInstance().LOGI(`UnifiedPlayerService: Failed to refresh favorite status: ${error}`);
     }
   }
+
+  public async toggleFavorite(): Promise<void> {
+    try {
+      const currentSong = this.getCurrentSong();
+      if (!currentSong) {
+        LogUtils.getInstance().LOGI('UnifiedPlayerService: No current song to toggle favorite');
+        return;
+      }
+
+      const isFavorite = this.getCurrentSongFavoriteState();
+      const newFavState = isFavorite ? 0 : 1;
+
+       this.getTable().updateIsFavByFilePath(currentSong.filePath, newFavState, () => {
+          this.refreshFavoriteStatus();
+      });
+
+
+      // 广播状态更新
+      this.broadcastCurrentState();
+
+      LogUtils.getInstance().LOGI(`UnifiedPlayerService: Toggled favorite for ${currentSong.name} to ${newFavState}`);
+    } catch (error) {
+      LogUtils.getInstance().LOGI(`UnifiedPlayerService: Failed to toggle favorite: ${error}`);
+    }
+  }
   private getTable(): MediaTable {
     if (this.table) {
       return this.table;

+ 4 - 2
entry/src/main/ets/common/widget/EnhancedFormUpdateService.ets

@@ -352,7 +352,8 @@ export class EnhancedFormUpdateService {
         showCover: adaptedData.showCover,
         widgetSize: adaptedData.widgetSize,
         timestamp: Date.now(),
-        imgName: adaptedData.imgName || ''
+        imgName: adaptedData.imgName || '',
+        isFavorite: adaptedData.isFavorite,
       };
 
 
@@ -379,7 +380,8 @@ export class EnhancedFormUpdateService {
           widgetSize: formattedData.widgetSize,
           timestamp: formattedData.timestamp,
           imgName: formattedData.imgName,
-          formImages: {} as Record<string, number>
+          formImages: {} as Record<string, number>,
+          isFavorite: formattedData.isFavorite
         };
 
         // 设置图片文件描述符

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

@@ -39,6 +39,7 @@ export class FormLayoutManager {
     switch (size) {
       case WidgetSize.SMALL:
         return this.adaptForSmallWidget(baseData, widgetData);
+      case WidgetSize.RECTANGLE:
       case WidgetSize.MEDIUM:
         return this.adaptForMediumWidget(baseData, widgetData);
       case WidgetSize.SQUARE:
@@ -88,7 +89,8 @@ export class FormLayoutManager {
       
       // 图片相关字段(初始为空,会在EntryFormAbility中设置)
       imgName: undefined,
-      formImages: undefined
+      formImages: undefined,
+      isFavorite:false
     };
   }
 
@@ -114,7 +116,8 @@ export class FormLayoutManager {
       widgetSize: baseData.widgetSize,
       timestamp: baseData.timestamp,
       imgName: baseData.imgName,
-      formImages: baseData.formImages
+      formImages: baseData.formImages,
+      isFavorite:baseData.isFavorite
     };
   }
 
@@ -142,7 +145,8 @@ export class FormLayoutManager {
       widgetSize: baseData.widgetSize,
       timestamp: baseData.timestamp,
       imgName: baseData.imgName,
-      formImages: baseData.formImages
+      formImages: baseData.formImages,
+      isFavorite:baseData.isFavorite
     };
   }
 
@@ -170,7 +174,8 @@ export class FormLayoutManager {
       widgetSize: baseData.widgetSize,
       timestamp: baseData.timestamp,
       imgName: baseData.imgName,
-      formImages: baseData.formImages
+      formImages: baseData.formImages,
+      isFavorite:baseData.isFavorite
     };
   }
 
@@ -196,7 +201,8 @@ export class FormLayoutManager {
       widgetSize: baseData.widgetSize,
       timestamp: baseData.timestamp,
       imgName: baseData.imgName,
-      formImages: baseData.formImages
+      formImages: baseData.formImages,
+      isFavorite:baseData.isFavorite
     };
   }
 
@@ -219,6 +225,8 @@ export class FormLayoutManager {
     switch (size) {
       case WidgetSize.SMALL:
         return 'widget/pages/PlayerWidgetSmall';
+      case WidgetSize.RECTANGLE:
+        return 'widget/pages/PlayerWidgetRectangle';
       case WidgetSize.MEDIUM:
         return 'widget/pages/PlayerWidgetMedium';
       case WidgetSize.SQUARE:

+ 8 - 6
entry/src/main/ets/common/widget/WidgetDataManager.ets

@@ -255,29 +255,31 @@ export class WidgetDataManager {
       isPlaying: data.playState.isPlaying,
       isPaused: data.playState.isPaused,
       isLoading: data.playState.isLoading,
-      
+
       // 歌曲信息
       songTitle: this.truncateText(data.currentSong.title, 20),
       songArtist: this.truncateText(data.currentSong.artist, 15),
       songAlbum: this.truncateText(data.currentSong.album, 15),
-      coverImage: data.currentSong.coverImagePath && data.currentSong.coverImagePath.trim() !== '' ? data.currentSong.coverImagePath : '',
-      
+      coverImage: data.currentSong.coverImagePath && data.currentSong.coverImagePath.trim() !== '' ?
+      data.currentSong.coverImagePath : '',
+
       // 播放进度
       currentTime: data.progress.currentTimeText,
       totalTime: data.progress.totalTimeText,
       progressPercentage: data.progress.percentage,
-      
+
       // 控制按钮状态(使用修复后的值)
       hasNext: hasNext,
       hasPrevious: hasPrevious,
-      
+
       // 卡片配置
       showProgress: data.config.showProgress,
       showCover: data.config.showCover,
       widgetSize: data.config.size as string,
 
       // 时间戳用于强制更新
-      timestamp: Date.now()
+      timestamp: Date.now(),
+      isFavorite: false
     };
     return formattedData;
   }

+ 2 - 0
entry/src/main/ets/common/widget/WidgetSizeAdapter.ets

@@ -236,6 +236,8 @@ export class WidgetSizeAdapter {
         return WidgetSize.MEDIUM;
       case 3: // 4x3
         return WidgetSize.LARGE;
+      case 4: // 2x4
+        return WidgetSize.RECTANGLE;
       default:
         hilog.warn(0x0000, TAG, `Unknown dimension: ${dimension}, using medium as default`);
         return WidgetSize.MEDIUM;

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

@@ -9,6 +9,7 @@ export enum WidgetSize {
   SMALL = 'small',    // 1x2
   MEDIUM = 'medium',  // 2x4
   SQUARE = 'square',  // 2x2
+  RECTANGLE = 'rectangle', // 2x4
   LARGE = 'large'     // 4x3 (已删除)
 }
 
@@ -187,6 +188,7 @@ export interface FormattedWidgetData {
   progressPercentage: number;
   hasNext: boolean;
   hasPrevious: boolean;
+  isFavorite: boolean;
   showProgress: boolean;
   showCover: boolean;
   widgetSize: string;

+ 25 - 72
entry/src/main/ets/entryability/EntryAbility.ets

@@ -56,6 +56,7 @@ interface PlaylistBroadcast {
   hasPrevious: boolean;
   currentIndex: number;
   totalCount: number;
+  isFavorite: boolean;
 }
 
 interface BroadcastData {
@@ -521,6 +522,25 @@ export default class EntryAbility extends UIAbility {
                 }
             });
 
+            // 监听收藏事件
+            this.callee.on('toggleFavorite', (data: rpc.MessageSequence) => {
+                try {
+                    hilog.info(0x0000, 'Heanup2', `🎵 Widget call: toggleFavorite received`);
+                    const params: Record<string, Object> = JSON.parse(data.readString()) as Record<string, Object>;
+                    hilog.info(0x0000, 'Heanup2', `Widget toggleFavorite params: ${JSON.stringify(params)}`);
+
+                    // 异步发送收藏事件到主应用
+                    this.sendWidgetControlEvent('TOGGLE_FAVORITE', params).catch((error: Error) => {
+                        hilog.error(0x0000, 'Heanup2', `❌ toggleFavorite async error: ${error}`);
+                    });
+
+                    return new MyParcelable(5, 'toggleFavorite_success');
+                } catch (error) {
+                    hilog.error(0x0000, 'Heanup2', `❌ toggleFavorite handler error: ${error}`);
+                    return new MyParcelable(-5, 'toggleFavorite_error');
+                }
+            });
+
             hilog.info(0x0000, 'Heanup2', '🎵 Widget call listeners registered successfully (Enhanced)');
         } catch (err) {
             hilog.error(0x0000, 'Heanup2', `❌ Failed to register widget call listeners: ${JSON.stringify(err as BusinessError)}`);
@@ -659,6 +679,11 @@ export default class EntryAbility extends UIAbility {
                     hilog.info(0x0000, 'Heanup2', '✅ Widget command: Open app (handled by UI)');
                     break;
 
+                case 'TOGGLE_FAVORITE':
+                    await unifiedService.toggleFavorite();
+                    hilog.info(0x0000, 'Heanup2', '✅ Widget command: Toggle favorite');
+                    break;
+
                 default:
                     hilog.warn(0x0000, 'Heanup2', `❓ Unknown widget command: ${command}`);
                     break;
@@ -879,78 +904,6 @@ export default class EntryAbility extends UIAbility {
      * 广播当前播放器状态给卡片
      */
     private broadcastCurrentPlayerState(): void {
-        try {
-            const unifiedService = UnifiedPlayerService.getInstance();
-            const currentState = unifiedService.getCurrentState();
-            const currentSong = unifiedService.getCurrentSong();
-            const playlist = unifiedService.getPlaylist();
-            const currentIndex = unifiedService.getCurrentIndex();
-
-            if (currentSong) {
-                // 构建播放器状态广播数据
-                const broadcastData: BroadcastData = {
-                    playState: {
-                        isPlaying: currentState.isPlaying || false,
-                        isPaused: currentState.isPaused || true,
-                        isLoading: currentState.isLoading || false
-                    } as PlayStateBroadcast,
-                    currentSong: {
-                        id: currentSong.id || '',
-                        title: currentSong.name || '暂无播放',
-                        artist: currentSong.artist || '未知艺术家',
-                        album: currentSong.album || '未知专辑',
-                        coverImagePath: currentSong.pixelMapPath || '',
-                        duration: currentSong.duration ? Number(currentSong.duration) : 0
-                    } as SongBroadcast,
-                    progress: {
-                        currentPosition: currentState.currentPosition || 0,
-                        duration: currentState.duration || 0,
-                        percentage: this.calculatePercentage(currentState.currentPosition || 0, currentState.duration || 0),
-                        currentTimeText: this.formatTime(Math.floor((currentState.currentPosition || 0) / 1000)),
-                        totalTimeText: this.formatTime(Math.floor((currentState.duration || 0) / 1000))
-                    } as ProgressBroadcast,
-                    playlist: {
-                        hasNext: currentState.hasNext || false,
-                        hasPrevious: currentState.hasPrevious || false,
-                        currentIndex: currentIndex,
-                        totalCount: playlist.length
-                    } as PlaylistBroadcast
-                };
-
-                // 发送状态变化事件
-                const publishInfo: PublishInfo = {
-                    data: JSON.stringify(broadcastData)
-                };
-
-                // 使用emitter发送事件
-                const eventData: EventDataWrapper = {
-                    data: broadcastData
-                };
-                emitter.emit({ eventId: 1001 }, eventData); // 使用特定的事件ID
-
-                hilog.info(0x0000, 'Heanup2', `📡 Broadcasted current player state: ${currentSong.name}, isPlaying=${currentState.isPlaying}`);
-            } else {
-                hilog.info(0x0000, 'Heanup2', '📡 No current song to broadcast');
-            }
-        } catch (error) {
-            hilog.error(0x0000, 'Heanup2', `❌ Failed to broadcast current player state: ${error}`);
-        }
-    }
 
-    /**
-     * 计算播放进度百分比
-     */
-    private calculatePercentage(current: number, total: number): number {
-        if (total <= 0) return 0;
-        return Math.min(100, Math.max(0, (current / total) * 100));
-    }
-
-    /**
-     * 格式化时间显示
-     */
-    private formatTime(seconds: number): string {
-        const mins = Math.floor(seconds / 60);
-        const secs = Math.floor(seconds % 60);
-        return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
     }
 }

+ 3 - 0
entry/src/main/ets/entryformability/EntryFormAbility.ets

@@ -21,6 +21,7 @@ const TAG = 'Heanup EntryFormAbility';
  */
 interface ExtendedWidgetData extends FormattedWidgetData {
   formImages?: Record<string, number>;
+  isFavorite: boolean;
 }
 
 /**
@@ -41,6 +42,7 @@ function copyWidgetData(target: ExtendedWidgetData, overrides: Partial<ExtendedW
     target.progressPercentage,
     hasNext: overrides.hasNext !== undefined ? overrides.hasNext : target.hasNext,
     hasPrevious: overrides.hasPrevious !== undefined ? overrides.hasPrevious : target.hasPrevious,
+    isFavorite: overrides.isFavorite !== undefined ? overrides.isFavorite : target.isFavorite,
     showProgress: overrides.showProgress !== undefined ? overrides.showProgress : target.showProgress,
     showCover: overrides.showCover !== undefined ? overrides.showCover : target.showCover,
     widgetSize: overrides.widgetSize !== undefined ? overrides.widgetSize : target.widgetSize,
@@ -345,6 +347,7 @@ implements SizeChangeListener {
         progressPercentage: formattedData.progressPercentage,
         hasNext: formattedData.hasNext,
         hasPrevious: formattedData.hasPrevious,
+        isFavorite: formattedData.isFavorite,
         showProgress: formattedData.showProgress,
         showCover: formattedData.showCover,
         widgetSize: formattedData.widgetSize,

+ 41 - 143
entry/src/main/ets/widget/pages/PlayerWidgetMedium.ets

@@ -15,20 +15,24 @@ const MediumCoverAlignRules: Record<string, Record<string, string | VerticalAlig
   'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }
 };
 
-const TitleAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
+const MediumTitleAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
   'left': { 'anchor': 'musicCover', 'align': HorizontalAlign.End },
   'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }
 };
+const MediumCollectAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
+  'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
+  'right': { 'anchor': '__container__', 'align': HorizontalAlign.End }
+};
 
-const PlayControlAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
+const MediumPlayControlAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
   'bottom': { 'anchor': '__container__', 'align': VerticalAlign.Bottom },
   'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start },
   'right': { 'anchor': '__container__', 'align': HorizontalAlign.End }
 };
 
-let storageUpdateCall = new LocalStorage();
+let mediumStorageUpdateCall = new LocalStorage();
 
-@Entry(storageUpdateCall)
+@Entry(mediumStorageUpdateCall)
 @Component
 struct PlayerWidgetMedium {
   // 卡片数据属性 - 使用LocalStorageProp与FormExtensionAbility通信
@@ -40,6 +44,7 @@ struct PlayerWidgetMedium {
   @LocalStorageProp('hasNext') hasNext: boolean = false;
   @LocalStorageProp('hasPrevious') hasPrevious: boolean = false;
   @LocalStorageProp('isLoading') isLoading: boolean = false;
+  @LocalStorageProp('isFavorite') isFavorite: boolean = false; // Favorite status
   @LocalStorageProp('imageColorHex') imageColorHex: string = '2A2A2A';
   @LocalStorageProp('imgName') imgName: string = ''; // 图片文件名,用于memory://协议
 
@@ -69,135 +74,6 @@ struct PlayerWidgetMedium {
     return url.startsWith('http://') || url.startsWith('https://');
   }
 
-  /**
-   * 生成渐变色
-   * 根据封面图片的主色调生成合适的渐变背景
-   */
-  private generateGradientColors(): [string, number][] {
-    // 如果没有封面或颜色信息,使用默认渐变
-    if (!this.imageColorHex || this.imageColorHex.trim() === '' || this.imageColorHex === '2A2A2A') {
-      return this.getDefaultGradientColors();
-    }
-
-    const baseColor = this.imageColorHex;
-    
-    // 解析RGB值
-    const r = parseInt(baseColor.substring(0, 2), 16);
-    const g = parseInt(baseColor.substring(2, 4), 16);
-    const b = parseInt(baseColor.substring(4, 6), 16);
-    
-    // 计算亮度,用于判断是深色还是浅色
-    const brightness = (r * 299 + g * 587 + b * 114) / 1000;
-    
-    // 根据亮度调整渐变策略
-    if (brightness < 60) {
-      // 深色封面:从更深的色调渐变到原色调
-      return this.generateDarkGradient(r, g, b);
-    } else if (brightness > 180) {
-      // 浅色封面:降低亮度,创建柔和渐变
-      return this.generateLightGradient(r, g, b);
-    } else {
-      // 中等亮度:标准渐变
-      return this.generateStandardGradient(r, g, b);
-    }
-  }
-
-  /**
-   * 生成深色渐变(适用于深色封面)
-   */
-  private generateDarkGradient(r: number, g: number, b: number): [string, number][] {
-    // 生成更深的起始色
-    const darkerR = Math.max(0, Math.floor(r * 0.2));
-    const darkerG = Math.max(0, Math.floor(g * 0.2));
-    const darkerB = Math.max(0, Math.floor(b * 0.2));
-    
-    // 中间色稍微提亮
-    const midR = Math.floor(r * 0.6);
-    const midG = Math.floor(g * 0.6);
-    const midB = Math.floor(b * 0.6);
-    
-    // 终点色适度提亮
-    const lightR = Math.min(255, Math.floor(r * 1.2));
-    const lightG = Math.min(255, Math.floor(g * 1.2));
-    const lightB = Math.min(255, Math.floor(b * 1.2));
-    
-    const darkerColor = `#ff${darkerR.toString(16).padStart(2, '0')}${darkerG.toString(16).padStart(2, '0')}${darkerB.toString(16).padStart(2, '0')}`;
-    const midColor = `#ff${midR.toString(16).padStart(2, '0')}${midG.toString(16).padStart(2, '0')}${midB.toString(16).padStart(2, '0')}`;
-    const lightColor = `#ff${lightR.toString(16).padStart(2, '0')}${lightG.toString(16).padStart(2, '0')}${lightB.toString(16).padStart(2, '0')}`;
-    
-    return [
-      [darkerColor, 0.0],
-      [midColor, 0.6],
-      [lightColor, 1.0]
-    ];
-  }
-
-  /**
-   * 生成浅色渐变(适用于浅色封面)
-   */
-  private generateLightGradient(r: number, g: number, b: number): [string, number][] {
-    // 大幅降低亮度作为起始色
-    const darkerR = Math.floor(r * 0.3);
-    const darkerG = Math.floor(g * 0.3);
-    const darkerB = Math.floor(b * 0.3);
-    
-    // 中间色适度降低亮度
-    const midR = Math.floor(r * 0.5);
-    const midG = Math.floor(g * 0.5);
-    const midB = Math.floor(b * 0.5);
-    
-    // 终点色保持相对较暗,避免过亮
-    const endR = Math.floor(r * 0.7);
-    const endG = Math.floor(g * 0.7);
-    const endB = Math.floor(b * 0.7);
-    
-    const darkerColor = `#ff${darkerR.toString(16).padStart(2, '0')}${darkerG.toString(16).padStart(2, '0')}${darkerB.toString(16).padStart(2, '0')}`;
-    const midColor = `#ff${midR.toString(16).padStart(2, '0')}${midG.toString(16).padStart(2, '0')}${midB.toString(16).padStart(2, '0')}`;
-    const endColor = `#ff${endR.toString(16).padStart(2, '0')}${endG.toString(16).padStart(2, '0')}${endB.toString(16).padStart(2, '0')}`;
-    
-    return [
-      [darkerColor, 0.0],
-      [midColor, 0.5],
-      [endColor, 1.0]
-    ];
-  }
-
-  /**
-   * 生成标准渐变(适用于中等亮度封面)
-   */
-  private generateStandardGradient(r: number, g: number, b: number): [string, number][] {
-    // 生成较深的颜色作为渐变起始
-    const darkerR = Math.max(0, Math.floor(r * 0.4));
-    const darkerG = Math.max(0, Math.floor(g * 0.4));
-    const darkerB = Math.max(0, Math.floor(b * 0.4));
-    
-    // 生成中等亮度的颜色
-    const midR = Math.floor(r * 0.7);
-    const midG = Math.floor(g * 0.7);
-    const midB = Math.floor(b * 0.7);
-    
-    const darkerColor = `#ff${darkerR.toString(16).padStart(2, '0')}${darkerG.toString(16).padStart(2, '0')}${darkerB.toString(16).padStart(2, '0')}`;
-    const midColor = `#ff${midR.toString(16).padStart(2, '0')}${midG.toString(16).padStart(2, '0')}${midB.toString(16).padStart(2, '0')}`;
-    const originalColor = `#ff${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
-    
-    return [
-      [darkerColor, 0.0],
-      [midColor, 0.5],
-      [originalColor, 1.0]
-    ];
-  }
-
-  /**
-   * 获取默认渐变色(当没有封面时使用)
-   */
-  private getDefaultGradientColors(): [string, number][] {
-    return [
-      ['#ff1a1a1a', 0.0],  // 深灰色
-      ['#ff2a2a2a', 0.5],  // 中灰色
-      ['#ff3a3a3a', 1.0]   // 浅灰色
-    ];
-  }
-
   /**
    * 获取专辑封面
    */
@@ -224,6 +100,30 @@ struct PlayerWidgetMedium {
 
   build() {
     RelativeContainer() {
+      // 收藏按钮
+      Button() {
+        SymbolGlyph(this.isFavorite ? $r('sys.symbol.heart_fill') : $r('sys.symbol.heart'))
+          .fontSize(24)
+          .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true)
+          .fontColor(['#E5FFFFFF'])
+      }
+      .width(40)
+      .height(40)
+      .backgroundColor(Color.Transparent)
+      .alignRules(MediumCollectAlignRules)
+      .onClick(() => {
+        console.info(`Heanup PlayerWidgetMedium: Favorite button clicked`);
+        if (!this.isLoading) {
+          postCardAction(this, {
+            'action': 'call',
+            'abilityName': 'EntryAbility',
+            'params': {
+              'formId': this.formId,
+              'method': 'toggleFavorite'
+            }
+          });
+        }
+      })
       // 歌曲标题
       Text(this.getDisplayTitle())
         .fontSize(16)
@@ -232,7 +132,7 @@ struct PlayerWidgetMedium {
         .fontColor(Color.White)
         .textOverflow({ overflow: TextOverflow.Ellipsis })
         .maxLines(1)
-        .alignRules(TitleAlignRules)
+        .alignRules(MediumTitleAlignRules)
         .margin({ left: 16, top: 8 })
         .id('musicTitle')
 
@@ -274,10 +174,9 @@ struct PlayerWidgetMedium {
       Row() {
         // 上一首按钮
         Button() {
-          Image($r('app.media.hm_previous'))
-            .width(20)
-            .height(20)
-            .fillColor('#E5FFFFFF')
+          SymbolGlyph($r('sys.symbol.backward_end_fill'))
+            .fontSize(36)
+            .fontColor(['#E5FFFFFF'])
         }
         .width(40)
         .height(40)
@@ -325,10 +224,9 @@ struct PlayerWidgetMedium {
 
         // 下一首按钮
         Button() {
-          Image($r('app.media.hm_next'))
-            .width(20)
-            .height(20)
-            .fillColor('#E5FFFFFF')
+          SymbolGlyph($r('sys.symbol.forward_end_fill'))
+            .fontSize(36)
+            .fontColor(['#E5FFFFFF'])
         }
         .width(40)
         .height(40)
@@ -350,7 +248,7 @@ struct PlayerWidgetMedium {
       }
       .width('100%')
       .justifyContent(FlexAlign.Center)
-      .alignRules(PlayControlAlignRules)
+      .alignRules(MediumPlayControlAlignRules)
       .id('playControls')
     }
     .height('100%')

+ 384 - 0
entry/src/main/ets/widget/pages/PlayerWidgetRectangle.ets

@@ -0,0 +1,384 @@
+/**
+ * 矩形封面播放器卡片 (2x4)
+ * 显示专辑封面、歌曲信息和播放控制
+ */
+
+// 定义对齐规则
+const RectangleSingerAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
+  'top': { 'anchor': 'musicTitle', 'align': VerticalAlign.Bottom },
+  'left': { 'anchor': 'musicCover', 'align': HorizontalAlign.End }
+};
+
+const RectangleCoverAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
+  'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start },
+  'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }
+};
+
+const RectangleTitleAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
+  'left': { 'anchor': 'musicCover', 'align': HorizontalAlign.End },
+  'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }
+};
+
+const RectanglePlayControlAlignRules: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
+  'bottom': { 'anchor': '__container__', 'align': VerticalAlign.Bottom },
+  'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start },
+  'right': { 'anchor': '__container__', 'align': HorizontalAlign.End }
+};
+
+let rectangleStorageUpdateCall = new LocalStorage();
+
+@Entry(rectangleStorageUpdateCall)
+@Component
+struct PlayerWidgetRectangle {
+  // 卡片数据属性 - 使用LocalStorageProp与FormExtensionAbility通信
+  @LocalStorageProp('formId') formId: string = '202504'; // New formId for this card
+  @LocalStorageProp('isPlaying') isPlaying: boolean = false;
+  @LocalStorageProp('songTitle') songTitle: string = 'Dream It Possible';
+  @LocalStorageProp('songArtist') songArtist: string = 'Delacey';
+  @LocalStorageProp('coverImage') coverImage: string = '';
+  @LocalStorageProp('hasNext') hasNext: boolean = false;
+  @LocalStorageProp('hasPrevious') hasPrevious: boolean = false;
+  @LocalStorageProp('isLoading') isLoading: boolean = false;
+  @LocalStorageProp('isFavorite') isFavorite: boolean = false; // Favorite status
+  @LocalStorageProp('imageColorHex') imageColorHex: string = '2A2A2A';
+  @LocalStorageProp('imgName') imgName: string = ''; // 图片文件名,用于memory://协议
+
+  /**
+   * 格式化歌曲标题显示
+   */
+  private getDisplayTitle(): string {
+    if (!this.songTitle || this.songTitle.trim() === '') {
+      return '暂无播放';
+    }
+    return this.songTitle;
+  }
+
+  /**
+   * 格式化艺术家显示
+   */
+  private getDisplayArtist(): string {
+    if (!this.songArtist || this.songArtist.trim() === '') {
+      return '未知艺术家';
+    }
+    return this.songArtist;
+  }
+  /**
+   * 检查是否为网络URL
+   */
+  private isNetworkUrl(url: string): boolean {
+    return url.startsWith('http://') || url.startsWith('https://');
+  }
+
+  /**
+   * 生成渐变色
+   * 根据封面图片的主色调生成合适的渐变背景
+   */
+  private generateGradientColors(): [string, number][] {
+    // 如果没有封面或颜色信息,使用默认渐变
+    if (!this.imageColorHex || this.imageColorHex.trim() === '' || this.imageColorHex === '2A2A2A') {
+      return this.getDefaultGradientColors();
+    }
+
+    const baseColor = this.imageColorHex;
+    
+    // 解析RGB值
+    const r = parseInt(baseColor.substring(0, 2), 16);
+    const g = parseInt(baseColor.substring(2, 4), 16);
+    const b = parseInt(baseColor.substring(4, 6), 16);
+    
+    // 计算亮度,用于判断是深色还是浅色
+    const brightness = (r * 299 + g * 587 + b * 114) / 1000;
+    
+    // 根据亮度调整渐变策略
+    if (brightness < 60) {
+      // 深色封面:从更深的色调渐变到原色调
+      return this.generateDarkGradient(r, g, b);
+    } else if (brightness > 180) {
+      // 浅色封面:降低亮度,创建柔和渐变
+      return this.generateLightGradient(r, g, b);
+    } else {
+      // 中等亮度:标准渐变
+      return this.generateStandardGradient(r, g, b);
+    }
+  }
+
+  /**
+   * 生成深色渐变(适用于深色封面)
+   */
+  private generateDarkGradient(r: number, g: number, b: number): [string, number][] {
+    // 生成更深的起始色
+    const darkerR = Math.max(0, Math.floor(r * 0.2));
+    const darkerG = Math.max(0, Math.floor(g * 0.2));
+    const darkerB = Math.max(0, Math.floor(b * 0.2));
+    
+    // 中间色稍微提亮
+    const midR = Math.floor(r * 0.6);
+    const midG = Math.floor(g * 0.6);
+    const midB = Math.floor(b * 0.6);
+    
+    // 终点色适度提亮
+    const lightR = Math.min(255, Math.floor(r * 1.2));
+    const lightG = Math.min(255, Math.floor(g * 1.2));
+    const lightB = Math.min(255, Math.floor(b * 1.2));
+    
+    const darkerColor = `#ff${darkerR.toString(16).padStart(2, '0')}${darkerG.toString(16).padStart(2, '0')}${darkerB.toString(16).padStart(2, '0')}`;
+    const midColor = `#ff${midR.toString(16).padStart(2, '0')}${midG.toString(16).padStart(2, '0')}${midB.toString(16).padStart(2, '0')}`;
+    const lightColor = `#ff${lightR.toString(16).padStart(2, '0')}${lightG.toString(16).padStart(2, '0')}${lightB.toString(16).padStart(2, '0')}`;
+    
+    return [
+      [darkerColor, 0.0],
+      [midColor, 0.6],
+      [lightColor, 1.0]
+    ];
+  }
+
+  /**
+   * 生成浅色渐变(适用于浅色封面)
+   */
+  private generateLightGradient(r: number, g: number, b: number): [string, number][] {
+    // 大幅降低亮度作为起始色
+    const darkerR = Math.floor(r * 0.3);
+    const darkerG = Math.floor(g * 0.3);
+    const darkerB = Math.floor(b * 0.3);
+    
+    // 中间色适度降低亮度
+    const midR = Math.floor(r * 0.5);
+    const midG = Math.floor(g * 0.5);
+    const midB = Math.floor(b * 0.5);
+    
+    // 终点色保持相对较暗,避免过亮
+    const endR = Math.floor(r * 0.7);
+    const endG = Math.floor(g * 0.7);
+    const endB = Math.floor(b * 0.7);
+    
+    const darkerColor = `#ff${darkerR.toString(16).padStart(2, '0')}${darkerG.toString(16).padStart(2, '0')}${darkerB.toString(16).padStart(2, '0')}`;
+    const midColor = `#ff${midR.toString(16).padStart(2, '0')}${midG.toString(16).padStart(2, '0')}${midB.toString(16).padStart(2, '0')}`;
+    const endColor = `#ff${endR.toString(16).padStart(2, '0')}${endG.toString(16).padStart(2, '0')}${endB.toString(16).padStart(2, '0')}`;
+    
+    return [
+      [darkerColor, 0.0],
+      [midColor, 0.5],
+      [endColor, 1.0]
+    ];
+  }
+
+  /**
+   * 生成标准渐变(适用于中等亮度封面)
+   */
+  private generateStandardGradient(r: number, g: number, b: number): [string, number][] {
+    // 生成较深的颜色作为渐变起始
+    const darkerR = Math.max(0, Math.floor(r * 0.4));
+    const darkerG = Math.max(0, Math.floor(g * 0.4));
+    const darkerB = Math.max(0, Math.floor(b * 0.4));
+    
+    // 生成中等亮度的颜色
+    const midR = Math.floor(r * 0.7);
+    const midG = Math.floor(g * 0.7);
+    const midB = Math.floor(b * 0.7);
+    
+    const darkerColor = `#ff${darkerR.toString(16).padStart(2, '0')}${darkerG.toString(16).padStart(2, '0')}${darkerB.toString(16).padStart(2, '0')}`;
+    const midColor = `#ff${midR.toString(16).padStart(2, '0')}${midG.toString(16).padStart(2, '0')}${midB.toString(16).padStart(2, '0')}`;
+    const originalColor = `#ff${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
+    
+    return [
+      [darkerColor, 0.0],
+      [midColor, 0.5],
+      [originalColor, 1.0]
+    ];
+  }
+
+  /**
+   * 获取默认渐变色(当没有封面时使用)
+   */
+  private getDefaultGradientColors(): [string, number][] {
+    return [
+      ['#ff1a1a1a', 0.0],  // 深灰色
+      ['#ff2a2a2a', 0.5],  // 中灰色
+      ['#ff3a3a3a', 1.0]   // 浅灰色
+    ];
+  }
+
+  /**
+   * 获取专辑封面
+   */
+  private getCoverImage(): Resource | string {
+    console.info(`Heanup PlayerWidgetRectangle: getCoverImage called, coverImage='${this.coverImage}', imgName='${this.imgName}'`);
+
+    // 优先使用通过formImages传递的本地图片(支持网络图片下载后的显示)
+    if (this.imgName && this.imgName.trim() !== '') {
+      const memoryUrl = 'memory://' + this.imgName;
+      console.info(`Heanup PlayerWidgetRectangle: Using memory image: ${memoryUrl}`);
+      return memoryUrl;
+    }
+
+    // 如果有coverImage且不是网络URL,使用本地路径
+    if (this.coverImage && this.coverImage.trim() !== '' && !this.isNetworkUrl(this.coverImage)) {
+      console.info(`Heanup PlayerWidgetRectangle: Using local cover image: ${this.coverImage}`);
+      return this.coverImage;
+    }
+
+    // 默认使用内置图片
+    console.info(`Heanup PlayerWidgetRectangle: Using default cover image`);
+    return $r('app.media.ic_avatar4'); // 使用默认专辑封面
+  }
+
+  build() {
+    RelativeContainer() {
+      // 歌曲标题
+      Text(this.getDisplayTitle())
+        .fontSize(16)
+        .fontWeight(FontWeight.Bold)
+        .width('60%')
+        .fontColor(Color.White)
+        .textOverflow({ overflow: TextOverflow.Ellipsis })
+        .maxLines(1)
+        .alignRules(RectangleTitleAlignRules)
+        .margin({ left: 16, top: 8 })
+        .id('musicTitle')
+
+      // 艺术家名称
+      Text(this.getDisplayArtist())
+        .fontSize(12)
+        .fontColor('#CCFFFFFF')
+        .fontWeight(FontWeight.Normal)
+        .maxLines(1)
+        .alignRules(RectangleSingerAlignRules)
+        .margin({ left: 16, top: 2 })
+        .id('singerText')
+
+      // 专辑封面
+      Image(this.getCoverImage())
+        .width(88)
+        .height(88)
+        .borderRadius(8) // Square cover with rounded corners
+        .alignRules(RectangleCoverAlignRules)
+        .id('musicCover')
+        .onClick(() => {
+          postCardAction(this, {
+            'action': 'router',
+            'abilityName': 'EntryAbility'
+          });
+        })
+
+      // 播放控制按钮区域
+      Row() {
+        // 上一首按钮
+        Button() {
+          Image($r('app.media.hm_previous'))
+            .width(20)
+            .height(20)
+            .fillColor('#E5FFFFFF')
+        }
+        .width(40)
+        .height(40)
+        .backgroundColor(Color.Transparent)
+        .opacity(this.hasPrevious ? 1.0 : 0.5)
+        .onClick(() => {
+          console.info(`Heanup PlayerWidgetRectangle: Previous button clicked`);
+          if (!this.isLoading && this.hasPrevious) {
+            postCardAction(this, {
+              'action': 'call',
+              'abilityName': 'EntryAbility',
+              'params': {
+                'formId': this.formId,
+                'method': 'prevSong'
+              }
+            });
+          }
+        })
+
+        // 播放/暂停按钮
+        Button() {
+          SymbolGlyph(this.isPlaying ? $r('sys.symbol.pause_round_triangle_fill') : $r('sys.symbol.play_round_triangle_fill'))
+            .fontSize(36)
+            .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true)
+            .fontColor(['#E5FFFFFF'])
+        }
+        .width(48)
+        .height(48)
+        .backgroundColor(Color.Transparent)
+        .margin({ left: 20, right: 20 })
+        .onClick(() => {
+          console.info('Heanup PlayerWidgetRectangle: Play/Pause button clicked');
+          if (!this.isLoading) {
+            postCardAction(this, {
+              'action': 'call',
+              'abilityName': 'EntryAbility',
+              'params': {
+                'formId': this.formId,
+                'method': 'playPause',
+                'widgetIsPlaying': this.isPlaying // 传递卡片当前显示的播放状态
+              }
+            });
+          }
+        })
+
+        // 下一首按钮
+        Button() {
+          Image($r('app.media.hm_next'))
+            .width(20)
+            .height(20)
+            .fillColor('#E5FFFFFF')
+        }
+        .width(40)
+        .height(40)
+        .backgroundColor(Color.Transparent)
+        .opacity(this.hasNext ? 1.0 : 0.5)
+        .onClick(() => {
+          console.info(`Heanup PlayerWidgetRectangle: Next button clicked`);
+          if (!this.isLoading && this.hasNext) {
+            postCardAction(this, {
+              'action': 'call',
+              'abilityName': 'EntryAbility',
+              'params': {
+                'formId': this.formId,
+                'method': 'nextSong'
+              }
+            });
+          }
+        })
+        
+        // 收藏按钮
+        Button() {
+          SymbolGlyph(this.isFavorite ? $r('sys.symbol.heart_fill') : $r('sys.symbol.heart'))
+            .width(20)
+            .height(20)
+            .fontColor(['#E5FFFFFF'])
+        }
+        .width(40)
+        .height(40)
+        .backgroundColor(Color.Transparent)
+        .onClick(() => {
+          console.info(`Heanup PlayerWidgetRectangle: Favorite button clicked`);
+          if (!this.isLoading) {
+            postCardAction(this, {
+              'action': 'call',
+              'abilityName': 'EntryAbility',
+              'params': {
+                'formId': this.formId,
+                'method': 'toggleFavorite'
+              }
+            });
+          }
+        })
+      }
+      .width('100%')
+      .justifyContent(FlexAlign.Center)
+      .alignRules(RectanglePlayControlAlignRules)
+      .id('playControls')
+    }
+    .height('100%')
+    .width('100%')
+    .padding(12)
+    .backgroundImage(this.getCoverImage())
+    .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+    .backgroundImageSize(ImageSize.Cover)
+    .onClick(() => {
+      console.info('Heanup PlayerWidgetRectangle: Container clicked, jumping to main app');
+      postCardAction(this, {
+        'action': 'router',
+        'abilityName': 'EntryAbility'
+      });
+    })
+  }
+}

+ 3 - 0
entry/src/main/resources/base/media/ic_favorite.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.5 0C12.76 0 11.09 0.81 10 2.09C8.91 0.81 7.24 0 5.5 0C2.42 0 0 2.42 0 5.5C0 9.28 3.4 12.36 8.55 17.04L10 18.35L11.45 17.03C16.6 12.36 20 9.28 20 5.5C20 2.42 17.58 0 14.5 0ZM10 15.45L2 7.22C2 4.53 4.53 2 7.22 2C8.66 2 10 2.67 10 4C10 2.67 11.34 2 12.78 2C15.47 2 18 4.53 18 7.22L10 15.45Z" fill="#E5FFFFFF"/>
+</svg>

+ 3 - 0
entry/src/main/resources/base/media/ic_favorite_filled.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 18.35L8.55 17.03C3.4 12.36 0 9.28 0 5.5C0 2.42 2.42 0 5.5 0C7.24 0 8.91 0.81 10 2.09C11.09 0.81 12.76 0 14.5 0C17.58 0 20 2.42 20 5.5C20 9.28 16.6 12.36 11.45 17.04L10 18.35Z" fill="#E5FFFFFF"/>
+</svg>

+ 19 - 0
entry/src/main/resources/base/profile/form_config.json

@@ -56,6 +56,25 @@
       "supportDimensions": [
         "2*2"
       ]
+    },
+    {
+      "name": "PlayerWidgetRectangle",
+      "description": "方形封面播放器卡片",
+      "src": "./ets/widget/pages/PlayerWidgetRectangle.ets",
+      "uiSyntax": "arkts",
+      "window": {
+        "designWidth": 720,
+        "autoDesignWidth": true
+      },
+      "colorMode": "auto",
+      "isDefault": false,
+      "updateEnabled": true,
+      "scheduledUpdateTime": "10:30",
+      "updateDuration": 1,
+      "defaultDimension": "2*4",
+      "supportDimensions": [
+        "2*4"
+      ]
     }
   ]
 }

+ 0 - 8
oh-package-lock.json5

@@ -18,7 +18,6 @@
     "@pura/harmony-utils@^1.2.4": "@pura/harmony-utils@1.2.4",
     "@pura/spinkit@^1.0.4": "@pura/spinkit@1.0.4",
     "@seagazer/cclyric@lib": "@seagazer/cclyric@lib",
-    "@simplepeng/spider-man@^1.0.1": "@simplepeng/spider-man@1.0.1",
     "@sj/ffmpeg@^1.2.5": "@sj/ffmpeg@1.2.5",
     "@taobao-ohos/utdid_sdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.32/oh_modules/@cashier_alipay/cashiersdk/lib/utdid_sdk-1.0.9.har": "@taobao-ohos/utdid_sdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.32/oh_modules/@cashier_alipay/cashiersdk/lib/utdid_sdk-1.0.9.har",
     "class-transformer@^0.5.1": "class-transformer@0.5.1",
@@ -130,13 +129,6 @@
       "resolved": "lib",
       "registryType": "local"
     },
-    "@simplepeng/spider-man@1.0.1": {
-      "name": "@simplepeng/spider-man",
-      "version": "1.0.1",
-      "integrity": "sha512-rulclyolBPbYUzEav8crOr5WDuQs+zVWpLtefE2ei0YmBIbVUrpJN8mDRPNI+5kHZdkbYufKWyM/IzbVB/KoKg==",
-      "resolved": "https://repo.harmonyos.com/ohpm/@simplepeng/spider-man/-/spider-man-1.0.1.har",
-      "registryType": "ohpm"
-    },
     "@sj/ffmpeg@1.2.5": {
       "name": "@sj/ffmpeg",
       "version": "1.2.5",