|
@@ -135,6 +135,24 @@ export interface WidgetFormData {
|
|
|
formImages: Record<string, number>;
|
|
formImages: Record<string, number>;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 缓存的静态卡片数据接口
|
|
|
|
|
+ * 用于缓存不会随播放状态改变的数据,避免重复计算和IO
|
|
|
|
|
+ */
|
|
|
|
|
+interface CachedWidgetData {
|
|
|
|
|
+ // 歌曲的基本信息
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ artist: string;
|
|
|
|
|
+ album: string;
|
|
|
|
|
+ pixelMapPath: string; // 原始封面路径
|
|
|
|
|
+ duration: number;
|
|
|
|
|
+ filePath: string;
|
|
|
|
|
+ imageColorHex: string;
|
|
|
|
|
+ // 处理后的最终图片路径(本地或缓存),用于快速打开
|
|
|
|
|
+ finalImagePath: string | null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 统一播放器服务接口
|
|
* 统一播放器服务接口
|
|
|
* 提供统一的播放控制接口,替代LocalMusic中的播放器逻辑
|
|
* 提供统一的播放控制接口,替代LocalMusic中的播放器逻辑
|
|
@@ -451,6 +469,9 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
private readonly MAX_CACHE_SIZE_MB: number = 50; // 最大缓存大小:50MB
|
|
private readonly MAX_CACHE_SIZE_MB: number = 50; // 最大缓存大小:50MB
|
|
|
private hasChecked: boolean=false;
|
|
private hasChecked: boolean=false;
|
|
|
|
|
|
|
|
|
|
+ // 卡片数据缓存
|
|
|
|
|
+ private widgetDataCache: CachedWidgetData | null = null;
|
|
|
|
|
+
|
|
|
private constructor() {
|
|
private constructor() {
|
|
|
this.playerManager = PlayerManager.getInstance();
|
|
this.playerManager = PlayerManager.getInstance();
|
|
|
this.stateModel = new PlayerStateModel();
|
|
this.stateModel = new PlayerStateModel();
|
|
@@ -3007,19 +3028,20 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public async getWidgetFormData(loadImage: boolean = true): Promise<widgeData | undefined> {
|
|
public async getWidgetFormData(loadImage: boolean = true): Promise<widgeData | undefined> {
|
|
|
- let imgMap: Record<string, number> = {};
|
|
|
|
|
this.updatePlaylistStateInModelSync();
|
|
this.updatePlaylistStateInModelSync();
|
|
|
- const formData: VideoItem = this.getCurrentSong() as VideoItem; // VideoItem
|
|
|
|
|
|
|
+ const formData: VideoItem = this.getCurrentSong()!; // VideoItem
|
|
|
const songState: PlayerState = this.getCurrentState(); // PlayerState
|
|
const songState: PlayerState = this.getCurrentState(); // PlayerState
|
|
|
-
|
|
|
|
|
|
|
+ let imgMap: Record<string, number> = {};
|
|
|
|
|
+
|
|
|
// 从持久化存储获取所有formId - 使用新的getContext方法
|
|
// 从持久化存储获取所有formId - 使用新的getContext方法
|
|
|
let context = this.getContext();
|
|
let context = this.getContext();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 如果还是没有context,尝试从AppStorage获取
|
|
// 如果还是没有context,尝试从AppStorage获取
|
|
|
if (!context) {
|
|
if (!context) {
|
|
|
context = AppStorage.get('context') as common.UIAbilityContext;
|
|
context = AppStorage.get('context') as common.UIAbilityContext;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 1. 准备动态数据 (这部分总是实时的)
|
|
|
// 构建卡片数据,包含VideoItem和PlayerState信息
|
|
// 构建卡片数据,包含VideoItem和PlayerState信息
|
|
|
const widgetPlayerState: WidgetPlayerState = {
|
|
const widgetPlayerState: WidgetPlayerState = {
|
|
|
isPlaying: songState.isPlaying || false,
|
|
isPlaying: songState.isPlaying || false,
|
|
@@ -3044,7 +3066,7 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
if (!formData) {
|
|
if (!formData) {
|
|
|
- console.log('UnifiedPlayerService getWidgetFormData: No formData available - formData:'+json.stringify(formData))
|
|
|
|
|
|
|
+ LogUtils.getInstance().LOGI('UnifiedPlayerService getWidgetFormData: No current song, returning default data.');
|
|
|
const defaultData: widgeData = {
|
|
const defaultData: widgeData = {
|
|
|
id: '',
|
|
id: '',
|
|
|
name: '未知歌曲',
|
|
name: '未知歌曲',
|
|
@@ -3075,96 +3097,87 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
}
|
|
}
|
|
|
return defaultData;
|
|
return defaultData;
|
|
|
}
|
|
}
|
|
|
- console.log('UnifiedPlayerService getWidgetFormData - context: '+json.stringify(context)+" - formData:"+json.stringify(formData))
|
|
|
|
|
- // 处理封面图片
|
|
|
|
|
- let imgName: string = '';
|
|
|
|
|
-
|
|
|
|
|
- // 如果歌曲有封面图片路径,尝试处理图片
|
|
|
|
|
- if (loadImage && formData.pixelMapPath && formData.pixelMapPath.trim() !== '') {
|
|
|
|
|
- try {
|
|
|
|
|
- // 生成唯一的图片名称,确保每次更新时图片能正确刷新
|
|
|
|
|
- imgName = 'songCover_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
|
|
|
|
|
|
|
|
- // 判断是否为网络图片
|
|
|
|
|
- if (formData.pixelMapPath.startsWith('http://') || formData.pixelMapPath.startsWith('https://')) {
|
|
|
|
|
- // 处理网络图片:使用缓存机制
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Processing network image ${formData.pixelMapPath}`);
|
|
|
|
|
|
|
+ // 2. 处理静态数据(利用缓存)
|
|
|
|
|
+ // 如果歌曲已切换,则清空缓存
|
|
|
|
|
+ if (this.widgetDataCache?.filePath !== formData.filePath) {
|
|
|
|
|
+ this.widgetDataCache = null;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ if (!this.widgetDataCache) {
|
|
|
|
|
+ LogUtils.getInstance().LOGI(`UnifiedPlayerService: Cache miss for [${formData.name}], generating static data.`);
|
|
|
|
|
+ // 缓存未命中,生成并缓存静态数据
|
|
|
|
|
+ let finalImagePath: string | null = null;
|
|
|
|
|
+ if (loadImage && formData.pixelMapPath && formData.pixelMapPath.trim() !== '') {
|
|
|
|
|
+ if (formData.pixelMapPath.startsWith('http')) {
|
|
|
|
|
+ // 网络图片,走缓存/下载逻辑
|
|
|
if (this.imageCacheDir) {
|
|
if (this.imageCacheDir) {
|
|
|
const cacheFileName = this.generateCacheFileName(formData.pixelMapPath);
|
|
const cacheFileName = this.generateCacheFileName(formData.pixelMapPath);
|
|
|
- const cacheFilePath = this.imageCacheDir + '/' + cacheFileName;
|
|
|
|
|
-
|
|
|
|
|
- // 检查缓存是否存在且有效
|
|
|
|
|
|
|
+ const cacheFilePath = `${this.imageCacheDir}/${cacheFileName}`;
|
|
|
if (await this.isCacheValid(cacheFilePath)) {
|
|
if (await this.isCacheValid(cacheFilePath)) {
|
|
|
- // 使用缓存文件
|
|
|
|
|
- LogUtils.getInstance().LOGI(`UnifiedPlayerService updateAllForms: Using cached image: ${cacheFilePath}`);
|
|
|
|
|
- try {
|
|
|
|
|
- const file = fileIo.openSync(cacheFilePath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
- imgMap[imgName] = file.fd;
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Opened cached image with fd ${file.fd}, imgName: ${imgName}`);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Failed to open cached image: ${error}`);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ finalImagePath = cacheFilePath;
|
|
|
} else {
|
|
} else {
|
|
|
- // 下载到缓存
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Downloading image to cache: ${cacheFilePath}`);
|
|
|
|
|
const downloadSuccess = await this.downloadImageToCache(formData.pixelMapPath, cacheFilePath);
|
|
const downloadSuccess = await this.downloadImageToCache(formData.pixelMapPath, cacheFilePath);
|
|
|
-
|
|
|
|
|
if (downloadSuccess) {
|
|
if (downloadSuccess) {
|
|
|
- try {
|
|
|
|
|
- const file = fileIo.openSync(cacheFilePath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
- imgMap[imgName] = file.fd;
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Opened downloaded image with fd ${file.fd}, imgName: ${imgName}`);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Failed to open downloaded image: ${error}`);
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Failed to download network image ${formData.pixelMapPath}`);
|
|
|
|
|
|
|
+ finalImagePath = cacheFilePath;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- LogUtils.getInstance().LOGI(`UnifiedPlayerService updateAllForms: Image cache directory not available`);
|
|
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // 处理本地图片文件
|
|
|
|
|
- let file = fileIo.openSync(formData.pixelMapPath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
- imgMap[imgName] = file.fd;
|
|
|
|
|
-
|
|
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Opened local cover image ${formData.pixelMapPath} with fd ${file.fd}, imgName: ${imgName}`);
|
|
|
|
|
|
|
+ // 本地图片
|
|
|
|
|
+ finalImagePath = formData.pixelMapPath;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.widgetDataCache = {
|
|
|
|
|
+ id: formData.id || '',
|
|
|
|
|
+ name: formData.name || '',
|
|
|
|
|
+ artist: formData.artist || '',
|
|
|
|
|
+ album: formData.album || '',
|
|
|
|
|
+ pixelMapPath: formData.pixelMapPath || '',
|
|
|
|
|
+ duration: typeof formData.duration === 'string' ? parseInt(formData.duration || '0') : (formData.duration || 0),
|
|
|
|
|
+ filePath: formData.filePath || '',
|
|
|
|
|
+ imageColorHex: '2A2A2A', // 默认颜色,UI侧会根据图片重新提取
|
|
|
|
|
+ finalImagePath: finalImagePath
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ LogUtils.getInstance().LOGI(`UnifiedPlayerService: Cache hit for [${formData.name}].`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 组合静态和动态数据,并处理图片文件描述符
|
|
|
|
|
+ const cachedData = this.widgetDataCache;
|
|
|
|
|
+
|
|
|
|
|
+ let imgName: string = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (cachedData.finalImagePath) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 每次都重新打开文件获取新的fd,这是最安全的方式
|
|
|
|
|
+ const file = fileIo.openSync(cachedData.finalImagePath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
+ // 使用时间戳和随机数生成唯一名称,强制卡片刷新图片
|
|
|
|
|
+ imgName = `songCover_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
|
|
|
+ imgMap[imgName] = file.fd;
|
|
|
|
|
+ // 注意:我们不关闭这个fd,系统会在处理完卡片数据后关闭它
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- LogUtils.getInstance()
|
|
|
|
|
- .LOGI(`UnifiedPlayerService updateAllForms: Failed to process cover image ${formData.pixelMapPath}: ${JSON.stringify(error as BusinessError)}`);
|
|
|
|
|
- // 如果处理图片失败,使用空的imgMap
|
|
|
|
|
|
|
+ LogUtils.getInstance().LOGI(`UnifiedPlayerService: Failed to open image file [${cachedData.finalImagePath}]: ${JSON.stringify(error)}`);
|
|
|
imgMap = {};
|
|
imgMap = {};
|
|
|
imgName = '';
|
|
imgName = '';
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- LogUtils.getInstance().LOGI(`UnifiedPlayerService updateAllForms: No cover image path available`);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 平铺数据结构,确保桌面卡片能正确接收
|
|
|
|
|
|
|
+ // 4. 构建最终的完整数据对象
|
|
|
const widgetData: widgeData = {
|
|
const widgetData: widgeData = {
|
|
|
- // VideoItem数据平铺
|
|
|
|
|
- id: formData.id || '',
|
|
|
|
|
- name: formData.name || '',
|
|
|
|
|
- artist: formData.artist || '',
|
|
|
|
|
- album: formData.album || '',
|
|
|
|
|
- pixelMapPath: formData.pixelMapPath || '',
|
|
|
|
|
- duration: typeof formData.duration === 'string' ? parseInt(formData.duration || '0') : (formData.duration || 0),
|
|
|
|
|
- filePath: formData.filePath || '',
|
|
|
|
|
|
|
+ // 从缓存读取静态数据
|
|
|
|
|
+ id: cachedData.id,
|
|
|
|
|
+ name: cachedData.name,
|
|
|
|
|
+ artist: cachedData.artist,
|
|
|
|
|
+ album: cachedData.album,
|
|
|
|
|
+ pixelMapPath: cachedData.pixelMapPath,
|
|
|
|
|
+ duration: cachedData.duration,
|
|
|
|
|
+ filePath: cachedData.filePath,
|
|
|
imgName: imgName,
|
|
imgName: imgName,
|
|
|
- imageColorHex: '2A2A2A',
|
|
|
|
|
|
|
+ imageColorHex: cachedData.imageColorHex,
|
|
|
|
|
|
|
|
- // PlayerState数据平铺
|
|
|
|
|
|
|
+ // PlayerState数据平铺 (实时)
|
|
|
isPlaying: widgetPlayerState.isPlaying,
|
|
isPlaying: widgetPlayerState.isPlaying,
|
|
|
isPaused: widgetPlayerState.isPaused,
|
|
isPaused: widgetPlayerState.isPaused,
|
|
|
isLoading: widgetPlayerState.isLoading,
|
|
isLoading: widgetPlayerState.isLoading,
|
|
@@ -3173,15 +3186,15 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
hasPrevious: widgetPlayerState.hasPrevious,
|
|
hasPrevious: widgetPlayerState.hasPrevious,
|
|
|
playMode: widgetPlayerState.playMode,
|
|
playMode: widgetPlayerState.playMode,
|
|
|
|
|
|
|
|
- // 播放列表信息平铺
|
|
|
|
|
|
|
+ // 播放列表信息平铺 (实时)
|
|
|
currentIndex: widgetPlaylistInfo.currentIndex,
|
|
currentIndex: widgetPlaylistInfo.currentIndex,
|
|
|
totalCount: widgetPlaylistInfo.totalCount,
|
|
totalCount: widgetPlaylistInfo.totalCount,
|
|
|
|
|
|
|
|
- // 时间信息平铺
|
|
|
|
|
|
|
+ // 时间信息平铺 (实时)
|
|
|
currentTimeText: widgetTimeInfo.currentTimeText,
|
|
currentTimeText: widgetTimeInfo.currentTimeText,
|
|
|
totalTimeText: widgetTimeInfo.totalTimeText,
|
|
totalTimeText: widgetTimeInfo.totalTimeText,
|
|
|
progressPercentage: widgetTimeInfo.progressPercentage,
|
|
progressPercentage: widgetTimeInfo.progressPercentage,
|
|
|
-
|
|
|
|
|
|
|
+ isFavorite: this.getCurrentSongFavoriteState(), // 收藏状态 (实时)
|
|
|
// 保持嵌套结构兼容性
|
|
// 保持嵌套结构兼容性
|
|
|
playerState: widgetPlayerState,
|
|
playerState: widgetPlayerState,
|
|
|
playlistInfo: widgetPlaylistInfo,
|
|
playlistInfo: widgetPlaylistInfo,
|
|
@@ -3189,7 +3202,6 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
|
|
|
|
|
// 卡片图片显示必填字段
|
|
// 卡片图片显示必填字段
|
|
|
formImages: imgMap,
|
|
formImages: imgMap,
|
|
|
- isFavorite: this.getCurrentSongFavoriteState(),
|
|
|
|
|
};
|
|
};
|
|
|
return widgetData;
|
|
return widgetData;
|
|
|
}
|
|
}
|