|
|
@@ -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'
|
|
|
+ });
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|