import TitleBar from './TitleBar' import { curves, display, PiPWindow, promptAction, router, SymbolGlyphModifier, window } from '@kit.ArkUI'; import { VideoItem } from '../viewmodel/VideoItem'; import { AppUtil, ArrayUtil, Base64Util, DateUtil, DeviceUtil, DisplayUtil, FileUtil, ImageUtil, LogUtil, MD5, PreferencesUtil, RandomUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'; import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@kit.ArkData'; import { CommonConstants } from '../common/constants/CommonConstants'; import Logger from '../common/util/Logger'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { Utility } from '../common/util/Utility'; import { BusinessError, emitter } from '@kit.BasicServicesKit'; import commonEventManager from '@ohos.commonEventManager'; import { BubbleBean } from '../viewmodel/BubbleBean'; import { PopupPosition, XPopup } from '@chinalike/popup'; import { common, ConfigurationConstant } from '@kit.AbilityKit'; import { AnimationHelper, DialogAction, DialogHelper } from '@pura/harmony-dialog'; import { fileIo, fileUri, picker } from '@kit.CoreFileKit'; import { MessageEvents, util, worker, ErrorEvent } from '@kit.ArkTS'; import { Verify } from './Verify'; import { RotatingCover } from './RotatingCover'; import { PlayConstants } from '../common/constants/PlayConstants'; import { effectKit } from '@kit.ArkGraphics2D'; import { ColorConversion } from '../common/util/ColorConversion'; import { LyricController, LyricParser, LyricView2 } from '@seagazer/cclyric'; import { AvSessionController } from '../controller/AvSessionController'; import { AvSessionWidgetListener } from '../common/widget/AvSessionWidgetListener'; import { WidgetData, PlayProgress, PlayState, SongInfo, PlaylistState, PlayerStateBroadcastData } from '../common/widget/WidgetTypes'; import { PlayerStateListener, PlayerState, PlayerError } from '../common/service/PlayerStateModel'; import { WIDGET_CONTROL_EVENT, WIDGET_REQUEST_STATE_EVENT, PLAYER_STATE_CHANGED_EVENT, PLAYER_SONG_CHANGED_EVENT, PLAYER_PROGRESS_CHANGED_EVENT } from '../common/widget/WidgetEventConstants'; import { IjkMediaPlayer, // DeviceChangeReason, InterruptEvent, InterruptHintType, LogUtils } from '@ohos/ijkplayer'; import { PlayStatus } from '../common/PlayStatus'; import fs from '@ohos.file.fs'; import { image } from '@kit.ImageKit'; import { AVCastPicker, AVCastPickerState, AVCastPickerStyle, avSession } from '@kit.AVSessionKit'; import { UniversalDetector } from '@ohos/juniversalchardet'; import { SettingPage } from '../pages/SettingPage'; import { secondToTime } from '../common/util/CommUtils'; import { CommonConstants2 } from '../common/util/CommonConstants2'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { LazyDataSource } from '../common/util/LazyDataSource'; import MediaTable from '../common/util/MediaTable'; import NetAxiosUtil from '../common/util/NetAxiosUtil'; import ImageUtils from '../common/util/ImageUtils'; import { ringtone } from '@kit.RingtoneKit'; import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem'; import { resourceManager } from '@kit.LocalizationKit'; import { deviceInfo } from '@kit.BasicServicesKit'; import { HSBColorPicker, HSBColorPickerLayout } from '@keke/color-picker' import app from '@system.app'; import { TextNodeController } from './PipLyricTextBuilder'; import { IndexerView } from './IndexerView'; import { KeyCode } from '@kit.InputKit'; import { KnockController } from '../controller/KnockController'; import { WidgetCommand, EventData, WidgetControlParams } from '../common/widget/WidgetTypes'; import { UnifiedPlayerService, IPlayerService } from '../common/service/UnifiedPlayerService'; // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg'; const TAG = 'LocalMusic'; const DEFAULT_INDEX = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] //排序类型 function getTypeOrder(type: number) { switch (type) { case CommonConstants.TYPE_IS_DIR: return 1; // First case CommonConstants.TYPE_IS_CSJAD: return 2; // Middle case CommonConstants.TYPE_LOCAL: return 3; // Last default: return 4; // Unknown types, if any, go last } } const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets"); // 定义接口 interface HiCarAspectRatio { ratio: number; name: string; } const ITEM_HEIGHT_SMALL: number = 48; // 列表项小高度 const ITEM_HEIGHT: number = 58; // 列表项中高度 const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度 //功能强大的音乐播放器 @Preview @Component export struct LocalMusic { @State opacityValueImage: number = 1; @State tipPopup:boolean = false @Consume mType: number; @StorageProp('themeColor') themeColor: string = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR); themeMode: number = 0; @State titleName: string = '首页' @State textStr: string = '' // static readonly STR_MUSIC_VIDEO: string = 'Audios'; // private download_path: string = ''; @Consume rootPath: string //音频根目录 @Consume currentPath: string static readonly STR_LOCK_VIDEO: string = '.私密音频'; static readonly STR_FAC_VIDEO: string = '.我的收藏'; @State lockPath: string = '' @State favPath: string = '' static readonly STR_HISTORY_MUSIC: string = '.最近播放'; @State historyPath: string = '' @State historyList: Array = [] @Consume isHistory: boolean static readonly HISTORY_MUSIC: string = 'music_historyList'; private table: MediaTable = new MediaTable(getContext(this)) @State isZero: boolean = false @State fileList: Array = [] @State dirList: Array = [] @Consume videoLocalList: Array @State dataSource: LazyDataSource = new LazyDataSource(this.videoLocalList) @State mediaKuList: Array = []; //媒体库文件 @State artistList: Array = []; //艺术家文件 @State artistMap: Map = new Map(); //艺术家Map @State albumList: Array = []; //专辑文件 @State albumMap: Map = new Map(); //专辑Map private mScrollMap: Map = new Map(); // 艺术家、专辑名->滚动index @State imageOpacities: number[] = []; @Consume isCanBack: boolean @Consume @Watch('onModeChange') modeType: number; // 0首页,1媒体库,2艺术家,3专辑 @State showSingleLyric: boolean = false @State sortType: number = 4 //默认排序方式 @State isShowSimi: boolean = false //是否显示私密音频 @State isShowFAV: boolean = true //是否显示我的收藏 @State isShowTitleBar: boolean = true //是否显示分类导航条 @State isShowAllBar: boolean = true //是否显示播放全部条 @State isShowHistory: boolean = true //是否显示最近播放 @State isCustomizeBg: boolean = false //自定义背景界面 @State isGridMusic: boolean = false //是否网格布局 @State isCircleBtn: boolean = false //是否圆形播放按钮 @State twoFingerType: number = 3 //双支放大缩小的类型 @State isScrollHide: boolean = false //是否滚动隐藏 private widgetEventSubscriber: commonEventManager.CommonEventSubscriber | null = null; // 卡片事件订阅者 private lastProgressBroadcastTime: number = 0; // 上次进度广播时间 private readonly PROGRESS_BROADCAST_INTERVAL: number = 1000; // 进度广播间隔(1秒) private lastStateBroadcastTime: number = 0; // 上次状态广播时间 private readonly STATE_BROADCAST_INTERVAL: number = 500; // 状态广播间隔(0.5秒) @State isSameTimePlay: boolean = false //是否和其他app同时播放 @State isStartAutoPlay: boolean = false //启动后自动播放 @State isShowPlayPageBack: boolean = false //是否显示播放页返回键 @State isShowHeader: boolean = true @State isPlayListBgGrass: boolean = true //是否播放列表玻璃透明效果 @State isShowSingleLineLyric: boolean = false //是否显示播放页返回键 @State isMemoryLastPlay: boolean = false //是否启用应用退出记忆最后一首的播放进度 @State isCoverTopBig: boolean = false //顶部大封面部分手机显示会和播放控制页重叠 @State isSwipe: boolean = false //listItem的左滑开关 @State openSkipSongAnimate: boolean = true//切歌动画效果 @State customizeBgPath: string | undefined = ''; @State isDarkMode: boolean = false @State lyricTextWeight: number = 400 @State lyricTextWeightPip: number = 400 @State currentSwiperIndex: number = 0 @State blurValue: number = 0 //背景模糊 @State bgBrightness: number = 0 //背景亮度 private listScroller: ListScroller = new ListScroller() private playListScroller: Scroller = new Scroller() @StorageProp('isLandscape') isLandscape: boolean = false; @StorageProp('topRectHeight') topRectHeight: number = px2vp(AppUtil.getStatusBarHeight()); @StorageProp('windowWidth') windowWidth: number = 0; @StorageProp('windowHeight') windowHeight: number = 0; @StorageProp('isHiCarStatus') isHiCarStatus: boolean = false; @StorageProp('curDisplayIsHiCar') curDisplayIsHiCar: boolean = false; private UNKONWN:string = ''; private listArea: Area = { width: 0, height: 0, position: {}, globalPosition: {} } @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET; onColorModeChange() { this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK if (this.isCustomizeBg) { this.titleBarModel.setRightTitleStateNormalStyleColor(Color.Transparent) this.titleBarModel.setTitleBarBackground(Color.Transparent) this.titleBarModel.setTitleBarBottomLineColor(Color.Transparent) this.titleBarModel.setLeftTitleStateNormalStyleColor(Color.Transparent) } else { if (!this.isDarkMode) { this.titleBarModel.setTitleBarBackground(this.themeColor) this.titleBarModel.setTitleBarBottomLineColor(this.themeColor) this.titleBarModel.setRightTitleStateNormalStyleColor(this.themeColor) this.titleBarModel.setLeftTitleStateNormalStyleColor(this.themeColor) } else { this.titleBarModel.setTitleBarBackground($r('app.color.title_bar_bg')) this.titleBarModel.setTitleBarBottomLineColor($r('app.color.title_bar_bg')) this.titleBarModel.setRightTitleStateNormalStyleColor($r('app.color.title_bar_bg')) this.titleBarModel.setLeftTitleStateNormalStyleColor($r('app.color.title_bar_bg')) } } } onModeChange() { this.isFavMusic = false if (this.modeType === 2 || this.modeType === 3) { this.titleBarModel.setRightIcon(null) } else { this.titleBarModel.setRightIcon(($r('app.media.add'))) } LogUtil.info('onecold onModeChange = ' + this.modeType) switch (this.modeType) { case 0: this.getSortedFiles(this.currentPath) break case 1: this.titleBarModel.setLeftIconMainNoAnimate($r('app.media.menu')) this.updateListData(this.mediaKuList) this.titleBarModel.setTitleName(Utility.resourceToString(this.context, $r('app.string.media_ku'))) break case 2: this.titleBarModel.setLeftIconMainNoAnimate($r('app.media.menu')) this.updateListData(this.artistList) this.titleBarModel.setTitleName(Utility.resourceToString(this.context, $r('app.string.artist'))) break case 3: this.titleBarModel.setLeftIconMainNoAnimate($r('app.media.menu')) this.updateListData(this.albumList) this.titleBarModel.setTitleName(Utility.resourceToString(this.context, $r('app.string.album'))) break } } @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined; // @StorageLink('isPlay') isPlay: boolean = false; @StorageLink('isShowPlay') isShowPlay: boolean = false; @StorageLink('songList') songList: Array = []; @State translateY: number = 0; @State isShowSheet: boolean = false; @State isShowSheetView: boolean = false; @State isShowTimeCloseView: boolean = false; private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Vertical }); @StorageLink('currIndex') curIndex: number = 0; @Consume isShowDrawer: boolean; @Consume offsetX: number; @State isFac: boolean = false; @State favList: Array = [] @Consume isFavMusic: boolean @State isClickedPLayAll: boolean = false; private knockController: KnockController | undefined = undefined; imagesF: ImageFrameInfo[] = [ { src: $r("app.media.app_loading0") }, { src: $r("app.media.app_loading1") }, { src: $r("app.media.app_loading2") }, { src: $r("app.media.app_loading3") }, ] @State animationState: AnimationStatus = AnimationStatus.Initial // 缓存机制 private cache: Map> = new Map(); private stringCache: Map = new Map(); // 新增字符串缓存 // 持久化缓存 private async saveCacheToStorage() { const cacheArray = Array.from(this.cache.entries()); const stringCacheArray = Array.from(this.stringCache.entries()); await PreferencesUtil.putSync('music_cache', JSON.stringify(cacheArray)); await PreferencesUtil.putSync('music_string_cache', JSON.stringify(stringCacheArray)); } // 载入缓存 private async loadCacheFromStorage() { const cacheString = await PreferencesUtil.getStringSync('music_cache', ''); const stringCacheString = await PreferencesUtil.getStringSync('music_string_cache', ''); // 载入字符串缓存 if (cacheString) { const cacheArray: [string, Array][] = JSON.parse(cacheString); this.cache = new Map(cacheArray); } if (stringCacheString) { const stringCacheArray: [string, string][] = JSON.parse(stringCacheString); this.stringCache = new Map(stringCacheArray); } } // 查找缓存 private findCache(key: string): Array | undefined { return this.cache.get(key); } // 查找字符串缓存 private findStringCache(key: string): string | undefined { return this.stringCache.get(key); } // 添加缓存 private addCache(key: string, value: Array) { this.cache.set(key, value); } // 添加字符串缓存 private addStringCache(key: string, value: string) { this.stringCache.set(key, value); } // 删除缓存 private deleteCache(key: string) { this.cache.delete(key); this.saveCacheToStorage() } // 多选机制 @State selectedFiles: Array = []; @State isMultiSelect: boolean = false; @State isAllSelected: boolean = false; @State appName: string = '' @State isHasDir: boolean = false @State packName: string = '' context = this.getUIContext().getHostContext() as common.UIAbilityContext @State titleBarModel: TitleBar.Model = new TitleBar.Model() .setTitleTextStyle(FontStyle.Normal) .setTitleBarStyle(TitleBar.BarStyle.TRANSPARENT) .setLeftIcon($r('app.media.menu')) .setLeftIconWidth(26) .setLeftIconHeight(26) .setTitleName('首页') .setTitleFontSize(18) .setTitleFontColor(Color.White) .setTitleBarBackground(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor) .setTitleBarBottomLineColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor) .setLeftTitleStateNormalStyleColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor) .setRightIcon($r('app.media.add')) .setOnRightClickListener(() => { this.showSheelDialog() }) // .setOnTitleClickListener(() => { // this.showPupDialog() // }) .setOnLeftClickListener(() => { this.doSwipBack() }) doSwipBack() { if (this.modeType !== 0 && this.isCanBack) { this.onModeChange() this.isCanBack = false this.titleBarModel.setLeftIconMain($r('app.media.menu')) // 返回专辑列表时恢复滚动偏移量,支持列表和网格 setTimeout(() => { if (this.modeType == 2) { if (this.isGridMusic) { let lastOffset = this.mScrollMap.get('lastGridArtistScrollOffset') ?? 0 this.scroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } else { let lastOffset = this.mScrollMap.get('lastListArtistScrollOffset') ?? 0 this.listScroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } } else if (this.modeType == 3) { if (this.isGridMusic) { let lastOffset = this.mScrollMap.get('lastGridAlBumScrollOffset') ?? 0 this.scroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } else { let lastOffset = this.mScrollMap.get('lastListAlBumScrollOffset') ?? 0 this.listScroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } } }, 200) return } if (this.isHistory || this.isFavMusic) { this.isHistory = false this.getSortedFiles(this.currentPath) return } if (this.currentPath === this.rootPath || this.modeType !== 0) { animateTo({ duration: 555 }, () => { // 动画闭包内控制Image组件的出现和消失 this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) return } //返回上一级歌单的滚动位置 setTimeout(() => { if (this.modeType == 0) { if (this.isGridMusic) { let lastOffset = this.mScrollMap.get('lastGridDirScrollOffset') ?? 0 this.scroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } else { let lastOffset = this.mScrollMap.get('lastListDirScrollOffset') ?? 0 this.listScroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } } }, 300) this.currentPath = Utility.getParentDirectory(this.currentPath) Logger.info('this.currentPath2 = ' + this.currentPath) this.getSortedFiles(this.currentPath) } // 初始化统一播放器服务 private async initUnifiedPlayerService(): Promise { try { await this.unifiedPlayerService.initialize(this.context); // 等待数据恢复完成后再获取播放列表 LogUtils.getInstance().LOGI('LocalMusic: Waiting for UnifiedPlayerService data restoration...'); await this.unifiedPlayerService.waitForDataRestoration(5000); // 从UnifiedPlayerService恢复播放列表和状态 const restoredPlaylist = this.unifiedPlayerService.getPlaylist(); const restoredIndex = this.unifiedPlayerService.getCurrentIndex(); const restoredSong = this.unifiedPlayerService.getCurrentSong(); if (ArrayUtil.isNotEmpty(restoredPlaylist)) { // 恢复播放列表到LocalMusic this.songList = restoredPlaylist; this.curIndex = restoredIndex; this.currentSong = restoredSong || undefined; // 更新UI数据源 this.sonDataSource.pushArrayData(this.songList); // 如果有当前歌曲,更新UI显示 if (this.currentSong) { this.videoUrl = this.currentSong.filePath; this.name = this.currentSong.name; this.artist = this.currentSong.artist; this.cover = this.currentSong.pixelMapPath; } LogUtils.getInstance().LOGI(`LocalMusic: Restored playlist from UnifiedPlayerService - ${this.songList.length} songs, index ${this.curIndex}, current song: ${this.currentSong?.name || 'null'}`); } else { LogUtils.getInstance().LOGI('LocalMusic: No playlist restored from UnifiedPlayerService, songList length = ' + this.songList.length); if (ArrayUtil.isNotEmpty(this.songList)) { // 如果LocalMusic有播放列表但UnifiedPlayerService没有,设置到服务中 this.unifiedPlayerService.setPlaylist(this.songList, this.curIndex); LogUtils.getInstance().LOGI(`LocalMusic: Set existing playlist to UnifiedPlayerService - ${this.songList.length} songs`); } } // 添加状态监听器,保持UI同步 class LocalMusicStateListener implements PlayerStateListener { private localMusic: LocalMusic; constructor(localMusic: LocalMusic) { this.localMusic = localMusic; } onStateChanged(state: PlayerState): void { LogUtils.getInstance().LOGI(`LocalMusic: StateListener triggered - isPlaying: ${state.isPlaying}, isPaused: ${state.isPaused}`); // 直接使用状态模型的状态,这是最权威的状态源 const isPlaying = state.isPlaying; const isPaused = state.isPaused; // 同步播放状态到LocalMusic的UI状态 const previousStatus: PlayStatus = this.localMusic.CONTROL_PlayStatus; // 根据状态模型确定UI状态 if (isPlaying) { this.localMusic.CONTROL_PlayStatus = PlayStatus.PLAY; } else if (isPaused) { this.localMusic.CONTROL_PlayStatus = PlayStatus.PAUSE; } else { this.localMusic.CONTROL_PlayStatus = PlayStatus.INIT; } // 更新播放状态相关的UI this.localMusic.setIsPlaying(isPlaying); this.localMusic.updateSessionPlayState(isPlaying); LogUtils.getInstance().LOGI(`LocalMusic: State sync - Previous: ${previousStatus}, New: ${this.localMusic.CONTROL_PlayStatus}, isPlaying: ${isPlaying}`); // 强制触发UI更新,无论状态是否变化 this.localMusic.playChange(); this.localMusic.watchStatus(); // 更新动画状态 if (isPlaying) { this.localMusic.animationState = AnimationStatus.Running; this.localMusic.mDestroyPage = false; } else { this.localMusic.animationState = AnimationStatus.Paused; this.localMusic.mDestroyPage = true; } LogUtils.getInstance().LOGI(`LocalMusic: UI update completed - CONTROL_PlayStatus: ${this.localMusic.CONTROL_PlayStatus}, globalIsPlaying: ${this.localMusic.isPlaying}`); // 同步播放模式 if (this.localMusic.playType !== state.playMode) { this.localMusic.playType = state.playMode; this.localMusic.setCurrentPlayMode(); } // 同步音量和速度 this.localMusic.volume = state.volume; this.localMusic.playSpeed = state.speed; LogUtils.getInstance().LOGI(`LocalMusic: State synchronized - isPlaying=${state.isPlaying}, mode=${state.playMode}`); } onSongChanged(song: VideoItem): void { // 立即重置时间显示状态,防止新歌曲时间显示被上一首歌曲的时间"卡住" this.localMusic.oldSeconds = 0; this.localMusic.currentTime = "00:00"; this.localMusic.lastSongPath = song.filePath; // 更新当前歌曲路径 this.localMusic.justSwitched = true; // 标记歌曲刚刚切换 // 同步当前歌曲信息 this.localMusic.currentSong = song; this.localMusic.videoUrl = song.filePath; this.localMusic.name = song.name; this.localMusic.artist = song.artist; this.localMusic.cover = song.pixelMapPath; // 更新当前索引 const currentIndex: number = this.localMusic.unifiedPlayerService.getCurrentIndex(); this.localMusic.curIndex = currentIndex; // 更新最近播放时间 this.localMusic.updateLastPlayTimeStr(song.filePath); } onProgressChanged(progress: PlayProgress): void { // 同步进度更新到UI this.localMusic.syncProgressFromService(progress); // 广播进度更新到卡片 this.localMusic.broadcastProgressIfNeeded(); } onError(error: PlayerError): void { this.localMusic.handlePlaybackError(); } } const stateListener = new LocalMusicStateListener(this); this.unifiedPlayerService.addStateListener(stateListener); // 不设置LocalMusic自己的AVSession监听器,完全依赖UnifiedPlayerService // UnifiedPlayerService已经设置了AVSession监听器,状态变化会通过StateListener传播到LocalMusic LogUtils.getInstance().LOGI('LocalMusic: Relying on UnifiedPlayerService for AVSession handling'); // 初始化完成后,立即同步当前状态到UI setTimeout(() => { try { const currentState = this.unifiedPlayerService.getCurrentState(); const actuallyPlaying = this.unifiedPlayerService.getActualPlayingState(); LogUtils.getInstance().LOGI(`LocalMusic: Syncing current state - StateModel: ${currentState.isPlaying}, ActualPlayer: ${actuallyPlaying}`); // 手动触发状态同步,确保UI显示正确(使用实际播放器状态) stateListener.onStateChanged(currentState); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic: Failed to sync current state: ${error}`); } }, 200); // 短延迟,确保组件完全初始化 LogUtils.getInstance().LOGI('LocalMusic: UnifiedPlayerService initialized successfully'); // 初始化完成后,开始加载本地文件 this.loadLocalFilesAfterServiceInit(); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic: Failed to initialize UnifiedPlayerService: ${error}`); ToastUtil.showToast('播放器服务初始化失败'); // 即使服务初始化失败,也要加载本地文件 this.loadLocalFilesAfterServiceInit(); } } private loadLocalFilesAfterServiceInit() { // 加载本地文件,然后恢复播放列表 this.getSortedFiles(this.rootPath).then(async () => { this.isFavMusic = false; // 数据恢复已在initUnifiedPlayerService中完成,直接获取播放列表 LogUtils.getInstance().LOGI('LocalMusic: Getting playlist from UnifiedPlayerService after file loading'); // 尝试从UnifiedPlayerService恢复播放列表 const unifiedPlaylist = this.unifiedPlayerService.getPlaylist(); const unifiedIndex = this.unifiedPlayerService.getCurrentIndex(); const unifiedSong = this.unifiedPlayerService.getCurrentSong(); if (ArrayUtil.isNotEmpty(unifiedPlaylist)) { // 使用UnifiedPlayerService的数据 this.songList = unifiedPlaylist; this.curIndex = unifiedIndex; this.currentSong = unifiedSong || undefined; LogUtils.getInstance().LOGI(`LocalMusic: Using playlist from UnifiedPlayerService - ${this.songList.length} songs, index ${this.curIndex}, current song: ${this.currentSong?.name || 'null'}`); } else { // 回退到旧的存储方式 LogUtils.getInstance().LOGI('LocalMusic: UnifiedPlayerService playlist empty, falling back to legacy storage'); this.songList = PreferencesUtil.getSync('LastMusicList', []) as Array this.currentSong = PreferencesUtil.getSync('LastMusicInfo', undefined) as VideoItem if (ArrayUtil.isEmpty(this.songList)) { const currentFileList = this.getCurFileList(); LogUtils.getInstance().LOGI(`LocalMusic: No legacy playlist found, current directory has ${currentFileList.length} songs`); LogUtils.getInstance().LOGI(`LocalMusic: Current path: ${this.currentPath}, Root path: ${this.rootPath}`); this.songList = currentFileList; LogUtils.getInstance().LOGI(`LocalMusic: Using current file list as fallback - ${this.songList.length} songs`); } else { this.curIndex = Utility.getIndexFromList(this.songList, this.currentSong.filePath) LogUtils.getInstance().LOGI(`LocalMusic: Using legacy playlist storage - ${this.songList.length} songs, index ${this.curIndex}`); } } if (ArrayUtil.isNotEmpty(this.songList)) { this.isFirstStartPlay = true this.sonDataSource.pushArrayData(this.songList) if (this.currentSong === undefined) { this.isFirstStartPlay = false this.currentSong = this.songList[0] } this.videoUrl = this.currentSong.filePath this.name = this.currentSong.name this.cover = this.currentSong.pixelMapPath this.artist = this.currentSong.artist } }); } // 组件生命周期 aboutToAppear() { if(PreferencesUtil.getBooleanSync('isFirstTiped',true)){ this.tipPopup = !this.tipPopup PreferencesUtil.putSync('isFirstTiped',false) } this.initSetting() this.UNKONWN = Utility.resourceToString(this.context, $r('app.string.unknown')); this.topRectHeight = px2vp(AppUtil.getStatusBarHeight()); Utility.getAppName(getContext(this)).then((appName: string) => { this.appName = appName }) this.packName = AppUtil.getBundleName() this.loadCacheFromStorage(); // 加载缓存 // 初始化统一播放器服务,然后加载文件 this.initUnifiedPlayerService().then(() => { this.mkDownLoadDir(); }); // 注意:getSortedFiles的调用已经移到initUnifiedPlayerService完成后 let eventMusic: emitter.InnerEvent = { eventId: 2 } // 监听广播事件(打开其他应用处理) emitter.on(eventMusic, (eventData: emitter.EventData) => { this.saveVideoDatas([eventData.data?.message], true) }); this.setAvSessionListener(); //侧滑广播接收时间 let eventUpdateSwipeBack: emitter.InnerEvent = { eventId: 888 } // 监听广播事件(打开其他应用处理) emitter.on(eventUpdateSwipeBack, (eventData: emitter.EventData) => { animateTo({ duration: 555 }, () => { this.doSwipBack() }) }); //ScanFilePage广播接收时间 let eventScanUpdate: emitter.InnerEvent = { eventId: 101 } // 监听ScanFilePage广播事件(更新数据库) emitter.on(eventScanUpdate, (eventData: emitter.EventData) => { this.doUpdateData() }); let eventSetting: emitter.InnerEvent = { eventId: 333 } // 监听广播事件(通用设置配置更新) emitter.on(eventSetting, (eventData: emitter.EventData) => { this.doChangeSetting() }); // 监听卡片控制事件(来自EntryAbility的转发) let eventWidgetControl: emitter.InnerEvent = { eventId: 9001 } emitter.on(eventWidgetControl, (eventData: emitter.EventData) => { LogUtils.getInstance().LOGI(`🎵 LocalMusic received widget control via emitter: ${JSON.stringify(eventData.data)}`); if (eventData.data && (eventData.data as Record)['command']) { // 将data转换为EventData类型 const widgetEventData = eventData.data as Record; const typedEventData: EventData = { command: widgetEventData['command'] as WidgetCommand, params: (widgetEventData['params'] as WidgetControlParams) || {}, timestamp: (widgetEventData['timestamp'] as number) || Date.now(), source: (widgetEventData['source'] as string) || 'unknown' }; } }); let event: Callback = (event) => { LogUtils.getInstance().LOGI(`onecold 与其他应用音频冲突 被截断点event: ${JSON.stringify(event)}`); this.savePlaybackPosition() if (event.hintType === InterruptHintType.INTERRUPT_HINT_PAUSE) { this.pause(); } else if (event.hintType === InterruptHintType.INTERRUPT_HINT_RESUME) { this.startPlayOrResumePlay(); } else if (event.hintType === InterruptHintType.INTERRUPT_HINT_STOP) { // 对于STOP事件,使用pause而不是stop,保持播放状态以便恢复 LogUtils.getInstance().LOGI('onecold 音频冲突STOP事件,使用pause保持播放状态'); this.pause(); } } // 直接在LocalMusic中处理音频中断,响应更快 this.unifiedPlayerService.getIjkPlayer()?.on('audioInterrupt', event); // 音频设备断连回调处理 // let deviceChangeEvent: Callback = (event) => { // LogUtils.getInstance().LOGI(`Heanup deviceChange event: ${JSON.stringify(event)}`); // if (event.reason === DeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) { // 音频设备断开连接 // this.pause(); // } // } // this.mIjkMediaPlayer.on('deviceChange', deviceChangeEvent); this.makeWorker() //折叠屏的屏幕显示模式变化 display.on('foldDisplayModeChange', (data) => { this.doChangeBarHeight() }); display.on('foldStatusChange', (data) => { Logger.info('onecold foldStatusChange ') }); this.isCoverRectangle = PreferencesUtil.getBooleanSync(SettingPage.IS_COVER_RECTANGLE, true) // 读取 themeMode 并同步 currentColorMode this.themeMode = PreferencesUtil.getNumberSync(SettingPage.THEME_MODE, 0) this.applyThemeMode(this.themeMode) let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR); AppStorage.setOrCreate('themeColor', themeColor); this.themeColor = themeColor; this.doChangeSetting() // 安全地注册窗口大小变化监听器 if (this.windowClass) { this.windowClass.on('windowSizeChange', (size) => { LogUtil.info('onecold windowSizeChange') this.doChangeBarHeight() let viewWidth = px2vp(size.width); let viewHeight = px2vp(size.height); if(this.isPhoneLan()){ this.is_auto_hide_progress = false if(this.isHiCar()){ return } setTimeout(() => { this.is_auto_hide_progress = true }, 6000) }else{ this.startAutoHide() } }); } this.eventHub.on('onStateChange', (fg: boolean) => { if (fg && this.curState === 'STARTED') { this.stopPip(); } }); } startAutoHide() { if(this.isHiCar()){ return } if(PreferencesUtil.getBooleanSync(SettingPage.IS_AUTO_HIDE_PROGRESS,false)){ console.info('onecold startAutoHide') this.is_auto_hide_progress = false setTimeout(() => { this.is_auto_hide_progress = true }, 6000) } } applyThemeMode(mode: number) { let colorMode = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET if (mode === 1) { colorMode = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT } else if (mode === 2) { colorMode = ConfigurationConstant.ColorMode.COLOR_MODE_DARK } AppStorage.setOrCreate('currentColorMode', colorMode) AppStorage.setOrCreate('isDarkMode', colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) PreferencesUtil.putSync(SettingPage.THEME_MODE, mode) AppStorage.setOrCreate('themeMode', mode) this.isDarkMode = colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK // 只有强制浅色/深色时才主动设置 const context = getContext(this) as common.UIAbilityContext context.getApplicationContext().setColorMode(colorMode) } initSetting() { this.sortType = PreferencesUtil.getNumberSync(SettingPage.SORT_TYPE, 4) this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false) this.customizeBgPath = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '') this.isGridMusic = PreferencesUtil.getBooleanSync(SettingPage.IS_GRID_MUSIC, true) this.isScrollHide = PreferencesUtil.getBooleanSync(SettingPage.IS_SCROLL_HIDE, false) this.isSameTimePlay = PreferencesUtil.getBooleanSync(SettingPage.IS_SAMETIME_PLAY, false) this.isSavePlayMode = PreferencesUtil.getBooleanSync(SettingPage.iS_SAVE_PLAY_MODE, true) this.blurValue = PreferencesUtil.getNumberSync(SettingPage.CUSTOMIZE_BG_BLUR, 0) this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0) this.isStartAutoPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_START_AUTO_PLAY, false) this.isMemoryLastPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_MEMORY_LAST_PLAY, false) this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true) this.isShowAllBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_ALLBAR, true) this.twoFingerType = PreferencesUtil.getNumberSync(SettingPage.TWO_FINGER_TYPE, 3) this.isCoverTop = PreferencesUtil.getBooleanSync(SettingPage.IS_COVER_TOP, false) this.isCircleBtn = PreferencesUtil.getBooleanSync(SettingPage.IS_CIRCLE_BTN, true) this.isShowPlayPageBack = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_PLAYPAGE_BACK, true) this.isCoverTopBig = PreferencesUtil.getBooleanSync(SettingPage.IS_COVER_TOP_BIG, false) this.isShowSingleLineLyric = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_SLLYRIC, false) this.longPressSpeed = PreferencesUtil.getNumberSync(SettingPage.LONG_PRESS_SPEED, 3) this.isPlayListBgGrass = PreferencesUtil.getBooleanSync(SettingPage.IS_PLAYLIST_BG_GRASS, true) this.isShowHeader = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_HEADER, true) this.volume = PreferencesUtil.getNumberSync('DefalutVolume', this.volume) this.isSwipe = PreferencesUtil.getBooleanSync(SettingPage.IS_SWIPE, true) this.openSkipSongAnimate = PreferencesUtil.getBooleanSync(SettingPage.OPEN_SKIPSONG_ANIMATE, true) //如果是hicar连接状态,这些都是false if(this.isHiCar()){ this.isShowTitleBar = false//不显示导航栏 if(!(this.windowWidth>1800&&this.windowHeight>=1200))//小屏幕不可以Grid { this.isShowHeader = false//二级菜单不显示头部 this.isGridMusic= false//用list不用网格 this.isCoverTopBig = false } this.isShowPlayPageBack = true //播放页返回键要显示,方便驾驶员操作 } if (this.isSavePlayMode) { this.playType = PreferencesUtil.getNumberSync('musicPlayType', 0) } if (this.isCustomizeBg) { this.titleBarModel.setRightTitleStateNormalStyleColor(Color.Transparent) this.titleBarModel.setTitleBarBackground(Color.Transparent) this.titleBarModel.setTitleBarBottomLineColor(Color.Transparent) this.titleBarModel.setLeftTitleStateNormalStyleColor(Color.Transparent) } else { if (!this.isDarkMode) { this.titleBarModel.setTitleBarBackground(this.themeColor) this.titleBarModel.setTitleBarBottomLineColor(this.themeColor) this.titleBarModel.setRightTitleStateNormalStyleColor(this.themeColor) this.titleBarModel.setLeftTitleStateNormalStyleColor(this.themeColor) } else { this.titleBarModel.setTitleBarBackground($r('app.color.title_bar_bg')) this.titleBarModel.setTitleBarBottomLineColor($r('app.color.title_bar_bg')) this.titleBarModel.setRightTitleStateNormalStyleColor($r('app.color.title_bar_bg')) this.titleBarModel.setLeftTitleStateNormalStyleColor($r('app.color.title_bar_bg')) } } } doChangeSetting() { this.initSetting() this.deleteCache(this.currentPath) if (this.modeType === 0) { this.getSortedFiles(this.currentPath) } } doChangeBarHeight() { if (this.currentWidthBreakpoint !== WidthBreakpoint.WIDTH_SM || (this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_MD && this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_SM)) { this.setBarHeightNormal() } } onPageShow() { this.knockController?.immersiveListening(); app.setImageCacheCount(100); // 设置解码前图片数据内存缓存上限为100MB (100MB=100*1024*1024B=104857600B) app.setImageRawDataCacheSize(104857600); Logger.info('onecold onPageShow currentBreakpoint= ' + this.currentBreakpoint) // 修复音频中断后的状态同步问题 this.syncPlaybackStateOnShow(); } /** * 页面显示时同步播放状态,修复音频中断后的状态不一致问题 */ private syncPlaybackStateOnShow(): void { try { const currentState = this.unifiedPlayerService.getCurrentState(); const actualPlaying = this.unifiedPlayerService.getActualPlayingState(); LogUtils.getInstance().LOGI(`onPageShow state sync - StateModel: isPlaying=${currentState.isPlaying}, isPaused=${currentState.isPaused}, Actual: ${actualPlaying}`); // 如果状态不一致,强制同步 if (currentState.isPlaying !== actualPlaying) { LogUtils.getInstance().LOGI(`State inconsistency detected, forcing sync`); this.setIsPlaying(actualPlaying); this.CONTROL_PlayStatus = actualPlaying ? PlayStatus.PLAY : PlayStatus.INIT; this.playChange(); this.watchStatus(); } } catch (error) { LogUtils.getInstance().LOGI(`syncPlaybackStateOnShow error: ${error}`); } } //开启线程查看各个数据库 makeWorker() { setTimeout(() => { if (PreferencesUtil.getBooleanSync('isFirstApp', true)) { ToastUtil.showToast('请到文件扫描页面导入音乐!') //发送worker通知扫描文件入库,用户反馈4000多首用这个方法扫描会闪退。 // workerInstance.postMessage({ // code: 1, // data1: this.context, // data2: this.rootPath, // data3: this.lockPath, // data4: PreferencesUtil.getStringSync('COVER_API', ''), // }); PreferencesUtil.putSync('isFirstApp', false) } else { workerInstance.postMessage({ code: 2, data: this.context }); workerInstance.postMessage({ code: 3, data: this.context }); workerInstance.postMessage({ code: 4, data: this.context }); this.getHistoryList(false) } }, 1000) workerInstance.onmessage = (e: MessageEvents): void => { switch (e.data.code) { case 101: //第一次扫描文件夹入库 ToastUtil.showToast('刷新同步数据库成功!') Logger.info('onecold 扫描数据库结束 this.mediaKuList length= ' + this.mediaKuList.length) this.doUpdateData() break; case 102: //查询媒体库列表 this.mediaKuList = e.data.data Logger.info('onecold 查询媒体库列表 this.mediaKuList length= ' + this.mediaKuList.length) Utility.doSortListAscending(this.mediaKuList) AppStorage.setOrCreate('mediaKuList', this.mediaKuList); if (this.modeType === 1) { this.updateListData(this.mediaKuList) } break; case 103: //收到查询艺术家列表 this.artistMap = e.data.data1; this.artistList = e.data.data2; break; case 104: //收到查询专辑列表 this.albumMap = e.data.data1; this.albumList = e.data.data2 //查询完毕在启动是否自动播放 if (this.isStartAutoPlay) { this.startPlayOrResumePlay() } break; } }; // 在调用terminate后,执行onexit workerInstance.onexit = (code) => { console.log("main thread terminate"); } //若Worker处于已销毁或正在销毁等非运行状态时,调用其功能接口,会抛出相应的错误。 // 使用Worker模块时,需要在主线程中注册onerror接口,否则当worker线程出现异常时会发生jscrash问题。 workerInstance.onerror = (err: ErrorEvent) => { console.log("onerror" + err.message); } // let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext; //碰一碰分享的监听 this.knockController = KnockController.getInstance(this.context); this.knockController?.immersiveListening(); } onPageHide(): void { this.knockController?.immersiveDisableListening(); } @State isFirstStartPlay: boolean = false //获取download_path async mkDownLoadDir() { const documentViewPicker = new picker.DocumentViewPicker() let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD }) this.rootPath = new fileUri.FileUri(documentSaveResult[0]).path // this.rootPath = this.download_path this.lockPath = this.rootPath + '/' + LocalMusic.STR_LOCK_VIDEO this.favPath = this.rootPath + '/' + LocalMusic.STR_FAC_VIDEO this.historyPath = this.rootPath + '/' + LocalMusic.STR_HISTORY_MUSIC Logger.info('onecold rootPath = ' + this.rootPath) Logger.info('onecold lockPath = ' + this.lockPath) if (!FileUtil.accessSync(this.lockPath)) { FileUtil.mkdirSync(this.lockPath) } if (!FileUtil.accessSync(this.favPath)) { FileUtil.mkdirSync(this.favPath) } if (!FileUtil.accessSync(this.historyPath)) { FileUtil.mkdirSync(this.historyPath) } this.getSortedFiles(this.rootPath).then(() => { this.isFavMusic = false //穿山甲 // this.loadBannerAd(CSJUtil.getBannerID()) LogUtils.getInstance().LOGI('LocalMusic: mkDownLoadDir - Files loaded, skipping playlist restoration (already done in initUnifiedPlayerService)'); // 播放列表恢复逻辑已经在 initUnifiedPlayerService 中完成,这里不需要重复 // 只需要确保UI状态是最新的 if (ArrayUtil.isNotEmpty(this.songList)) { this.isFirstStartPlay = true this.sonDataSource.pushArrayData(this.songList) // 不要强制设置为第一首歌曲,保持从UnifiedPlayerService恢复的状态 // 只有在完全没有当前歌曲且没有有效索引时才设置默认值 if (this.currentSong === undefined && this.curIndex < 0 && this.songList.length > 0) { this.isFirstStartPlay = false this.curIndex = 0 this.currentSong = this.songList[0] LogUtils.getInstance().LOGI('LocalMusic: mkDownLoadDir - No current song found, setting to first song as fallback'); } else if (this.currentSong === undefined && this.curIndex >= 0 && this.curIndex < this.songList.length) { // 如果有有效索引但没有当前歌曲,从索引恢复 this.currentSong = this.songList[this.curIndex] LogUtils.getInstance().LOGI(`LocalMusic: mkDownLoadDir - Restored current song from index ${this.curIndex}: ${this.currentSong.name}`); } if (this.currentSong) { this.videoUrl = this.currentSong.filePath this.name = this.currentSong.name this.cover = this.currentSong.pixelMapPath AppStorage.setOrCreate('currentSong',this.currentSong); } LogUtils.getInstance().LOGI(`LocalMusic: mkDownLoadDir - UI updated with existing playlist: ${this.songList.length} songs, current index: ${this.curIndex}, current song: ${this.currentSong?.name || 'none'}`); } else { this.name = '空空如也' LogUtils.getInstance().LOGI('LocalMusic: mkDownLoadDir - No playlist available, showing empty state'); } }) this.getFavList(false) } getFavList(isFavRefresh: boolean) { this.table.queryByisFav(1, async (result: VideoItem[]) => { this.favList = result if (isFavRefresh || this.isFavMusic) { //如果是我的收藏,更新我的收藏数据 this.videoLocalList = this.favList this.updateListData(this.videoLocalList) } }) } getHistoryList(isRefresh: boolean) { this.table.queryRecentPlayedRecords(50, async (result: VideoItem[]) => { this.historyList = result // for (let i = 0; i < this.historyList.length; i++) { // // console.info(`onecold gengxin 1: ${this.historyList[i].name}`); // console.info(`onecold gengxin 1: ${this.historyList[i].lastPlayedStr}`); // } if (isRefresh) { this.videoLocalList = this.historyList this.updateListData(this.videoLocalList, true) } }) } // 组件消失生命周期 aboutToDisappear() { console.info('LifeCycleComponent aboutToDisappear'); this.knockController?.immersiveDisableListening(); // 移除 this.curIndex = 0,避免重置当前播放索引 // 保持当前播放状态,让UnifiedPlayerService管理播放状态的持久化 emitter.off(2); emitter.off(101); emitter.off(888); emitter.off(333); this.mDestroyPage = true; // UnifiedPlayerService会自动管理屏幕常亮状态 // this.mIjkMediaPlayer.setScreenOnWhilePlaying(false); // 已移除,使用UnifiedPlayerService if (this.CONTROL_PlayStatus != PlayStatus.INIT) { this.stop(); } // 移除音频中断监听器 this.unifiedPlayerService.getIjkPlayer()?.off('audioInterrupt'); if (workerInstance) { workerInstance.terminate(); } this.stopPip(); this.destroyPipController(); this.syncPlaybackStateOnShow(); // 清理卡片事件监听器 if (this.widgetEventSubscriber) { try { commonEventManager.unsubscribe(this.widgetEventSubscriber, (err: BusinessError | undefined) => { if (err) { LogUtils.getInstance().error(`Failed to unsubscribe widget events: ${JSON.stringify(err)}`); } else { LogUtils.getInstance().LOGI('Widget event listener unsubscribed successfully'); } }); } catch (error) { LogUtils.getInstance().error(`Error during widget event cleanup: ${error}`); } } this.avSessionController.unregisterSessionListener(); } async getSortedFiles(curPath: string, isWorkerPost?: boolean, destPath?: string, isOpen?: boolean) { this.isFavMusic = false if (isWorkerPost) { workerInstance.postMessage({ code: 2, data: this.context }); //刷新媒体库列表 } if (this.modeType !== 0) { return } this.isCanBack = false this.currentPath = curPath this.titleBarModel.setTitleName(getFileDirName(this.currentPath, this.rootPath)) if (curPath === this.rootPath) { this.titleBarModel.setLeftIconMain($r('app.media.menu')) } else { this.currentTitleName = getFileDirName(this.currentPath, this.rootPath) if (this.rootPath === Utility.getParentDirectory(curPath)) { this.titleBarModel.setLeftIconMain($r('app.media.left_back_white')) } } // Get the current file list and calculate its hash this.fileList = FileUtil.listFileSync(curPath); const currentHash = simpleHash(this.fileList); // Check cache const cachedValue = this.findCache(curPath); Logger.info('onecold cachedValue = ' + cachedValue) const cachedHash = this.findStringCache(curPath + '_hash'); const cachedDirList = this.findCache(curPath + "_dirList"); if (cachedValue !== undefined) { // this.videoLocalList = cachedValue; Logger.info('onecold 有缓存 cachedValue = ' + cachedValue) if (this.currentPath != this.rootPath) { //第一个封面赋值给currentTitleCover this.currentTitleCover = Utility.getFirstCoverFromList(cachedValue) } this.updateListData(cachedValue) } if (cachedDirList !== undefined) { this.dirList = cachedDirList } //通过当前hash值和缓存hash判断list有没有变化 if (cachedValue && cachedHash === currentHash) { return; } this.fileList = FileUtil.listFileSync(curPath) let directories: Array = []; let files: Array = []; for (let i = 0; i < this.fileList.length; i++) { console.info(`The name of file: ${this.fileList[i]}`); let path = curPath + '/' + this.fileList[i] if (FileUtil.isDirectory(path)) { let item: VideoItem = new VideoItem(this.fileList[i].toString(), this.fileList[i].toString(), path, CommonConstants.TYPE_IS_DIR, 0, '') if (this.isShowDir(item.name)) { directories.push(item); } // directories.sort((a, b) => a.cTime.localeCompare(b.cTime)); // Utility.doSortListAscending(directories) } else { if (this.currentPath === this.lockPath&&Utility.isMeidaByExtension(path)) { let item: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, path, CommonConstants.TYPE_LOCAL, false); if (!path.endsWith('.lrc') && Utility.isMeidaByExtension(path) && !path.endsWith('.srt')) { files.push(item); // files.sort((a, b) => a.cTime.localeCompare(b.cTime)); // this.doSortListAscending(files) } } } } this.table.queryByParentPath(curPath, async (result: VideoItem[]) => { if (this.currentPath !== this.lockPath) { files = result; } files.sort((a, b) => a.cTime.localeCompare(b.cTime)); Utility.doSortListAscending(files) if (this.currentPath === this.rootPath) { this.dirList = directories this.addCache(curPath + "_dirList", this.dirList); } //插入广告先注释掉 // if (this.isShowAd && ArrayUtil.isNotEmpty(directories)) { // let adItem: VideoItem = new VideoItem('', '', '', CommonConstants.TYPE_IS_CSJAD, 0, '') // directories.push(adItem) // } // 合并文件夹和文件,文件夹在前,文件在后,广告在中间 this.videoLocalList = directories.concat(files); this.updateListData(this.videoLocalList) if (ArrayUtil.isNotEmpty(files)) { if (this.currentPath != this.rootPath) { if (files) { this.currentTitleCover = Utility.getFirstCoverFromList(files) } } // 缓存结果 this.addCache(curPath, this.videoLocalList); await this.saveCacheToStorage(); // 保存缓存至本地存储 this.addStringCache(curPath + '_hash', currentHash); } console.info(`onecold gengxin 1: ${this.videoLocalList.length}`); // for (let i = 0; i < this.videoLocalList.length; i++) { // // console.info(`onecold gengxin 1: ${this.videoLocalList[i].pixelMapPath}`); // // } // this.setButtonStatus() if (isOpen) { if (destPath === undefined) { // 处理 destPath 为 undefined 的情况 return } if (destPath.endsWith('.lrc')) { ToastUtil.showToast('导入歌词成功!请注意歌词文件和歌曲要同个歌单路径。') if (StrUtil.isNotEmpty(this.videoUrl)) { let lyricPath = this.videoUrl.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc' this.initLyric(lyricPath); } return } let item2 = new VideoItem(Utility.getMediaNameByUri(destPath), destPath, destPath, 0, 0, '') this.doPlay(item2) this.isShowPlay = true } }); } //扫描文件夹入库 async scanDirectory(curPath: string): Promise { const files = FileUtil.listFileSync(curPath); let mediaItems: VideoItem[] = []; Logger.info('onecold scanDirectory start ') for (const file of files) { const fPath = `${curPath}/${file}`; if (fPath === this.lockPath) { Logger.info(`Skipping locked path: ${fPath}`); continue; } if (FileUtil.isDirectory(fPath)) { // Recursively scan subdirectories const subDirItems = await this.scanDirectory(fPath); mediaItems = mediaItems.concat(subDirItems); } else { Logger.info('onecold scanDirectory filePath = ' + fPath) // Process media files if (!fPath.endsWith('.lrc') && Utility.isMeidaByExtension(fPath) && !fPath.endsWith('.srt')) { let mediaItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, fPath, CommonConstants.TYPE_LOCAL, true); mediaItems.push(mediaItem); this.table.insert(mediaItem, (id: number) => { }); } } } return mediaItems; } updateListData(mList: Array, noSort?: boolean) { animateTo({ duration: 888 }, () => { this.opacityItem = 0; }); setTimeout(() => { this.videoLocalList = mList; if (!noSort) { if (this.modeType === 0 || this.modeType === 1) { this.sortType = PreferencesUtil.getNumberSync(SettingPage.SORT_TYPE, 4) this.doSortType(this.sortType) } } this.dataSource.pushArrayData(this.videoLocalList) // 如果当前有播放列表,检查是否需要更新播放列表到统一播放器服务 if (ArrayUtil.isNotEmpty(this.songList)) { const globalVideoList = Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL) as VideoItem[]; // 只有在没有现有播放列表时,才使用当前目录的文件列表 // 避免在文件列表更新时破坏用户的播放列表 if (ArrayUtil.isEmpty(this.songList) && ArrayUtil.isNotEmpty(globalVideoList)) { this.songList = globalVideoList; this.sonDataSource.pushArrayData(this.songList); // 同步到统一播放器服务进行持久化 this.syncPlaylistToService(); } } animateTo({ duration: 888 }, () => { this.opacityItem = 1; }); this.setButtonStatus() }, 200); } //排序模式和多选模式 showPupDialog() { if (this.modeType === 2 || this.modeType === 3) { this.titleBarModel.setRightIcon(null) } else { this.titleBarModel.setRightIcon(($r('app.media.add'))) } let dataBean = new BubbleBean() dataBean.data = ["首页", "媒体库", "艺术家", "专辑", "同步数据"] dataBean.color = this.themeColor dataBean.modeType = this.modeType dataBean.imageRes = [$r('app.media.home_press'), $r('app.media.kp_music'), $r('app.media.music_menu'), $r('app.media.llq'), $r('app.media.refresh')] let pup: XPopup; if (this.isDarkMode) { pup = XPopup.Builder() .setPopupPosition(PopupPosition.BOTTOM) .atView('pup_positon_music') .setBackgroundColor(Color.Black) .asBubble(wrapBuilder(customPopupBuilder), dataBean) .show() } else { pup = XPopup.Builder() .setPopupPosition(PopupPosition.BOTTOM) .atView('pup_positon_music') .setBackgroundColor(Color.White) .asBubble(wrapBuilder(customPopupBuilder), dataBean) .show() } dataBean.onItemClick = (i) => { XPopup.dismissTop() switch (i) { case 0: //首页 this.modeType = 0 this.getSortedFiles(this.currentPath) break case 1: this.titleBarModel.setLeftIconMain($r('app.media.menu')) this.modeType = 1 this.updateListData(this.mediaKuList) this.titleBarModel.setTitleName(Utility.resourceToString(this.context, $r('app.string.media_ku'))) break case 2: this.modeType = 2 this.titleBarModel.setLeftIconMain($r('app.media.menu')) this.updateListData(this.artistList) this.titleBarModel.setTitleName(Utility.resourceToString(this.context, $r('app.string.artist'))) break case 3: this.modeType = 3 this.titleBarModel.setLeftIconMain($r('app.media.menu')) this.updateListData(this.albumList) this.titleBarModel.setTitleName(Utility.resourceToString(this.context, $r('app.string.album'))) break case 4: //同步数据 this.asyncCurrentPathData() break } } } getCurFileList(): Array { let files: Array = []; if (ArrayUtil.isNotEmpty(this.videoLocalList)) { for (let i = 0; i < this.videoLocalList.length; i++) { if (this.videoLocalList[i].type === CommonConstants.TYPE_LOCAL) { files.push(this.videoLocalList[i]); } } } return files } /** * 刷新播放列表中歌曲的封面信息(从当前文件列表同步最新封面) */ async refreshPlaylistCovers(): Promise { if (ArrayUtil.isEmpty(this.songList)) { return; } try { // 获取当前目录的最新文件列表(包含最新的封面信息) const currentFileList = this.getCurFileList(); // 如果当前文件列表为空,直接返回 if (ArrayUtil.isEmpty(currentFileList)) { return; } // 为播放列表中的每首歌曲更新封面信息 let hasUpdates = false; this.songList.forEach((song, index) => { // 在当前文件列表中查找对应的歌曲 const matchedSong = currentFileList.find(file => file.filePath === song.filePath); if (matchedSong && StrUtil.isNotEmpty(matchedSong.pixelMapPath) && matchedSong.pixelMapPath !== song.pixelMapPath) { // 更新播放列表中的封面信息 this.songList[index].pixelMapPath = matchedSong.pixelMapPath; hasUpdates = true; LogUtils.getInstance().LOGI(`LocalMusic: Updated cover for ${song.name}: ${matchedSong.pixelMapPath}`); } }); // 如果有更新,刷新UI数据源 if (hasUpdates) { this.sonDataSource.pushArrayData(this.songList); LogUtils.getInstance().LOGI(`LocalMusic: Refreshed covers for playlist with ${this.songList.length} songs`); } } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic: Error refreshing playlist covers: ${error.message}`); } } setButtonStatus() { if (ArrayUtil.isNotEmpty(this.videoLocalList)) { this.isZero = false } else { this.isZero = true } if (this.currentPath !== this.rootPath && ArrayUtil.isNotEmpty(Utility.getGlobalNameList(this.videoLocalList, CommonConstants.TYPE_IS_DIR))) { this.isHasDir = true //不为空,说明有子文件夹 } else { this.isHasDir = false } } showRankDialog() { if (this.modeType === 2 || this.modeType === 3) { //动作面板 DialogHelper.showBottomSheetDialog({ title: "请选择排序模式", maskColor: Color.Transparent, // sheets: ["按名称升序", "按名称降序"], sheets: [ { value: '按名称升序', fontColor: $r('app.color.text_color') }, { value: '按名称降序', fontColor: $r('app.color.text_color') }, ], backgroundColor: $r('app.color.bg_card'), height: '30%', transition: AnimationHelper.transitionInDown(555), onAction: (index) => { switch (index) { case 0: Utility.doSortListAscending(this.videoLocalList) break; case 1: Utility.doSortListDescending(this.videoLocalList) break; } this.updateListData(this.videoLocalList) } }) return } //动作面板 DialogHelper.showBottomSheetDialog({ title: "请选择排序模式", maskColor: Color.Transparent, height: '75%', // sheets: ["按艺术家升序", "按艺术家降序", "按专辑升序", "按专辑降序", "按名称升序", "按名称降序", "按添加时间升序","按添加时间降序"], sheets: [ { value: "按艺术家升序", fontColor: $r('app.color.text_color') }, { value: "按艺术家降序", fontColor: $r('app.color.text_color') }, { value: "按专辑升序", fontColor: $r('app.color.text_color') }, { value: "按专辑降序", fontColor: $r('app.color.text_color') }, { value: "按名称升序", fontColor: $r('app.color.text_color') }, { value: "按名称降序", fontColor: $r('app.color.text_color') }, { value: "按添加时间升序", fontColor: $r('app.color.text_color') }, { value: "按添加时间降序", fontColor: $r('app.color.text_color') }, ], backgroundColor: $r('app.color.bg_card'), transition: AnimationHelper.transitionInDown(555), onAction: (index) => { this.doSortType(index) PreferencesUtil.put(SettingPage.SORT_TYPE, index) this.updateListData(this.videoLocalList, true) } }) } doSortType(index: number) { switch (index) { case 0: this.videoLocalList.sort((a: VideoItem, b: VideoItem) => { // 类型排序优先级 const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } // 处理艺术家可能为undefined的字符串比较 const artistA = a.artist?.trim() || ''; // 可选添加 trim() 处理空格 const artistB = b.artist?.trim() || ''; return artistA.localeCompare(artistB); }); break; case 1: this.videoLocalList.sort((a: VideoItem, b: VideoItem) => { // 类型排序优先级 const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } // 处理艺术家可能为undefined的字符串比较 const artistA = a.artist?.trim() || ''; // 可选添加 trim() 处理空格 const artistB = b.artist?.trim() || ''; return artistB.localeCompare(artistA); }); break; case 2: this.videoLocalList.sort((a: VideoItem, b: VideoItem) => { // 类型排序优先级 const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } // 处理专辑可能为undefined的字符串比较 const albumA = a.album?.trim() || ''; // 可选添加 trim() 处理空格 const albumB = b.album?.trim() || ''; return albumA.localeCompare(albumB); }); break; case 3: this.videoLocalList.sort((a: VideoItem, b: VideoItem) => { // 类型排序优先级 const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } // 处理专辑可能为undefined的字符串比较 const albumA = a.album?.trim() || ''; // 可选添加 trim() 处理空格 const albumB = b.album?.trim() || ''; return albumB.localeCompare(albumA); }); break; case 4: Utility.doSortListAscending(this.videoLocalList) break; case 5: Utility.doSortListDescending(this.videoLocalList) break; case 6: this.videoLocalList.sort((a, b) => { // First, sort by type const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } // If types are the same, sort by cTime in ascending order return a.cTime.localeCompare(b.cTime); }); break; case 7: this.videoLocalList.sort((a, b) => { // First, sort by type const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } // If types are the same, sort by cTime in descending order return b.cTime.localeCompare(a.cTime); }); break; } } showSheelDialog() { // if(PreferencesUtil.getBooleanSync('isFirstMusic',true)) { // this.showTips() // return // } this.showAddDialog() } showAddDialog() { if (this.modeType === 2 || this.modeType === 3) { return } DialogHelper.showActionSheetDialog({ title: "请选择添加方式(建议歌词文件一起导入)", // sheets: ["新建歌单", "手动导入", "自动导入"], sheets: [ { value: "新建歌单", fontColor: $r('app.color.text_color') }, { value: "手动导入", fontColor: $r('app.color.text_color') }, { value: "自动导入", fontColor: $r('app.color.text_color') }, { value: "全选导入", fontColor: $r('app.color.text_color') }, ], maskColor: Color.Transparent, backgroundColor: $r('app.color.bg_card'), transition: AnimationHelper.transitionInDown(555), onAction: (index) => { switch (index) { case 0: if (this.modeType === 0) { this.showMkDialog() } else { ToastUtil.showToast('请切换到首页才能新建歌单!') } break; case 1: this.callFilePickerSelectFile() break; case 2: this.showTips() break; case 3: this.goSelectMusic() break; } } }) } //拉起音频 goSelectMusic() { if (DeviceUtil.getDeviceType() == resourceManager.DeviceType.DEVICE_TYPE_TABLET || DeviceUtil.getDeviceType() == resourceManager.DeviceType.DEVICE_TYPE_PC || DeviceUtil.getDeviceType() == resourceManager.DeviceType.DEVICE_TYPE_2IN1 ) { ToastUtil.showToast('暂时不支持该设备') return } try { let documentSelectOptions = new picker.DocumentSelectOptions(); documentSelectOptions.authMode = true documentSelectOptions.multiAuthMode = true documentSelectOptions.mergeMode = picker.MergeTypeMode.AUDIO; let documentPicker = new picker.DocumentViewPicker(this.context); documentPicker.select(documentSelectOptions).then((documentSelectResult: Array) => { if (documentSelectResult !== null && documentSelectResult !== undefined) { this.saveVideoDatas(documentSelectResult) } console.info('DocumentViewPicker.select successfully, documentSelectResult uri: ' + JSON.stringify(documentSelectResult)); }).catch((err: BusinessError) => { console.error(`DocumentViewPicker.select failed with err, code is: ${err.code}, message is: ${err.message}`); }); } catch (error) { let err: BusinessError = error as BusinessError; console.error('AudioViewPicker failed with err: ' + JSON.stringify(err)); } } async showTips() { DialogHelper.showTipsDialog({ title: '使用提示:自动导入教程', content: '用系统自带的文件管理器将:\n\n1、音频文件放入' + '\n\n我的设备/Download/' + this.appName + '\n\n2、无需繁琐的勾选和分类步骤。\n\n3、每个子文件夹将被组成一个分组。' + '\n\n4、歌词文件的文件名必须和目标歌曲文件名相同(不包含后缀名)。\n\n5、歌词文件和目标歌曲文件必须同个目录。' + '\n\n6、电脑上Download的目录是\nDownload/' + this.packName + '。\n\n7、放好文件后如果没显示点击刷新按钮。\n', onAction: (action) => { if (action == DialogAction.TWO) { PreferencesUtil.putSync('isFirstMusic', false) router.pushUrl({ url: 'pages/ScanFilePage' }); } } }) } showMkDialog() { DialogHelper.showTextInputDialog({ title: '新建歌单', maskColor: Color.Transparent, text: '', transition: AnimationHelper.transitionInDown(666), onChange: (text) => { console.error("onChange: " + text); }, onAction: (action, dialogId, content) => { // ToastUtil.showToast('content = ' +content) if (action == DialogAction.TWO) { const newName = content const newPath = this.currentPath + '/' + newName FileUtil.mkdir(newPath, true).then(() => { this.cache.delete(this.currentPath); // 删除目标路径缓存 this.getSortedFiles(this.currentPath) }).catch((error: BusinessError) => { console.error(error.message); }); } } }) } @State progress: number = 0; async saveVideoDatas(uris: string[], isOpen?: boolean) { if (ArrayUtil.isEmpty(uris)) { return; } let newUris: string[] = []; this.progress = 0 DialogHelper.showLoadingProgress({ progress: this.progress, backCancel: false, autoCancel: false, loadColor: this.themeColor, fontColor: this.themeColor }); // 计算所有文件的总大小 let totalSize = 0; for (let uri of uris) { const sourceFile = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); totalSize += (await fileIo.stat(sourceFile.fd)).size; await fileIo.close(sourceFile.fd); } let totalRead = 0; let destPath = this.currentPath + '/' + Utility.getMediaNameByUri(uris[0]); Logger.info(TAG, 'destPath uri: ' + destPath); for (let i = 0; i < uris.length; i++) { let filePath = this.currentPath + '/' + Utility.getMediaNameByUri(uris[i]); Logger.info(TAG, 'filePath uri: ' + filePath); newUris.push(filePath); try { const sourceFile = await fileIo.open(uris[i], fileIo.OpenMode.READ_ONLY); const destFile = await fileIo.open(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE); const buffSize = 1024 * 1024; // 1MB 缓冲区大小 const buffer = new ArrayBuffer(buffSize); let len = await fileIo.read(sourceFile.fd, buffer); while (len > 0) { await fileIo.write(destFile.fd, buffer.slice(0, len)); totalRead += len; // 计算总进度 this.progress = Math.floor((totalRead / totalSize) * 100); DialogHelper.updateLoading('正在导入', this.progress); len = await fileIo.read(sourceFile.fd, buffer); } await fileIo.close(sourceFile.fd); await fileIo.close(destFile.fd); if (this.currentPath !== this.lockPath) { //判断不是私密音乐,才入库 if (!filePath.endsWith('.lrc') && Utility.isMeidaByExtension(filePath) && !filePath.endsWith('.srt')) { let mediaItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, filePath, CommonConstants.TYPE_LOCAL, true); this.table.insert(mediaItem, (id: number) => { // 删除目标路径缓存 this.cache.delete(this.currentPath); this.getSortedFiles(this.currentPath, true, destPath, isOpen) }); } } } catch (error) { Logger.error(TAG, 'saveVideoDatas failed with err: ' + JSON.stringify(error)); } } DialogHelper.closeLoading() this.isZero = false // setTimeout(() => { // // 删除目标路径缓存 // this.cache.delete(this.currentPath); // this.getSortedFiles(this.currentPath,true,destPath,isOpen) // }, 500); Logger.info(TAG, 'scan select video result:' + newUris) } //拉起音频 // goSelectMusic(){ // // try { // let audioSelectOptions = new picker.AudioSelectOptions(); // let audioPicker = new picker.AudioViewPicker(this.context); // audioPicker.select(audioSelectOptions).then((audioSelectResult: Array) => { // if (audioSelectResult !== null && audioSelectResult !== undefined) { // this.saveVideoDatas(audioSelectResult) // } // console.info('AudioViewPicker.select successfully, audioSelectResult uri: ' + JSON.stringify(audioSelectResult)); // }).catch((err: BusinessError) => { // // console.error('AudioViewPicker.select failed with err: ' + JSON.stringify(err)); // }); // } catch (error) { // let err: BusinessError = error as BusinessError; // console.error('AudioViewPicker failed with err: ' + JSON.stringify(err)); // } // // // // } goSelectImage(item: VideoItem) { if (!item) { return } let selectUris: Array = []; let photoPicker = new photoAccessHelper.PhotoViewPicker(); let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE photoSelectOptions.maxSelectNumber = 1; // 选择媒体文件的最大数目 photoPicker.select(photoSelectOptions).then(async (photoSelectResult: photoAccessHelper.PhotoSelectResult) => { //用一个全局变量存储返回的uri selectUris = photoSelectResult.photoUris; console.info('photoViewPicker.select to file succeed and uris are:' + selectUris); //使用fs.openSync接口,通过uri打开这个文件得到fd let file = fs.openSync(selectUris[0], fs.OpenMode.READ_ONLY); console.info('file fd: ' + file.fd); let name = await MD5.digestSync(this.videoUrl) let imagePath = this.context.filesDir + FileUtil.separator + name imagePath = fileUri.getUriFromPath(imagePath) let file2 = fileIo.openSync(imagePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE) fileIo.copyFileSync(file.fd, file2.fd) fileIo.closeSync(file); fileIo.closeSync(file2); if (item.filePath == this.currentSong?.filePath) { this.cover = imagePath } if (item) { item.pixelMapPath = imagePath } this.table.updatePixelMapPath(item.filePath, imagePath, (success: boolean, error?: string) => { if (success) { this.doUpdateData() console.log(" onecold 更新音乐封面成功,数据库已同步"); } else { console.error(" onecold 更新音乐封面数据库失败原因: " + error); } }); LogUtil.debug("onecold this.cover =" + this.cover) }).catch((err: BusinessError) => { console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`); }) } // 拉起picker选择文件管理器 async callFilePickerSelectFile(): Promise { try { let DocumentSelectOptions = new picker.DocumentSelectOptions(); if (DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_PHONE) { DocumentSelectOptions.fileSuffixFilters = CommonConstants.MEDIA_FORMAT } else { DocumentSelectOptions.mergeMode = picker.MergeTypeMode.AUDIO } let documentPicker = new picker.DocumentViewPicker(); documentPicker.select(DocumentSelectOptions).then((DocumentSelectResult) => { Logger.info(TAG, 'DocumentViewPicker.select successfully, DocumentSelectResult uri: ' + JSON.stringify(DocumentSelectResult)); if (DocumentSelectResult !== null && DocumentSelectResult !== undefined) { this.saveVideoDatas(DocumentSelectResult) } }).catch((err: BusinessError) => { Logger.error(TAG, 'DocumentViewPicker.select failed with err: ' + JSON.stringify(err)); }); } catch (err) { Logger.error(TAG, 'DocumentViewPicker failed with err: ' + JSON.stringify(err)); } } showWarnIsDeleteFile(item: VideoItem, index: string, filePath: string) { //操作确认类弹出框 DialogHelper.showAlertDialog({ maskColor: Color.Transparent, content: "确定要删除这个文件吗?", backgroundColor: Color.Grey, transition: AnimationHelper.transitionInUp(555), onAction: (action) => { if (action == DialogAction.ONE) { // ToastUtil.showToast(`您点击了取消按钮`); } else if (action == DialogAction.TWO) { // ToastUtil.showToast(`您点击了确认按钮`); this.doDelete(item, index + '', filePath) } } }) } //删除单个索引index的视频 doDelete(item: VideoItem, index: string, filePath: string) { if (ArrayUtil.isNotEmpty(this.videoLocalList)) { Logger.info(TAG, 'delete filePath = ' + filePath); if (item.type === CommonConstants.TYPE_IS_DIR) { //旧文件删除 this.table.deleteDataForParentPath(item.filePath, () => { FileUtil.rmdir(filePath).then(() => { this.videoLocalList.splice(Number(index), 1) this.deleteCache(this.currentPath) this.getSortedFiles(this.currentPath, true) }).catch((error: BusinessError) => { console.error(error.message); }); }); } else { this.table.deleteData(item, () => { FileUtil.unlink(filePath) .catch((error: BusinessError) => { console.error(error.message); // 可以根据 error.code 判断是否为"文件不存在",如需特殊处理 }) .finally(() => { this.videoLocalList.splice(Number(index), 1) this.deleteCache(this.currentPath) this.getSortedFiles(this.currentPath, true) }); }); } } } //重命名的对话框 showReNameDialog(item: VideoItem, index: string, filePath: string) { DialogHelper.showTextInputDialog({ title: '重命名', text: item.type === CommonConstants.TYPE_IS_DIR ? item.name : item.fileName, maskColor: Color.Transparent, backgroundColor: Color.Grey, transition: AnimationHelper.transitionInDown(666), onChange: (text) => { console.error("onChange: " + text); }, onAction: (action, dialogId, content) => { // ToastUtil.showToast('content = ' +content) if (action == DialogAction.TWO) { const newName = content const newPath = this.currentPath + '/' + newName console.log("onecold item = " + item.type); this.doReName(index, newName, filePath, newPath, item) } } }) } //剪切 移动到另个文件夹的对话框 showCutDialog(isCurrent: boolean, item: VideoItem, index: number, id: string) { Logger.info('onecold showCutDialog') let mList: Array = [] let rPath = this.rootPath if (isCurrent) { mList = Utility.getGlobalNameList(this.videoLocalList, CommonConstants.TYPE_IS_DIR); rPath = this.currentPath } else { mList = Utility.getNameList(this.dirList); rPath = this.rootPath } let dataBean = new BubbleBean() dataBean.data = mList dataBean.color = this.themeColor dataBean.onItemClick = (i) => { const newPath = rPath + '/' + mList[i] + '/' + item.fileName Logger.info('newPath = ' + newPath) FileUtil.moveFile(item.filePath, newPath, 1).then(async () => { ToastUtil.showToast('移动成功') this.table.deleteData(item, () => { }); // let newItem = new VideoItem(item.name,newPath,newPath,item.type,item.videoSize,item.cTime,item.pixelMap, // item.size,item.pixelMapToString,item.artist,item.album,item.fileName) let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, true); if (!newPath.includes(this.lockPath)) { //如果移动到私密,就不插入数据库 this.table.insert(newItem, (id: number) => { //加入数据库 }); } this.cache.delete(this.currentPath); // 删除原路径缓存 this.cache.delete(rPath + '/' + mList[i]); // 删除目标路径缓存 this.getSortedFiles(this.currentPath, true) }).catch((error: BusinessError) => { console.error(error.message); }); XPopup.dismissTop() } let popupPosition: PopupPosition = PopupPosition.BOTTOM if (ArrayUtil.isNotEmpty(this.videoLocalList) && this.videoLocalList.length > 7) { popupPosition = PopupPosition.TOP } if (index <= 3) { popupPosition = PopupPosition.BOTTOM } let pup: XPopup if (this.isDarkMode) { pup = XPopup.Builder() .setPopupPosition(popupPosition) .atView(id)// .setHeight(this.isCoverOpacity()?100:420) .setBackgroundColor(Color.Black) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show() } else { pup = XPopup.Builder() .setPopupPosition(popupPosition) .atView(id)// .setHeight(this.isCoverOpacity()?280:420) .setBackgroundColor(Color.White) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show() } } //复制 到另个文件夹的对话框 showCopyDialog(isCurrent: boolean, item: VideoItem, index: number, id: string) { Logger.info('onecoldv showCopyDialog') let mList: Array = [] let rPath = this.rootPath if (isCurrent) { mList = Utility.getGlobalNameList(this.videoLocalList, CommonConstants.TYPE_IS_DIR); rPath = this.currentPath } else { mList = Utility.getNameList(this.dirList); rPath = this.rootPath } let dataBean = new BubbleBean() dataBean.data = mList dataBean.isCopy = true dataBean.color = this.themeColor dataBean.onItemClick = (i) => { const newPath = rPath + '/' + mList[i] + '/' + item.fileName Logger.info('newPath = ' + newPath) FileUtil.copyFile(item.filePath, newPath, 0).then(async () => { ToastUtil.showToast('复制成功') this.cache.delete(rPath + '/' + mList[i]); // 删除目标路径缓存 this.getSortedFiles(this.currentPath) // let newItem = new VideoItem(item.name,newPath,newPath,item.type,item.videoSize,item.cTime,item.pixelMap, // item.size,item.pixelMapToString,item.artist,item.album,item.fileName) let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, true); if (!newPath.includes(this.lockPath)) { //如果移动到私密,就不插入数据库 this.table.insert(newItem, (id: number) => { //加入数据库 }); } }).catch((error: BusinessError) => { console.error(error.message); }); XPopup.dismissTop() } let popupPosition: PopupPosition = PopupPosition.BOTTOM if (ArrayUtil.isNotEmpty(this.videoLocalList) && this.videoLocalList.length > 7) { popupPosition = PopupPosition.TOP } if (index <= 3) { popupPosition = PopupPosition.BOTTOM } let pup: XPopup if (this.isDarkMode) { pup = XPopup.Builder() .setPopupPosition(popupPosition) .atView(id) .setMaxHeight('60%') .setBackgroundColor(Color.Black) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show(); } else { pup = XPopup.Builder() .setPopupPosition(popupPosition) .atView(id) .setMaxHeight('60%') .setBackgroundColor(Color.White) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show(); } } //重命名索引index的视频 doReName(index: string, newName: string, oldPath: string, newPath: string, info: VideoItem) { FileUtil.rename(oldPath, newPath).then(async () => { if (ArrayUtil.isNotEmpty(this.videoLocalList)) { if (info.type === CommonConstants.TYPE_IS_DIR) { //如果是文件夹先删除源数据,再添加新数据 this.table.deleteDataForParentPath(oldPath, (success: boolean, error?: string) => { if (success) { console.log(" onecold deleteDataForParentPath 文件夹重命名成功,数据库已同步"); } else { console.error(" onecold deleteDataForParentPath 文件夹重命名数据库失败原因: " + error); } }); this.scanDirectory(newPath) } else if (info.type === CommonConstants.TYPE_LOCAL) { this.table.updateRename(newName, oldPath, newPath, (success: boolean, error?: string) => { if (success) { console.log(" onecold 重命名成功,数据库已同步"); } else { console.error(" onecold 重命名数据库失败原因: " + error); } }); } let mediaItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, true); this.videoLocalList[index] = mediaItem; } this.cache.delete(this.currentPath) this.getSortedFiles(this.currentPath, true) }).catch((error: BusinessError) => { console.error(error.message); }); } build() { Scroll() { Column() { if (!this.isShowCoverHeader()) { Column() { Line().width('100%').height('100%') } .height(this.topRectHeight) .backgroundColor(this.isCustomizeBg ? Color.Transparent : (this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) TitleBar({ model: $titleBarModel }) .id('pup_positon_music') } Stack() { Column() { XComponent({ id: 'xcomponentId', type: 'surface', libraryname: 'ijkplayer_napi', controller: this.xcomponentController }) .onLoad((event?: object) => { if (!!event) { this.initDelayPlay(event); } }) .onKeyEvent((event?: KeyEvent) => { // If the button type is pressed, the subsequent code will not be executed; the specific button logic will be executed upon release. if (!event || event.type !== KeyType.Down) { return; } //空格键 Space key controls pause/play if (event.keyCode === KeyCode.KEYCODE_SPACE) { this.playOrPause() } // Right-click to fast forward if (event.keyCode === KeyCode.KEYCODE_DPAD_RIGHT) { this.sessionFastForwardCallback(15); } // Left-click to rewind if (event.keyCode === KeyCode.KEYCODE_DPAD_LEFT) { this.sessionRewindCallback(15); } }) .onDestroy(() => { }) .width('100%')// .width(this.videoWidth) .aspectRatio(this.videoAspectRatio) } .aspectRatio(this.videoAspectRatio) .justifyContent(FlexAlign.Center) .backgroundColor(Color.Transparent) .visibility(Visibility.Hidden) .height(CommonConstants.FULL_PERCENT) .width(CommonConstants.FULL_PERCENT) Stack() { Row() { Blank() .layoutWeight(1) IndexerView( { indexArray: DEFAULT_INDEX, selectedIndex: this.selectedIndex, textSize: 12, selectedColor: "#ff419ee5", normalColor: "#ffa0a0a0", floatSelectedTextColor: "#ffffff", floatSelectedViewColor: "#ff419ee5", onIndexChanged: (index) => { this.listScroller.scrollToIndex(index) } }) .width(24) .height("50%") }.visibility(Visibility.None) Column() { if (!this.isShowCoverHeader()) { this.tabTitle() } if (this.isGridMusic) { this.getGridView() } else { this.getListView() } if (this.modeType == 0) { Column() { Button('同步数据', { type: ButtonType.Capsule, stateEffect: true }) .width(130) .height(50) .margin({ top: 100, bottom: 40 }) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onClick(() => { this.asyncCurrentPathData() }) } .height(100) .position({ top: 260 }) .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }) .animation({ duration: 500 }), TransitionEffect.scale({ x: 0, y: 0 }))) .justifyContent(FlexAlign.Center) .opacity(this.isZero ? 1 : 0) .visibility(this.isZero && !this.isFavMusic && !this.isHistory && this.modeType === 0 ? Visibility.Visible : Visibility.None) .animation({ duration: 500, curve: 'ease-in-out' // 可选动画曲线 }) .width('100%') } } .layoutWeight(1) .height('100%') } .layoutWeight(1) .height('100%') Row() { Button(this.isAllSelected ? '全不选' : '全选', { type: ButtonType.Circle, stateEffect: true }) .width('35%') .height(60) .margin({ top: 10, bottom: 10, right: 6 }) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => { if (this.isAllSelected) { // 全不选 this.selectedFiles = []; } else { // 全选 this.selectedFiles = [...this.getCurFileList()]; } this.isAllSelected = !this.isAllSelected; }) Button('剪切', { type: ButtonType.Circle, stateEffect: true }) .width('35%') .height(60) .id('music_cut') .margin({ top: 10, bottom: 10, right: 6 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .backgroundColor(this.themeColor) .onClick(() => { this.showCutDialogForMultipleFiles(false, 'music_cut') }) Button('移动', { type: ButtonType.Circle, stateEffect: true }) .width('35%') .height(60) .id('music_cut_current') .visibility(this.isHasDir ? Visibility.Visible : Visibility.None) .margin({ top: 10, bottom: 10, right: 6 }) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => { this.showCutDialogForMultipleFiles(true, 'music_cut_current') }) Button('复制', { type: ButtonType.Circle, stateEffect: true }) .width('35%') .height(60) .id('music_copy_current')// .visibility(this.isHasDir?Visibility.None:Visibility.None) .margin({ top: 10, bottom: 10, right: 6 }) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => { this.showCopyDialogForMultipleFiles(false, 'music_copy_current') }) Button('删除', { type: ButtonType.Circle, stateEffect: true }) .width('35%') .height(60) .margin({ top: 10, bottom: 10, right: 6 }) .visibility(this.isFavMusic ? Visibility.None : Visibility.Visible) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => { this.showWarnIsDelete() }) Button('取消', { type: ButtonType.Circle, stateEffect: true }) .width('35%') .height(60) .margin({ top: 10, bottom: 10, right: 6 }) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => { this.selectedFiles = []; this.isAllSelected = false this.isMultiSelect = false }) } .width('100%') .justifyContent(FlexAlign.Center) .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None) .opacity(this.isMultiSelect ? 1 : 0) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .position(this.isShowCoverHeader() ? { bottom: 78 } : { bottom: this.isPhoneLan() ? 130 : 168 }) // 将 Row 固定在底部 } .width('100%') .height('100%') .alignContent(Alignment.Top) Column() { this.PlayController() } .hitTestBehavior(HitTestMode.Transparent) .position({ bottom: 0 }) // 将 Row 固定在底部 .opacity(this.barBottomOpacity) } .width('100%') .height('100%') .backgroundImage(this.isCustomizeBg ? this.customizeBgPath : $r('app.color.bottom_control_background')) .backgroundImageSize(this.isCoverOpacity() ? { width: '100%' } : { height: '100%' }) .backgroundImagePosition(Alignment.Center) .backdropBlur(this.blurValue) .backgroundBrightness({ rate: this.isCustomizeBg ? 0.1 : 0, lightUpDegree: this.bgBrightness }) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) } } asyncCurrentPathData() { if (this.modeType == 0) { workerInstance.postMessage({ code: 1, data1: this.context, data2: this.currentPath, data3: this.lockPath, data4: PreferencesUtil.getStringSync('COVER_API', ''), }); } } // 处理文件选择的函数 handleFileSelection(item: VideoItem, checked: boolean) { if (checked) { if (!this.selectedFiles.some(x => x.filePath == item.filePath)) { this.selectedFiles.push(item); } } else { const index = this.selectedFiles.findIndex(x => x.filePath == item.filePath); if (index > -1) { this.selectedFiles.splice(index, 1); } } } showWarnIsDelete() { //操作确认类弹出框 DialogHelper.showAlertDialog({ content: "确定要删除这些文件吗?", transition: AnimationHelper.transitionInUp(666), maskColor: Color.Transparent, backgroundColor: Color.Grey, onAction: (action) => { if (action == DialogAction.ONE) { // ToastUtil.showToast(`您点击了取消按钮`); } else if (action == DialogAction.TWO) { // ToastUtil.showToast(`您点击了确认按钮`); this.deleteMultipleFiles() } } }) } //多选删除文件 deleteMultipleFiles() { if (ArrayUtil.isNotEmpty(this.selectedFiles)) { this.selectedFiles.forEach((item) => { Logger.info(TAG, 'delete filePath = ' + item.filePath); if (item.type === CommonConstants.TYPE_IS_DIR) { this.table.deleteDataForParentPath(item.filePath, () => { FileUtil.rmdir(item.filePath).then(() => { this.videoLocalList = this.videoLocalList.filter(v => v !== item); this.selectedFiles = []; this.isMultiSelect = false this.isAllSelected = false this.cache.delete(this.currentPath); this.getSortedFiles(this.currentPath, true); }).catch((error: Error) => { console.error(error.message); }); }); } else { this.table.deleteData(item, () => { FileUtil.unlink(item.filePath).catch((error: Error) => { console.error(error.message); }).finally(() => { this.videoLocalList = this.videoLocalList.filter(v => v !== item); this.selectedFiles = []; this.isMultiSelect = false this.isAllSelected = false this.cache.delete(this.currentPath); this.getSortedFiles(this.currentPath, true); }); }); } }); } } //多选移动文件 showCutDialogForMultipleFiles(isCurrent: boolean, viewId: string) { Logger.info('onecold showCutDialogForMultipleFiles') let mList: Array = [] let rPath = this.rootPath if (isCurrent) { mList = Utility.getGlobalNameList(this.videoLocalList, CommonConstants.TYPE_IS_DIR); rPath = this.currentPath } else { mList = Utility.getNameList(this.dirList); rPath = this.rootPath } let dataBean = new BubbleBean(); dataBean.data = mList; dataBean.color = this.themeColor dataBean.onItemClick = (i) => { const newDirPath = rPath + '/' + mList[i]; // Logger.info('newDirPath = ' + newDirPath) this.selectedFiles.forEach((item) => { const newPath = newDirPath + '/' + item.fileName; FileUtil.moveFile(item.filePath, newPath, 1).then(async () => { ToastUtil.showToast('移动成功'); this.table.deleteData(item, () => { this.cache.delete(this.currentPath); // 删除原路径缓存 this.cache.delete(newDirPath); // 删除目标路径缓存 this.getSortedFiles(this.currentPath, true); }); // let newItem = new VideoItem(item.name,newPath,newPath,item.type,item.videoSize,item.cTime,item.pixelMap, // item.size,item.pixelMapToString,item.artist,item.album,item.fileName) let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, true); if (!newPath.includes(this.lockPath)) { //如果移动到私密,就不插入数据库 this.table.insert(newItem, (id: number) => { //加入数据库 }); } }).catch((error: Error) => { console.error(error.message); }); }); this.isMultiSelect = false this.selectedFiles = []; this.isAllSelected = false XPopup.dismissTop(); }; let pup: XPopup if (this.isDarkMode) { pup = XPopup.Builder() .setPopupPosition(PopupPosition.TOP) .atView(viewId) .setBackgroundColor(Color.Black) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show(); } else { pup = XPopup.Builder() .setPopupPosition(PopupPosition.TOP) .atView(viewId) .setBackgroundColor(Color.White) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show(); } } doFav(item: VideoItem) { LogUtil.info('onecold doFav isFav=' + item.isFav) let isFav = 0 if (Utility.getIsFav(this.favList, item)) { isFav = 0 } else { isFav = 1 } this.table.updateIsFavByFilePath(item.filePath, isFav, async (result: boolean) => { if (result) { if (isFav === 1) { ToastUtil.showToast('收藏成功'); } else { ToastUtil.showToast('取消收藏成功'); } // 先更新收藏列表和其他UI this.deleteCache(this.currentPath) this.getFavList(false) if (this.modeType === 0 && !this.isFavMusic) { LogUtil.info('onecold doFav currentPath =' + this.currentPath) this.getSortedFiles(this.currentPath) } workerInstance.postMessage({ code: 2, data: this.context }); workerInstance.postMessage({ code: 3, data: this.context }); workerInstance.postMessage({ code: 4, data: this.context }); // 刷新UnifiedPlayerService的收藏状态,这会自动更新AVSession await this.unifiedPlayerService.refreshFavoriteStatus(); } }) } //多选复制文件 showCopyDialogForMultipleFiles(isCurrent: boolean, viewId: string) { console.log('onecold showCopyDialogForMultipleFiles'); let mList: Array = [] let rPath = this.rootPath if (isCurrent) { mList = Utility.getGlobalNameList(this.videoLocalList, CommonConstants.TYPE_IS_DIR); rPath = this.currentPath } else { mList = Utility.getNameList(this.dirList); rPath = this.rootPath } let dataBean = new BubbleBean(); dataBean.data = mList; dataBean.isCopy = true dataBean.color = this.themeColor dataBean.onItemClick = (i) => { const newDirPath = rPath + '/' + mList[i]; // Logger.info('newDirPath = ' + newDirPath) this.selectedFiles.forEach((item) => { const newPath = newDirPath + '/' + item.fileName; FileUtil.copyFile(item.filePath, newPath, 0).then(async () => { ToastUtil.showToast('复制成功'); this.cache.delete(newDirPath); // 删除目标路径缓存 this.getSortedFiles(this.currentPath); // let newItem = new VideoItem(item.name,newPath,newPath,item.type,item.videoSize,item.cTime,item.pixelMap, // item.size,item.pixelMapToString,item.artist,item.album,item.fileName) let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, true); if (!newPath.includes(this.lockPath)) { //如果移动到私密,就不插入数据库 this.table.insert(newItem, (id: number) => { //加入数据库 }); } }).catch((error: Error) => { console.error(error.message); }); }); this.isMultiSelect = false this.selectedFiles = []; this.isAllSelected = false XPopup.dismissTop(); }; let pup: XPopup if (this.isDarkMode) { pup = XPopup.Builder() .setPopupPosition(PopupPosition.TOP) .atView(viewId) .setBackgroundColor(Color.Black) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show(); } else { pup = XPopup.Builder() .setPopupPosition(PopupPosition.TOP) .atView(viewId) .setBackgroundColor(Color.White) .asBubble(wrapBuilder(cutPopupBuilder), dataBean) .show(); } } @Builder private DirItem(item: VideoItem, index?: number) { Button({ type: ButtonType.Normal, stateEffect: true }) { Row() { Image(this.modeType !== 0 && StrUtil.isNotEmpty(item.pixelMapPath) ? item.pixelMapPath : $r('app.media.music_group')) .height(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 35 : 30) .width(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 35 : 30) .fillColor(this.themeColor)// .fontSize(33) .alt($r('app.media.music_group')) .borderRadius('100%') .clip(true) .margin({ left: 20, top: 8, bottom: 8 }) Column() { Text(item.name.startsWith('.') ? item.name.replace(/\./g, '') : item.name) .fontSize(this.twoFingerType == 1 ? 12 : this.twoFingerType == 2 ? 15 : 18) .maxLines(1)// .width('88%') .animation({ duration: 555, curve: 'ease-in-out', }) .transition({ type: TransitionType.Insert, opacity: 0, translate: { x: 160 } }) .margin({ left: 10, right: 20 }) .fontColor($r('app.color.text_color')) Row() { Text(`${this.artistMap.get(item.name)?.length ?? 0}首`) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .visibility(this.modeType === 2 ? Visibility.Visible : Visibility.None) .fontColor($r('app.color.text_color')) .margin({ left: 10 }) Text(`${this.albumMap.get(item.name)?.length ?? 0}首`) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .visibility(this.modeType === 3 ? Visibility.Visible : Visibility.None) .fontColor($r('app.color.text_color')) .margin({ left: 10 }) Blank() } } .height('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) Blank() Image($r('app.media.arrow_right')) .height(25) .margin({ right: 10 }) .alignSelf(ItemAlign.Center) .id(item.filePath) } } .backgroundColor(Color.Transparent) .height(this.twoFingerType == 3 ? ITEM_HEIGHT_BIG : this.twoFingerType == 2 ? ITEM_HEIGHT : ITEM_HEIGHT_SMALL) .width('100%') .visibility(this.isShowDir(item.name) ? Visibility.Visible : Visibility.None) .opacity(this.opacityItem) // 绑定透明度 } isShowDir(dirName: string) { this.isShowSimi = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_SIMI, false) this.isShowFAV = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_FAV, true) this.isShowHistory = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_HISTORY, true) if (dirName === undefined) { return true } if (dirName === LocalMusic.STR_LOCK_VIDEO) { return this.isShowSimi } if (dirName === LocalMusic.STR_FAC_VIDEO) { return this.isShowFAV } if (dirName === LocalMusic.STR_HISTORY_MUSIC) { return this.isShowHistory } return true } @State opacityItem: number = 1; // 控制透明度的状态变量 @Builder private MusicItem(item: VideoItem, index?: number) { Button({ type: ButtonType.Normal, stateEffect: true }) { Row() { Stack() { Image(StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.music_red') : item.pixelMapPath) .fillColor(this.themeColor) .height(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 35 : 30) .width(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 35 : 30) .alt($r('app.media.music_red')) .borderRadius('100%') .clip(true) .opacity(this.opacityItem)// 绑定透明度 .margin({ left: 20 }) } Column() { Column() { Text(item.name) .fontSize(this.twoFingerType == 1 ? 12 : this.twoFingerType == 2 ? 15 : 18) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .animation({ duration: 555, curve: 'Linear', }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .margin({ left: this.twoFingerType == 3 ? 6 : 8 }) .bindPopup(this.longItemFilePath === item.filePath, { builder: this.MenuBuilder(item,index,item.filePath), placement: Placement.Top, // autoCancel: true, mask: {color:'#33000000'}, // popupColor: Color.Yellow, enableArrow: false,//是否显示箭头 showInSubWindow: false, onStateChange: (e) => { if (!e.isVisible) { this.longItemFilePath = '' this.tempLyricContent = '' } } }) .gesture(LongPressGesture() .onAction(async () => { //如果是专辑和艺术家的首页,不能长按 if((this.modeType===2||this.modeType==3)&&!this.isCanBack) return //如果是我的收藏 最近播放等也不能长按 if(item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC ) return this.longItemFilePath = item.filePath this.tempLyricContent = await this.getLyricContent(item) LogUtil.info('长按this.tempLyricContent = '+this.tempLyricContent) }) .onActionEnd((event: GestureEvent) => { }) ) Row() { Column(){ Text(item.md5Str?.includes('Lossless')? Utility.resourceToString(this.context,$r('app.string.lossless')):item.md5Str)//音质 .fontSize(this.twoFingerType == 1 ? 8 : this.twoFingerType == 2 ? 9 : 11) .padding({ top: 3,right:6,left:6,bottom:3 }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .fontWeight(500) .borderRadius(12) .visibility(StrUtil.isEmpty(item.md5Str)?Visibility.None:Visibility.Visible) .margin({ left: this.twoFingerType == 3 ? 6 : 8 }) .backgroundColor('#FFC107') } .padding({ top: 8 }) Text(StrUtil.isEmpty(item.artist) ? item.cTime : item.artist + ' ' + item?.album) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .margin({ left: this.twoFingerType == 3 ? 6 : 8 }) Blank() Text(item.size) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .visibility(StrUtil.isEmpty(item.artist) ? Visibility.Visible : Visibility.None) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .margin({ left: 10 }) } .gesture(LongPressGesture() .onAction(async () => { //如果是专辑和艺术家的首页,不能长按 if((this.modeType===2||this.modeType==3)&&!this.isCanBack) return //如果是我的收藏 最近播放等也不能长按 if(item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC ) return this.longItemFilePath = item.filePath this.tempLyricContent = await this.getLyricContent(item) LogUtil.info('长按this.tempLyricContent = '+this.tempLyricContent) }) .onActionEnd((event: GestureEvent) => { }) ) } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') .margin({ left: 5 }) .layoutWeight(1) Column() { Checkbox({ name: 'checkbox' + index }) .select(this.selectedFiles.some(x => x.filePath === item.filePath)) .selectedColor(this.themeColor) .shape(CheckBoxShape.CIRCLE) .opacity(this.isMultiSelect ? 1 : 0) .animation({ duration: 666, curve: 'Smooth' // 可选动画曲线 }) .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => this.handleFileSelection(item, checked)) .margin({ left: 20, top: 8, bottom: 8 }) .width(22) .height(22) ImageAnimator() .images(this.imagesF)// 动画数组 .duration(1000)// 持续 .state(this.animationState)// 动画状态 .fillMode(FillMode.Forwards) .width(18) .margin({ right: 20, top: 8, bottom: 8 }) .visibility(this.videoUrl === item.filePath ? this.isMultiSelect ? Visibility.None : Visibility.Visible : Visibility.None) .height(18) .iterations(-1) // 播放次数 } } } .backgroundColor(Color.Transparent) .width('100%') .height(this.twoFingerType == 3 ? ITEM_HEIGHT_BIG : this.twoFingerType == 2 ? ITEM_HEIGHT : ITEM_HEIGHT_SMALL) } private readonly tabs: string[] = ['文件夹', '媒体库', '艺术家', '专辑'] @Builder tabTitle() { Column() { // 使用 Stack 布局来实现绝对定位效果 Stack() { // 背景动画容器 Row() { ForEach(this.tabs, (i: string, index) => { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) { Flex() .backgroundColor(this.isDarkMode ? $r('app.color.tab_item_bg') : this.themeColor) .borderRadius(20) .padding(10) .margin(6) .width('100%') .height('100%') .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .visibility(this.modeType === index ? Visibility.Visible : Visibility.None) .opacity(this.modeType === index ? 1 : 0) .animation({ duration: 666, playMode: PlayMode.Normal, curve: 'ease-in-out' }); } .layoutWeight(1) .margin({ left: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 20 : 5, right: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 20 : 5, bottom: 1 }); }); } .width('100%') .height(50); // 导航栏容器 Row() { ForEach(this.tabs, (item: string, index) => { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) { Text(item) .fontSize(16) .fontColor(this.modeType === index ? Color.White : $r('app.color.text_color')) .padding(15) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.modeType = index; this.onModeChange(); }); } .layoutWeight(1) .stateStyles({ pressed: { opacity: 0.6 } // 按压反馈 }); }); } .width('100%') .height(this.topBarHeight) .opacity(this.barOpacity) .backgroundColor(Color.Transparent); } .width('100%') .height(this.topBarHeight) .opacity(this.barOpacity) Line() .width('100%') .height(1) .backgroundColor($r('app.color.index_background')) .opacity(this.barOpacity) .visibility(this.isCustomizeBg ? Visibility.Hidden : Visibility.Visible) } .visibility(this.isShowTitleBar ? Visibility.Visible : Visibility.None) } @State currentTitleName: string = '' //点击歌单,艺术家,专辑进去后的标题 @State currentTitleCover: string | ResourceStr = '' //点击歌单,艺术家,专辑进去后的封面 isShowCoverHeader() { if (!this.isShowHeader) { return false } if (this.modeType == 0) { if (this.isHistory || this.isFavMusic) { return true } if (this.currentPath != this.rootPath) { return true } } return (this.modeType == 2 || this.modeType == 3) && this.isCanBack } @Builder coverHeader() { if (this.isShowCoverHeader()) { Column() { Stack() { // 居中标题 Text('') .fontSize(18) .fontColor(Color.White) .align(Alignment.Center) // 左右按钮 Row() { Image($r('app.media.left_back_white')) .width(28) .height(28) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ left: 12, right: 8 }) .onClick(() => { animateTo({ duration: 555 }, () => { this.doSwipBack() }) }) Blank().flexGrow(1) if (this.modeType == 0 && !this.isHistory && !this.isFavMusic) { Image($r('app.media.add')) .width(25) .height(25) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ left: 8, right: 15 }) .onClick(() => { this.showAddDialog() }) } } .height(48) .width('100%') .alignItems(VerticalAlign.Center) } .height(48) .width('100%') Row() { Column() { Image(this.currentTitleCover) .height(100) .width(100) .alt(this.imageLabel) .borderRadius(8) .clickEffect({ level: ClickEffectLevel.HEAVY }) } .alignItems(HorizontalAlign.Start) .margin({ left: 30, top: 3, bottom: 15 }) Column() { Column() { Text(this.currentTitleName) .fontSize(16) .maxLines(1) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_color')) .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 }) if (this.modeType == 3 && ArrayUtil.isNotEmpty(this.videoLocalList)) { Text(`艺术家:${this.videoLocalList[0].artist}`) .fontSize(14) .padding({ top: 8 }) .maxLines(1) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_color')) .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 }) if(this.videoLocalList[0].genre){ Text(`风格:${this.videoLocalList[0].genre}`) .fontSize(14) .padding({ top: 8 }) .maxLines(1) .visibility(StrUtil.isEmpty(this.videoLocalList[0].genre)|| this.videoLocalList[0].genre.includes(this.UNKONWN) ?Visibility.None:Visibility.Visible) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_color')) .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 }) } if(this.videoLocalList[0].year){ Text(`发行时间:${this.videoLocalList[0].year}`) .fontSize(14) .padding({ top: 8 }) .maxLines(1) .visibility(StrUtil.isEmpty(this.videoLocalList[0].year)|| this.videoLocalList[0].year.includes(this.UNKONWN) ?Visibility.None:Visibility.Visible) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_color')) .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 }) } } if(this.modeType != 3){ Row() { Text(`共${this.videoLocalList.length}首歌`) .fontSize(13) .fontWeight(FontWeight.Bold) .padding({ top: 8 }) .fontColor($r('app.color.text_color')) .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 }) Blank() } } } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') .layoutWeight(1) .margin({ left: 20 }) } this.listViewTitle() } .backgroundImage(this.getCoverTitleBg()) .height(250) .width('100%') .backgroundImageSize({ height: '100%', width: '100%' }) .backgroundBlurStyle(this.isCustomizeBg ? BlurStyle.NONE : BlurStyle.BACKGROUND_ULTRA_THICK) .padding({ top: this.topRectHeight, bottom: this.bottomBarHeight + 48 }) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) .justifyContent(FlexAlign.SpaceBetween) .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 0.7, y: 0.7 }).animation({ duration: 500 }), TransitionEffect.scale({ x: 0, y: 0 }))) } else { this.listViewTitle() } } getCoverTitleBg(): ResourceStr | image.PixelMap { if (this.isCustomizeBg) { return ('rgba(255,255,255,0.2)') } if (this.isDarkMode) { return $r('app.color.user_center_card_background') } return this.currentTitleCover } @Builder listViewTitle() { Column() { Row({ space: 8 }) { Row({ space: 8 }) { Image($r("app.media.hm_playlist")) .width(25) .margin({ left: 8 }) .fillColor(this.themeColor) Text(`共找到${this.artistList.length}位艺术家`) .fontColor($r('app.color.text_color')) .fontSize(14) .opacity(this.opacityItem) .visibility(this.modeType === 2 ? Visibility.Visible : Visibility.None) Text(`共找到${this.albumList.length}张专辑`) .fontColor($r('app.color.text_color')) .opacity(this.opacityItem) .visibility(this.modeType === 3 ? Visibility.Visible : Visibility.None) .fontSize(14) } .layoutWeight(1) .visibility(this.isSearchMode ? Visibility.None : Visibility.Visible) Row() { TextInput({ placeholder: this.modeType === 2 ? '输入艺术家名称...' : '输入专辑名称...', text: this.searchText }) .height(35) .onChange((value: string) => { this.onSearchInput(value); }) .layoutWeight(1) Text(`取消`) .margin({ left: 10, right: 10 }) .fontColor($r('app.color.text_color')) .fontSize(16) .onClick(() => { this.isSearchMode = false this.searchText = '' if (this.modeType === 2) { this.updateListData(this.artistList) } else { this.updateListData(this.albumList) } }) } .visibility(this.isSearchMode ? Visibility.Visible : Visibility.None) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) Blank() Image($r("app.media.search")) .width(25) .visibility(this.modeType === 2 || this.modeType === 3 ? Visibility.Visible : Visibility.None) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 })// .opacity(this.opacityItem) .fillColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.isSearchMode = true }) Image($r("app.media.top_rank")) .width(25)// .opacity(this.opacityItem) .fillColor(this.themeColor) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.showRankDialog() }) Image(this.isGridMusic ? $r('app.media.list') : $r("app.media.grid")) .width(25) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .fillColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.isGridMusic = !this.isGridMusic }) } .visibility(((this.modeType === 2 || this.modeType === 3) && !this.isCanBack) ? Visibility.Visible : Visibility.None) .width('100%') .padding(10) .height(50) .border({ width: { bottom: 1 }, color: this.isCustomizeBg ? Color.Transparent : '#12ec5c87' }) Row({ space: 8 }) { Row({ space: 8 }) { Image($r("app.media.hm_play")) .width(25) .margin({ left: 8 }) .fillColor(this.themeColor) Text(`播放全部`) .fontColor($r('app.color.text_color')) .fontSize(14) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) Text(`(${Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL).length}首)`) .fontColor($r('app.color.text_color')) .fontSize(14) .opacity(this.opacityItem) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) } .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .visibility(this.isSearchMode ? Visibility.None : Visibility.Visible) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) // .backgroundColor(this.isClickedPLayAll ? $r('app.color.button_click') : Color.Transparent) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { this.isClickedPLayAll = true; } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { this.isClickedPLayAll = false; } }) .onClick(() => { // ToastUtil.showToast('当前的手机的折叠状态 ='+DisplayUtil.getFoldStatus()) if (this.modeType === 0) { if (ArrayUtil.isNotEmpty(this.getCurFileList())) { this.doPlay(this.getCurFileList()[0]) } } else { if (ArrayUtil.isNotEmpty(this.mediaKuList)) { this.doPlay(this.mediaKuList[0]) } } }) Row() { TextInput({ placeholder: '输入名称...', text: this.searchText }) .height(35) .onChange((value: string) => { this.onSearchInput(value); }) .layoutWeight(1) Text(`取消`) .margin({ left: 10, right: 10 }) .fontColor($r('app.color.text_color')) .fontSize(16) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.isSearchMode = false this.searchText = '' this.updateListData(this.mediaKuList) }) } .visibility(this.isSearchMode ? Visibility.Visible : Visibility.None) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) Blank() Image($r("app.media.search")) .width(25)// .visibility(this.modeType===1?Visibility.Visible:Visibility.None) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 })// .opacity(this.opacityItem) .fillColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.isSearchMode = true }) Image($r("app.media.refresh")) .fillColor(this.themeColor) .width(25) .visibility(this.isHistory || this.isCanBack || this.isSearchMode ? Visibility.None : Visibility.Visible) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { if (this.isFavMusic) { this.getFavList(true) } else { if(this.modeType == 0){ this.asyncCurrentPathData() }else if(this.modeType ==1){ workerInstance.postMessage({ code: 2, data: this.context }); } } }) Image($r("app.media.top_rank")) .width(25) .visibility(this.isHistory || this.isSearchMode ? Visibility.None : Visibility.Visible)// .opacity(this.opacityItem) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .fillColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.showRankDialog() }) Image(this.isMultiSelect ? $r("app.media.cancel_multi") : $r("app.media.top_flower")) .width(24) .visibility(this.isHistory || this.isSearchMode ? Visibility.None : Visibility.Visible) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .fillColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.isMultiSelect = !this.isMultiSelect }) SymbolGlyph($r('sys.symbol.trash')) .fontColor([this.themeColor]) .fontSize(25) .effectStrategy(1) .visibility(this.isHistory ? Visibility.Visible : Visibility.None) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.clearVideoHistory() //清空播放记录 }) Image(this.isGridMusic ? $r('app.media.list') : $r("app.media.grid")) .width(24) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .fillColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .bindPopup($$this.tipPopup, { builder: this.popupBuilder, placement: Placement.Bottom, maskColor: 0x33000000, enableArrow: true, onStateChange: (e) => { if (!e.isVisible) { this.tipPopup = false; } } }) .onClick(() => { this.isGridMusic = !this.isGridMusic }) } .width('100%') .padding(10) .height(50) .visibility(this.modeType === 0 || this.modeType === 1 || this.isCanBack ? Visibility.Visible : Visibility.None) .border({ width: { bottom: 1 }, color: this.isCustomizeBg ? Color.Transparent : '#12ec5c87' }) } .visibility(this.isShowAllBar ? Visibility.Visible : Visibility.None) } // 第二步:popup构造器定义弹框内容 @Builder popupBuilder() { Column({ space: 2 }) { Text('友情提示:双指缩放可以放大缩小列表') .fontSize(12) .fontWeight(FontWeight.Regular) .fontColor($r('app.color.text_color')) } .justifyContent(FlexAlign.SpaceAround) .width(220) .height(55) .padding(15) } //搜索功能的实现 @State searchText: string = ''; // 用户输入内容 @State filteredList: Array = []; // 过滤后的结果 @State isSearchMode: boolean = false // 实时搜索逻辑(带防抖) private onSearchInput(value: string) { this.searchText = value.trim(); let mSearchList: Array = [] switch (this.modeType) { case 0: mSearchList = this.videoLocalList break; case 1: mSearchList = this.mediaKuList break; case 2: mSearchList = this.artistList break; case 3: mSearchList = this.albumList break; } // 新增条件判断:空输入时显示所有数据 if (this.searchText === '') { this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新 } else { this.filteredList = mSearchList.filter((item: VideoItem) => { return item.name.toLowerCase().includes(this.searchText.toLowerCase()); }); } this.updateListData(this.filteredList); // this.isSearchMode = false; // 根据业务需求决定是否启用 } @State dragItem: number = -1 @State scaleItem: number = -1 @State neighborItem: number = -1 @State neighborScale: number = -1 @State offsetY: number = 0 private dragRefOffset: number = 0 private ITEM_INTV: number = 78 scaleSelect(item: number): number { if (this.scaleItem === item) { return 1.05 } else if (this.neighborItem === item) { return this.neighborScale } else { return 1 } } itemMove(index: number, newIndex: number): void { if (newIndex < 0 || newIndex >= this.videoLocalList.length) { return; } let tmp = this.videoLocalList.splice(index, 1); this.videoLocalList.splice(newIndex, 0, tmp[0]); this.dataSource.pushArrayData(this.videoLocalList) } // Grid布局的开始 private dragRefOffSetX: number = 0; private dragRefOffSetY: number = 0; private FIX_VP_X: number = 180; private FIX_VP_Y: number = 84; isDraggable(index: number): boolean { return index >= 0; } getRowEnd(item: number) { if (item.toString() === '0') { return 1; } else { return 0; } } // [Start itemMove_start] // itemMoveGrid(index: number, newIndex: number): void { // if (!this.isDraggable(newIndex)) { // return; // } // let tmp = this.videoLocalList.splice(index, 1); // this.videoLocalList.splice(newIndex, 0, tmp[0]); // // this.bigItemIndex = this.videoLocalList.findIndex((item) => item === 0); // } isInLeft(index: number) { return index % 2 == 0; } down(index: number): void { if ([this.videoLocalList.length - 1, this.videoLocalList.length - 2].includes(index)) { return; } this.offsetY -= this.FIX_VP_Y; this.dragRefOffSetY += this.FIX_VP_Y; this.itemMove(index, index + 1); } up(index: number): void { if (!this.isDraggable(index - 2)) { return; } this.offsetY += this.FIX_VP_Y; this.dragRefOffSetY -= this.FIX_VP_Y; this.itemMove(index, index - 1); } left(index: number): void { if (this.isInLeft(index)) { return; } if (!this.isDraggable(index - 1)) { return; } this.offsetX += this.FIX_VP_X; this.dragRefOffSetX -= this.FIX_VP_X; this.itemMove(index, index - 1) } right(index: number): void { if (!this.isInLeft(index)) { return; } if (!this.isDraggable(index + 1)) { return; } this.offsetX -= this.FIX_VP_X; this.dragRefOffSetX += this.FIX_VP_X; this.itemMove(index, index + 1) } private scroller: Scroller = new Scroller(); @State startIndex: number = 0 @State endIndex: number = 0 @State scaleValue: number = 1 @State pinchValue: number = 1 layoutOptionsForHeader: GridLayoutOptions = { regularSize: [1, 1], // 只支持[1, 1] irregularIndexes: [0], // 索引为0的GridItem占用一行 }; @Builder getGridView() { Grid(this.scroller, this.layoutOptionsForHeader) { GridItem() { if (this.isShowCoverHeader()) { this.coverHeader() } else { this.listViewTitle() } } LazyForEach(this.dataSource, (item: VideoItem, index: number) => { GridItem() { Stack({ alignContent: Alignment.Center }) { this.MusicItemGrid(item, index) } } .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 0.8, y: 0.8 }).animation({ duration: 500 }), TransitionEffect.scale({ x: 0, y: 0 }))) .clickEffect({ level: ClickEffectLevel.LIGHT }) .scale({ x: this.scaleItem === index ? 1.02 : 1, y: this.scaleItem === index ? 1.02 : 1 }) .zIndex(this.dragItem === index ? 1 : 0) .translate(this.dragItem === index ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 }) .hitTestBehavior(this.isDraggable(this.videoLocalList.indexOf(item)) ? HitTestMode.Default : HitTestMode.None) //长按拖动代码 .gesture( GestureGroup(GestureMode.Sequence, LongPressGesture({ repeat: true }) .onAction(() => { this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => { this.scaleItem = index; }) }) .onActionEnd(() => { this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => { this.scaleItem = -1; }) }), PanGesture({ fingers: 1, direction: null, distance: 0 }) .onActionStart(() => { this.dragItem = index; this.dragRefOffSetX = 0; this.dragRefOffSetY = 0; }) .onActionUpdate((event: GestureEvent) => { this.offsetX = event.offsetX - this.dragRefOffSetX; this.offsetY = event.offsetY - this.dragRefOffSetY; this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { let index = this.videoLocalList.indexOf(item); if (this.offsetY >= this.FIX_VP_Y / 2 && (this.offsetX <= 44 && this.offsetX >= -44)) { this.down(index); } else if (this.offsetY <= -this.FIX_VP_Y / 2 && (this.offsetX <= 44 && this.offsetX >= -44)) { this.up(index); } else if (this.offsetX >= this.FIX_VP_X / 2 && (this.offsetY <= 50 && this.offsetY >= -50)) { this.right(index); } else if (this.offsetX <= -this.FIX_VP_Y / 2 && (this.offsetY <= 50 && this.offsetY >= -50)) { this.left(index); } }) // if (this.offsetY > this.FIX_VP_Y * 0.7) { // this.scroller.scrollToIndex(index + 10, true,ScrollAlign.CENTER); // } }) .onActionEnd(() => { this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.dragItem = -1; }) this.getUIContext().animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => { this.scaleItem = -1; }) }) ) .onCancel(() => { this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.dragItem = -1; }) this.getUIContext().animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => { this.scaleItem = -1; }) }) ) // [End gesture_start] // [EndExclude GridItem_start] }, (item: VideoItem) => item.filePath) } .width(this.isShowHeader ? '100%' : '94%') .height('100%') .editMode(true) .transition(TransitionEffect.move(TransitionEdge.BOTTOM) .animation({ duration: 500, curve: Curve.Ease })) // .margin({ bottom: this.isCoverOpacity() ? 80 : 175 }) .margin({ bottom: this.isCoverOpacity() ? (this.isShowCoverHeader() ? this.topBarHeight+30 : this.topBarHeight+90) : (this.isShowCoverHeader() ? this.topBarHeight+45 : this.topBarHeight+128) }) .layoutWeight(1) .scrollBar(BarState.Off) .supportAnimation(true) .cachedCount(this.twoFingerType==1?5:this.twoFingerType==2?4:3) // .columnsTemplate('1fr '.repeat(this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM ? 2 : 5)) .columnsTemplate( this.twoFingerType == 3 ? 'repeat(auto-fit, 160)' : this.twoFingerType == 2 ? 'repeat(auto-fit, 110)' : 'repeat(auto-fit, 80)' ) .rowsGap(this.isShowDrawer ? 25 : (this.twoFingerType == 3 ? 10 : this.twoFingerType == 2 ? 5 : 1)) .visibility(this.isGridMusic ? Visibility.Visible : Visibility.None) .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 }) .gesture(PinchGesture({ fingers: 2 }) .onActionEnd((event: GestureEvent) => { this.setGridTwoFingers() }) .onActionUpdate(e => { this.scaleValue = this.pinchValue * e.scale }) .onActionStart(e => { })) .enableScrollInteraction(true) // 滚轴滑动,记录下滑动时的起始位置和终点位置 .onScrollIndex((start: number, end: number) => { this.startIndex = start this.endIndex = end }) .onScrollFrameBegin((offset: number) => { //滚动小屏幕 比如puraX外屏和手机横屏可以隐藏bottomBarHeight topBarHeight // if (this.isPhoneLan()) { // this.setBarHeightHide(offset) // } else if (this.currentWidthBreakpoint !== WidthBreakpoint.WIDTH_SM || (this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_MD && this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_SM)) { if (this.isScrollHide) { this.setBarHeightHide2(offset) } else { this.setBarHeightNormal() } } else { this.setBarHeightHide(offset) } return { offsetRemain: offset }; }) //允许拖拽音乐和视频到List或Grid上自动导入视频 .allowDrop([uniformTypeDescriptor.UniformDataType.AUDIO,uniformTypeDescriptor.UniformDataType.VIDEO]) .onDrop((event?: DragEvent) => { try { let dragData: UnifiedData = (event as DragEvent).getData() as UnifiedData; if (dragData !== undefined) { let records: unifiedDataChannel.UnifiedRecord[] = dragData.getRecords(); if (records.length > 0) { for (let i = 0; i < records.length; i++) { let types = records[i].getTypes(); if (types.includes(uniformTypeDescriptor.UniformDataType.FILE_URI)) { const fileUriUds = records[i].getEntry(uniformTypeDescriptor.UniformDataType.FILE_URI) as uniformDataStruct.FileUri; let typeDescriptor = uniformTypeDescriptor.getTypeDescriptor(fileUriUds.fileType); if (typeDescriptor.belongsTo(uniformTypeDescriptor.UniformDataType.AUDIO) ||typeDescriptor.belongsTo(uniformTypeDescriptor.UniformDataType.VIDEO)) { this.targetFile = fileUriUds.oriUri; this.saveVideoDatas([this.targetFile]) hilog.info(0x0000, 'Heanup', '当前targetFile:' + this.targetFile); } } } } else { hilog.info(0x0000, TAG, `%{public}s`, `dragData arr is null`); } } else { hilog.info(0x0000, TAG, `%{public}s`, `dragData is undefined`); } } catch (error) { const err = error as BusinessError; hilog.error(0x0000, TAG, `startDataLoading errorCode: ${err.code}, errorMessage: ${err.message}`); } }) } @State targetFile: string = ''; setGridTwoFingers() { this.pinchValue = this.scaleValue; Logger.info('this.pinchValue = ' + this.pinchValue); if (this.pinchValue >= 1) { this.twoFingerType++ Logger.info('this.twoFingerType1 = ' + this.twoFingerType); if (this.twoFingerType > 3) { this.twoFingerType = 3 } } else { this.twoFingerType-- Logger.info('this.twoFingerType2 = ' + this.twoFingerType); if (this.twoFingerType < 1) { this.twoFingerType = 3 this.isGridMusic = false } } PreferencesUtil.put(SettingPage.TWO_FINGER_TYPE, this.twoFingerType) this.pinchValue = 1; this.scaleValue = 1; } @State longItemFilePath: string = '' @State tempLyricContent: string = '' @Builder private MusicItemGrid(item: VideoItem, index: number) { Button({ type: ButtonType.Normal, stateEffect: false }) { Stack() { Column() { Image(StrUtil.isEmpty(item.pixelMapPath) ? Utility.getMusisBg2(index) : item.pixelMapPath) .height(this.getGridHeight()) .width(this.getGridWight()) .alt($r('app.media.ic_avatar2')) .clip(true) .borderRadius({ topLeft: 12, topRight: 12, bottomLeft: 0, bottomRight: 0 }) .draggable(false) .opacity(this.opacityItem)// 绑定透明度 // .transition(TransitionEffect.move(TransitionEdge.BOTTOM) // .animation({ duration: 500, curve: Curve.Ease })) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .margin({ left: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20, right: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20 }) Column() { Text(item.name.startsWith('.') ? item.name.replace(/\./g, '') : item.name) .fontSize(this.twoFingerType == 1 ? 14 : this.twoFingerType == 2 ? 16 : 18) .maxLines(1) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .animation({ duration: 555, curve: 'Linear', }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) Column() { Text(`${this.artistMap.get(item.name)?.length ?? 0}首`) .fontSize(11) .fontSize(this.twoFingerType == 1 ? 10 : this.twoFingerType == 2 ? 12 : 14) .visibility(this.modeType === 2 && !this.isCanBack ? Visibility.Visible : Visibility.None) .fontColor($r('app.color.text_color')) Text(`${this.albumMap.get(item.name)?.length ?? 0}首`) .fontSize(11) .fontSize(this.twoFingerType == 1 ? 10 : this.twoFingerType == 2 ? 12 : 14) .visibility(this.modeType === 3 && !this.isCanBack ? Visibility.Visible : Visibility.None) .fontColor($r('app.color.text_color')) Row(){ Column(){ Text(item.md5Str?.includes('Lossless')? Utility.resourceToString(this.context,$r('app.string.lossless')):item.md5Str)//音质 .fontSize(this.twoFingerType == 1 ? 8 : this.twoFingerType == 2 ? 9 : 11) .padding({ top: 3,right:6,left:6,bottom:3 }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .fontWeight(500) .borderRadius(12) .margin({ left: this.twoFingerType == 3 ? 6 : 8 }) .backgroundColor('#FFC107') .visibility(StrUtil.isEmpty(item.md5Str)?Visibility.None:Visibility.Visible) } .margin({ top: 2 ,right:6}) .visibility((this.modeType == 3 && !this.isCanBack)||(this.modeType == 2 && !this.isCanBack) ||(this.modeType == 0&&item.type==CommonConstants.TYPE_IS_DIR)? Visibility.None : Visibility.Visible) Text(StrUtil.isEmpty(item.artist) ? item.duration: item.artist) .fontSize(this.twoFingerType == 1 ? 10 : this.twoFingerType == 2 ? 12 : 13) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .margin({ top: 2}) .visibility(item.type == CommonConstants.TYPE_IS_ARTIST || item.type == CommonConstants.TYPE_IS_ALBUM || item.type == CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .fontWeight(FontWeight.Medium) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) } } .alignItems(HorizontalAlign.Center) // 关键:使内容水平居中 } .bindPopup(this.longItemFilePath === item.filePath, { builder: this.MenuBuilder(item, index, item.filePath), placement: Placement.Top, // autoCancel: true, mask: { color: '#33000000' }, // popupColor: Color.Yellow, enableArrow: false, //是否显示箭头 showInSubWindow: false, onStateChange: (e) => { if (!e.isVisible) { this.longItemFilePath = '' this.tempLyricContent = '' } } }) .gesture(LongPressGesture() .onAction(async () => { //如果是专辑和艺术家的首页,不能长按 if ((this.modeType === 2 || this.modeType == 3) && !this.isCanBack) { return } //如果是我的收藏 最近播放等也不能长按 if (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC ) { return } this.longItemFilePath = item.filePath this.tempLyricContent = await this.getLyricContent(item) LogUtil.info('长按this.tempLyricContent = ' + this.tempLyricContent) }) .onActionEnd((event: GestureEvent) => { }) ) .height(this.twoFingerType == 3 ? 60 : this.twoFingerType == 2 ? 55 : 42) .width(this.getGridWight()) .justifyContent(FlexAlign.Center) .margin({ left: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20, right: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20 }) } .width(this.getGridWight()) .borderRadius(12) .backgroundImage(item.pixelMapPath) .backgroundImageSize({ height: '100%', width: '100%' }) .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK) .animation({ curve: Curve.Sharp, duration: 300 }) Checkbox({ name: 'checkbox' + index }) .select(this.selectedFiles.some(x => x.filePath == item.filePath)) .selectedColor(this.themeColor) .shape(CheckBoxShape.CIRCLE) .opacity(this.isMultiSelect ? 1 : 0) .animation({ duration: 666, curve: 'Smooth' // 可选动画曲线 }) .visibility(this.isMultiSelect && item.type !== CommonConstants.TYPE_IS_DIR ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => this.handleFileSelection(item, checked)) .margin({ left: 20, top: 8, bottom: 8 }) .width(38) .height(38) } } .backgroundColor(Color.Transparent) .width('100%') .padding({ top: 15 }) .height(this.getGridAllHeight()) .onClick(() => { if (this.isMultiSelect && item.type !== CommonConstants.TYPE_IS_DIR) { const isChecked = this.selectedFiles.some(x => x.filePath == item.filePath); this.handleFileSelection(item, !isChecked); } else { this.doPlay(item, index) } }) } getGridAllHeight() { let heightG = 222 switch (this.twoFingerType) { case 1: heightG = 118 break; case 2: heightG = 168 break; case 3: heightG = 222 break; } return heightG } getGridHeight() { let heightG = 150 switch (this.twoFingerType) { case 1: heightG = 55 break; case 2: heightG = 100 break; case 3: heightG = 150 break; } return heightG } getGridWight() { let wightG = 158 switch (this.twoFingerType) { case 1: wightG = 80 break; case 2: wightG = 110 break; case 3: wightG = 158 break; } return wightG } @State isShowDetail:boolean = false @Builder MenuBuilder(item: VideoItem, index: number, filePath: string) { Scroll() { Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier(Utility.getIsFav(this.favList, item) ? $r('sys.symbol.heart_fill') : $r('sys.symbol.heart')), content: Utility.getIsFav(this.favList, item) ? '取消收藏' : '收藏' }) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { if (item) { this.doFav(item) } this.longItemFilePath = '' }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.forward_end_fill')), content: '下一首播放' }) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { this.addToNextPlay(item); this.longItemFilePath = '' }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')), content: '编辑信息' }) .bindSheet($$this.isShowEdit, this.editSheet(item), { height: this.isCoverOpacity() ? '95%' : '88%', dragBar: true, showClose: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, title: { title: '编辑信息' } }) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { this.isShowEdit = !this.isShowEdit; }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.rename')), content: $r('app.string.rename') }) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC ? Visibility.None : Visibility.Visible) : Visibility.Visible) .onClick(() => { this.showReNameDialog(item, index + '', filePath) this.longItemFilePath = '' }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')), content: $r('app.string.delete') }) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC ? Visibility.None : Visibility.Visible) : Visibility.Visible) .onClick(() => { this.showWarnIsDeleteFile(item, index + '', filePath) this.longItemFilePath = '' }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.info_circle')), content: $r('app.string.detail') }) .bindSheet($$this.isShowDetail, this.detailSheet(item), { height:this.isCoverOpacity()?'95%': '95%', dragBar: true, showClose: true, blurStyle:BlurStyle.Thin, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, backgroundColor:Color.Transparent, title: { title: '详情信息' } }) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { this.isShowDetail = !this.isShowDetail }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.share')), content: $r('app.string.share') }) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { Utility.doShareMusic(item, getContext(this) as common.UIAbilityContext) this.longItemFilePath = '' }) } .font({ size: 15, weight: FontWeight.Normal }) } .width(180) } .height(item.type === CommonConstants.TYPE_IS_DIR ? 150 : 400) } private listMaxScrollOffsetY: number = 0 @State selectedIndex: number = -1 setlistTwoFingers() { this.pinchValue = this.scaleValue; Logger.info('this.pinchValue = ' + this.pinchValue); if (this.pinchValue >= 1) { this.twoFingerType++ Logger.info('this.twoFingerType1 = ' + this.twoFingerType); if (this.twoFingerType > 3) { this.twoFingerType = 1 this.isGridMusic = true } } else { this.twoFingerType-- Logger.info('this.twoFingerType2 = ' + this.twoFingerType); if (this.twoFingerType < 1) { this.twoFingerType = 1 } } PreferencesUtil.put(SettingPage.TWO_FINGER_TYPE, this.twoFingerType) this.pinchValue = 1; this.scaleValue = 1; } @Builder getListView() { List({ scroller: this.listScroller }) { ListItemGroup({ header: this.coverHeader() }) { LazyForEach(this.dataSource, (item: VideoItem, index: number) => { ListItem() { Column() { if (item.type === CommonConstants.TYPE_IS_DIR || item.type === CommonConstants.TYPE_IS_ARTIST || item.type === CommonConstants.TYPE_IS_ALBUM) { this.DirItem(item, index) } else if (item.type === CommonConstants.TYPE_IS_CSJAD) { //如果是穿山甲广告 } else { this.MusicItem(item, index) } } .shadow(this.scaleItem === index ? { radius: 70, color: '#15000000', offsetX: 0, offsetY: 0 } : { radius: 0, color: '#15000000', offsetX: 0, offsetY: 0 }) .animation({ curve: Curve.Sharp, duration: 300 }) } // .transition(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 })) .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }) .animation({ duration: 500 }), TransitionEffect.scale({ x: 0, y: 0 }))) .clickEffect({ level: ClickEffectLevel.LIGHT }) .swipeAction(( (this.isSwipe&&this.modeType == 0 || this.modeType == 1 || ((this.modeType == 3 || this.modeType == 2) && this.isCanBack)) && item.type !== CommonConstants.TYPE_IS_CSJAD && !this.isHistory) ? { end: this.DeleteButton(item, index, item.filePath) } : {}) //左滑 .onClick(() => { if (this.isMultiSelect && item.type !== CommonConstants.TYPE_IS_DIR) { const isChecked = this.selectedFiles.some(x => x.filePath == item.filePath); this.handleFileSelection(item, !isChecked); } else { this.doPlay(item, index) } }) //拖动List关键代码开始 .scale({ x: this.scaleSelect(index), y: this.scaleSelect(index) }) .zIndex(this.dragItem === index ? 1 : 0) .translate(this.dragItem === index ? { y: this.offsetY } : { y: 0 }) .gesture( GestureGroup(GestureMode.Sequence, LongPressGesture({ repeat: true }) .onAction((event?: GestureEvent) => { animateTo({ curve: Curve.Friction, duration: 300 }, () => { this.scaleItem = index }) }) .onActionEnd(() => { animateTo({ curve: Curve.Friction, duration: 300 }, () => { this.scaleItem = -1 }) }), PanGesture({ fingers: 1, direction: null, distance: 0 }) .onActionStart(() => { this.dragItem = index // 记录当前拖动项索引 this.dragRefOffset = 0 // 重置偏移基准 }) .onActionUpdate((event: GestureEvent) => { this.offsetY = event.offsetY - this.dragRefOffset; this.neighborItem = -1; let indexA = this.videoLocalList.indexOf(item); // LogUtil.info('onecold indexA = '+indexA +' index=='+index) let curveValue: curves.ICurve = curves.initCurve(Curve.Sharp); let value: number = 0; if (this.offsetY < 0) { value = curveValue.interpolate(-this.offsetY / this.ITEM_INTV); this.neighborItem = indexA - 1; this.neighborScale = 1 - value / 20; } else if (this.offsetY > 0) { value = curveValue.interpolate(this.offsetY / this.ITEM_INTV); this.neighborItem = indexA + 1; this.neighborScale = 1 - value / 20; } if (this.offsetY > this.ITEM_INTV / 2 && indexA + 1 < this.videoLocalList.length) { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.offsetY -= this.ITEM_INTV; this.dragRefOffset += this.ITEM_INTV; this.itemMove(indexA, indexA + 1); }); } else if (this.offsetY < -this.ITEM_INTV / 2 && indexA - 1 >= 0) { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.offsetY += this.ITEM_INTV; this.dragRefOffset -= this.ITEM_INTV; this.itemMove(indexA, indexA - 1); }); } let curListOffset = this.listScroller.currentOffset() // 获取手指信息 let fingerInfo = event.fingerList[0] let clickPercentY = (fingerInfo.globalY - Number(this.listArea.globalPosition.y)) / Number(this.listArea.height) if (clickPercentY > 0.8 && !this.listScroller.isAtEnd()) { let scrollVelocity = clickPercentY > 0.9 ? 4 : 2 if (this.listMaxScrollOffsetY - curListOffset.yOffset > scrollVelocity + 5) { this.listScroller.scrollTo({ xOffset: 0, yOffset: curListOffset.yOffset += scrollVelocity }) } } else if (clickPercentY < 0.2 && curListOffset.yOffset >= 0) { let scrollVelocity = clickPercentY < 0.1 ? 4 : 2 if (curListOffset.yOffset > scrollVelocity + 5) { this.listScroller.scrollTo({ xOffset: 0, yOffset: curListOffset.yOffset -= scrollVelocity }) } } }) .onActionEnd((event: GestureEvent) => { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.dragItem = -1 this.neighborItem = -1 }) animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => { this.scaleItem = -1 }) }) ) .onCancel(() => { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.dragItem = -1 this.neighborItem = -1 }) animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => { this.scaleItem = -1 }) }) ) //拖动List关键代码结束 }, (item: VideoItem) => item.filePath) } } .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 }) .gesture(PinchGesture({ fingers: 2 }) .onActionEnd((event: GestureEvent) => { this.setlistTwoFingers() }) .onActionUpdate(e => { this.scaleValue = this.pinchValue * e.scale }) .onActionStart(e => { })) // .divider({ strokeWidth: 1, color: this.isDarkMode? '#333333':'#ffe9f0f0' }) .margin({ bottom: this.isCoverOpacity() ? (this.isShowCoverHeader() ? this.topBarHeight+25 : this.topBarHeight+80) : (this.isShowCoverHeader() ? this.topBarHeight+35 : this.isHiCarSmall()?this.topBarHeight+90:this.topBarHeight+125) }) .cachedCount(6) .borderRadius(20) .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }), TransitionEffect.scale({ x: 0, y: 0 }))) // .transition(TransitionEffect.move(TransitionEdge.BOTTOM) // .animation({ duration: 500, curve: Curve.Ease })) .layoutWeight(1) .onScrollIndex((start) => { this.selectedIndex = start }) .visibility(this.isGridMusic ? Visibility.None : Visibility.Visible) .lanes( new BreakpointType({ sm: 1, md: 2, lg: 2, xl: 3, xxl: 4 }).getValue(this.currentBreakpoint), new BreakpointType({ sm: 0, md: 12, lg: 12, xl: 16, xxl: 20 }).getValue(this.currentBreakpoint) ) .onAreaChange((oldValue: Area, newValue: Area) => { this.listArea = newValue if (this.twoFingerType == 3) { this.listMaxScrollOffsetY = this.videoLocalList.length * (ITEM_HEIGHT_BIG) - 10 } else if (this.twoFingerType == 2) { this.listMaxScrollOffsetY = this.videoLocalList.length * (ITEM_HEIGHT) - 10 } else if (this.twoFingerType == 1) { this.listMaxScrollOffsetY = this.videoLocalList.length * (ITEM_HEIGHT_SMALL) - 10 } }) .onScrollFrameBegin((offset: number) => { //滚动小屏幕 比如puraX外屏和手机横屏可以隐藏bottomBarHeight topBarHeight // if (this.isPhoneLan()) { // this.setBarHeightHide(offset) // } else if (this.currentWidthBreakpoint !== WidthBreakpoint.WIDTH_SM || (this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_MD && this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_SM)) { if (this.isScrollHide) { this.setBarHeightHide2(offset) } else { this.setBarHeightNormal() } } else { this.setBarHeightHide(offset) } return { offsetRemain: offset }; }) //拖拽pc或者pad的用鼠标拖拽音乐和视频到List或Grid上自动导入视频 .allowDrop([uniformTypeDescriptor.UniformDataType.AUDIO,uniformTypeDescriptor.UniformDataType.VIDEO]) .onDrop((event?: DragEvent) => { try { let dragData: UnifiedData = (event as DragEvent).getData() as UnifiedData; if (dragData !== undefined) { let records: unifiedDataChannel.UnifiedRecord[] = dragData.getRecords(); if (records.length > 0) { for (let i = 0; i < records.length; i++) { let types = records[i].getTypes(); if (types.includes(uniformTypeDescriptor.UniformDataType.FILE_URI)) { const fileUriUds = records[i].getEntry(uniformTypeDescriptor.UniformDataType.FILE_URI) as uniformDataStruct.FileUri; let typeDescriptor = uniformTypeDescriptor.getTypeDescriptor(fileUriUds.fileType); if (typeDescriptor.belongsTo(uniformTypeDescriptor.UniformDataType.AUDIO) ||typeDescriptor.belongsTo(uniformTypeDescriptor.UniformDataType.VIDEO)) { this.targetFile = fileUriUds.oriUri; this.saveVideoDatas([this.targetFile]) hilog.info(0x0000, 'Heanup', '当前targetFile:' + this.targetFile); } } } } else { hilog.info(0x0000, TAG, `%{public}s`, `dragData arr is null`); } } else { hilog.info(0x0000, TAG, `%{public}s`, `dragData is undefined`); } } catch (error) { const err = error as BusinessError; hilog.error(0x0000, TAG, `startDataLoading errorCode: ${err.code}, errorMessage: ${err.message}`); } }) } setBarHeightNormal() { this.hideDone = false; this.getUIContext().animateTo({ duration: 300 }, () => { this.bottomBarHeight = 55 + 0; this.topBarHeight = 50 + 0; this.barOpacity = 1; this.barBottomOpacity = 1 this.currentYOffset = 0; this.isHiding = false; }); } setBarHeightHide(offset: number) { if (offset > 0 && !this.hideDone) { this.currentYOffset += offset; if (this.currentYOffset <= 100) { this.bottomBarHeight = this.bottomBarHeight * (1 - this.currentYOffset / 100); this.topBarHeight = this.topBarHeight * (1 - this.currentYOffset / 100); this.barOpacity = 1 - this.currentYOffset / 100; this.barBottomOpacity = 1 - this.currentYOffset / 100; } else { this.topBarHeight = 0; this.bottomBarHeight = 0; this.barBottomOpacity = 0 this.barOpacity = 0; this.hideDone = true; } this.isHiding = true; } if (offset < 0 && this.isHiding) { this.hideDone = false; this.getUIContext().animateTo({ duration: 300 }, () => { this.bottomBarHeight = 55 + 0; this.topBarHeight = 50 + 0; this.barOpacity = 1; this.barBottomOpacity = 1 this.currentYOffset = 0; this.isHiding = false; }); } } //这个方法没隐藏bottomBar,只隐藏topbar setBarHeightHide2(offset: number) { if (offset > 0 && !this.hideDone) { this.currentYOffset += offset; if (this.currentYOffset <= 100) { this.topBarHeight = this.topBarHeight * (1 - this.currentYOffset / 100); this.barOpacity = 1 - this.currentYOffset / 100; } else { this.topBarHeight = 0; this.barOpacity = 0; this.hideDone = true; } this.isHiding = true; } if (offset < 0 && this.isHiding) { this.hideDone = false; this.getUIContext().animateTo({ duration: 300 }, () => { this.topBarHeight = 50 + 0; this.barOpacity = 1; this.currentYOffset = 0; this.isHiding = false; }); } } private doPlay(item: VideoItem, index?: number, isFromSonPlayList?: boolean) { switch (item.type) { case CommonConstants.TYPE_IS_DIR: // 进入歌单前安全保存当前滚动偏移量,支持列表和网格 let offsetB = 0 if (this.isGridMusic) { if (this.scroller && typeof this.scroller.currentOffset === 'function') { let cur = this.scroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offsetB = cur.yOffset } } this.mScrollMap.set('lastGridDirScrollOffset', offsetB) } else { if (this.listScroller && typeof this.listScroller.currentOffset === 'function') { let cur = this.listScroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offsetB = cur.yOffset } } this.mScrollMap.set('lastListDirScrollOffset', offsetB) } if (item.name === LocalMusic.STR_LOCK_VIDEO) { this.showVerifyDialog() this.currentTitleName = '私密音频' this.currentTitleCover = $r('app.media.ic_avatar5') } else if (item.name === LocalMusic.STR_HISTORY_MUSIC) { this.titleBarModel.setTitleName('最近播放') this.currentTitleCover = $r('app.media.ic_avatar2') this.currentTitleName = '最近播放' //加载动画效果 this.updateListData(this.historyList, true) // this.videoLocalList = this.historyList if (ArrayUtil.isEmpty(this.historyList)) { ToastUtil.showToast('最近播放记录空空如也') } else { this.currentTitleCover = Utility.getFirstCoverFromList(this.historyList) } this.isHistory = true this.titleBarModel.setLeftIconMain($r('app.media.left_back_white')) } else if (item.name === LocalMusic.STR_FAC_VIDEO) { this.titleBarModel.setTitleName('我的收藏') this.currentTitleName = '我的收藏' this.currentTitleCover = $r('app.media.ic_avatar1') //加载动画效果 this.updateListData(this.favList) if (ArrayUtil.isEmpty(this.favList)) { ToastUtil.showToast('无音乐收藏记录') } else { this.currentTitleCover = Utility.getFirstCoverFromList(this.favList) } this.isFavMusic = true this.titleBarModel.setLeftIconMain($r('app.media.left_back_white')) } else { this.currentTitleName = item.name this.currentPath = item.filePath this.currentTitleCover = $r('app.media.ic_avatar5') this.getSortedFiles(this.currentPath) } //进入歌单的时候的滚动位置在顶部 if (this.isGridMusic) { this.scroller.scrollToIndex(0) } setTimeout(() => { if (this.modeType == 0) { if (!this.isGridMusic) { this.listScroller.scrollToIndex(0) } } }, 200) break; case CommonConstants.TYPE_IS_ARTIST: // 进入艺术家前安全保存当前滚动偏移量,支持列表和网格 let offsetA = 0 if (this.isGridMusic) { if (this.scroller && typeof this.scroller.currentOffset === 'function') { let cur = this.scroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offsetA = cur.yOffset } } this.mScrollMap.set('lastGridArtistScrollOffset', offsetA) } else { if (this.listScroller && typeof this.listScroller.currentOffset === 'function') { let cur = this.listScroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offsetA = cur.yOffset } } this.mScrollMap.set('lastListArtistScrollOffset', offsetA) } let mList = this.artistMap.get(item.name) if (mList !== undefined && ArrayUtil.isNotEmpty(mList)) { Utility.doSortListAscending(mList) this.isCanBack = true this.titleBarModel.setTitleName(item.name) this.currentTitleName = '艺术家:' + item.name if (item.pixelMapPath) { this.currentTitleCover = item.pixelMapPath } this.updateListData(mList) this.titleBarModel.setLeftIconMain($r('app.media.left_back_white')) } //进入歌单的时候的滚动位置在顶部 if (this.isGridMusic) { this.scroller.scrollToIndex(0) } break; case CommonConstants.TYPE_IS_ALBUM: // 进入专辑前安全保存当前滚动偏移量,支持列表和网格 let offset = 0 if (this.isGridMusic) { if (this.scroller && typeof this.scroller.currentOffset === 'function') { let cur = this.scroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offset = cur.yOffset } } this.mScrollMap.set('lastGridAlBumScrollOffset', offset) } else { if (this.listScroller && typeof this.listScroller.currentOffset === 'function') { let cur = this.listScroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offset = cur.yOffset } } this.mScrollMap.set('lastListAlBumScrollOffset', offset) } let albumList = this.albumMap.get(item.name) if (albumList !== undefined && ArrayUtil.isNotEmpty(albumList)) { Utility.doSortListAscending(albumList) this.isCanBack = true this.titleBarModel.setTitleName(item.name) this.currentTitleName = '专辑:' + item.name if (item.pixelMapPath) { this.currentTitleCover = item.pixelMapPath } this.updateListData(albumList) this.titleBarModel.setLeftIconMain($r('app.media.left_back_white')) this.mScrollMap.set(item.name, this.selectedIndex) } //进入歌单的时候的滚动位置在顶部 if (this.isGridMusic) { this.scroller.scrollToIndex(0) } break; case CommonConstants.TYPE_IS_CSJAD: return case CommonConstants.TYPE_LOCAL: // 移除自动停止逻辑,让UnifiedPlayerService处理播放器状态管理 // if (this.CONTROL_PlayStatus !== PlayStatus.INIT) { // this.stop(); // } // 检查是否正在投播 if (this.isCurrentlyCasting()) { // 更新当前歌曲信息 this.curIndex = index as number; this.currentSong = item; this.videoUrl = this.currentSong.filePath; this.artist = this.currentSong.artist; this.name = this.currentSong.name; this.cover = this.currentSong.pixelMapPath; this.changeCasting(); return; } if (isFromSonPlayList) { //点击来自右下角的播放列表 this.currentSong = item if (index !== undefined) { this.curIndex = index } } else { // 点击文件列表中的歌曲,需要判断是否要创建新的播放列表 let globalVideoList = Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL) as VideoItem[]; this.curIndex = Utility.getCurIndexFromGlobalList(globalVideoList, item.filePath) // 检查当前播放列表是否包含这首歌 const currentSongIndex = this.songList.findIndex(song => song.filePath === item.filePath); if (currentSongIndex >= 0 && ArrayUtil.isNotEmpty(this.songList)) { // 如果当前播放列表已经包含这首歌,使用现有播放列表 this.curIndex = currentSongIndex; this.currentSong = this.songList[this.curIndex]; } else { // 如果当前播放列表不包含这首歌,或者没有播放列表,则创建新的 this.songList = globalVideoList; this.sonDataSource.pushArrayData(this.songList); this.currentSong = globalVideoList[this.curIndex]; } } this.videoUrl = this.currentSong.filePath this.name = this.currentSong.name this.cover = this.currentSong.pixelMapPath this.artist = this.currentSong.artist // 重置时间显示状态,防止新歌曲时间显示被上一首歌曲的时间"卡住" this.oldSeconds = 0; this.currentTime = "00:00"; this.currentTime = "00:00"; this.lastSongPath = this.currentSong.filePath; this.justSwitched = true; // 标记歌曲刚刚切换 // 同步播放列表到UnifiedPlayerService this.syncPlaylistToService(); this.startPlayOrResumePlay() break; } } @Builder PlayController() { Column() { Line().width('100%').height(1).backgroundColor($r('app.color.index_background')) .visibility(this.isCustomizeBg ? Visibility.Hidden : Visibility.Visible) Row() { Row() { RotatingCover({ songLabel: this.currentSong?.pixelMapPath }) Column() { Text(this.currentSong?.name) .fontSize(16) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .maxLines(1) .fontColor($r('app.color.text_color')) .fontWeight(500) Row() { Text(this.currentSong?.artist) .margin({ top: 2 }) .fontSize(13) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .fontColor($r('app.color.text_color')) } } .alignItems(HorizontalAlign.Start) .margin({ right: 22 }) } .padding({ right: 18 }) .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => { if (ArrayUtil.isNotEmpty(this.songList)) { this.isShowPlay = true; let lyricPath = this.videoUrl.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc' this.initLyric(lyricPath); this.startAutoHide() } else { ToastUtil.showToast('当前播放列表为空,请先导入音乐。') } }) Row() { Image($r('app.media.hm_previous')) .height(28) .width(28) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .margin({ left: 8, right: 16 }) .fillColor(this.themeColor) .displayPriority(2) .onClick(() => { if (ArrayUtil.isNotEmpty(this.songList)) { this.playPrevious() } else { ToastUtil.showToast('当前播放列表为空,请先导入音乐。') } }) //播放进度条 Stack() { Progress({ value: Math.floor(this.progressValue), type: ProgressType.Ring, }) .color(this.themeColor)// 进度条前景色为灰色 .backgroundColor($r('app.color.index_background')) .height(40) .aspectRatio(CommonConstants.ASPECT_RATIO) Image(this.CONTROL_PlayStatus === PlayStatus.PLAY ? $r('app.media.hm_pause') : $r('app.media.hm_play2')) .height(38) .width(38) .displayPriority(3) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .fillColor(this.themeColor) .onClick(() => { if (ArrayUtil.isNotEmpty(this.songList)) { this.playOrPause() } else { ToastUtil.showToast('当前播放列表为空,请先导入音乐。') } }) } Image($r('app.media.hm_next')) .height(28) .width(28) .margin({ right: 16, left: 16 }) .fillColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .displayPriority(2) .onClick(() => { if (ArrayUtil.isNotEmpty(this.songList)) { this.playNext() } else { ToastUtil.showToast('当前播放列表为空,请先导入音乐。') } }) Image($r('app.media.hm_playlist')) .height(28) .width(28) .displayPriority(1) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .fillColor(this.themeColor) .bindSheet($$this.isShowSheet, this.PlayListSheet(), { height: '95%', dragBar: true, showClose: true, blurStyle: BlurStyle.Thin, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, backgroundColor: this.isPlayListBgGrass ? Color.Transparent : $r('app.color.silvery'), title: { title: Utility.resourceToString(this.context, $r('app.string.current_play_list')) + `(${this.songList.length}首)` } }) .onClick(() => { if (ArrayUtil.isNotEmpty(this.songList)) { this.isShowSheet = !this.isShowSheet; this.isFrontWhite = false LogUtil.info('onecold scrollToIndex = ' + this.curIndex) setTimeout(() => { this.playListScroller.scrollToIndex(this.curIndex, true, ScrollAlign.CENTER) }, 123) } else { ToastUtil.showToast('当前播放列表为空,请先导入音乐。') } }) } } .width('100%') .height(this.bottomBarHeight) .justifyContent(FlexAlign.Center) .hitTestBehavior(HitTestMode.Transparent) .backgroundColor(this.isCustomizeBg ? Color.Transparent : $r('app.color.bottom_control_background')) .bindContentCover($$this.isShowPlay, this.MusicPlayBuilder(), { // modalTransition: ModalTransition.DEFAULT, transition: AnimationHelper.transitionInDown(666), backgroundColor: 'rgba(0, 0, 0, 0.9)', }) .padding({ left: 16, right: 16 }) Line() .width('100%') .height(25) .backgroundColor($r('app.color.bottom_control_background'))// .visibility(this.isPhoneLan()?Visibility.None:this.isCustomizeBg?Visibility.Hidden:Visibility.Visible) .visibility(this.isCustomizeBg ? Visibility.Hidden : Visibility.Visible) } .clickEffect({ level: ClickEffectLevel.HEAVY }) } @State isFrontWhite: boolean = false //播放列表布局 @Builder PlayListSheet() { Column() { this.PlayList() } .width('100%') .height('100%') .onAppear(() => { // 播放列表显示时刷新封面信息 this.refreshPlaylistCovers(); }) } @State titleStr: string = '' @State ablumStr: string = '' @State artistStr: string = '' @State lyricConStr: string = '' @State currentEditItem: string = '' doUpdateData() { setTimeout(() => { if (this.modeType === 0) { this.deleteCache(this.currentPath) this.getSortedFiles(this.currentPath) } workerInstance.postMessage({ code: 2, data: this.context }); workerInstance.postMessage({ code: 3, data: this.context }); workerInstance.postMessage({ code: 4, data: this.context }); }, 999) } //编辑信息 doEdit(item: VideoItem) { if (!item) { return } if (StrUtil.isEmpty(this.titleStr)) { ToastUtil.showToast('标题不能为空') return } this.table.updateMediaInfo(item.filePath, this.titleStr, this.artistStr, this.ablumStr, (success: boolean, error?: string) => { this.isShowEdit = false if (success) { console.log(" onecold 编辑信息成功,数据库已同步"); ToastUtil.showToast('保存成功') this.isShowMoreView = false this.name = this.titleStr if (item.filePath == this.currentSong?.filePath) { this.currentSong.name = this.titleStr this.artist = this.artistStr this.currentSong.artist = this.artistStr this.currentSong.album = this.ablumStr } this.doUpdateData() } else { ToastUtil.showToast('保存失败' + error?.toString()) console.error(" onecold 编辑信息数据库失败原因: " + error); } }); let doChangeLyric = false if (item.filePath == this.currentSong?.filePath) { if (this.lyricConStr !== this.lyricContent) { doChangeLyric = true } } else if (this.lyricConStr !== this.tempLyricContent) { doChangeLyric = true } if (doChangeLyric) { let lyricPath = item.filePath.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc' let jiaMilyricPath = lyricPath.substring(0, lyricPath.lastIndexOf('.')) + '.lrcc' let isJiaMi = false; let realLyricPath = lyricPath if (FileUtil.accessSync(jiaMilyricPath)) { isJiaMi = true realLyricPath = jiaMilyricPath console.info("onecold doEdit找到加密本地歌词 "); } else { isJiaMi = false realLyricPath = lyricPath console.info("onecold doEdit本地歌词 "); } this.saveDataToFile(this.lyricConStr, realLyricPath, isJiaMi) } } updateLyricToDB() { } @Builder editSheet(item: VideoItem) { Scroll() { Column() { this.editDetail(item) } } .width('100%') .height('100%') } @Builder editDetail(item: VideoItem) { Column() { if (item) { Row() { Stack() { Image(StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.music_red') : item.pixelMapPath) .fillColor(this.themeColor) .height(33) .width(33) .borderRadius('100%') .clip(true) .opacity(this.opacityItem)// 绑定透明度 .margin({ left: 20 }) } .width('15%') Row() { Column() { Text(item.name) .fontSize(14) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .animation({ duration: 555, curve: 'Linear', }) .fontColor(this.themeColor) .margin({ left: 10 }) Row() { Text(item.artist) .fontSize(11) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .padding({ top: 8 }) .fontColor(this.themeColor) .margin({ left: 10 }) } } .height('100%') .width(100) .visibility(this.isDarkMode ? Visibility.None : Visibility.Visible) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) Button('本地封面') .fontColor(Color.White) .fontSize(12) .height(38) .width(88) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ right: 5, left: 12 }) .onClick(async () => { this.goSelectImage(item) }) Button('获取封面') .fontColor(Color.White) .fontSize(12) .height(38) .width(88) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ right: 12, left: 5 }) .onClick(async () => { this.doSearchCover(item) }) } .height('100%') .justifyContent(FlexAlign.Start) .layoutWeight(1) } .width('100%') .height(58) Row() { Text('标题:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) TextInput({ text: item.name }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.titleStr = val }) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('艺术家:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) TextInput({ text: item.artist }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.artistStr = val }) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('专辑名:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) TextInput({ text: item.album }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.ablumStr = val }) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('歌词:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) TextArea({ text: item.filePath === this.currentSong?.filePath ? this.lyricContent : this.tempLyricContent }) .type(TextAreaType.NORMAL) .height(275) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.lyricConStr = val }) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Button('保存') .fontColor(Color.White) .layoutWeight(1) .height(50) .width(100) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ right: 20, bottom: 20 }) .onClick(async () => { this.doEdit(item) }) Button('取消') .fontColor(Color.White) .layoutWeight(1) .height(50) .width(100) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ left: 20, bottom: 20 }) .onClick(async () => { this.isShowEdit = false }) } .margin({ left: 38, right: 38, top: 10, bottom: 10 }) .width(250) .height(60) } } .margin({ bottom: 20 }) } @Builder detailSheet(item:VideoItem) { Scroll() { Column() { this.songDetail(item) } } .width('100%') .height('100%') } @Builder songDetail(currentItem:VideoItem) { Column() { if (currentItem) { Row() { Stack() { Image(StrUtil.isEmpty(currentItem.pixelMapPath) ? $r('app.media.music_red') : currentItem.pixelMapPath) .fillColor(this.themeColor) .height(33) .width(33) .borderRadius('100%') .clip(true) .opacity(this.opacityItem)// 绑定透明度 .margin({ left: 20 }) } .width('15%') Column() { Column() { Text(currentItem.name) .fontSize(14) .maxLines(1) .animation({ duration: 555, curve: 'Linear', }) .fontColor($r('app.color.text_color')) .margin({ left: 10 }) Row() { Text(StrUtil.isEmpty(currentItem.artist) ? currentItem.cTime : currentItem.artist + ' ' + currentItem?.album) .fontSize(11) .padding({ top: 8 }) .fontColor($r('app.color.text_color')) .margin({ left: 10 }) } } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') } .width('100%') .height(58) .justifyContent(FlexAlign.SpaceBetween) Row() { Text('歌手名:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem?.artist ? currentItem.artist : '未知歌手') .fontSize(14) .fontColor(Color.White) .margin({ left: 10 }) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('专辑名:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem?.album ?currentItem.album : '未知专辑') .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('时长:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(StrUtil.isEmpty(currentItem.duration)? (this.isShowDetailMore?this.totalTime:'未知'):currentItem.duration) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('采样率:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(Utility.convertToKHz(currentItem.sampleRate)) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('比特率:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.bit_rate) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('风格:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.genre) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('发行时间:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.year) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('质量评分:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.probe_score+"") .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('音轨号:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.track) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('格式:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(Utility.formatMimeType(currentItem.mimeType)) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('播放次数:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.playCount?.toString()) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('流数量:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.nb_streams?.toString()) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('文件大小:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.size) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('文件名:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.fileName) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('修改时间:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.cTime) .fontSize(14) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('存放目录:') .fontSize(14) .fontColor(Color.White) .margin({ left: 22 }) Text(currentItem.filePath) .fontSize(12) .margin({ left: 10 }) .fontColor(Color.White) .layoutWeight(1) } .margin({ top: 10, bottom: 10 }) .width('100%') .justifyContent(FlexAlign.Start) } } .margin({ bottom: 20 }) } //定时关闭布局 @Builder TimeCloseSheet() { Scroll() { Column() { this.TimeClose() } } .width('100%') .height('100%') } //倒计时关闭音乐,退出App功能实现。 @State selectedMinutes: number = 0 // 0表示未开启 @State remainingSeconds: number = 0 // 预设时间选项 private presetTimes: number[] = [0, 5, 15, 30, 45, 60, -1] private formatTime(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = seconds % 60 return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } // 开始倒计时 private handleTimeSelect(minutes: number) { if (minutes === this.selectedMinutes) { this.stopCountdown(); this.selectedMinutes = 0; return; } this.selectedMinutes = minutes; this.remainingSeconds = minutes * 60; this.startCountdown(); // 新增调用 } @Builder TimeClose() { Column() { // 倒计时显示区域 Text(this.selectedMinutes > 0 ? `剩余时间:${this.formatTime(this.remainingSeconds)}` : "定时关闭未开启") .fontSize(17) .fontColor($r('app.color.text_color')) .margin({ top: 2, bottom: 8 }); Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start }) { ForEach(this.presetTimes, (minutes: number, index: number) => { // 单行时间选项容器 Row() { // 文字描述区域 Text(this.getDisplayText(minutes)) .fontSize(15) .fontColor(minutes === this.selectedMinutes ? this.themeColor : $r('app.color.text_color'))// 选中态颜色变化 .textAlign(TextAlign.Start) .layoutWeight(1); // 占据剩余空间 // 复选框样式的选择器 Row() { if (minutes === this.selectedMinutes) { // 选中状态 - 带勾号的圆形复选框 Row() { // 使用系统内置的勾号图标 Image($r('sys.media.ohos_ic_public_ok')) .width(14) .height(14) .fillColor(Color.White) } .width(24) .height(24) .borderRadius(12) .backgroundColor(this.themeColor) .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) } else { // 未选中状态的圆形边框 Row() .width(24) .height(24) .borderRadius(12) .border({ width: 2, color: $r('app.color.text_color'), style: BorderStyle.Solid }) .backgroundColor(Color.Transparent) } } .width(24) .height(24) .margin({ left: 20 }) .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) } .width('100%') .padding({ top: 18, bottom: 18, left: 18, right: 18 }) .backgroundColor(minutes === this.selectedMinutes ? $r('app.color.timeColor_bg') : $r('app.color.bottom_control_background')) // 选中背景高亮 .borderRadius(12) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ bottom: 18 }) .onClick(() => this.handleSelect(minutes)); // 点击整行触发选择 }); } .width('100%') .padding(16) } } // 获取显示文本 private getDisplayText(minutes: number): string { if (minutes === -1) { return Utility.resourceToString(getContext(this), $r('app.string.customize')); } return minutes === 0 ? Utility.resourceToString(getContext(this), $r('app.string.close')) : `${minutes}` + Utility.resourceToString(getContext(this), $r('app.string.minute_after')); } // 处理选择事件 private handleSelect(minutes: number): void { if (minutes === -1) { // 自定义 // 弹出自定义时间对话框 DialogHelper.showTextInputDialog({ title: '自定义时间', maskColor: Color.Transparent, inputType: InputType.Number, text: '', onChange: (text) => { console.error("onChange: " + text); }, onAction: (action, dialogId, content) => { if (action == DialogAction.TWO) { const customMinutes = Number(content); if (customMinutes > 0) { this.handleTimeSelect(customMinutes); // 设置自定义时间 ToastUtil.showToast("设置成功," + customMinutes + "分钟后定时关闭"); } } } }); } else { this.handleTimeSelect(minutes); // 统一调用选择处理方法 } } // 添加定时器引用 private timerId: number = -1; // 启动/重置倒计时 private startCountdown() { // 清除旧定时器 if (this.timerId !== -1) { clearInterval(this.timerId); } // 创建新定时器 if (this.selectedMinutes > 0) { this.timerId = setInterval(() => { if (this.remainingSeconds === 15) { this.showConfirmationDialog(); } if (this.remainingSeconds > 0) { this.remainingSeconds -= 1; } else { this.stopCountdown(); this.handleTimeout(); } }, 1000); } } // 15倒计时后再次提示显示确认是否关闭App对话框 private showConfirmationDialog() { DialogHelper.showAlertDialog({ backgroundColor: $r('app.color.btn_green'), title: '确认关闭', maskColor: Color.Transparent, content: "马上到达定时关闭时间,您是否确定要退出应用?", borderStyle: BorderStyle.Dashed, primaryButton: '暂不关闭', secondaryButton: '确定', onAction: (action) => { if (action == DialogAction.ONE) { //暂不关闭,取消定时 this.stopCountdown(); this.selectedMinutes = 0 } else if (action == DialogAction.TWO) { } } }) } // 停止倒计时 private stopCountdown() { if (this.timerId !== -1) { clearInterval(this.timerId); this.timerId = -1; } } // 超时处理 private handleTimeout() { // 执行播放器关闭逻辑 this.selectedMinutes = 0; ToastUtil.showToast(" 定时关闭已完成"); this.stop(); // 假设存在的播放器实例 //退出APP const mContext = getContext(this) as common.UIAbilityContext mContext.terminateSelf(); AvSessionController.getInstance(true).unregisterSessionListener() } //倒计时关闭音乐,退出App功能实现结束。 //播放列表的拖动List 重新排序 @State sonDataSource: LazyDataSource = new LazyDataSource(this.songList) //左滑操作 移动文件 重命名 删除等操作 @Builder sonButton(item: VideoItem, index: number, filePath: string) { Row() { //加入下一首播放 Button() { Image($r('app.media.next_to_play')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .backgroundColor(this.themeColor) .margin(5) .onClick(() => { this.addToNextPlay(item); }) Button() { Image($r('app.media.close')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .onClick(() => { this.removeFromPlaylistViaService(Number(index)); }) Button() { Image($r('app.media.share2')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .onClick(() => { Utility.doShareMusic(item, getContext(this) as common.UIAbilityContext) }) } } itemMoveSon(index: number, newIndex: number): void { if (newIndex < 0 || newIndex >= this.songList.length) { return; } try { // 使用UnifiedPlayerService进行播放列表重排序 const removedSong = this.unifiedPlayerService.removeFromPlaylist(index); if (removedSong) { this.unifiedPlayerService.addToPlaylist(removedSong, newIndex); } // 更新本地播放列表 let tmp = this.songList.splice(index, 1); this.songList.splice(newIndex, 0, tmp[0]); this.sonDataSource.pushArrayData(this.songList); // 更新当前索引 this.curIndex = Utility.getIndexFromList(this.songList, this.videoUrl); } catch (error) { // 错误处理:回退到原有逻辑 let tmp = this.songList.splice(index, 1); this.songList.splice(newIndex, 0, tmp[0]); this.sonDataSource.pushArrayData(this.songList); this.curIndex = Utility.getIndexFromList(this.songList, this.videoUrl); } } @Builder PlayList() { List({ scroller: this.playListScroller }) { LazyForEach(this.sonDataSource, (item: VideoItem, index: number) => { ListItem() { this.MusicItemSon(item, index) } .swipeAction({ end: this.sonButton(item, index, item.filePath) }) //左滑 .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }), TransitionEffect.scale({ x: 0, y: 0 }))) .clickEffect({ level: ClickEffectLevel.MIDDLE }) //拖动List关键代码开始 .scale({ x: this.scaleSelect(index), y: this.scaleSelect(index) }) .zIndex(this.dragItem === index ? 1 : 0) .translate(this.dragItem === index ? { y: this.offsetY } : { y: 0 }) .gesture( GestureGroup(GestureMode.Sequence, LongPressGesture({ repeat: true }) .onAction((event?: GestureEvent) => { animateTo({ curve: Curve.Friction, duration: 300 }, () => { this.scaleItem = index }) }) .onActionEnd(() => { animateTo({ curve: Curve.Friction, duration: 300 }, () => { this.scaleItem = -1 }) }), PanGesture({ fingers: 1, direction: null, distance: 0 }) .onActionStart(() => { this.dragItem = index // 记录当前拖动项索引 this.dragRefOffset = 0 // 重置偏移基准 }) .onActionUpdate((event: GestureEvent) => { this.offsetY = event.offsetY - this.dragRefOffset; this.neighborItem = -1; let indexA = this.songList.indexOf(item); // LogUtil.info('onecold indexA = '+indexA +' index=='+index) let curveValue: curves.ICurve = curves.initCurve(Curve.Sharp); let value: number = 0; if (this.offsetY < 0) { value = curveValue.interpolate(-this.offsetY / this.ITEM_INTV); this.neighborItem = indexA - 1; this.neighborScale = 1 - value / 20; } else if (this.offsetY > 0) { value = curveValue.interpolate(this.offsetY / this.ITEM_INTV); this.neighborItem = indexA + 1; this.neighborScale = 1 - value / 20; } if (this.offsetY > this.ITEM_INTV / 2 && indexA + 1 < this.songList.length) { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.offsetY -= this.ITEM_INTV; this.dragRefOffset += this.ITEM_INTV; this.itemMoveSon(indexA, indexA + 1); }); } else if (this.offsetY < -this.ITEM_INTV / 2 && indexA - 1 >= 0) { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.offsetY += this.ITEM_INTV; this.dragRefOffset -= this.ITEM_INTV; this.itemMoveSon(indexA, indexA - 1); }); } }) .onActionEnd((event: GestureEvent) => { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.dragItem = -1 this.neighborItem = -1 }) animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => { this.scaleItem = -1 }) }) ) .onCancel(() => { animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.dragItem = -1 this.neighborItem = -1 }) animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => { this.scaleItem = -1 }) }) ) //拖动List关键代码结束 .onClick(() => { this.doPlay(item, index, true) }) }, (item: VideoItem) => item.filePath) } .width('100%') .height('100%') // .transition(TransitionEffect.move(TransitionEdge.BOTTOM) // .animation({ duration: 500, curve: Curve.Ease })) .cachedCount(10) .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 }) .gesture(PinchGesture({ fingers: 2 }) .onActionEnd((event: GestureEvent) => { this.setlistTwoFingers() }) .onActionUpdate(e => { this.scaleValue = this.pinchValue * e.scale }) .onActionStart(e => { })) .padding({ bottom: 0 }) } @Builder private MusicItemSon(item: VideoItem, index?: number) { Button({ type: ButtonType.Normal, stateEffect: true }) { Row() { Stack() { Image(StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.music_red') : item.pixelMapPath) .height(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 35 : 30) .width(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 35 : 30) .alt($r('app.media.music_red')) .fillColor(this.themeColor) .borderRadius('100%') .clip(true) .margin({ left: 20 }) } .width('15%') Column() { Column() { Text(item.name) .fontSize(this.twoFingerType == 1 ? 12 : this.twoFingerType == 2 ? 15 : 18) .fontColor(this.curIndex === index ? this.themeColor : this.isFrontWhite ? Color.White : $r('app.color.text_color')) .maxLines(1) .margin({ left: this.twoFingerType == 3 ? 18 : 10 }) Row() { Column(){ Text(item.md5Str?.includes('Lossless')? Utility.resourceToString(this.context,$r('app.string.lossless')):item.md5Str)//音质 .fontSize(this.twoFingerType == 1 ? 8 : this.twoFingerType == 2 ? 9 : 11) .padding({ top: 3,right:6,left:6,bottom:3 }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .fontWeight(500) .borderRadius(12) .margin({ left: this.twoFingerType == 3 ? 12 : 10 }) .backgroundColor('#FFC107') } .padding({ top: 8 }) Text(StrUtil.isEmpty(item.artist) ? item.cTime : item.artist + ' ' + item?.album) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .fontColor(this.curIndex === index ? this.themeColor : this.isFrontWhite ? $r('app.color.playlistText_color') : $r('app.color.text_color')) .margin({ left: this.twoFingerType == 3 ? 5 : 3 }) Blank() Text(item.size) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .visibility(StrUtil.isEmpty(item.artist) ? Visibility.Visible : Visibility.None) .fontColor(this.curIndex === index ? $r('app.color.title_bar_bg_text') : this.isFrontWhite ? $r('app.color.playlistText_color') : $r('app.color.text_color')) .margin({ left: 10 }) } } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') .width('65%') Column() { ImageAnimator() .images(this.imagesF)// 动画数组 .duration(1000)// 持续 .state(this.animationState)// 动画状态 .fillMode(FillMode.Forwards) .width(18) .visibility(this.videoUrl === item.filePath ? this.isMultiSelect ? Visibility.None : Visibility.Visible : Visibility.None) .height(18) .iterations(-1) // 播放次数 } .width('20%') } } .backgroundColor(Color.Transparent) .width('100%') .height(this.twoFingerType == 3 ? ITEM_HEIGHT_BIG : this.twoFingerType == 2 ? ITEM_HEIGHT : ITEM_HEIGHT_SMALL) .shadow(this.scaleItem === index ? { radius: 70, color: '#15000000', offsetX: 0, offsetY: 0 } : { radius: 0, color: '#15000000', offsetX: 0, offsetY: 0 }) .animation({ curve: Curve.Sharp, duration: 300 }) } @Builder MusicPlayBuilder() { Column() { this.MusicPlayerView() } .height('100%') .width('100%') .onDisAppear(() => { this.translateY = 0; }) .translate({ y: this.translateY }) .gesture( PanGesture(this.panOption) .onActionUpdate((event?: GestureEvent) => { if (event) { // 只允许向下滑动,向上滑动时限制为0 this.translateY = Math.max(0, event.offsetY); } }) .onActionEnd((event?: GestureEvent) => { // 使用更低的阈值和滑动速度判断 const minDistance = 100; // 最小滑动距离 100vp const minVelocity = 500; // 最小滑动速度 // 获取滑动速度(如果可用) const velocity = event?.velocityY || 0; // 判断是否应该关闭:距离足够 或者 速度足够快 const shouldClose = this.translateY > minDistance || velocity > minVelocity; if (shouldClose) { // 添加关闭动画 this.getUIContext().animateTo({ duration: 300, curve: Curve.Smooth }, () => { this.isShowPlay = false; this.translateY = 0; }) } else { // 回弹动画 this.getUIContext().animateTo({ duration: 300, curve: Curve.Smooth }, () => { this.translateY = 0; }) } }) ) } @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD; //一多界面适配 @StorageProp('currentHeightBreakpoint') currentHeightBreakpoint: HeightBreakpoint | undefined = HeightBreakpoint.HEIGHT_LG; @StorageProp('currentWidthBreakpoint') currentWidthBreakpoint: WidthBreakpoint | undefined = WidthBreakpoint.WIDTH_SM; @State topBarHeight: number = 50 @State barOpacity: number = 1 @State barBottomOpacity: number = 1 private hideDone: boolean = false; @State currentYOffset: number = 0; @State bottomBarHeight: number = 60; private isHiding: boolean = false; //是否是Pura X外屏,或者小屏幕 isPuraWP() { return this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM && this.currentHeightBreakpoint === HeightBreakpoint.HEIGHT_MD } //是否是Pura X外屏,或者小屏幕,手机横屏 isCoverOpacity() { if (this.isPuraWP()) { return true } if (this.isPhoneLan()) { return true } if (deviceInfo.marketName.includes('Pura X') && this.currentHeightBreakpoint === HeightBreakpoint.HEIGHT_SM) { return true } return false } //是不是手机横屏 isPhoneLan() { // LogUtil.info('twocold this.currentHeightBreakpoint = '+this.currentHeightBreakpoint) // LogUtil.info('twocold this.currentWidthBreakpoint = '+this.currentWidthBreakpoint) LogUtil.info('hicar isPhoneLan width= ' + this.windowWidth); LogUtil.info('hicar isPhoneLan height= ' + this.windowHeight); LogUtil.info(`twocold hicarWindow size: ${this.windowWidth}x${this.windowHeight}, this.isHiCarStatus: ${this.isHiCarStatus}`); LogUtil.info(`twocold hicar Window size: ${this.windowWidth}x${this.windowHeight}, isHiCarSmall: ${this.isHiCarSmall()}`); if (this.isHiCar()) { return false } if (DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_PHONE && this.currentHeightBreakpoint == HeightBreakpoint.HEIGHT_SM && this.currentWidthBreakpoint == WidthBreakpoint.WIDTH_MD&&this.isLandscape) { //如果是手机横屏,返回true return true } return false } //是不是1920x720的hicar的屏幕分辨率 isHiCarKuanBianPing(){ const hiCarAspectRatios: HiCarAspectRatio[] = [ { ratio: 1920 / 720, name: "1920x720" }, ]; const currentRatio: number = this.windowWidth / this.windowHeight; const RATIO_TOLERANCE: number = 0.18; // 宽高比容差 LogUtil.info('twocold currentRatio = ' + currentRatio) for (const hiCarRatio of hiCarAspectRatios) { LogUtil.info('twocold hiCarRatio= ' + hiCarRatio.ratio) LogUtil.info('twocold Math.abs(currentRatio - hiCarRatio.ratio)= ' + Math.abs(currentRatio - hiCarRatio.ratio)) if (Math.abs(currentRatio - hiCarRatio.ratio) <= RATIO_TOLERANCE) { // LogUtil.info(`HiCar detected: ${hiCarRatio.name}, actual: ${this.windowWidth}x${this.windowHeight}`); return true&&this.isHiCar(); } } return false; } //是不是hicar的屏幕分辨率 isHiCarSmall() { if(this.windowWidth>1800&&this.windowHeight>=1200)//大屏幕可以Grid return false const hiCarAspectRatios: HiCarAspectRatio[] = [ { ratio: 800 / 480, name: "800x480" }, { ratio: 1280 / 720, name: "1280x720" }, { ratio: 1920 / 720, name: "1920x720" }, ]; const currentRatio: number = this.windowWidth / this.windowHeight; const RATIO_TOLERANCE: number = 0.18; // 宽高比容差 LogUtil.info('twocold currentRatio = ' + currentRatio) for (const hiCarRatio of hiCarAspectRatios) { LogUtil.info('twocold hiCarRatio= ' + hiCarRatio.ratio) LogUtil.info('twocold Math.abs(currentRatio - hiCarRatio.ratio)= ' + Math.abs(currentRatio - hiCarRatio.ratio)) if (Math.abs(currentRatio - hiCarRatio.ratio) <= RATIO_TOLERANCE) { // LogUtil.info(`HiCar detected: ${hiCarRatio.name}, actual: ${this.windowWidth}x${this.windowHeight}`); return true&&this.isHiCarStatus; } } return false; } isHiCar() { return this.isHiCarStatus&&this.curDisplayIsHiCar; } //是不是正常的手机竖屏 isPhonePortrait() { // LogUtil.info('twocold this.currentHeightBreakpoint = ' + this.currentHeightBreakpoint) // LogUtil.info('twocold this.currentWidthBreakpoint = ' + this.currentWidthBreakpoint) // LogUtil.info('Heanup 当前设备类型:' + DeviceUtil.getDeviceType()) if (DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_PHONE && this.currentHeightBreakpoint == HeightBreakpoint.HEIGHT_LG && this.currentWidthBreakpoint == WidthBreakpoint.WIDTH_SM) { //如果是手机横屏,返回false return true } return false } @State showSpeedDialog: boolean = false @State longPressSpeed: number = 3 //长按默认倍数 @Builder SpeedDialog() { Stack() { Column() { Text('已切换至' + this.longPressSpeed + '倍速播放') .fontColor(Color.White) .visibility(Visibility.Hidden) } .backgroundColor(Color.Black) .borderRadius(20) .padding({ top: 10, right: 20, left: 15, bottom: 20 }) .opacity(0.6) Text('已切换至' + this.longPressSpeed + '倍速播放') .fontColor(Color.White) } .transition(TransitionEffect.OPACITY.animation({ duration: 500, curve: Curve.Ease })) .zIndex(5) .margin({ top: 80 }) } @Builder MusicPlayerView() { Column() { Column() { Column() { //播放标题 Column() { this.PlayTitle() } .position({ x: 0, y: this.isCoverOpacity() ? 15 : 40 }) .width('100%') .margin({ top: 0 }) .height(PlayConstants.HEIGHT) .visibility(DeviceUtil.getDeviceType() == resourceManager.DeviceType.DEVICE_TYPE_2IN1 ? Visibility.Visible : this.isShowPlayPageBack ? Visibility.Visible : Visibility.None) if (this.showSpeedDialog) { this.SpeedDialog() } Stack() { //手势拖动快进 Column() .width(CommonConstants.FULL_PERCENT) .height('100%') .gesture( GestureGroup(GestureMode.Parallel, // 长按屏幕倍数播放 LongPressGesture({ repeat: true }) .onAction(() => { if (this.CONTROL_PlayStatus === PlayStatus.PLAY) { this.showSpeedDialog = true; this.unifiedPlayerService.setPlaybackSpeed(this.longPressSpeed); } }) .onActionEnd(() => { this.showSpeedDialog = false; this.setPlaybackSpeedViaService(this.playSpeed); }), //双击屏幕暂停或者播放 TapGesture({ count: 2 }) .onAction((event: GestureEvent) => { if (this.CONTROL_PlayStatus === PlayStatus.PLAY) { ToastUtil.showToast('已暂停') } this.playOrPause() }) ) ) } .height('65%') Swiper() { this.CoverInfo() this.PlayerLyrics() } .onChange((index: number) => { this.currentSwiperIndex = index }) .indicator(false) // .indicator(this.currentBreakpoint === BreakpointTypeEnum.SM ? // new DotIndicator() // .top(this.isCoverOpacity()?0:10) // // .selectedColor($r('app.color.select_swiper')) // .color($r('app.color.slider_track')):false // ) .displayCount(new BreakpointType({ sm: 1, md: 2, lg: 2, xl: 2, xxl: 2 }).getValue(this.currentBreakpoint)) .clip(false) .loop(false) .autoPlay(false) // .interval(8000) .position({ x: 0, y: this.isCoverOpacity() ? 30 : 40 }) .height('100%') .hitTestBehavior(HitTestMode.Transparent) Image($r('app.media.icon_replay')) .objectFit(ImageFit.Auto) .width('120px') .height('120px') .position({ x: '45.1%', y: '47.5%' })// .visibility(this.replayVisible) .border({ width: 0 }) .visibility(Visibility.None) .borderStyle(BorderStyle.Dashed)// .hitTestBehavior(HitTestMode.Transparent) .onClick(() => { this.startPlayOrResumePlay(); }) } .width(CommonConstants.FULL_PERCENT) .height(CommonConstants.FULL_PERCENT) .justifyContent(FlexAlign.End) } .justifyContent(FlexAlign.End) .margin({ bottom: 8 }) } // .backgroundImage(this.imageLabelBg) .backgroundImage(this.isPuraWP() && this.currentSwiperIndex === 0 ? (StrUtil.isEmpty(this.cover) ? this.imageLabel : this.cover) : this.imageLabelBg) .backgroundImageSize(this.isPuraWP() ? { width: '100%' } : { height: '150%', width: '100%' }) // .backgroundImagePosition(Alignment.Center) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .backgroundBlurStyle(this.isPuraWP() && this.currentSwiperIndex === 0 ? BlurStyle.NONE : BlurStyle.BACKGROUND_ULTRA_THICK) .hitTestBehavior(HitTestMode.Transparent) // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }), // TransitionEffect.scale({ x: 0, y: 0 }) )) .backgroundBrightness({ rate: this.isPuraWP() ? 0.1 : 0, lightUpDegree: -0.1 }) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .onClick(() => { // this.startAutoHide() if(this.isHiCar()) return if(this.is_auto_hide_progress){ this.is_auto_hide_progress = false } this.startAutoHide() }) } @State progressValue: number = 0; @State currentTime: string = "00:00"; @State totalTime: string = "00:00"; @State loadingVisible: Visibility = Visibility.None; @State replayVisible: Visibility = Visibility.None; @State slideEnable: boolean = false; @State aspRatio: number = 0.5; @State mContext: object | undefined = undefined; // private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; @State mFirst: boolean = true; @State mDestroyPage: boolean = false; @State playSpeed: number = 1; @State oldSeconds: number = 0; @State isSeekTo: boolean = false; @State isCurrentTime: boolean = false; @State lastSongPath: string = ""; // 用于检测歌曲切换 @State justSwitched: boolean = false; // 标记歌曲刚刚切换 @State lastSwitchTime: number = 0; // 记录最后一次歌曲切换的时间戳 @State videoWidth: string = '100%'; @State videoHeight: string = '100%'; @State initAspectRatio: number = 1; @State videoAspectRatio: number = this.initAspectRatio; private videoUrl: string = ''; private last: number = 0; @State videoParentAspectRatio: number = this.initAspectRatio; // 使用UnifiedPlayerService替代ijkMediaPlayer // private mIjkMediaPlayer = IjkMediaPlayer.getInstance(); // 已移除,使用UnifiedPlayerService private unifiedPlayerService: IPlayerService = UnifiedPlayerService.getInstance(); @State CONTROL_PlayStatus: number = PlayStatus.INIT; @State PROGRESS_MAX_VALUE: number = 100; @State updateProgressTimer: number = 0; @State isHide: boolean = false; //是否因此播放器的按钮,默认是进入横屏模式隐藏,点击视频的时候显示 @State name: string = ''; // @State loop: boolean = false; @State playType: number = 0; //0:连续播放 1:单片重复播放 2:正常播放 3:随机播放 @State multiple: string = '1.0X'; xcomponentController: XComponentController = new XComponentController() private windowClass: window.Window = globalThis.windowClass @State volume: number = 0.3; @State volumeShow: boolean = PlayConstants.VOLUME_SHOW; @State bright: number = PlayConstants.BRIGHT; @State brightShow: boolean = PlayConstants.BRIGHT_SHOW; @State seekToShow: boolean = PlayConstants.BRIGHT_SHOW; @State screenWidth: number = 0 @State screenHeight: number = 0 @State statusBarHeight: number = 0 @State isYesFull: boolean = false private seekValue: number = 0 @State isVideoLock: boolean = false; // Whether the video playback is locked eventHub = getContext().eventHub; //投播组件 private avSessionController: AvSessionController = AvSessionController.getInstance(false); private avSessionWidgetListener: AvSessionWidgetListener = AvSessionWidgetListener.getInstance(); private castController: avSession.AVCastController | undefined = undefined; @State isCastPlaying: boolean = false; @State isCasting: boolean = false; // 是否正在投播中 private currentCastDevice: avSession.DeviceInfo | undefined = undefined; // 当前投播设备 @State currentTime2: number = 0; @State currentStringTime: string = '00:00'; @State duration: number = 0; @State isReady: boolean = false; @State durationTime: number = 0; @State durationStringTime: string = '00:00'; @StorageLink('isPlaying') @Watch('animationRoFun') isPlaying: boolean = false; // @State isPlaying: boolean = false; private castSeek: boolean = false; private castItem: avSession.AVQueueItem | undefined = undefined; // @State isBgPlayOpen:boolean = true //是否启用后台播放 @State imageLabel: PixelMap | Resource = CommonConstants.musicBgList[0]; @State imageLabelBg: PixelMap | Resource = CommonConstants.musicBgList[0]; @State rotateAngle2: number = 0 @StorageLink('imageColor') imageColor: string = 'rgba(0, 0, 2, 1.00)'; @State cover: string | undefined = ''; @State artist: string | undefined = '' // 1.初始化controller private lyricController: LyricController = new LyricController() private lyricControllerXF: LyricController = new LyricController() //悬浮歌词控制器 private lyricControllerSingle: LyricController = new LyricController() //播放页单行歌词控制器 private parser = new LyricParser() @State randomColor: string = 'rgb(0,0,0)' @State randomShakenX: number = 0 @State randomShakenY: number = 0 @State lyricTextSize: number = 24 @State lyricContent: string = '' @State isDebug: boolean = false @State isHightLightCenter: boolean = true /** * 初始化歌词加载与展示逻辑 * @param lyricPath 歌词文件路径(支持普通.lrc和加密.lrcc) * @param isOnLineAndToast 是否弹出Toast提示(可选)获取在线歌词为true * @param isApi2 是否使用API2获取歌词(可选) * * 主要流程: * 1. 设置歌词显示样式(字号、颜色、行距、对齐、动画等)。 * 2. 优先尝试本地读取歌词文件(支持加密和普通两种后缀)。 * 3. 若本地无歌词且满足条件(调试模式或会员且安装时间超过2天),则联网获取歌词。 * 4. 获取到歌词后,解析并设置到lyricController,并保存到本地。 * 5. 本地有歌词时,读取文件内容(自动检测编码,支持解密),解析后设置到lyricController。 * 6. 解析失败或读取失败时,清空歌词显示。 * * 注意事项: * - 仅会员且安装时间超过2天才可联网获取歌词(调试模式isDebug=true时跳过限制)。 * - 支持加密歌词文件(.lrcc),自动检测并解密。 * - 歌词内容解析后通过lyricController驱动UI渲染。 */ async initLyric(lyricPath: string, isOnLineAndToast?: boolean, isApi2?: boolean) { if (StrUtil.isEmpty(lyricPath)) { return } this.initPipLyricSetting() this.lyricControllerSingle .setTextSize(15) .setCacheSize(4) .setTextColor("#FFFFFF") .setHighlightColor("#FFFFFF") .setHighlightScale(1.2) .setEmptyHint("") .setAlignMode('center') .setAnimationDuration(500) .setSingleLine(true) this.lyricContent = '' this.lyricController .setTextSize(this.currentLyricSize) .setCacheSize(4) .setTextColor("#DDDDDD") .setHighlightColor("#FFFFFF") .setEmptyHint("") .setAnimationDuration(1000) this.currentLyricAlignMode = PreferencesUtil.getNumberSync('LyricAlignMode', 1) this.blurDegree = PreferencesUtil.getNumberSync('setBlurDegree', 3) this.setBlurDegree(this.blurDegree, false) this.setLyricAlignMode(this.currentLyricAlignMode, false) this.setLyricTextSize(PreferencesUtil.getNumberSync('LyricTextSize', 18), false) this.setHighLyricTextSize(PreferencesUtil.getNumberSync('HighLyricTextSize', 1.23), false) this.lyricController.setLineSpace(PreferencesUtil.getNumberSync('LyricLineSpace', 10)) this.changeLyricColor(PreferencesUtil.getStringSync('LyricColor', '#FFFFFF'), false) this.changeLyricHightLightColor(PreferencesUtil.getStringSync('LyricHighLightColor', '#FFFFFF'), false) this.isHightLightCenter = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_SIMI, true) this.lyricController.setHightLightCenter(this.isHightLightCenter) this.lyricTextWeight = PreferencesUtil.getNumberSync('lyricTextWeight', 400) this.lyricController.setTextWeight(this.lyricTextWeight) this.showSingleLyric = false this.timeOffset = 0 this.lyricController.setLyric(null) this.lyricControllerSingle.setLyric(null) let jiaMilyricPath = lyricPath.substring(0, lyricPath.lastIndexOf('.')) + '.lrcc' console.log("onecold lyricPath =" + lyricPath) let neiqianLrc = '' let lyContent = this.currentSong?.lyricContent if(lyContent&&StrUtil.isNotEmpty(lyContent)){//取数据库里面的歌词lyContent neiqianLrc = lyContent } if (StrUtil.isNotEmpty(neiqianLrc) && !isOnLineAndToast) { console.log("onecold 找到内嵌歌词 neiqianLrc =" + neiqianLrc) //赋值给this.lyricContent,播控中心才可以显示歌词 this.lyricContent = neiqianLrc // 将文件内容按行分割成字符串数组 let lines = neiqianLrc.split('\n').map(line => line.trim()); // 3.解析歌词 let lyric = this.parser.parse(lines); // 4.设置歌词 this.lyricController.setLyric(lyric); this.lyricControllerXF.setLyric(lyric) this.lyricControllerSingle.setLyric(lyric) return } if (isOnLineAndToast || ((!FileUtil.accessSync(lyricPath) && !FileUtil.accessSync(jiaMilyricPath)) && !Utility.isVideoByExtension(this.videoUrl))) { if (this.currentSong !== undefined) { // ToastUtil.showShort('正在为你搜索在线歌词!') //判断是不是赞助会员,是会员的话才开启歌词功能 // if (!this.isDebug) { // if (!Utility.isNoble()) { // LogUtil.debug("onecold 不是赞助会员") // return // } // if(!Utility.isPassInstallTime(33)){ // LogUtil.debug("onecold not pass time") // // LogUtil.debug("onecold 用户安装app没超过12天" ) // return // } // } let artist = this.currentSong?.artist if (artist === undefined) { artist = '' } if (isApi2 === undefined) { isApi2 = false } NetAxiosUtil.getLyric(this.name, artist, isApi2).then((res) => { // LogUtil.debug("onecold res =" + res) if (StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') { if (StrUtil.isNotEmpty(res)) { this.lyricContent = res let lines = res.split('\n').map(line => line.trim()); // 3.解析歌词 let lyric = this.parser.parse(lines); // 4.设置歌词 this.lyricController.setLyric(lyric); this.lyricControllerXF.setLyric(lyric) this.lyricControllerSingle.setLyric(lyric) this.showSingleLyric = true // 4.保存歌词文件lyric到本地 this.saveDataToFile(res, jiaMilyricPath, true) } else { this.lyricController.setLyric(null) this.lyricControllerXF.setLyric(null) this.lyricControllerSingle.setLyric(null) if (isOnLineAndToast) { ToastUtil.showShort('未获取到歌词!') } } } else { this.lyricController.setLyric(null) this.lyricControllerXF.setLyric(null) this.lyricControllerSingle.setLyric(null) if (isOnLineAndToast) { ToastUtil.showShort('未获取到歌词!') } } }) } else { this.lyricController.setLyric(null) this.lyricControllerXF.setLyric(null) this.lyricControllerSingle.setLyric(null) if (isOnLineAndToast) { ToastUtil.showShort('未获取到歌词!') } } return } try { let isJiaMi = false; let realLyricPath = lyricPath if (FileUtil.accessSync(jiaMilyricPath)) { isJiaMi = true realLyricPath = jiaMilyricPath console.info("onecold 找到加密本地歌词 "); } else { isJiaMi = false realLyricPath = lyricPath console.info("onecold 找到本地歌词 "); } // 3.读取文件内容并指定编码为 UTF-8 let file = fs.openSync(realLyricPath, fs.OpenMode.READ_ONLY); const stat = await fileIo.stat(file.fd); const arrayBuffer = new ArrayBuffer(stat.size); fs.read(file.fd, arrayBuffer) .then((readLen: number) => { console.info("read file data succeed"); // let buf = buffer.from(arrayBuffer, 0, readLen); let buf = new Uint8Array(arrayBuffer, 0, readLen); // 检查 buf 是否为空 if (buf.length === 0) { throw new Error("Buffer is empty after reading the file"); } try { // 检测文件编码 // let detectedEncoding = chardet.detect(buf); // console.info("onecoldT Detected encoding: " + detectedEncoding); let detectedEncoding = this.detect(arrayBuffer) console.info("onecoldT Detected encoding: " + detectedEncoding); // 使用检测到的编码解码,解决中文乱码问题 let textDecoder = new util.TextDecoder(detectedEncoding || 'utf-8'); console.info("onecoldT Detected textDecoder: " + textDecoder); let fileContent = textDecoder.decode(buf); console.info(`onecoldT The content of file1: ${fileContent}`); //解密 if (isJiaMi) { fileContent = StrUtil.unit8ArrayToStr(Base64Util.decodeSync(fileContent)) } console.info(`onecold The content of file2: ${fileContent}`); this.lyricContent = fileContent // 将文件内容按行分割成字符串数组 let lines = fileContent.split('\n').map(line => line.trim()); console.info(`onecold The content of file: ${fileContent}`); // 3.解析歌词 let lyric = this.parser.parse(lines); // 4.设置歌词 this.lyricController.setLyric(lyric); this.lyricControllerXF.setLyric(lyric) this.lyricControllerSingle.setLyric(lyric) this.showSingleLyric = true console.info(`The content of file: ${fileContent}`); } catch (chardetError) { console.error("chardet.detect failed with error: " + chardetError.message); this.lyricController.setLyric(null); this.lyricControllerXF.setLyric(null) this.lyricControllerSingle.setLyric(null) } }) .catch((err: BusinessError) => { this.lyricController.setLyric(null) this.lyricControllerXF.setLyric(null) this.lyricControllerSingle.setLyric(null) console.error("read file data failed with error message: " + err.message + ", error code: " + err.code); }) .catch((err: Error) => { this.lyricController.setLyric(null) this.lyricControllerXF.setLyric(null) this.lyricControllerSingle.setLyric(null) console.error("read file 2 data failed with error message: " + err.message); }) .finally(() => { fs.closeSync(file); }); } catch (error) { this.lyricController.setLyric(null) this.lyricControllerSingle.setLyric(null) this.lyricControllerXF.setLyric(null) Logger.error(TAG, 'init Lyric failed with err: ' + JSON.stringify(error)); } } detect(data: ArrayBuffer): string { //创建检测对象 let detector: UniversalDetector = new UniversalDetector(); // 传入检测数据并实时判断编码格式 detector.handleData(data, 0, data.byteLength); // 标记检测数据 读取结束 detector.dataEnd(); // 获取检测结果 let detected: string = detector.getDetectedCharset(); // 释放资源 detector.reset(); return detected; } //初始化悬浮歌词的自定义设置 initPipLyricSetting() { this.lyricControllerXF .setTextSize(this.currentLyricSize) .setCacheSize(4) .setTextColor("#DDDDDD") .setHighlightColor("#FFFFFF")// .setLineSpace(18) .setEmptyHint("") .setAnimationDuration(1000) this.currentLyricAlignModePip = PreferencesUtil.getNumberSync('LyricAlignModePip', 0) this.blurDegreePip = PreferencesUtil.getNumberSync('setBlurDegreePip', 3) this.setBlurDegree(this.blurDegreePip, true) this.setLyricAlignMode(this.currentLyricAlignModePip, true) this.setLyricTextSize(PreferencesUtil.getNumberSync('LyricTextSizePip', 18), true) this.setHighLyricTextSize(PreferencesUtil.getNumberSync('HighLyricTextSizePip', 1.23), true) this.lyricControllerXF.setLineSpace(PreferencesUtil.getNumberSync('LyricLineSpacePip', 10)) this.changeLyricColor(PreferencesUtil.getStringSync('LyricColorPip', '#FFFFFF'), true) this.changeLyricHightLightColor(PreferencesUtil.getStringSync('LyricHighLightColorPip', '#FFFFFF'), true) this.lyricControllerXF.setHightLightCenter(true) this.lyricTextWeightPip = PreferencesUtil.getNumberSync('lyricTextWeightPip', 400) this.lyricControllerXF.setTextWeight(this.lyricTextWeightPip) this.pipBg = PreferencesUtil.getStringSync('BGColorPip', '#FFFF54') this.pipBgIndex = PreferencesUtil.getNumberSync('pipBgIndex', 0) if (this.pipBgIndex === 0) { if (this.cover) { this.pipBg = this.cover } else { this.pipBg = this.imageColor } this.nodeController.updateBgColor(this.pipBg, this.lyricControllerXF) } } //根据item返回歌词内容 async getLyricContent(item: VideoItem): Promise { try { let filePath = item.filePath; let lyricPath = filePath.substring(0, filePath.lastIndexOf('.')) + '.lrc'; let jiaMilyricPath = lyricPath.substring(0, lyricPath.lastIndexOf('.')) + '.lrcc'; let isJiaMi = false; let realLyricPath = lyricPath; if (FileUtil.accessSync(jiaMilyricPath)) { isJiaMi = true; realLyricPath = jiaMilyricPath; console.info("onecold 找到加密本地歌词"); } else { console.info("onecold 找到本地歌词"); } let file = fs.openSync(realLyricPath, fs.OpenMode.READ_ONLY); const stat = await fileIo.stat(file.fd); const arrayBuffer = new ArrayBuffer(stat.size); const readLen = await fs.read(file.fd, arrayBuffer); console.info("read file data succeed"); let buf = new Uint8Array(arrayBuffer, 0, readLen); if (buf.length === 0) { throw new Error("Buffer is empty after reading the file"); } // let detectedEncoding = chardet.detect(buf); let detectedEncoding = this.detect(arrayBuffer) console.info("Detected encoding: " + detectedEncoding); let textDecoder = new util.TextDecoder(detectedEncoding || 'utf-8'); let fileContent = textDecoder.decode(buf); console.info(`onecold The content of file1: ${fileContent}`); if (isJiaMi) { fileContent = StrUtil.unit8ArrayToStr(Base64Util.decodeSync(fileContent)); } console.info(`onecold The content of file2: ${fileContent}`); fs.closeSync(file); return fileContent; } catch (error) { console.error('init Lyric failed with err: ' + JSON.stringify(error)); return ''; } } async saveDataToFile(content: string, filePath: string, isJiaMi?: boolean) { if (StrUtil.isEmpty(filePath)) { return } try { //将content进行加密 // let jiaMilyricPath = filePath.substring(0,filePath.lastIndexOf('.'))+'.lrcc' let lyricContent = '' if (isJiaMi) { lyricContent = Base64Util.encodeToStrSync(StrUtil.strToUint8Array(content)) } else { lyricContent = content } const file = await fileIo.open(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE); // 清空文件内容 await fileIo.truncate(file.fd, 0); await fileIo.write(file.fd, lyricContent); await fileIo.close(file); console.log('onecold lyric file Data saved successfully.'); } catch (error) { console.error('onecold lyric file Failed to save data:', error); } } /** * Get largest proportion color of an image. */ getImageColor() { if (!this.context) { return; } let bg = Utility.getMusisBg() this.imageLabel = bg ColorConversion.setSysBarLightBackground(true); this.context.resourceManager.getMediaContent(bg) .then((value: Uint8Array) => { let buffer = value.buffer as ArrayBuffer; image.createImageSource(buffer).createPixelMap().then((pixelMap) => { effectKit.createColorPicker(pixelMap, (error, colorPicker) => { if (error) { Logger.error('Failed to create color picker.'); } else { let color = colorPicker.getLargestProportionColor(); let colorArr = ColorConversion.dealColor(color.red, color.green, color.blue); this.imageColor = `rgba(${colorArr[0]}, ${colorArr[1]}, ${colorArr[2]}, 1)`; } }) let headFilter = effectKit.createEffect(pixelMap); if (headFilter !== null) { headFilter.blur(15); headFilter.getEffectPixelMap().then((value) => { this.imageLabelBg = value; }) } }) .catch((error: BusinessError) => { Logger.error(`${error.code} + ${error.message}`) }) }) .catch((error: BusinessError) => { Logger.error(`${error.code} + ${error.message}`) }) } setImageColor(cover: string) { if (!this.context) { return; } if (cover === undefined) { return; } ImageUtils.imagePathToPixelMap(cover).then((value) => { let headFilter = effectKit.createEffect(value); if (headFilter !== null) { headFilter.blur(15); headFilter.getEffectPixelMap().then((value) => { this.imageLabelBg = value; }) } }).catch((error: BusinessError) => { Logger.error(`${error.code} + ${error.message}`) }) } setImageColor2(cover: string) { if (!this.context) { return; } if (cover === undefined) { return; } LogUtils.getInstance().LOGI("setImageColor2--> cover ==" + cover); ImageUtils.getImageDealData(getContext(this), cover, this.videoUrl).then((data) => { this.imageColor = data.imageColor; if (data.blurPixelMap) { this.imageLabelBg = data.blurPixelMap; } }) } // 定义开始旋转的方法 // 定时器用于一百毫秒执行一次旋转角度 @State timer: number = 0 @State rotateAngle: number = 0 //封面旋转动画 animationRoFun() { if (this.isPlaying && !this.isCoverRectangle) { this.timer = setInterval(() => { this.rotateAngle += 1 }, 100) this.startRotation() } else { clearInterval(this.timer) this.stopRotation() } } // 定义开始旋转的方法 startRotation() { if (this.isPlaying) { animateTo({ duration: 777, iterations: 1, curve: Curve.Linear }, () => { this.rotateAngle2 = 0 }) } } // 回调,暂停和播放按钮回调,下一首和上一首 playChange() { // if(this.isCoverRectangle){ // return // } const isPlaying = this.CONTROL_PlayStatus === PlayStatus.PLAY; // 同步所有播放状态 this.setIsPlaying(isPlaying); this.animationState = isPlaying ? AnimationStatus.Running : AnimationStatus.Paused; // 记录状态变化日志 LogUtils.getInstance().LOGI(`LocalMusic: playChange - CONTROL_PlayStatus: ${this.CONTROL_PlayStatus}, isPlaying: ${isPlaying}, globalIsPlaying: ${this.isPlaying}`); // 强制触发UI刷新 - 这对于@State变量应该是自动的,但我们确保一下 setTimeout(() => { LogUtils.getInstance().LOGI(`LocalMusic: UI refresh triggered - CONTROL_PlayStatus: ${this.CONTROL_PlayStatus}`); }, 50); } // 手动同步状态的方法,用于调试 public forceSyncState(): void { const currentState = this.unifiedPlayerService.getCurrentState(); LogUtils.getInstance().LOGI(`LocalMusic: Force sync - UnifiedPlayerService state: isPlaying=${currentState.isPlaying}, isPaused=${currentState.isPaused}`); // 手动触发状态同步 if (currentState.isPlaying) { this.CONTROL_PlayStatus = PlayStatus.PLAY; } else if (currentState.isPaused) { this.CONTROL_PlayStatus = PlayStatus.PAUSE; } else { this.CONTROL_PlayStatus = PlayStatus.INIT; } this.setIsPlaying(currentState.isPlaying); this.playChange(); LogUtils.getInstance().LOGI(`LocalMusic: Force sync completed - CONTROL_PlayStatus: ${this.CONTROL_PlayStatus}`); } // 定义停止旋转的方法 stopRotation() { if (this.isCoverRectangle) { this.rotateAngle = 0 } else { //唱针以左上角为点选择逆时针40度 animateTo({ duration: 1000, curve: Curve.Linear, iterations: 1, }, () => { this.rotateAngle2 = -40; }); } } @Builder private LyricsTopItem() { Row() { Stack() { Image(StrUtil.isEmpty(this.cover) ? this.imageLabel : this.cover) .height(55) .width(55) .alt(this.imageLabel) .borderRadius(8) .clickEffect({ level: ClickEffectLevel.HEAVY }) .margin({ left: this.currentLyricAlignMode === 0 ? 28 : -15 }) } .width('18%') Column() { Column() { Text(this.name) .fontSize(16) .maxLines(1) .fontWeight(FontWeight.Bolder) .fontColor(this.currentLyricColor) .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 }) Row() { Text(this.currentSong?.artist !== undefined ? this.currentSong?.artist : '') .fontSize(13) .fontWeight(FontWeight.Bold) .padding({ top: 8 }) .fontColor(this.currentLyricColor) .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 }) Blank() } } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') .width('65%') Column() { Image($r('app.media.lyric')) .width(28) .aspectRatio(CommonConstants.ASPECT_RATIO) .margin({ right: 30 }) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onClick(() => { this.isLyricSetting = !this.isLyricSetting; }) .bindSheet($$this.isLyricSetting, this.lyricSettingSheet(false), { height: this.isCoverOpacity() ? '95%' : '88%', dragBar: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, showClose: true, blurStyle: BlurStyle.Thin, backgroundColor: Color.Transparent, // title: { title: $r('app.string.lyric_setting') } }) } .width('18%') } .width('82%') .height(58) .justifyContent(FlexAlign.SpaceBetween) } //歌词显示 @Builder PlayerLyrics() { Column() { Column() { this.LyricsTopItem() } .height(this.isPuraWP() ? 15 : 58) .visibility(this.currentBreakpoint !== BreakpointTypeEnum.SM ? Visibility.None : Visibility.Visible) .margin({ left: 6, top: this.isPuraWP() ? 5 : 38 }) .width("100%") Column() { LyricView2({ controller: this.lyricController, enableSeek: true, seekUIColor: "#ff0000", // 滑动定位的按钮和文本颜色 seekLineColor: "#80ffffff", // 滑动定位线颜色 seekUIStyle: "listItem", // 滑动定位样式(seekLine传统样式,listItem类似抖音汽水音乐样式) onSeekAction: (position: number) => { // 滑动歌词触发seek定位回调 if (!this.isPlaying) { this.startPlayOrResumePlay() } else { this.isSeekTo = true; this.mDestroyPage = false; this.showLoadIng(); this.seekTo(position + ""); this.isSeekTo = false; } return true } }) .width("100%") .layoutWeight(1) .margin({ left: this.currentLyricAlignMode === 0 ? 0 : 50, top: 2, bottom: this.isPhoneLan()||this.isHiCarKuanBianPing() ? 40 : 10 }) if (this.isPhoneLan()) { Column() { this.playCenterView() } .margin({ bottom: 20, right: this.currentLyricAlignMode == 0 ? 0 : 30 }) } } .position({ top: this.currentBreakpoint !== BreakpointTypeEnum.SM ? (this.isPhoneLan() ? 0 : 45) : (this.isPuraWP() ? 58 : 99) }) .justifyContent(FlexAlign.Center) .height(this.isCoverOpacity() ? '95%' : '80%') } } @Builder SingleLyricView() { Column() { Column() { LyricView2({ controller: this.lyricControllerSingle, enableSeek: false, seekUIColor: "#ff0000", // 滑动定位的按钮和文本颜色 seekLineColor: "#80ffffff", // 滑动定位线颜色 seekUIStyle: "listItem", // 滑动定位样式(seekLine传统样式,listItem类似抖音汽水音乐样式) }) } .height(66) .margin({ bottom: 20 }) } } //播放器的底部控制中心,上一首,暂停,下一首,播放进度条等 @Builder BottomControl() { Column() { //播放 暂停,下一首,上一首 this.playCenterView() //播放进度条 this.playProgressView() } .position({ bottom: this.isHiCarSmall()?50:(this.isCoverOpacity() ? 55 : 100) }) // 将 固定在底部 } //播放控制上一首 下一首 暂停和播放 @Builder playCenterView() { Row() { Row({ space: 20 }) { Button({ type: ButtonType.Circle, stateEffect: true }) { //更多功能 Image($r('app.media.menu')) .width(24) .clickEffect({ level: ClickEffectLevel.MIDDLE }) .bindSheet($$this.isShowMoreView, this.PlayMoreSheet(), { height: this.isCoverOpacity() ? '95%' : '93%', preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, dragBar: true, showClose: true, blurStyle: BlurStyle.Thin, backgroundColor: Color.Transparent, }) } .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .backgroundColor(Color.Transparent) .onClick(() => { this.isShowMoreView = !this.isShowMoreView; }) Button({ type: ButtonType.Circle, stateEffect: true }) { Image($r('app.media.ic_previous')) .width(33) .clickEffect({ level: ClickEffectLevel.HEAVY }) .aspectRatio(CommonConstants.ASPECT_RATIO) } .onClick(async () => { this.playPrevious() }) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .backgroundColor(Color.Transparent) .margin({ left: 5 }) } Button({ type: ButtonType.Circle, stateEffect: true }) { Column() { Image(this.CONTROL_PlayStatus === PlayStatus.PLAY ? (this.isCircleBtn ? $r('app.media.hm_pause') : $r('app.media.ic_public_play')) : (this.isCircleBtn ? $r('app.media.hm_play2') : $r('app.media.ic_public_pause'))) .width(this.isPhoneLan() ? 48 : 60) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .fillColor(Color.White) .aspectRatio(CommonConstants.ASPECT_RATIO) .onClick(async () => { this.playOrPause() }) } } .backgroundColor(Color.Transparent) .layoutWeight(1) .margin({ left: 38, right: 38 }) Row({ space: 20 }) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Image($r('app.media.ic_next')) .width(33) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .aspectRatio(CommonConstants.ASPECT_RATIO) .onClick(() => { this.playNext(); }) } .backgroundColor(Color.Transparent) .margin({ right: 5 }) .width(33) Button({ type: ButtonType.Circle, stateEffect: true }) { //添加或取消收藏 Image(Utility.getIsFav(this.favList, this.currentSong) ? $r('app.media.add_fac_light') : $r('app.media.add_fac')) .width(24) .clickEffect({ level: ClickEffectLevel.MIDDLE }) .aspectRatio(CommonConstants.ASPECT_RATIO) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onClick(async () => { if (this.currentSong) { this.doFav(this.currentSong) } }) } .backgroundColor(Color.Transparent) .width(24) } // .margin({ left: 5 }) } .width('100%') .margin({ left: 10, right: 10, bottom: this.isCoverOpacity() ? 0 : 20 }) .justifyContent(FlexAlign.Center) } //播放进度条 @Builder playProgressView() { Row() { Button({ type: ButtonType.Circle, stateEffect: true }) { Column() { if (this.playType === 0) { Image($r('app.media.loop')) .width($r('app.float.control_image_width')) .aspectRatio(CommonConstants.ASPECT_RATIO) } else if (this.playType === 1) { Image($r('app.media.single')) .width($r('app.float.control_image_width')) .aspectRatio(CommonConstants.ASPECT_RATIO) } else if (this.playType === 2) { Image($r('app.media.normal_play')) .width($r('app.float.control_image_width')) .aspectRatio(CommonConstants.ASPECT_RATIO) } else if (this.playType === 3) { Image($r('app.media.random')) .width($r('app.float.control_image_width')) .aspectRatio(CommonConstants.ASPECT_RATIO) } } } .visibility(this.is_auto_hide_progress ? Visibility.None : Visibility.Visible) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .backgroundColor(Color.Transparent) .margin({ left: 20, right: 10 }) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onClick(async () => { this.setLoopMode() }) Text(this.currentTime) .fontSize($r('app.float.slider_font_size')) .fontColor(Color.White) .margin(3) Slider({ value: this.progressValue, min: 0, max: this.PROGRESS_MAX_VALUE, step: 1, style: SliderStyle.OutSet }) .width('600px') .blockColor('rgba(255,255,255,1)') .trackColor('rgba(255,255,255,0.3)') .selectedColor(Color.White) .trackThickness(PlayConstants.PROGRESS_TRACK_THICKNESS) .layoutWeight(1) .margin({ left: PlayConstants.PROGRESS_MARGIN_LEFT }) .showSteps(false) .showTips(true) .enabled(this.slideEnable) .onChange((value: number, mode: SliderChangeMode) => { // if (mode == 2) { this.isSeekTo = true; this.mDestroyPage = false; this.showLoadIng(); LogUtils.getInstance().LOGI("slider-->seekValue start:" + value); // 通过UnifiedPlayerService获取时长并计算拖动位置 const currentState = this.unifiedPlayerService.getCurrentState(); let seekValue = value * (currentState.duration / 100); this.seekTo(seekValue + ""); // this.setProgress() LogUtils.getInstance().LOGI("slider-->seekValue end:" + seekValue); this.isSeekTo = false; // } }) Text(this.totalTime) .fontSize($r('app.float.slider_font_size')) .fontColor(Color.White) .margin(3) .margin({ left: PlayConstants.PROGRESS_MARGIN_LEFT }) //播放列表 Button({ type: ButtonType.Circle, stateEffect: true }) { Image($r('app.media.hm_playlist')) .width($r('app.float.control_image_width')) .fillColor(Color.White) .aspectRatio(CommonConstants.ASPECT_RATIO) .bindSheet($$this.isShowSheetView, this.PlayListSheet(), { height: '95%', dragBar: true, showClose: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, blurStyle: BlurStyle.Thin, backgroundColor: this.isPlayListBgGrass ? Color.Transparent : $r('app.color.silvery'), title: { title: Utility.resourceToString(this.context, $r('app.string.current_play_list')) + `(${this.songList.length}首)` } }) } .visibility( this.is_auto_hide_progress ? Visibility.None : Visibility.Visible) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .backgroundColor(Color.Transparent) .margin({ right: 20, left: 10 }) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onClick(() => { this.isShowSheetView = !this.isShowSheetView; if (this.isPlayListBgGrass) { this.isFrontWhite = true } else { this.isFrontWhite = false } setTimeout(() => { this.playListScroller.scrollToIndex(this.curIndex, true, ScrollAlign.CENTER) }, 123) }) } //x5的竖屏模式需要99% .width(this.isPhonePortrait() ? '93.4%' : this.currentHeightBreakpoint == 1 && this.currentWidthBreakpoint == 2 ? '99%' : '82%') .height(this.isPhoneLan() ? 25 : 33) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) } isBigScreen() { if (this.currentWidthBreakpoint == WidthBreakpoint.WIDTH_LG) { return true } if (DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_PHONE && this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM && this.currentHeightBreakpoint == HeightBreakpoint.HEIGHT_LG) { return false } else { return true } return false } @State is_auto_hide_progress: boolean = false //手机横屏自动隐藏播放进退条,点击屏幕可以显示,倒计时4秒后又自动隐藏 @State isCoverRectangle: boolean = false @State isCoverTop: boolean = true // 添加控制缩放的状态变量 @State scaleValueImage: number = 1 @Builder CoverInfo() { Column() { //如果是方形,判断是不是封面在顶部,如果不是顶部,显示musicNameInfo, 如果是圆形显示musicNameInfo if (this.isCoverRectangle) { if (!this.isCoverTop) { this.musicNameInfo(false) } } else { this.musicNameInfo(false) } Stack() { // 唱片旋转效果 Image($r('app.media.ic_music_disc')) .width(245) .height(245) .margin({ right: 20, left: 20 }) .aspectRatio(1) .opacity(this.isCoverOpacity() || this.isCoverRectangle ? 0 : 1) .visibility(this.isCoverOpacity() || this.isCoverRectangle ? Visibility.None : Visibility.Visible) .borderRadius('100%') .align(Alignment.Center) .clip(true)// .rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle }) .rotate({ angle: this.rotateAngle, centerX: "50%", centerY: "50%", }) .animation({ duration: 100, curve: Curve.Linear }) .shadow({ radius: 30, color: this.randomColor }) Column() { if (this.isCoverTop && !this.isCoverOpacity() && this.isCoverRectangle) { Image(StrUtil.isEmpty(this.cover) ? this.imageLabel : this.cover)// .height(this.isCoverOpacity() ? 250 : this.isCoverRectangle ? 320 : 162) .width(this.isBigScreen() ? '72%' : this.isCoverTopBig ? '100%' : '86%') .objectFit(this.isCoverRectangle ? ImageFit.Contain : ImageFit.Auto) .alt(this.imageLabel) .margin({ right: 8, left: 8, top: 8 }) .aspectRatio(1) .opacity(this.opacityValueImage) .scale({ x: this.scaleValueImage, y: this.scaleValueImage }) // 添加缩放效果 .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.8 }) .visibility(this.isPuraWP() ? Visibility.None : Visibility.Visible) .borderRadius(this.isCoverRectangle ? 20 : '100%') .align(Alignment.Center)// .clip(true) .rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle }) .onClick(() => { this.getImageColor() }) this.musicNameInfo(false) } else { Image(StrUtil.isEmpty(this.cover) ? this.imageLabel : this.cover) .height(this.isHiCarSmall()?(this.isHiCarKuanBianPing()?140:220):(this.isCoverOpacity() ? 250 : this.isCoverRectangle ? 320 : 162)) .objectFit(this.isCoverRectangle ? ImageFit.Contain : ImageFit.Auto) .alt(this.imageLabel) .margin({ right: 20, left: 20 }) .aspectRatio(1) .opacity(this.opacityValueImage) .scale({ x: this.scaleValueImage, y: this.scaleValueImage }) // 添加缩放效果 .visibility(this.isPuraWP() ? Visibility.None : Visibility.Visible) .borderRadius(this.isCoverRectangle ? 20 : '100%') .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.8 }) .align(Alignment.Center)// .clip(true) .rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle }) .onClick(() => { this.getImageColor() }) if (this.isPhoneLan()) { Text(this.name) .fontSize(18) .fontWeight(FontWeight.Bold) .margin({ top: 13 }) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(1) .width('99%') .textAlign(TextAlign.Center) .fontColor(Color.White) } } } // 唱针以左上角为点选择逆时针40度 Image($r('app.media.ic_music_cover_hand')) .height(130) .margin({ bottom: 220, top: 0 }) .align(Alignment.Top) .rotate({ angle: this.rotateAngle2, centerX: 0, centerY: 0 }) .opacity(this.isCoverOpacity() || this.isCoverRectangle ? 0 : 1) .visibility(this.isCoverOpacity() || this.isCoverRectangle ? Visibility.None : Visibility.Visible) Column({ space: 8 }) { Text(this.name) .fontSize(this.isPuraWP() ? 24 : (this.name.length >= 28 || this.isCoverOpacity() ? 18 : 22)) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .maxLines(1) .width('48%') .textAlign(TextAlign.Center) .fontColor(Color.White) .visibility(this.isPhoneLan() ? Visibility.None : Visibility.Visible) Text(this.artist) .fontSize(this.isPuraWP() ? 18 : (this.isCoverOpacity() ? 12 : 15)) .fontColor(Color.White) .fontWeight(FontWeight.Bold) .visibility(this.isPhoneLan() ? Visibility.None : Visibility.Visible) } .justifyContent(FlexAlign.Start) .zIndex(1) .visibility(this.isCoverOpacity() ? Visibility.Visible : Visibility.None) } .margin({ bottom: this.isCoverOpacity() || this.isCoverRectangle ? (this.isPhonePortrait() && this.isShowSingleLineLyric ? 20 : 55) : 0, top: this.isCoverOpacity() ? 0 : 20 }) if (this.isPhonePortrait() && this.isShowSingleLineLyric) { this.SingleLyricView() } if (this.isPhoneLan()) { Column() { this.playProgressView() } .position({ left: 40, bottom: 48 }) } else { this.BottomControl() } } .width(CommonConstants.FULL_PERCENT) .clip(true) } @Builder private musicNameInfo(isHidden: boolean) { Column({ space: 8 }) { Text(this.name) .fontSize(this.name.length >= 28 || this.isCoverOpacity() ? 18 : 22) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .maxLines(1) .width('99%') .textAlign(TextAlign.Center) .fontColor(Color.White) Text(this.artist + ' ' + this.currentSong?.album) .fontSize(this.isCoverOpacity() ? 12 : 15) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .fontColor(Color.White) .textAlign(TextAlign.Center) .visibility(StrUtil.isEmpty(this.artist)?Visibility.None:Visibility.Visible) .width('99%') .maxLines(1) } .margin({ top: this.isCoverOpacity() ? 20 : isHidden ? 40 : 20 }) .zIndex(1) .scale({ x: this.scaleValueImage, y: this.scaleValueImage }) // 添加缩放效果 .visibility(this.isCoverOpacity() || isHidden ? Visibility.None : Visibility.Visible) } @Builder private PlayTitle() { Row() { Row() { Image($r('app.media.ic_back_down')) .width($r('app.float.title_image_size'))// .aspectRatio(CommonConstants.ASPECT_RATIO) .hitTestBehavior(HitTestMode.Transparent) Text(this.name + ' ' + this.artist) .fontColor(Color.White) .fontSize($r('app.float.title_font_size')) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .maxLines(1) .visibility(Visibility.None) .margin({ left: 2, right: 30 }) } .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .visibility(this.isHide ? Visibility.Hidden : Visibility.Visible) .onClick(() => { this.isShowPlay = false; }) Column() { AVCastPicker({ normalColor: Color.White, onStateChange: this.onStateVastChange }) .width(24) .height(24) } .visibility(this.isHide ? Visibility.Hidden : Visibility.None) } .width(PlayConstants.ROW_WIDTH) } @Builder lyricSettingSheet(isPip: boolean) { Scroll() { this.lyricSetting(isPip) } .width('100%') .height('100%') } @Builder lyricSetting(isPip: boolean) { Column() { // 时间调节模块 if (!isPip) { this.BuildTimeControls() } // 字号控制模块 this.BuildFontControls(isPip) } } // 歌词时间偏移状态 @State timeOffset: number = 0.0 @State currentLyricColor: string = '#FFFFFF' @State currentLyricColorPip: string = '#FFFFFF' @State currentHighLightLyricColor: string = '#FF4081' @State currentHighLightLyricColorPip: string = '#FF4081' // 歌词字号状态 @State currentLyricSize: number = 20 @State currentLyricSizePip: number = 20 // 高亮歌词放大倍数 @State currentHightLyricSize: number = 1.2 @State currentHightLyricSizePip: number = 1.2 @State currentLyricLineSpace: number = 10 @State currentLyricLineSpacePip: number = 10 @State currentLyricAlignMode: number = 1 @State currentLyricAlignModePip: number = 1 @State blurDegree: number = 2 @State blurDegreePip: number = 2 @State isShowSelectColor: boolean = false @State isShowHLSelectColor: boolean = false @State isShowPipBgSelectColor: boolean = false @State pipLyWidth: number = 330 @State pipLyHeight: number = 120 // 构建颜色选择器 @Builder SelectColor(isHighColor: boolean, isPip: boolean, isBGPip?: boolean) { Row({ space: 10 }) { HSBColorPicker({ color: '#FFFFFF', radius: 8, layout: HSBColorPickerLayout.COLUMN, predefine: ['#8b27f4', '#73f9fc', '#fffe55', '#f5cee3', '#eb4827', '#e93bf4', '#3e68f4', '#c5e6d3', '#e4e4e4', '#fa7105'], onChange: (value: string) => isHighColor ? this.changeLyricHightLightColor(value, isPip) : this.changeLyricColor(value, isPip, isBGPip) }) .height(250) .layoutWeight(1) .padding(25) } .width('66%') } @State isOpenPip: boolean = false @State pipBgIndex: number = 0 // 构建字号控制器 @Builder BuildFontControls(isPip: boolean) { Column() { if (isPip) { Column() { Row() { Text('悬浮歌词') .margin({ left: 18 }) .fontSize(15) .fontColor(Color.White) .fontWeight(480) .layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.isOpenPip }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 18 }) .onChange((checked: boolean) => { this.isOpenPip = checked; if (this.isOpenPip) { this.startPip() } else { this.stopPip() } PreferencesUtil.put('isOpenPip', this.isOpenPip) }) .width(50) .height(30); } .height(55) } .backgroundColor(Color.Transparent) .borderRadius(15) .margin({ left: 30, right: 30, top: 0, bottom: 20 }) .padding(0) .border({ color: Color.White, width: 1.8 }) } Row() { Text(`歌词居中:`) .fontSize(14) .fontColor(Color.White) .margin({ left: 2 }) Row() { ForEach(['居中', '居左'], (size: string, index: number) => { Button(size) .width(60) .height(28) .fontSize(12) .fontColor('#FFFFFF') .backgroundColor((!isPip && this.currentLyricAlignMode === index) || (isPip && this.currentLyricAlignModePip === index) ? this.themeColor : Color.Transparent) .border({ color: (!isPip && this.currentLyricAlignMode === index) || (isPip && this.currentLyricAlignModePip === index) ? '#007DFF' : '#DDDDDD', width: 1.8 }) .onClick(() => { this.setLyricAlignMode(index, isPip) }) .margin({ left: 10, right: 8 }) }) } .width('76%') Blank() } .margin({ top: 10, bottom: 10, right: 20, left: 20 }) if (isPip) { Row() { Text(`悬浮背景:`) .fontSize(14) .fontColor(Color.White) Select([ { value: CommonConstants.PIP_LYRIC_BG[0] }, { value: CommonConstants.PIP_LYRIC_BG[1] }]) .font({ size: 13, weight: FontWeight.Medium }) .fontColor(Color.White) .margin({ left: 10 }) .selected(this.pipBgIndex) .value(CommonConstants.PIP_LYRIC_BG[this.pipBgIndex]) .onSelect(async (_index: number, text?: string | undefined) => { this.pipBgIndex = _index PreferencesUtil.put('pipBgIndex', this.pipBgIndex) if (this.pipBgIndex === 0) { if (this.cover) { this.pipBg = this.cover } else { this.pipBg = this.imageColor } this.nodeController.updateBgColor(this.pipBg, this.lyricControllerXF) } else { this.pipBg = PreferencesUtil.getStringSync('BGColorPip', '#FFFF54') } }) Column() { } .backgroundColor(this.pipBg) .margin({ left: 10, right: 10 }) .borderRadius(20) .height(28) .width(28) Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { SymbolGlyph($r('sys.symbol.paintbrush')) .fontColor([Color.White]) .effectStrategy(1) Text(`选择颜色`) .fontSize(13) .margin({ left: 10 }) .fontColor(Color.White) } } .visibility(this.pipBgIndex === 1 ? Visibility.Visible : Visibility.None) .backgroundColor(Color.Transparent) .border({ color: '#FFFFFF', radius: 20, width: 1.8 }) .height(36) .width(110) .onClick(() => { this.isShowPipBgSelectColor = !this.isShowPipBgSelectColor }) .bindPopup(this.isShowPipBgSelectColor, { builder: this.SelectColor(false, isPip, true), placement: Placement.Top, mask: { color: '#33000000' }, enableArrow: false, //是否显示箭头 showInSubWindow: false, onStateChange: (e) => { if (!e.isVisible) { this.isShowPipBgSelectColor = false } } }) } .width('76%') .margin({ bottom: 5, right: 45, left: 20 }) } if (!isPip) { Row() { Text(`高亮居中:`) .fontSize(14) .fontColor(Color.White) Column() { Toggle({ type: ToggleType.Switch, isOn: this.isHightLightCenter }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onChange((checked: boolean) => { this.isHightLightCenter = checked; PreferencesUtil.put(SettingPage.IS_SHOW_SIMI, this.isHightLightCenter) this.lyricController.setHightLightCenter(this.isHightLightCenter) }) .width(50) .height(30) .alignSelf(ItemAlign.Start) .margin({ left: 12 }) Blank() } .width('76%') } .margin({ left: 20, right: 15, top: 10, bottom: 10 }) } Row() { Text(`歌词颜色:`) .fontSize(14) .fontColor(Color.White) Text(isPip ? this.currentLyricColorPip : this.currentLyricColor) .fontSize(15) .fontColor(Color.White) .margin({ left: 12, right: 12 }) Column() { } .backgroundColor(isPip ? this.currentLyricColorPip : this.currentLyricColor) .margin({ right: 12 }) .borderRadius(20) .height(28) .width(28) Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { SymbolGlyph($r('sys.symbol.paintbrush')) .fontColor([Color.White]) .effectStrategy(1) Text(`选择颜色`) .fontSize(13) .margin({ left: 10 }) .fontColor(Color.White) } } .backgroundColor(Color.Transparent) .border({ color: '#FFFFFF', radius: 20, width: 1.8 }) .height(36) .width(110) .onClick(() => { this.isShowSelectColor = !this.isShowSelectColor }) .bindPopup(this.isShowSelectColor, { builder: this.SelectColor(false, isPip), placement: Placement.Top, mask: { color: '#33000000' }, enableArrow: false, //是否显示箭头 showInSubWindow: false, onStateChange: (e) => { if (!e.isVisible) { this.isShowSelectColor = false } } }) } .width('76%') .margin({ top: 10, bottom: 10, right: 45, left: 20 }) Row() { Text(`高亮颜色:`) .fontSize(14) .fontColor(Color.White) Text(isPip ? this.currentHighLightLyricColorPip : this.currentHighLightLyricColor) .fontSize(15) .fontColor(Color.White) .margin({ left: 12, right: 12 }) Column() { } .backgroundColor(isPip ? this.currentHighLightLyricColorPip : this.currentHighLightLyricColor) .margin({ right: 12 }) .borderRadius(20) .height(28) .width(28) Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { SymbolGlyph($r('sys.symbol.paintbrush')) .fontColor([Color.White]) .effectStrategy(1) Text(`选择颜色`) .fontSize(13) .margin({ left: 10 }) .fontColor(Color.White) } } .backgroundColor(Color.Transparent) .border({ color: '#FFFFFF', radius: 20, width: 1.8 }) .height(36) .width(110) .onClick(() => { this.isShowHLSelectColor = !this.isShowHLSelectColor }) .bindPopup(this.isShowHLSelectColor, { builder: this.SelectColor(true, isPip), placement: Placement.Top, mask: { color: '#33000000' }, enableArrow: false, //是否显示箭头 showInSubWindow: false, onStateChange: (e) => { if (!e.isVisible) { this.isShowHLSelectColor = false } } }) } .width('76%') .margin({ top: 10, bottom: 10, right: 45, left: 20 }) if (isPip) { Row() { Text(`显示宽度:`) .fontSize(14) .fontColor(Color.White) Slider({ value: this.pipLyWidth, min: 10, max: 800, step: 1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(6) .onChange((value: number) => { this.pipLyWidth = value if (this.pipController) { this.pipController.updateContentSize(this.pipLyWidth, this.pipLyHeight); PreferencesUtil.putSync('pipLyWidth', this.pipLyWidth) } }) .width('76%') } .margin({ left: 20, right: 15, top: 5, bottom: 5 }) Row() { Text(`显示高度:`) .fontSize(14) .fontColor(Color.White) Slider({ value: this.pipLyHeight, min: 10, max: 1000, step: 1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(6) .onChange((value: number) => { this.pipLyHeight = value if (this.pipController) { this.pipController.updateContentSize(this.pipLyWidth, this.pipLyHeight); PreferencesUtil.putSync('pipLyHeight', this.pipLyHeight) } }) .width('76%') } .margin({ left: 20, right: 15, top: 5, bottom: 5 }) } Row() { Text(`歌词字号:`) .fontSize(14) .fontColor(Color.White) Slider({ value: isPip ? this.currentLyricSizePip : this.currentLyricSize, min: 12, max: 30, step: 0.1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(6) .onChange((value: number) => { this.setLyricTextSize(value, isPip) }) .width('76%') } .margin({ left: 20, right: 15, top: 5, bottom: 5 }) Row() { Text(`高亮倍数:`) .fontSize(14) .fontColor(Color.White) Slider({ value: isPip ? this.currentHightLyricSizePip : this.currentHightLyricSize, min: 1, max: 2, step: 0.1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(6) .onChange((value: number) => { this.setHighLyricTextSize(value, isPip) }) .width('76%') } .margin({ left: 20, right: 15, top: 5, bottom: 5 }) Row() { Text(`歌词间隙:`) .fontSize(14) .fontColor(Color.White) Slider({ value: isPip ? this.currentLyricLineSpacePip : this.currentLyricLineSpace, min: 0, max: 100, step: 1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(6) .onChange((value: number) => { this.setLyricLineSpace(value, isPip) }) .width('76%') } .margin({ left: 20, right: 15, top: 5, bottom: 5 }) //取值间隔为100,默认为400,取值越大,字体越粗 Row() { Text(`歌词字重:`) .fontSize(14) .fontColor(Color.White) Slider({ value: isPip ? this.lyricTextWeightPip : this.lyricTextWeight, min: 100, max: 900, step: 100, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(6) .onChange((value: number) => { if (isPip) { this.lyricTextWeightPip = value this.lyricControllerXF.setTextWeight(this.lyricTextWeightPip) PreferencesUtil.putSync('lyricTextWeightPip', this.lyricTextWeightPip) } else { this.lyricTextWeight = value this.lyricController.setTextWeight(this.lyricTextWeight) PreferencesUtil.putSync('lyricTextWeight', this.lyricTextWeight) } }) .width('76%') } .margin({ left: 20, right: 15, top: 5, bottom: 5 }) if (!isPip) { Row() { Text(`歌词模糊:`) .fontSize(14) .fontColor(Color.White) Slider({ value: isPip ? this.blurDegreePip : this.blurDegree, min: 0, max: 5, step: 0.1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(6) .onChange((value: number) => { this.setBlurDegree(value, isPip) }) .width('76%') } .margin({ left: 20, right: 15, top: 5, bottom: 15 }) } Row() { this.pushLyricButton($r('app.media.cut_current'), 0, '本地歌词') this.pushLyricButton($r('app.media.white_search'), 1, '获取歌词') } .justifyContent(FlexAlign.Center) .margin({ top: 3, bottom: 10 }) } } private setBlurDegree(index: number, isPip: boolean) { if (isPip) { this.blurDegreePip = index this.lyricControllerXF.setBlurDegree(this.blurDegreePip) PreferencesUtil.putSync('setBlurDegreePip', this.blurDegreePip) } else { this.blurDegree = index this.lyricController.setBlurDegree(this.blurDegree) PreferencesUtil.putSync('setBlurDegree', this.blurDegree) } } private setLyricAlignMode(index: number, isPip: boolean) { if (isPip) { this.currentLyricAlignModePip = index if (this.currentLyricAlignModePip === 1) { this.lyricControllerXF.setAlignMode("left") } else { this.lyricControllerXF.setAlignMode("center") } PreferencesUtil.putSync('LyricAlignModePip', this.currentLyricAlignModePip) } else { this.currentLyricAlignMode = index if (this.currentLyricAlignMode === 1) { this.lyricController.setAlignMode("left") } else { this.lyricController.setAlignMode("center") } PreferencesUtil.putSync('LyricAlignMode', this.currentLyricAlignMode) } } private setLyricTextSize(index: number, isPip: boolean) { if (isPip) { this.currentLyricSizePip = index this.lyricControllerXF.setTextSize(this.currentLyricSizePip) PreferencesUtil.putSync('LyricTextSizePip', this.currentLyricSizePip) } else { this.currentLyricSize = index this.lyricController.setTextSize(this.currentLyricSize) PreferencesUtil.putSync('LyricTextSize', this.currentLyricSize) } } private setHighLyricTextSize(scale: number, isPip: boolean) { if (isPip) { this.currentHightLyricSizePip = scale this.lyricControllerXF .setHighlightScale(this.currentHightLyricSizePip) PreferencesUtil.putSync('HighLyricTextSizePip', scale) } else { this.currentHightLyricSize = scale this.lyricController .setHighlightScale(this.currentHightLyricSize) PreferencesUtil.putSync('HighLyricTextSize', scale) } } private setLyricLineSpace(index: number, isPip: boolean) { if (isPip) { this.currentLyricLineSpacePip = index this.lyricControllerXF.setLineSpace(this.currentLyricLineSpacePip) PreferencesUtil.putSync('LyricLineSpacePip', this.currentLyricLineSpacePip) } else { this.currentLyricLineSpace = index this.lyricController.setLineSpace(this.currentLyricLineSpace) PreferencesUtil.putSync('LyricLineSpace', this.currentLyricLineSpace) } } private changeLyricColor(color: string, isPip: boolean, isBG?: boolean) { LogUtil.info('onecold isBG = ' + isBG) if (isBG) { //如果是设置悬浮背景 this.pipBg = color PreferencesUtil.putSync('BGColorPip', this.pipBg) this.nodeController.updateBgColor(this.pipBg, this.lyricControllerXF) return } if (isPip) { this.currentLyricColorPip = color this.lyricControllerXF .setTextColor(this.currentLyricColorPip) PreferencesUtil.putSync('LyricColorPip', this.currentLyricColorPip) } else { this.currentLyricColor = color this.lyricController .setTextColor(this.currentLyricColor) PreferencesUtil.putSync('LyricColor', this.currentLyricColor) } } private changeLyricHightLightColor(color: string, isPip: boolean) { if (isPip) { this.currentHighLightLyricColorPip = color this.lyricControllerXF .setHighlightColor(this.currentHighLightLyricColorPip) PreferencesUtil.putSync('LyricHighLightColorPip', this.currentHighLightLyricColorPip) } else { this.currentHighLightLyricColor = color this.lyricController .setHighlightColor(this.currentHighLightLyricColor) PreferencesUtil.putSync('LyricHighLightColor', this.currentHighLightLyricColor) } } // 构建时间控制器 @Builder BuildTimeControls() { Column() { // 时间显示 Text(this.getTimeText()) .fontSize(18) .fontColor(Color.White) .margin({ bottom: 10 }) Row() { this.PrecisionTimeButton($r('app.media.ic_previous'), -0.5, ' -0.5s ') this.PrecisionTimeButton($r('app.media.loop'), 0, '重置') this.PrecisionTimeButton($r('app.media.ic_next'), 0.5, ' +0.5s ') } .justifyContent(FlexAlign.Center) } .margin({ top: 12, bottom: 8 }) } // 精度按钮组件 @Builder PrecisionTimeButton(icon: Resource, step: number, label: string) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Column() { Image(icon) .width(28) .margin({ bottom: 5 }) .opacity(this.isButtonDisabled(step) ? 0.5 : 1) Text(label) .fontSize(12) .fontColor('#FFFFFF') } } .backgroundColor(Color.Transparent) .enabled(!this.isButtonDisabled(step)) .onClick(() => this.handlePrecisionAdjust(step)) .padding(5) .width(66) .height(66) .margin({ left: 18, right: 18 }) .border({ color: '#FFFFFF', width: 1.8 }) } private handlePrecisionAdjust(step: number) { if (step === 0) { this.timeOffset = 0 return } // 精度处理:保留1位小数 const newOffset = Number((this.timeOffset + step).toFixed(5)) // 边界保护(±15.0秒) if (newOffset < -30.0 || newOffset > 30.0) { // this.triggerEdgeFeedback(newOffset) // 触发边界反馈 return } // 状态更新 this.timeOffset = newOffset } // 按钮禁用逻辑 private isButtonDisabled(step: number): boolean { const futureOffset = this.timeOffset + step return (futureOffset < -30.0) || (futureOffset > 30.0) } // 时间显示文本升级 private getTimeText(): string { const absValue = Math.abs(this.timeOffset) return this.timeOffset === 0 ? '歌词时间已重置' : `歌词已${this.timeOffset < 0 ? '延后' : '提前'} ${absValue.toFixed(1)} 秒` } // 导入 本地歌词 搜索歌词 @Builder pushLyricButton(icon: Resource, step: number, label: string) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { Image(icon) .width(14) .margin({ left: 13 }) Text(label) .fontSize(12) .margin({ left: 6 }) .fontColor('#FFFFFF') } } .onClick(() => { switch (step) { case 0: this.callFilePickerSelectFileForLyric() break; case 1: //判断是不是赞助会员 if (PreferencesUtil.getStringSync('LRC_API', '') === '') { this.isLyricSetting = false this.showTipsDialog() return } if (StrUtil.isNotEmpty(this.videoUrl)) { ToastUtil.showToast('正在获取') this.isLyricSetting = false let lyricPath = this.videoUrl.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc' this.initLyric(lyricPath, true); } break; } }) .padding(5) .width(110) .backgroundColor(Color.Transparent) .height(40) .margin({ left: 6, right: 6 }) .border({ color: '#FFFFFF', radius: 20, width: 1.8 }) } showVipDialog() { DialogHelper.showCustomContentDialog({ dialogId: 'vip', title: "友情提示", autoCancel: false, //点击遮障层时,不关闭弹窗 backCancel: true, //点击返回键,不关闭弹窗 contentBuilder: () => { this.customVipBuilder("该功能需开通会员,非常感谢您的支持!") }, buttons: [], }) } //去赞助的自定义内容 @Builder customVipBuilder(content: string) { Column() { Text(content) .fontColor(Color.Gray) .fontSize(16) .alignSelf(ItemAlign.Start) .margin({ bottom: 15 }) .fontSize(16) Row() { Button('取消') .fontColor(Color.White) .backgroundColor(this.themeColor)//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]}) .height(50) .layoutWeight(1) .stateEffect(true) .margin({ right: 6 }) .onClick(() => { DialogHelper.closeDialog('vip'); //关闭弹框 }) Button('开通') .fontColor(Color.White)//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]}) .layoutWeight(1) .height(50) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ left: 6 }) .onClick(() => { DialogHelper.closeDialog('vip'); //关闭弹框 this.isShowPlay = false this.mType = 1 //跳转到用户中心 // router.pushUrl({ // url: 'pages/VipPage' // }, router.RouterMode.Single); }) } } .width("100%") .padding(10) } showTipsDialog() { DialogHelper.showCustomContentDialog({ dialogId: 'tips', title: "友情提示", autoCancel: false, //点击遮障层时,不关闭弹窗 backCancel: true, //点击返回键,不关闭弹窗 contentBuilder: () => { this.customTipsBuilder("请到设置界面配置API服务器地址!") }, buttons: [], }) } @Builder customTipsBuilder(content: string) { Column() { Text(content) .fontColor(Color.Gray) .fontSize(16) .alignSelf(ItemAlign.Start) .margin({ bottom: 15 }) .fontSize(16) Row() { Button('取消') .fontColor(Color.White) .backgroundColor(this.themeColor) .height(50) .layoutWeight(1) .stateEffect(true) .margin({ right: 6 }) .onClick(() => { DialogHelper.closeDialog('tips'); //关闭弹框 }) Button('跳转') .fontColor(Color.White)// .layoutWeight(1) .height(50) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ left: 6 }) .onClick(() => { DialogHelper.closeDialog('tips'); //关闭弹框 this.isShowPlay = false this.mType = 3 // router.pushUrl({ // url: 'pages/SettingPage' // }, router.RouterMode.Single); }) } } .width("100%") .padding(10) } @Builder PlayMoreSheet() { Column() { this.MoreList() } .width('100%') .height('100%') } @Builder MoreList() { Scroll() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start }) { ForEach(this.moreItems, (more: MoreItem, index: number) => { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { if (more.id === 3&&this.currentSong) { Image(more.image) .width(24) .height(24) .margin({ left: 10, right: 10 }) Text(more.title) .fontSize(16) .fontColor(Color.White) .bindSheet($$this.isShowDetailMore, this.detailSheet(this.currentSong), { height: this.isCoverOpacity() ? '95%' : '95%', dragBar: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, showClose: true, blurStyle: BlurStyle.Thin, backgroundColor: Color.Transparent, title: { title: '歌曲信息' } }) } else if (more.id === 15 && this.currentSong) { //编辑信息 Image(more.image) .width(24) .height(24) .margin({ left: 10, right: 10 }) Text(more.title) .fontSize(16) .fontColor(Color.White) .bindSheet($$this.isShowEdit, this.editSheet(this.currentSong), { height: this.isCoverOpacity() ? '95%' : '93%', dragBar: true, showClose: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, // blurStyle:BlurStyle.Thin, // backgroundColor:Color.Transparent, title: { title: '编辑信息' } }) } else if (more.id === 5) { //定时关闭 Image(more.image) .width(24) .height(24) .margin({ left: 10, right: 10 }) Text(more.title) .fontSize(16) .fontColor(Color.White) .onClick(() => { this.doMore(more.id) }) .bindSheet($$this.isShowTimeCloseMore, this.TimeCloseSheet(), { height: this.isCoverOpacity() ? '95%' : '95%', preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, dragBar: true, showClose: true, title: { title: $r('app.string.time_close') } }) } else if (more.id === 2) { //歌词设置 Image(more.image) .width(24) .height(24) .margin({ left: 10, right: 10 }) Text(more.title) .fontSize(16) .fontColor(Color.White) } else if (more.id === 16) { //悬浮歌词设置 Image(more.image) .width(24) .height(24) .margin({ left: 10, right: 10 }) Text(more.title) .fontSize(16) .fontColor(Color.White) .bindSheet($$this.showPipLyric, this.lyricSettingSheet(true), { height: this.isCoverOpacity() ? '95%' : '95%', dragBar: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, showClose: true, blurStyle: BlurStyle.Thin, backgroundColor: Color.Transparent, title: { title: '悬浮歌词' } }) } else if (more.id === 11) { Image(more.image) .width(24) .height(24) .margin({ left: 10, right: 10 }) Text(more.title) .fontSize(16) .fontColor(Color.White) .bindSheet($$this.isJumpSetting, this.JumpTopEndSheet(), { height: this.isCoverOpacity() ? '95%' : '60%', dragBar: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, showClose: true, blurStyle: BlurStyle.Thin, backgroundColor: Color.Transparent, title: { title: '跳过头尾' } }) } else if (more.id === 12) { //投播 AVCastPicker({ customPicker: (): void => this.TPImageBuilder(more), onStateChange: (state) => { if (state == AVCastPickerState.STATE_APPEARING) { console.log(' The picker starts showing.'); } else if (state == AVCastPickerState.STATE_DISAPPEARING) { console.log(' The picker finishes presenting.'); } }, pickerStyle: AVCastPickerStyle.STYLE_PANEL, sessionType: 'video' }) .id('AVCastPicker') } else { Image(more.image) .width(24) .height(24) .margin({ left: 10, right: 10 }) .onClick(() => { this.doMore(more.id) }) Text(more.id == 17 ? (this.isLandscape ? '竖屏模式' : '横屏模式') : more.title) .fontSize(16) .fontColor(Color.White) .onClick(() => { this.doMore(more.id) }) Select([//倍速 { value: '0.25x' }, { value: '0.5x' }, { value: '0.75x' }, { value: '1x' }, { value: '1.25x' }, { value: '1.5x' }, { value: '1.75x' }, { value: '2x' }, { value: '3x' }]) .font({ size: 16, weight: FontWeight.Medium }) .fontColor($r('sys.color.white')) .margin({ left: 25 }) .visibility(more.id === 1 ? Visibility.Visible : Visibility.None) .selected(CommonConstants.video_speed_list.indexOf(this.playSpeed)) .value(Utility.optimizedFormat(this.playSpeed)) .onSelect(async (_index: number, text?: string | undefined) => { let speed = parseFloat(text?.replace('x', '') || '1'); if (!CommonConstants.video_speed_list.includes(speed)) { speed = 1; } // 使用UnifiedPlayerService设置播放速度 this.setPlaybackSpeedViaService(speed); }) Slider({ value: this.volume, min: 0, max: 1, step: 0.1, style: SliderStyle.OutSet }) .margin({ left: 18, right: 8 }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(9) .visibility(more.id === 9 ? Visibility.Visible : Visibility.None) .onChange((value: number) => { // 使用UnifiedPlayerService设置音量 this.setVolumeViaService(value); }) .layoutWeight(1) Blank() Toggle({ type: ToggleType.Switch, isOn: this.isMusicMemoryPlay }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .margin({ right: 15 }) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .visibility(more.id == 6 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 6) { this.isMusicMemoryPlay = checked; PreferencesUtil.put(SettingPage.iS_MUSIC_MEMORY_PLAY, this.isMusicMemoryPlay) if (this.isMusicMemoryPlay) { ToastUtil.showToast('记忆播放已开启') } else { ToastUtil.showToast('记忆播放已关闭') } } }) .width(48) .height(24); Toggle({ type: ToggleType.Switch, isOn: this.isMusicBGCover }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 7 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 7) { this.isMusicBGCover = checked; PreferencesUtil.put(SettingPage.iS_MUSIC_BG_COVER, this.isMusicBGCover) if (this.isMusicBGCover) { ToastUtil.showToast('播放背景随音乐封面已开启') } else { ToastUtil.showToast('播放背景随音乐封面已关闭') } } }) .width(48) .height(24); Toggle({ type: ToggleType.Switch, isOn: this.isSavePlayMode }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 8 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 8) { this.isSavePlayMode = checked; PreferencesUtil.put(SettingPage.iS_SAVE_PLAY_MODE, this.isSavePlayMode) if (this.isSavePlayMode) { ToastUtil.showToast('保存播放模式已开启') } else { ToastUtil.showToast('保存播放模式已关闭') } } }) .width(48) .height(24); Toggle({ type: ToggleType.Switch, isOn: this.isCoverRectangle }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 13 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 13) { this.isCoverRectangle = checked; PreferencesUtil.put(SettingPage.IS_COVER_RECTANGLE, this.isCoverRectangle) if (this.isCoverRectangle) { ToastUtil.showToast('封面方形已开启') this.animationRoFun() } else { ToastUtil.showToast('封面圆形已开启') this.animationRoFun() } } }) .width(48) .height(24); } } .width('100%') .height(45) .padding(15) .backgroundColor(Color.Transparent) .borderRadius(12) .margin({ bottom: 6 }) } .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.9 }) .transition(TransitionEffect.move(TransitionEdge.BOTTOM) .animation({ duration: 600, curve: Curve.Ease, delay: 60 * index })) .backgroundColor(Color.Transparent) .onClick(() => { this.doMore(more.id) }) }); } .width('100%') .padding(16) }.margin({ bottom: 18 }) } @Builder TPImageBuilder(more: MoreItem): void { Row() { Image($r('app.media.tuoping')) .width(22) .height(22) .margin({ left: 10, right: 10 }) Text(more.title) .fontSize(16) .fontColor(Color.White) } .width('100%') .layoutWeight(1) .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.Start) } private moreItems: MoreItem[] = [ { id: 1, image: $r('app.media.speed'), title: '倍速' }, { id: 2, image: $r('app.media.lyric'), title: '歌词' }, { id: 17, image: $r('app.media.full_4'), title: '横屏模式' }, { id: 16, image: $r('app.media.hua_zhong_hua'), title: '悬浮歌词' }, { id: 14, image: $r('app.media.cover_online'), title: '获取封面' }, { id: 15, image: $r('app.media.rename2'), title: '编辑信息' }, { id: 5, image: $r('app.media.time_close'), title: '定时关闭' }, { id: 10, image: $r('app.media.ring_tone'), title: '设为铃声' }, { id: 11, image: $r('app.media.time_close'), title: '跳过头尾' }, { id: 12, image: $r('app.media.toubo'), title: '投播' }, { id: 3, image: $r('app.media.hua_zhong_hua'), title: '详情' }, { id: 4, image: $r('app.media.share2'), title: '分享' }, { id: 9, image: $r('app.media.volume_white'), title: '调整音量' }, { id: 6, image: $r('app.media.memory'), title: '记忆播放' }, { id: 13, image: $r('app.media.rectangle'), title: '封面方形' }, { id: 8, image: $r('app.media.cut_current'), title: '保存播放模式' }, { id: 7, image: $r('app.media.playlist2'), title: '播放背景随音乐封面' }, ]; @State isShowMoreView: boolean = false; @State isMusicMemoryPlay: boolean = false //是否启用记忆播放 @State isShowTimeCloseMore: boolean = false @State isShowDetailMore: boolean = false @State isMusicBGCover: boolean = true @State isSavePlayMode: boolean = true @State isLyricSetting: boolean = false @State isJumpSetting: boolean = false @State isShowEdit: boolean = false @State showPipLyric: boolean = false async setRingTone() { if (StrUtil.isEmpty(this.videoUrl) || StrUtil.isEmpty(this.name)) { return } // 确定后的逻辑 let ringtoneTypeList: Array = ringtone.getSupportedRingtoneTypes(); LogUtil.info('onecold getSupportedRingtoneTypes : ' + JSON.stringify(ringtoneTypeList)); let dataTypeList: Array = ringtone.getSupportedDataTypes(ringtone.RingtoneType.NOTIFICATION); LogUtil.info('onecold getSupportedDataTypes: ' + JSON.stringify(dataTypeList)); //let fileName: string = audioPath.substring(audioPath.lastIndexOf('/') + 1, audioPath.lastIndexOf('.')); await ringtone.startRingtoneSetting(getContext(this) as common.UIAbilityContext , this.videoUrl, this.name).then(res => { LogUtil.info('onecold setFlag :' + res); }); } doMore(moreId: number) { switch (moreId) { case 2: //歌词 this.isLyricSetting = !this.isLyricSetting; this.isShowMoreView = false break; case 3: //详情 this.isShowDetailMore = !this.isShowDetailMore; break; case 4: //分享 if (this.currentSong !== undefined) { Utility.doShareMusic(this.currentSong, getContext(this) as common.UIAbilityContext) } this.isShowMoreView = false break; case 5: //定时关闭 this.isShowTimeCloseMore = !this.isShowTimeCloseMore; break; case 10: //设为铃声 this.setRingTone() this.isShowMoreView = false break; case 11: //跳过头尾 this.isJumpSetting = !this.isJumpSetting; break; case 13: //倍速 break; case 14: //搜索封面 if (this.currentSong !== undefined) { this.doSearchCover(this.currentSong) } this.isShowMoreView = false break; case 15: //编辑信息 this.isShowEdit = !this.isShowEdit; break; case 16: //悬浮歌词 this.showPipLyric = !this.showPipLyric; break; case 17: //横屏模式 if (this.isLandscape) { this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT); } else { this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE); } this.isShowMoreView = false break; } } /** * 切换横竖屏 * */ setOrientation(orientation: number) { if (this.windowClass) { this.windowClass.setPreferredOrientation(orientation).then(() => { Logger.info('setWindowOrientation: ' + orientation + ' Succeeded.'); }).catch((err: BusinessError) => { Logger.info('setWindowOrientation: ' + orientation + ' Failed. Cause: ' + JSON.stringify(err)); }); } } /** * 画中画功能(悬浮歌词功能) * */ private nodeController: TextNodeController = new TextNodeController('#000000', this.lyricControllerXF); pipController?: PiPWindow.PiPController; navigationId: string = ''; @State curState: string = ''; @State curError: ResourceStr = ''; @State pipBg: string = '#E8E8E8' @State buttonAction: string = ''; //开启画中画功能 async startPip() { this.nodeController = new TextNodeController(this.pipBg, this.lyricControllerXF); if (!this.pipController) { await this.createPipController(); } if (!this.pipController) { Logger.info(`[${TAG}] pipController create error`); return; } await this.pipController.startPiP(); } async stopPip() { if (!this.pipController) { Logger.info(`[${TAG}] pipController is not exist`); return; } this.isOpenPip = false await this.pipController.stopPiP(); } async createPipController() { this.pipController = await PiPWindow.create({ context: getContext(this), componentController: this.xcomponentController, navigationId: this.navigationId, templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY, controlGroups: [PiPWindow.VideoPlayControlGroup.VIDEO_PREVIOUS_NEXT], customUIController: this.nodeController, }); this.pipLyWidth = PreferencesUtil.getNumberSync('pipLyWidth', 330) this.pipLyHeight = PreferencesUtil.getNumberSync('pipLyHeight', 120) this.pipController.updateContentSize(this.pipLyWidth, this.pipLyHeight); this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => { this.onStateChange(state, reason); }); this.pipController.on('controlPanelActionEvent', (event: PiPWindow.PiPActionEventType, status?: number) => { this.onActionEvent(event, status); }); } destroyPipController() { if (!this.pipController) { return; } this.pipController.off('stateChange'); this.pipController.off('controlPanelActionEvent'); this.pipController = undefined; } onStateChange(state: PiPWindow.PiPState, reason: string) { switch (state) { case PiPWindow.PiPState.ABOUT_TO_START: this.curState = 'ABOUT_TO_START'; this.curError = $r('app.string.current_error_hint'); break; case PiPWindow.PiPState.STARTED: this.curState = 'STARTED'; this.curError = $r('app.string.current_error_hint'); break; case PiPWindow.PiPState.ABOUT_TO_STOP: this.curState = 'ABOUT_TO_STOP'; this.curError = $r('app.string.current_error_hint'); break; case PiPWindow.PiPState.STOPPED: this.curState = 'STOPPED'; this.curError = $r('app.string.current_error_hint'); this.stopPip() this.destroyPipController() break; case PiPWindow.PiPState.ABOUT_TO_RESTORE: this.curState = 'ABOUT_TO_RESTORE'; this.curError = $r('app.string.current_error_hint'); break; case PiPWindow.PiPState.ERROR: this.curState = 'ERROR'; this.curError = reason; break; default: break; } Logger.info(`[${TAG}] onecold onStateChange: ${this.curState}, reason: ${reason}`); } onActionEvent(event: PiPWindow.PiPActionEventType, status: number | undefined) { LogUtil.info('onecold onActionEvent = ' + event + ' status=' + status) switch (event) { case 'playbackStateChanged': if (status === 0) { this.pause(); } else { this.startPlayOrResumePlay(); } break; case 'nextVideo': // 切换到下一个视频 this.playNext() break; case 'previousVideo': // 切换到上一个视频 this.playPrevious() break; case 'previousVideo': // 切换到上一个视频 this.playPrevious() break; default: } this.buttonAction = event + `-status:${status}`; Logger.info(`[${TAG}] onActionEvent: ${this.buttonAction} status:${status}}`); } /** * 画中画功能(悬浮歌词功能)结束 * */ doSearchCover(item: VideoItem) { if (PreferencesUtil.getStringSync('COVER_API', '') === '') { this.isShowMoreView = false this.showTipsDialog() return } if (item !== undefined) { let artist = item?.artist if (StrUtil.isEmpty(artist) || artist === undefined) { artist = '' } this.searchCover(item, item.name, artist) } } searchCover(item: VideoItem, title: string, artist: string) { NetAxiosUtil.getLyricCover(title, artist, PreferencesUtil.getStringSync('COVER_API', '')).then(async (res) => { LogUtil.debug("onecold res =" + res) if (StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') { if (item.filePath == this.currentSong?.filePath) { this.cover = res if (this.currentSong) { this.currentSong.pixelMapPath = res } } this.table.updatePixelMapPath(item.filePath, res, (success: boolean, error?: string) => { if (success) { this.doUpdateData() console.log(" onecold 更新音乐封面成功,数据库已同步"); } else { console.error(" onecold 更新音乐封面数据库失败原因: " + error); } }); LogUtil.debug("onecold this.cover =" + this.cover) } }) } @State jumpTopTime: number = 0 @State jumpEndTime: number = 0 @State isOpenJump: boolean = false @Builder JumpTopEndSheet() { Scroll() { Column() { Column() { Text('仅对本播放列表有效') .fontSize(16) .fontColor($r('app.color.text_color')) // 跳过头尾 Column() { Row() { Text('跳过片头:') .fontSize(16) .fontColor($r('app.color.text_color')) Text(this.jumpTopTime + 's') .fontSize(16) .fontColor($r('app.color.text_color')) } .margin({ bottom: 5 }) Row() { Slider({ value: this.jumpTopTime, min: 0, max: 120, step: 1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(8) .onChange((value: number) => { this.jumpTopTime = value; }) .width('85%') } .alignItems(VerticalAlign.Top) } .margin({ top: 10, bottom: 10 }) // 跳过头尾 Column() { Row() { Text('跳过片尾:') .fontSize(16) .fontColor($r('app.color.text_color')) Text(this.jumpEndTime + 's') .fontSize(16) .fontColor($r('app.color.text_color')) } .margin({ bottom: 5 }) Row() { Slider({ value: this.jumpEndTime, min: 0, max: 120, step: 1, style: SliderStyle.OutSet }) .blockColor(this.themeColor) .trackColor($r('app.color.speed_text_color')) .selectedColor(Color.White) .trackThickness(8) .onChange((value: number) => { this.jumpEndTime = value; }) .width('85%') } .alignItems(VerticalAlign.Top) } .margin({ top: 10, bottom: 10 }) Row() { Button('保存设置') .fontColor(Color.White) .layoutWeight(1) .height(40) .width(80) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ left: 0, bottom: 20 }) .onClick(async () => { if (!Utility.isNoble()) { this.isJumpSetting = false this.isShowMoreView = false this.showVipDialog() return } if (this.currentSong === undefined) { return } if (this.modeType === 0) { let parentPath = FileUtil.getParentPath(this.videoUrl) PreferencesUtil.putSync(parentPath + 'isOpenJump', true) PreferencesUtil.putSync(parentPath + 'jumpEndTime', this.jumpEndTime) PreferencesUtil.putSync(parentPath + 'jumpTopTime', this.jumpTopTime) } else if (this.modeType === 1) { //媒体库 PreferencesUtil.putSync(1 + 'isOpenJump', true) PreferencesUtil.putSync(1 + 'jumpEndTime', this.jumpEndTime) PreferencesUtil.putSync(1 + 'jumpTopTime', this.jumpTopTime) } else if (this.modeType === 2) { //艺术家 if (this.currentSong?.artist) { PreferencesUtil.putSync(this.currentSong?.artist + 2 + 'isOpenJump', true) PreferencesUtil.putSync(this.currentSong?.artist + 2 + 'jumpEndTime', this.jumpEndTime) PreferencesUtil.putSync(this.currentSong?.artist + 2 + 'jumpTopTime', this.jumpTopTime) } } else if (this.modeType === 3) { //专辑 if (this.currentSong?.album) { PreferencesUtil.putSync(this.currentSong?.album + 3 + 'isOpenJump', true) PreferencesUtil.putSync(this.currentSong?.album + 3 + 'jumpEndTime', this.jumpEndTime) PreferencesUtil.putSync(this.currentSong?.album + 3 + 'jumpTopTime', this.jumpTopTime) } } ToastUtil.showShort("保存成功") this.isJumpSetting = false }) } .margin({ left: 38, right: 38, top: 10, bottom: 10 }) .width(120) .height(50) } } } .width('100%') .height('100%') } // 应用可以通过onStateChange接口监听组件显示/消失状态,当组件显示时建议不要销毁AVCastPicker的显示;当组件消失时,再根据业务隐藏AVCastPicker。 private onStateVastChange(state: AVCastPickerState) { if (state == AVCastPickerState.STATE_APPEARING) { LogUtils.getInstance().LOGI('投播选择器开始显示'); } else if (state == AVCastPickerState.STATE_DISAPPEARING) { LogUtils.getInstance().LOGI('投播选择器结束显示'); } } // 检查是否存在投播设备 private async checkCastingDeviceAvailability(): Promise { try { const session = this.avSessionController.getAvSession(); if (!session) { return false; } const outputDevice = await session.getOutputDevice(); if (!outputDevice || !outputDevice.devices || outputDevice.devices.length === 0) { return false; } // 检查是否有远程设备可用 const remoteDevices = outputDevice.devices.filter(device => device.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE ); return remoteDevices.length > 0; } catch (error) { LogUtils.getInstance().LOGI(`检查投播设备可用性失败: ${error}`); return false; } } // 获取当前输出设备信息 private async getCurrentOutputDevice(): Promise { try { const session = this.avSessionController.getAvSession(); if (!session) { return undefined; } return await session.getOutputDevice(); } catch (error) { LogUtils.getInstance().LOGI(`获取当前输出设备失败: ${error}`); return undefined; } } // 检查当前是否正在投播 private async checkCurrentCastingStatus(): Promise { try { const outputDevice = await this.getCurrentOutputDevice(); if (!outputDevice || !outputDevice.devices || outputDevice.devices.length === 0) { return false; } // 检查当前设备是否为远程设备 const currentDevice = outputDevice.devices[0]; return currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE; } catch (error) { LogUtils.getInstance().LOGI(`检查当前投播状态失败: ${error}`); return false; } } // 投播状态下的播放下一首 private async handleCastingPlayNext(): Promise { try { LogUtils.getInstance().LOGI('投播状态下切换下一首'); // 计算下一首的索引 let nextIndex = this.curIndex; if (this.playType === 1) { // 单曲循环 // 保持当前索引不变 } else if (this.playType === 2) { // 列表循环 nextIndex = (this.curIndex + 1) % this.songList.length; } else { // 顺序播放 if (this.curIndex < this.songList.length - 1) { nextIndex = this.curIndex + 1; } else { LogUtils.getInstance().LOGI('已是最后一首,停止投播'); this.endCasting(); return; } } // 更新当前歌曲信息 this.curIndex = nextIndex; this.currentSong = this.songList[this.curIndex]; this.videoUrl = this.currentSong.filePath; this.artist = this.currentSong.artist; this.name = this.currentSong.name; this.cover = this.currentSong.pixelMapPath; // 切换投播资源 await this.changeCasting(); LogUtils.getInstance().LOGI(`投播下一首完成: ${this.currentSong.name}`); } catch (error) { LogUtils.getInstance().LOGI(`投播下一首失败: ${error}`); ToastUtil.showToast(`投播切换失败: ${error}`); } } // 投播状态下的播放上一首 private async handleCastingPlayPrevious(): Promise { try { LogUtils.getInstance().LOGI('投播状态下切换上一首'); // 计算上一首的索引 let prevIndex = this.curIndex; if (this.playType === 1) { // 单曲循环 // 保持当前索引不变 } else if (this.playType === 2) { // 列表循环 prevIndex = this.curIndex === 0 ? this.songList.length - 1 : this.curIndex - 1; } else { // 顺序播放 if (this.curIndex > 0) { prevIndex = this.curIndex - 1; } else { LogUtils.getInstance().LOGI('已是第一首,停止投播'); this.endCasting(); return; } } // 更新当前歌曲信息 this.curIndex = prevIndex; this.currentSong = this.songList[this.curIndex]; this.videoUrl = this.currentSong.filePath; this.artist = this.currentSong.artist; this.name = this.currentSong.name; this.cover = this.currentSong.pixelMapPath; // 切换投播资源 await this.changeCasting(); LogUtils.getInstance().LOGI(`投播上一首完成: ${this.currentSong.name}`); } catch (error) { LogUtils.getInstance().LOGI(`投播上一首失败: ${error}`); ToastUtil.showToast(`投播切换失败: ${error}`); } } // 投播状态下的播放/暂停控制 private async handleCastingPlayPause(): Promise { try { if (this.isCastPlaying) { await this.sendCastControlCommand('pause'); LogUtils.getInstance().LOGI('投播暂停'); } else { await this.sendCastControlCommand('play'); LogUtils.getInstance().LOGI('投播播放'); } } catch (error) { LogUtils.getInstance().LOGI(`投播播放/暂停控制失败: ${error}`); } } // 投播状态下的进度控制 private async handleCastingSeek(position: number): Promise { try { await this.sendCastControlCommand('seek', position); LogUtils.getInstance().LOGI(`投播进度调节: ${position}ms`); } catch (error) { LogUtils.getInstance().LOGI(`投播进度调节失败: ${error}`); } } // 检查并恢复投播状态 private async checkAndRestoreCastingState(): Promise { try { LogUtils.getInstance().LOGI('检查当前投播状态...'); const session = this.avSessionController.getAvSession(); if (!session) { LogUtils.getInstance().LOGI('AVSession不可用,跳过投播状态检查'); return; } // 获取当前输出设备 const outputDevice = await session.getOutputDevice(); if (!outputDevice || !outputDevice.devices || outputDevice.devices.length === 0) { LogUtils.getInstance().LOGI('没有输出设备,当前为本地播放'); return; } const currentDevice = outputDevice.devices[0]; LogUtils.getInstance().LOGI(`当前输出设备: ${currentDevice.castCategory}, 协议: ${currentDevice.supportedProtocols}`); // 检查是否为远程设备(投播状态) if (currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE) { LogUtils.getInstance().LOGI('检测到投播状态,恢复投播控制器'); // 恢复投播状态 this.currentCastDevice = currentDevice; this.isCasting = true; // 获取投播控制器 this.castController = await session.getAVCastController(); if (this.castController) { // 设置投播监听器 this.setPlaybackStateChangeListener(); // 查询当前播放状态 const playbackState = await this.castController.getAVPlaybackState(); if (playbackState) { this.playbackStateChangeListener(playbackState); } // 同步投播状态到AppStorage let refToIsCasting: AbstractProperty | undefined = AppStorage.ref('isCasting'); refToIsCasting?.set(true); LogUtils.getInstance().LOGI('投播状态恢复成功'); ToastUtil.showToast('已连接到投播设备'); } else { LogUtils.getInstance().LOGI('获取投播控制器失败'); } } else { LogUtils.getInstance().LOGI('当前为本地播放状态'); } } catch (error) { LogUtils.getInstance().LOGI(`检查投播状态失败: ${error}`); } } private showSelectLyric() { DialogHelper.showActionSheetDialog({ title: '温馨提醒:\n1.歌词文件名必须和目标歌曲文件名相同\n2.歌词文件和目标歌曲要同个歌单路径。', // sheets: ["本地歌词", "搜索歌词"], sheets: [ { value: "本地歌词", fontColor: $r('app.color.text_color') }, { value: "搜索歌词", fontColor: $r('app.color.text_color') }, ], maskColor: Color.Transparent, backgroundColor: $r('app.color.bg_card'), transition: AnimationHelper.transitionInDown(666), onAction: (index) => { switch (index) { case 0: this.callFilePickerSelectFileForLyric() break; case 1: if (StrUtil.isNotEmpty(this.videoUrl)) { let lyricPath = this.videoUrl.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc' this.initLyric(lyricPath); } break; } } }) } // 拉起picker选择文件管理器 async callFilePickerSelectFileForLyric(): Promise { try { let DocumentSelectOptions = new picker.DocumentSelectOptions(); DocumentSelectOptions.maxSelectNumber = 1 DocumentSelectOptions.fileSuffixFilters = CommonConstants.LYRIC_FORMAT let documentPicker = new picker.DocumentViewPicker(); documentPicker.select(DocumentSelectOptions).then((DocumentSelectResult) => { Logger.info(TAG, 'DocumentViewPicker.select successfully, DocumentSelectResult uri: ' + JSON.stringify(DocumentSelectResult)); if (DocumentSelectResult !== null && DocumentSelectResult !== undefined) { this.saveVideoDatasLyric(DocumentSelectResult) } }).catch((err: BusinessError) => { Logger.error(TAG, 'DocumentViewPicker.select failed with err: ' + JSON.stringify(err)); }); } catch (err) { Logger.error(TAG, 'DocumentViewPicker failed with err: ' + JSON.stringify(err)); } } async saveVideoDatasLyric(uris: string[]) { if (ArrayUtil.isEmpty(uris)) { return } let newUris: string[] = []; for (let i = 0; i < uris.length; i++) { let filePath = getContext(this).filesDir + '/' + Utility.getMediaNameByUri(uris[i]) Logger.info(TAG, 'filePath uri: ' + filePath); newUris.push(filePath) let file = fileIo.openSync(uris[i], fileIo.OpenMode.READ_ONLY) let file2 = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE) fileIo.copyFileSync(file.fd, file2.fd) fileIo.closeSync(file); fileIo.closeSync(file2); this.initLyric(filePath) } } private initDelayPlay(context: object) { this.mContext = context; } startPipLyric() { if (this.pipController) { this.pipController.updatePiPControlStatus(PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE, PiPWindow.PiPControlStatus.PLAY); } this.isOpenPip = PreferencesUtil.getBooleanSync('isOpenPip', false) if (this.isOpenPip) { this.startPip() } else { this.stopPip() } } private async startPlayOrResumePlay() { try { this.startPipLyric() this.mDestroyPage = false; this.animationState = AnimationStatus.Running // 智能同步播放列表到UnifiedPlayerService if (ArrayUtil.isNotEmpty(this.songList)) { // 检查UnifiedPlayerService是否已经有播放列表 const servicePlaylist = this.unifiedPlayerService.getPlaylist(); const serviceIndex = this.unifiedPlayerService.getCurrentIndex(); if (ArrayUtil.isEmpty(servicePlaylist)) { // 如果服务没有播放列表,使用LocalMusic的播放列表 this.unifiedPlayerService.setPlaylist(this.songList, this.curIndex); } else { // 如果服务已有播放列表,同步服务的状态到LocalMusic const serviceSong = this.unifiedPlayerService.getCurrentSong(); if (serviceSong && serviceIndex >= 0 && serviceIndex < servicePlaylist.length) { this.songList = servicePlaylist; this.curIndex = serviceIndex; this.currentSong = serviceSong; this.sonDataSource.pushArrayData(this.songList); // 更新UI显示 this.videoUrl = serviceSong.filePath; this.name = serviceSong.name; this.artist = serviceSong.artist; this.cover = serviceSong.pixelMapPath; } } } // 使用UnifiedPlayerService开始播放 await this.unifiedPlayerService.startPlayOrResumePlay(); // 保持原有的UI更新逻辑 this.stopProgressTask(); this.startProgressTask(); this.watchStatus(); this.updateLastPlayTimeStr(this.videoUrl) // 更新播放状态 this.CONTROL_PlayStatus = PlayStatus.PLAY; this.setIsPlaying(true); // 注意:AVSession状态更新由UnifiedPlayerService统一处理,避免冲突 LogUtils.getInstance().LOGI("LocalMusic: startPlayOrResumePlay completed via UnifiedPlayerService"); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic startPlayOrResumePlay error: ${error}`); ToastUtil.showToast(`播放失败: ${error}`); // 错误处理:回退到原有逻辑 this.handlePlaybackError(); } } // 播放错误处理 private handlePlaybackError() { this.CONTROL_PlayStatus = PlayStatus.INIT; this.setIsPlaying(false); // 注意:AVSession状态更新由UnifiedPlayerService统一处理,避免冲突 this.mDestroyPage = true; } // 设置音量(通过UnifiedPlayerService) private setVolumeViaService(volume: number) { try { this.unifiedPlayerService.setVolume(volume); this.volume = volume; PreferencesUtil.putSync('DefalutVolume', volume); LogUtils.getInstance().LOGI(`LocalMusic: Volume set to ${volume} via UnifiedPlayerService`); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic setVolume error: ${error}`); ToastUtil.showToast(`设置音量失败: ${error}`); } } // 设置播放速度(通过UnifiedPlayerService) private setPlaybackSpeedViaService(speed: number) { try { this.unifiedPlayerService.setPlaybackSpeed(speed); this.playSpeed = speed; LogUtils.getInstance().LOGI(`LocalMusic: Playback speed set to ${speed} via UnifiedPlayerService`); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic setPlaybackSpeed error: ${error}`); ToastUtil.showToast(`设置播放速度失败: ${error}`); } } // 设置播放模式(通过UnifiedPlayerService) private setPlayModeViaService(mode: number) { try { this.unifiedPlayerService.setPlayMode(mode); this.playType = mode; PreferencesUtil.putSync('musicPlayType', mode); LogUtils.getInstance().LOGI(`LocalMusic: Play mode set to ${mode} via UnifiedPlayerService`); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic setPlayMode error: ${error}`); // 回退到原有逻辑 this.playType = mode; PreferencesUtil.putSync('musicPlayType', mode); } } // 播放指定索引的歌曲(通过UnifiedPlayerService) private async playSongAtIndexViaService(index: number) { try { // 确保播放列表已同步到UnifiedPlayerService this.unifiedPlayerService.setPlaylist(this.songList, this.curIndex); // 使用UnifiedPlayerService播放指定索引的歌曲 await this.unifiedPlayerService.playSongAtIndex(index); // 更新本地状态以保持UI同步 const currentIndex = this.unifiedPlayerService.getCurrentIndex(); const currentSong = this.unifiedPlayerService.getCurrentSong(); if (currentSong) { this.curIndex = currentIndex; this.currentSong = currentSong; this.videoUrl = currentSong.filePath; this.name = currentSong.name; this.artist = currentSong.artist; this.cover = currentSong.pixelMapPath; } } catch (error) { ToastUtil.showToast(`播放歌曲失败: ${error}`); // 错误处理:回退到原有逻辑 this.playIndex(index); } } // 同步播放列表到UnifiedPlayerService private syncPlaylistToService() { try { if (ArrayUtil.isNotEmpty(this.songList)) { // 记录调用栈信息以便调试 const stack = new Error().stack || 'No stack available'; const caller = stack.split('\n')[2] || 'Unknown caller'; this.unifiedPlayerService.setPlaylist(this.songList, this.curIndex); // 同步播放模式到UnifiedPlayerService this.unifiedPlayerService.setPlayMode(this.playType); LogUtils.getInstance().LOGI(`LocalMusic: Playlist and play mode synced to UnifiedPlayerService - ${this.songList.length} songs, mode: ${this.playType}, caller: ${caller.trim()}`); // 如果同步的是小播放列表,记录更多信息 if (this.songList.length <= 20) { LogUtils.getInstance().LOGI(`LocalMusic: Small playlist sync detected, current path: ${this.currentPath}, first few songs: ${this.songList.slice(0, 5).map(s => s.name).join(', ')}`); } } else { LogUtils.getInstance().LOGI('LocalMusic: syncPlaylistToService - No songs to sync'); } } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic syncPlaylistToService error: ${error}`); } } // 从播放列表移除歌曲(通过UnifiedPlayerService) private removeFromPlaylistViaService(index: number) { try { // 使用UnifiedPlayerService移除歌曲 const removedSong = this.unifiedPlayerService.removeFromPlaylist(index); if (removedSong) { // 更新本地播放列表 this.songList.splice(index, 1); this.sonDataSource.pushArrayData(this.songList); // 更新当前索引 this.curIndex = this.unifiedPlayerService.getCurrentIndex(); ToastUtil.showToast(`已移除 ${removedSong.name}`); } } catch (error) { // 错误处理:回退到原有逻辑 if (index >= 0 && index < this.songList.length) { const removedSong = this.songList.splice(index, 1)[0]; this.sonDataSource.pushArrayData(this.songList); ToastUtil.showToast(`已移除 ${removedSong.name}`); } } } // 同步进度更新到UI(从UnifiedPlayerService) private syncProgressFromService(progress: PlayProgress) { try { // 检测歌曲是否发生切换(额外保障机制) const currentSongPath = this.currentSong?.filePath || ""; let isSongChanged = false; if (this.lastSongPath !== currentSongPath && currentSongPath !== "") { console.log(`Heanup 在进度更新中检测到歌曲切换: ${this.lastSongPath} -> ${currentSongPath}`); console.log(`Heanup 歌曲切换前 oldSeconds: ${this.oldSeconds}, currentTime: ${this.currentTime}`); // 立即重置时间显示状态,防止新歌曲时间显示被上一首歌曲的时间"卡住" this.oldSeconds = 0; this.currentTime = "00:00"; this.lastSongPath = currentSongPath; this.justSwitched = true; // 标记歌曲刚刚切换 isSongChanged = true; console.log(`Heanup 歌曲切换后 oldSeconds: ${this.oldSeconds}, currentTime: ${this.currentTime}`); // 歌曲切换时,强制将播放位置重置为0,忽略服务报告的位置 progress.currentPosition = 0; console.log(`Heanup 歌曲切换,强制progress.currentPosition为0`); // 立即返回,不更新进度,等待新歌曲开始播放 return; } // 直接使用ijkplayer获取播放进度,避免状态同步延迟 try { const ijkPlayer = this.unifiedPlayerService.getIjkPlayer(); if (ijkPlayer && ijkPlayer.isPlaying()) { const currentPosition = ijkPlayer.getCurrentPosition(); const duration = ijkPlayer.getDuration(); if (duration > 0 && currentPosition >= 0) { // 更新进度条 this.slideEnable = true; let curPercent = currentPosition / duration; let pos = curPercent * 100; if (pos > this.PROGRESS_MAX_VALUE) { this.progressValue = this.PROGRESS_MAX_VALUE; } else { this.progressValue = pos; } // 更新时间显示 this.totalTime = this.stringForTime(duration); console.log(`Heanup 当前时间 (直接从ijkplayer获取) - ${currentPosition} / ${duration}`) console.log(`Heanup 更新前 - oldSeconds: ${this.oldSeconds}, currentTime: ${this.currentTime}`) this.isCurrentTime = true; this.currentTime = this.stringForTime(currentPosition); this.isCurrentTime = false; console.log(`Heanup 更新后 - oldSeconds: ${this.oldSeconds}, currentTime: ${this.currentTime}`) // 继续执行后续的歌词更新等逻辑 } } } catch (error) { console.error('LocalMusic: 获取ijkplayer播放进度失败', error); // 如果直接获取失败,回退到使用服务状态 this.updateProgressFromServiceState(progress); return; } // 如果直接获取ijkplayer失败或者不在播放状态,使用服务状态 this.updateProgressFromServiceState(progress); // 更新歌词位置 const lyricPosition = progress.currentPosition + this.timeOffset * 1000; if (this.lyricController) { this.lyricController.updatePosition(lyricPosition); } if (this.lyricControllerXF) { this.lyricControllerXF.updatePosition(lyricPosition); } if (this.lyricControllerSingle) { this.lyricControllerSingle.updatePosition(lyricPosition); } // 更新随机颜色(如果正在播放) const currentState = this.unifiedPlayerService.getCurrentState(); if (currentState.isPlaying) { this.randomColor = `rgb(${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)})`; } else { this.randomColor = 'rbg(0,0,0)'; } // 检查是否需要跳到下一首(片尾跳过功能) if (this.isOpenJump && progress.duration > this.jumpEndTime * 1000) { if (progress.currentPosition >= progress.duration - this.jumpEndTime * 1000) { this.playNext(); } } LogUtils.getInstance().LOGI(`LocalMusic: Progress synchronized from service - ${progress.currentPosition}/${progress.duration}ms`); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic syncProgressFromService error: ${error}`); } } /** * 从服务状态更新播放进度(回退方案) */ private updateProgressFromServiceState(progress: PlayProgress) { // 更新进度条 if (progress.duration > 0) { this.slideEnable = true; let curPercent = progress.currentPosition / progress.duration; let pos = curPercent * 100; if (pos > this.PROGRESS_MAX_VALUE) { this.progressValue = this.PROGRESS_MAX_VALUE; } else { this.progressValue = pos; } } // 更新时间显示 this.totalTime = this.stringForTime(progress.duration); if (progress.currentPosition > progress.duration) { progress.currentPosition = progress.duration; } console.log(`Heanup 当前时间 (从服务状态获取) - ${progress.currentPosition} / ${progress.duration}`) console.log(`Heanup 更新前 - oldSeconds: ${this.oldSeconds}, currentTime: ${this.currentTime}`) this.isCurrentTime = true; this.currentTime = this.stringForTime(progress.currentPosition); this.isCurrentTime = false; console.log(`Heanup 更新后 - oldSeconds: ${this.oldSeconds}, currentTime: ${this.currentTime}`) } updateLastPlayTimeStr(filePath: string) { let lastPlayTime = DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss') this.table.updateLastPlayedStrByFilePath(filePath, lastPlayTime, (success: boolean, error?: string) => { if (success) { this.getHistoryList(false) console.log(" onecold 更新最近播放时间成功,数据库已同步"); } else { console.error(" onecold 更新最近播放时间数据库失败原因: " + error); } }); } private completionNum(num: number): string | number { if (num < 10) { return '0' + num; } else { return num; } } // 纯粹的时间格式化函数,不包含任何业务逻辑 private stringForTime(timeMs: number): string { let totalSeconds: number = Math.floor(timeMs / 1000); let seconds: number = totalSeconds % 60; let minutes: number = Math.floor(totalSeconds / 60) % 60; let hours: number = Math.floor(totalSeconds / 3600); // 防抖逻辑:只在当前播放时间更新时生效 if (this.isCurrentTime) { // 如果歌曲刚刚切换,强制重置并使用新时间 if (this.justSwitched) { this.oldSeconds = seconds; // 直接使用新时间 this.justSwitched = false; // 重置标志 this.lastSwitchTime = Date.now(); // 记录切换时间 } else if (this.isSeekTo) { // 如果是拖动进度条,直接使用新时间 this.oldSeconds = seconds; } else { // 正常播放时的防抖逻辑 // 检查是否在切换后的冷却期内(切换后5秒内允许更大的时间跳跃) const timeSinceSwitch = this.lastSwitchTime ? Date.now() - this.lastSwitchTime : Number.MAX_VALUE; const isInCooldown = timeSinceSwitch < 5000; // 5秒冷却期 // 如果时间差值过大,根据情况处理 const timeDiff = Math.abs(seconds - this.oldSeconds); if (timeDiff > 10) { // 如果在冷却期内,允许更大的时间跳跃(可能是新歌曲开始播放) if (isInCooldown) { this.oldSeconds = seconds; } else { // 不在冷却期内,可能是异常情况,记录但使用新时间 this.oldSeconds = seconds; } } else if (this.oldSeconds <= seconds || seconds === 0) { // 正常时间递进或重置为0 this.oldSeconds = seconds; } else { // 时间倒退,使用防抖 seconds = this.oldSeconds; } } } const hoursStr = this.completionNum(hours); const minutesStr = this.completionNum(minutes); const secondsStr = this.completionNum(seconds); if (hours > 0) { return `${hoursStr}:${minutesStr}:${secondsStr}`; } else { return `${minutesStr}:${secondsStr}`; } } private setProgress() { let ijkPlayer=this.unifiedPlayerService.getIjkPlayer() as IjkMediaPlayer; let position = ijkPlayer.getCurrentPosition(); let duration = ijkPlayer.getDuration(); let pos = 0; if (duration > 0) { this.slideEnable = true; let curPercent = position / duration; pos = curPercent * 100; if (pos > this.PROGRESS_MAX_VALUE) { this.progressValue = this.PROGRESS_MAX_VALUE } else { this.progressValue = pos; } } // LogUtils.getInstance() // .LOGI("setProgress position:" + position + ",duration:" + duration + ",progressValue:" + pos); this.totalTime = this.stringForTime(duration); if (position > duration) { position = duration; } this.isCurrentTime = true; this.lyricController.updatePosition(position + this.timeOffset * 1000) this.lyricControllerXF.updatePosition(position + this.timeOffset * 1000) this.lyricControllerSingle.updatePosition(position + this.timeOffset * 1000) this.currentTime = this.stringForTime(position); this.isCurrentTime = false if (ijkPlayer.isPlaying()) { this.randomColor = `rgb(${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)})` // 判断是否播放到结束时间 if (this.isOpenJump && duration > this.jumpEndTime * 1000) { if (position >= duration - this.jumpEndTime * 1000) { this.playNext() this.broadcastProgressIfNeeded(); } } } else { this.randomColor = 'rbg(0,0,0)' } // try { // // 优先从UnifiedPlayerService获取进度信息 // const currentState = this.unifiedPlayerService.getCurrentState(); // let position = currentState.currentPosition; // let duration = currentState.duration; // // // 检测歌曲切换并重置播放位置 // const currentSongPath = this.currentSong?.filePath || ""; // if (this.lastSongPath !== currentSongPath && currentSongPath !== "") { // console.log(`Heanup setProgress检测到歌曲切换,重置position: ${position} -> 0`); // this.oldSeconds = 0; // this.currentTime = "00:00"; // this.lastSongPath = currentSongPath; // position = 0; // 强制重置播放位置 // } // // let pos = 0; // if (duration > 0) { // this.slideEnable = true; // let curPercent = position / duration; // pos = curPercent * 100; // if (pos > this.PROGRESS_MAX_VALUE) { // this.progressValue = this.PROGRESS_MAX_VALUE // } else { // this.progressValue = pos; // } // } // // this.totalTime = this.stringForTime(duration); // if (position > duration) { // position = duration; // } // // // // 更新歌词位置 // const lyricPosition = position + this.timeOffset * 1000; // this.lyricController.updatePosition(lyricPosition); // this.lyricControllerXF.updatePosition(lyricPosition); // this.lyricControllerSingle.updatePosition(lyricPosition); // // this.isCurrentTime = true; // this.currentTime = this.stringForTime(position); // this.isCurrentTime = false; // // // 检查播放状态以更新随机颜色和跳过逻辑 // const isPlaying = currentState.isPlaying; // if (isPlaying) { // this.randomColor = // `rgb(${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)})` // // // 判断是否播放到结束时间 // if (this.isOpenJump && duration > this.jumpEndTime * 1000) { // if (position >= duration - this.jumpEndTime * 1000) { // this.playNext() // } // } // } else { // this.randomColor = 'rbg(0,0,0)' // } // // // 节流广播进度更新到卡片 // this.broadcastProgressIfNeeded(); // } catch (error) { // LogUtils.getInstance().LOGI(`LocalMusic setProgress error: ${error}`); // } } private startProgressTask() { let that = this; this.updateProgressTimer = setInterval(() => { //LogUtils.getInstance().LOGI("startProgressTask"); if (!that.mDestroyPage) { that.setProgress(); } }, 300); } private stopProgressTask() { LogUtils.getInstance().LOGI("stopProgressTask"); clearInterval(this.updateProgressTimer); } private showLoadIng() { this.loadingVisible = Visibility.Visible; this.replayVisible = Visibility.None; } // 原来的 play 方法已移除,播放逻辑现在由 UnifiedPlayerService 统一处理 // private async play(url: string) { ... } 已移除 // 所有的播放器配置和监听器设置已移至 UnifiedPlayerService //保存最后播放的那首歌和已经对应的播放列表 saveLastPlayList() { PreferencesUtil.putSync('LastMusicInfo', this.currentSong) PreferencesUtil.putSync('LastMusicList', this.songList) } // 清空播放记录 clearVideoHistory() { this.table.clearPlayHistory((success: boolean, error?: string) => { if (success) { this.historyList = []; this.updateListData(this.historyList) // PreferencesUtil.putSync(LocalMusic.HISTORY_MUSIC, this.historyList) ToastUtil.showToast('清空播放记录成功') } else { console.error(" onecold 清空播放记录数据库失败原因: " + error); } }); } setLoopMode() { let itype = this.playType + 1 if (itype >= 4) { this.setPlayModeViaService(0); ToastUtil.showToast('连续播放') } else if (itype === 1) { this.setPlayModeViaService(1); ToastUtil.showToast('单曲循环') } else if (itype === 2) { this.setPlayModeViaService(2); ToastUtil.showToast('单曲播完') } else if (itype === 3) { this.setPlayModeViaService(3); ToastUtil.showToast('随机播放') } else if (itype === 0) { this.setPlayModeViaService(0); ToastUtil.showToast('连续播放') } this.setCurrentPlayMode() } public async setAvSessionListener() { if (!this.avSessionController) { console.log('heanup setAvSessionListener error: avSessionController is null'); return; } // 注意:基础播控监听器(play, pause, playNext, playPrevious, seek等) // 已经在UnifiedPlayerService中设置,这里不再重复设置,避免冲突 // 只设置LocalMusic特有的监听器 this.avSessionController.getAvSession()?.on('fastForward', this.sessionFastForwardCallback); this.avSessionController.getAvSession()?.on('rewind', this.sessionRewindCallback); this.avSessionController.getAvSession()?.on('setLoopMode', this.sessionSetLoopModeCallback); this.avSessionController.getAvSession()?.on('toggleFavorite', this.sessionToggleFavoriteCallback); // 设置投播设备变化监听器(这是投播功能的核心) this.avSessionController.getAvSession()?.on('outputDeviceChange', this.sessionOutputDeviceChange); console.log('heanup setAvSessionListener 完成 - 已跳过基础播控监听器(由UnifiedPlayerService处理)'); // 检查当前是否已经在投播状态 await this.checkAndRestoreCastingState(); } private sessionSetLoopModeCallback = (mode: number) => { ToastUtil.showToast('当前播放模式:' + mode) Logger.info('onecold 当前播放模式= ' + mode) let hmPlayMode = mode + 1 if (hmPlayMode >= 4) { hmPlayMode = 0 } if (hmPlayMode === 1) { this.setPlayModeViaService(1); ToastUtil.showToast('单曲循环') } else if (hmPlayMode === 0) { this.setPlayModeViaService(2); ToastUtil.showToast('单曲播完') } else if (hmPlayMode === 3) { this.setPlayModeViaService(3); ToastUtil.showToast('随机播放') } else if (hmPlayMode === 2) { this.setPlayModeViaService(0); ToastUtil.showToast('连续播放') } PreferencesUtil.putSync('musicPlayType', this.playType) // 应用收到设置循环模式的指令后,应用自定下一个模式,切换完毕后通过AVPlaybackState上报切换后的LoopMode。 let playBackState: avSession.AVPlaybackState = { loopMode: hmPlayMode, }; this.avSessionController.getAvSession()?.setAVPlaybackState(playBackState).then(() => { console.info(`set setLoopMode AVPlaybackState successfully`); }).catch((err: BusinessError) => { console.error(`Failed to setLoopMode set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); }); } setCurrentPlayMode() { if (!this.avSessionController) { return; } let mLoopMode = 0 switch (this.playType) { case 0: mLoopMode = avSession.LoopMode.LOOP_MODE_LIST //连续播放 break; case 1: mLoopMode = avSession.LoopMode.LOOP_MODE_SINGLE //单曲 break; case 2: mLoopMode = avSession.LoopMode.LOOP_MODE_SEQUENCE //正常播放 break; case 3: mLoopMode = avSession.LoopMode.LOOP_MODE_SHUFFLE //随机播放 break; } // 应用启动时/内部切换循环模式,需要把应用内的当前的循环模式设置给AVSession。 let playBState: avSession.AVPlaybackState = { loopMode: mLoopMode, isFavorite: Utility.getIsFav(this.favList, this.currentSong), }; this.avSessionController.getAvSession()?.setAVPlaybackState(playBState).then(() => { console.info(`set setLoopMode AVPlaybackState successfully`); }).catch((err: BusinessError) => { console.error(`Failed to setLoopMode set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); }); } private sessionToggleFavoriteCallback = (assetId: string) => { if (this.currentSong) { console.info(`on toggleFavorite - 播控中心收藏按钮被点击`); // 应用收到收藏命令,进行收藏处理。 // 注意:这里不需要立即更新AVSession状态,因为doFav方法内部会处理状态更新 this.doFav(this.currentSong); } } private sessionOutputDeviceChange = async (connectState: avSession.ConnectionState, device: avSession.OutputDeviceInfo) => { let currentDevice: avSession.DeviceInfo = device?.devices?.[0]; LogUtils.getInstance().LOGI(`投播设备状态变化: ${connectState}, 设备类型: ${currentDevice?.castCategory}, 协议: ${currentDevice?.supportedProtocols}`); if (currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_CONNECTED) { // 设备连接成功,开始投播 this.currentCastDevice = currentDevice; await this.startCasting(); // 检查设备支持的协议类型 if (currentDevice.supportedProtocols === avSession.ProtocolType.TYPE_CAST_PLUS_STREAM) { LogUtils.getInstance().LOGI('设备支持Cast+投播协议'); } else if (currentDevice.supportedProtocols === avSession.ProtocolType.TYPE_DLNA) { LogUtils.getInstance().LOGI('设备支持DLNA投播协议'); } // 检查DRM支持能力 if (currentDevice.supportedDrmCapabilities?.includes('3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c')) { LogUtils.getInstance().LOGI('设备支持chinaDRM'); // 监听许可证请求事件 this.castController?.on('keyRequest', this.keyRequestCallback); } } else if ((currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_DISCONNECTED) || (currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_LOCAL && connectState === avSession.ConnectionState.STATE_CONNECTED)) { // 设备断开或切换回本地 if (device) { this.endCasting(); this.currentCastDevice = undefined; } } }; private async startCasting() { try { LogUtils.getInstance().LOGI('开始投播流程'); // 获取投播控制器 this.castController = await this.avSessionController.getAvSession()?.getAVCastController(); if (!this.castController) { LogUtils.getInstance().LOGI('获取投播控制器失败'); ToastUtil.showToast('投播初始化失败'); return; } LogUtils.getInstance().LOGI('投播控制器获取成功'); // 设置投播状态监听 this.setPlaybackStateChangeListener(); // 暂停本地播放 this.pause(); // 初始化投播资源 this.initQueueItem(); // 准备并开始投播 await this.prepare(); // 更新投播状态 this.isCasting = true; this.isCastPlaying = true; this.castSeek = true; // 同步到AppStorage let refToIsCasting: AbstractProperty | undefined = AppStorage.ref('isCasting'); refToIsCasting?.set(true); // 申请长时任务,避免投播时应用被冻结 this.startCastingBackgroundTask(); LogUtils.getInstance().LOGI('投播启动成功'); ToastUtil.showToast('投播已开始'); } catch (error) { LogUtils.getInstance().LOGI(`投播启动失败: ${error}`); ToastUtil.showToast(`投播启动失败: ${error}`); this.endCasting(); } } async changeCasting() { try { LogUtils.getInstance().LOGI('投播资源切换开始'); // 确保投播控制器可用 if (!this.castController) { this.castController = await this.avSessionController.getAvSession()?.getAVCastController(); } if (!this.castController) { LogUtils.getInstance().LOGI('投播控制器不可用,无法切换资源'); return; } // 初始化新的投播资源 this.initQueueItem(); // 准备并开始播放新资源 await this.prepare(); // 确保监听器已设置 this.setPlaybackStateChangeListener(); // 更新播放状态 this.isCastPlaying = true; this.castSeek = true; LogUtils.getInstance().LOGI(`投播资源切换成功: ${this.currentSong?.name}`); } catch (error) { LogUtils.getInstance().LOGI(`投播资源切换失败: ${error}`); ToastUtil.showToast(`切换投播资源失败: ${error}`); } } public endCasting() { try { LogUtils.getInstance().LOGI('结束投播流程'); // 停止投播 this.avSessionController.getAvSession()?.stopCasting(); // 清理投播控制器监听 this.stopCast(); // 更新投播状态 this.isCasting = false; this.isCastPlaying = false; this.castSeek = false; // 同步到AppStorage let refToIsCasting: AbstractProperty | undefined = AppStorage.ref('isCasting'); refToIsCasting?.set(false); // 停止长时任务 this.stopCastingBackgroundTask(); // 恢复本地播放(可选) // 建议保存投播断开时的进度,用于本地播放继续 const lastCastPosition = this.currentTime2 * 1000; // 投播的当前进度 // 暂停本地播放器,让用户手动选择是否继续播放 this.pause(); this.setIsPlaying(false); // 如果需要自动恢复本地播放,可以取消注释以下代码 // if (lastCastPosition > 0) { // this.seekTo(lastCastPosition.toString()); // setTimeout(() => { // this.startPlayOrResumePlay(); // }, 500); // } LogUtils.getInstance().LOGI('投播结束成功'); ToastUtil.showToast('投播已结束'); } catch (error) { LogUtils.getInstance().LOGI(`结束投播失败: ${error}`); ToastUtil.showToast(`结束投播失败: ${error}`); } } public setIsPlaying(isPlayer: boolean) { this.isPlaying = isPlayer; } private setPlaybackStateChangeListener(): void { if (!this.castController) { LogUtils.getInstance().LOGI('投播控制器为空,无法设置监听器'); return; } try { // 监听播放状态变化 this.castController.on('playbackStateChange', 'all', this.playbackStateChangeListener); // 监听播放完毕事件 this.castController.on('endOfStream', this.reloadCasting); // 监听播控中心的上下一首切换 this.castController.on('playNext', this.playNextCallback); this.castController.on('playPrevious', this.playPreviousCallback); // 监听进度调节完成事件 this.castController.on('seekDone', this.seekDoneCallback); // 监听错误事件 this.castController.on('error', this.castErrorCallback); LogUtils.getInstance().LOGI('投播状态监听器设置完成'); } catch (error) { LogUtils.getInstance().LOGI(`设置投播监听器失败: ${error}`); } } private updateSessionPlayState(isPlay: boolean): void { // if(!this.isBgPlayOpen) // return Logger.info(TAG, `updateIsPlay isPlay: ${isPlay}`); try { // 通过UnifiedPlayerService获取当前位置 const currentState = this.unifiedPlayerService.getCurrentState(); this.setAVSessionPlayState({ state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: currentState.currentPosition, updateTime: new Date().getTime() } }); } catch (error) { LogUtils.getInstance().LOGI(`updateSessionPlayState error: ${error}`); // 如果获取失败,使用默认值 this.setAVSessionPlayState({ state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: 0, updateTime: new Date().getTime() } }); } } private setAVSessionPlayState(playbackState: avSession.AVPlaybackState): void { this.avSessionController.setAvSessionPlayState(playbackState) } private positionChange(position: number) { this.currentTime2 = position / 1000; this.currentStringTime = secondToTime(Math.floor(position / 1000)); } private playDurationChange(duration: number) { this.duration = duration; this.durationTime = Math.floor(this.duration / 1000); this.durationStringTime = secondToTime((this.durationTime)); } private playbackStateChangeListener = (playbackState: avSession.AVPlaybackState) => { try { LogUtils.getInstance().LOGI(`投播状态变化: ${playbackState.state}, 进度: ${playbackState?.position?.elapsedTime}ms`); // 处理播放时长变化 const duration = playbackState?.extras?.duration; if (typeof duration === 'number') { this.playDurationChange(duration as number); } // 处理播放进度变化 if (typeof playbackState?.position?.elapsedTime !== 'undefined') { this.positionChange(playbackState?.position?.elapsedTime); } // 处理播放状态变化 switch (playbackState.state) { case avSession.PlaybackState.PLAYBACK_STATE_PLAY: this.isCastPlaying = true; this.CONTROL_PlayStatus = PlayStatus.PLAY; this.setIsPlaying(true); break; case avSession.PlaybackState.PLAYBACK_STATE_PAUSE: this.isCastPlaying = false; this.CONTROL_PlayStatus = PlayStatus.PAUSE; this.setIsPlaying(false); break; case avSession.PlaybackState.PLAYBACK_STATE_STOP: case avSession.PlaybackState.PLAYBACK_STATE_PREPARE: case avSession.PlaybackState.PLAYBACK_STATE_INITIAL: this.isCastPlaying = false; this.CONTROL_PlayStatus = PlayStatus.INIT; this.setIsPlaying(false); break; default: LogUtils.getInstance().LOGI(`未处理的投播状态: ${playbackState.state}`); break; } // 处理音量变化 if (playbackState?.volume !== undefined) { LogUtils.getInstance().LOGI(`投播音量变化: ${playbackState.volume}`); // 可以在这里同步音量到UI } // 更新UI显示 this.playChange(); } catch (error) { LogUtils.getInstance().LOGI(`处理投播状态变化失败: ${error}`); } }; private reloadCasting = async () => { try { LogUtils.getInstance().LOGI('投播内容播放完毕,处理后续操作'); // 根据播放模式决定后续操作 switch (this.playType) { case 0: // 顺序播放 if (this.curIndex < this.songList.length - 1) { // 有下一首,自动切换 LogUtils.getInstance().LOGI('自动播放下一首'); this.playNext(); } else { // 没有下一首,停止投播或循环播放第一首 LogUtils.getInstance().LOGI('播放列表结束,停止投播'); this.endCasting(); } break; case 1: // 单曲循环 LogUtils.getInstance().LOGI('单曲循环,重新播放当前歌曲'); this.initQueueItem(); await this.prepare(); this.isCastPlaying = true; break; case 2: // 列表循环 if (this.curIndex < this.songList.length - 1) { // 播放下一首 this.playNext(); } else { // 回到第一首 this.curIndex = 0; this.currentSong = this.songList[0]; this.initQueueItem(); await this.prepare(); this.isCastPlaying = true; } break; case 3: // 随机播放 LogUtils.getInstance().LOGI('随机播放下一首'); this.playNext(); break; default: LogUtils.getInstance().LOGI('未知播放模式,停止投播'); this.endCasting(); break; } } catch (error) { LogUtils.getInstance().LOGI(`处理投播完毕事件失败: ${error}`); ToastUtil.showToast(`投播切换失败: ${error}`); } }; public playNextCallback = (): void => { LogUtils.getInstance().LOGI('投播设备请求播放下一首'); this.playNext(); }; public playPreviousCallback = (): void => { LogUtils.getInstance().LOGI('投播设备请求播放上一首'); this.playPrevious(); }; // 进度调节完成回调 private seekDoneCallback = (position: number): void => { LogUtils.getInstance().LOGI(`投播进度调节完成: ${position}ms`); // 更新本地进度显示 this.positionChange(position); }; // 投播错误回调 private castErrorCallback = (error: BusinessError): void => { LogUtils.getInstance().LOGI(`投播错误: ${error.message || error}`); ToastUtil.showToast(`投播错误: ${error.message || '未知错误'}`); // 可以选择自动结束投播或重试 // this.endCasting(); }; // DRM许可证请求回调 private keyRequestCallback: avSession.KeyRequestCallback = async (assetId: string, requestData: Uint8Array) => { try { LogUtils.getInstance().LOGI(`DRM许可证请求: ${assetId}`); // 根据assetId获取对应的DRM url(需要根据实际情况配置) let drmUrl: string = 'http://license.xxx.xxx.com:8080/drmproxy/getLicense'; // 从服务器获取许可证 let licenseResponseData = await this.getLicense(drmUrl, requestData); if (licenseResponseData) { // 处理DRM许可证响应 await this.castController?.processMediaKeyResponse(assetId, licenseResponseData); LogUtils.getInstance().LOGI('DRM许可证处理成功'); } else { LogUtils.getInstance().LOGI('获取DRM许可证失败'); ToastUtil.showToast('DRM许可证获取失败'); } } catch (error) { LogUtils.getInstance().LOGI(`DRM许可证处理失败: ${error}`); ToastUtil.showToast(`DRM许可证处理失败: ${error}`); } }; async prepare() { try { if (!this.castController || !this.castItem) { LogUtils.getInstance().LOGI('投播控制器或资源项为空,无法准备播放'); return; } LogUtils.getInstance().LOGI('准备投播资源...'); // 准备播放,进行资源加载和缓冲,不会触发真正的播放 await this.castController.prepare(this.castItem); LogUtils.getInstance().LOGI('投播资源准备完成'); // 启动播放,真正触发对端播放 await this.castController.start(this.castItem); LogUtils.getInstance().LOGI('投播播放启动成功'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); LogUtils.getInstance().LOGI(`投播准备或启动失败: ${errorMessage}`); ToastUtil.showToast(`投播播放失败: ${errorMessage}`); throw new Error(errorMessage); } } public initQueueItem() { try { let item = this.songList[this.curIndex]; if (!item) { LogUtils.getInstance().LOGI('当前歌曲信息为空,无法初始化投播资源'); return; } LogUtils.getInstance().LOGI(`初始化投播资源: ${item.name}`); let uri = fileUri.getUriFromPath(item.filePath); let file = fs.openSync(uri, fs.OpenMode.READ_ONLY); // 获取当前播放进度,用于投播时同步进度 const currentPosition = this.unifiedPlayerService.getCurrentPosition(); this.castItem = { itemId: this.curIndex, description: { assetId: item.filePath, title: item.name, artist: item.artist || this.UNKONWN, subtitle: item.album || '', // 本地资源投播,使用文件描述符 fdSrc: { fd: file.fd }, // 音频资源类型 mediaType: 'AUDIO', mediaSize: item.videoSize, // 同步当前播放进度到远端 startPosition: currentPosition, // 同步播放时长到远端显示 duration: this.duration, // 专辑封面(如果有) albumCoverUri: item.pixelMapPath, albumTitle: item.album || '', appName: this.appName, // 如果是DRM资源,需要配置支持的DRM类型 // drmScheme: '3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c', } }; LogUtils.getInstance().LOGI(`投播资源初始化完成 - 标题: ${item.name}, 进度: ${currentPosition}ms, 时长: ${this.duration}ms`); } catch (error) { LogUtils.getInstance().LOGI(`初始化投播资源失败: ${error}`); ToastUtil.showToast(`初始化投播资源失败: ${error}`); } } async stopCast() { try { LogUtils.getInstance().LOGI('清理投播监听器'); if (this.castController) { // 移除所有投播监听器 this.castController.off('playbackStateChange'); this.castController.off('endOfStream'); this.castController.off('playNext'); this.castController.off('playPrevious'); this.castController.off('seekDone'); this.castController.off('error'); this.castController.off('keyRequest'); LogUtils.getInstance().LOGI('投播监听器清理完成'); } // 清空投播控制器引用 this.castController = undefined; this.castItem = undefined; } catch (error) { LogUtils.getInstance().LOGI(`清理投播监听器失败: ${error}`); } } private async playOrPause() { if (!this.debounce()) { return; } // 如果正在投播,使用投播控制 if (this.isCurrentlyCasting()) { await this.handleCastingPlayPause(); return; } // 本地播放控制 if (this.CONTROL_PlayStatus === PlayStatus.PLAY) { this.pause(); } else { this.startPlayOrResumePlay(); this.playChange() } } private sessionFastForwardCallback = (time?: number) => { if (!time) { return; } try { // 通过UnifiedPlayerService获取当前位置 const currentState = this.unifiedPlayerService.getCurrentState(); const curPosition = currentState.currentPosition; let seeTime = curPosition + time * 1000; // 限制 seekValue 在合法范围内 if (seeTime < 0) { seeTime = 0; } else if (seeTime > currentState.duration) { seeTime = currentState.duration; } Logger.info('onecold seeTime= ' + seeTime) this.setSeekToActionProgress(seeTime) this.seekTo(seeTime + "") } catch (error) { LogUtils.getInstance().LOGI(`sessionFastForwardCallback error: ${error}`); } }; /** * Gesture method onActionUpdate. * * @param event Gesture event.手势拖动设置快进和后退 */ private setSeekToActionProgress(position: number) { // let position = this.mIjkMediaPlayer.getCurrentPosition(); let duration: number = this.unifiedPlayerService.getCurrentPosition(); let pos = 0; if (duration > 0) { this.slideEnable = true; let curPercent = position / duration; pos = curPercent * 100; if (pos > this.PROGRESS_MAX_VALUE) { this.progressValue = this.PROGRESS_MAX_VALUE } else { this.progressValue = pos; } } // LogUtils.getInstance() // .LOGI("setProgress position:" + position + ",duration:" + duration + ",progressValue:" + pos); this.totalTime = this.stringForTime(duration); if (position > duration) { position = duration; } this.isCurrentTime = true; this.currentTime = this.stringForTime(position); this.isCurrentTime = false; } private sessionRewindCallback = (time?: number) => { if (!time) { return; } const curPosition: number = this.unifiedPlayerService.getCurrentPosition() let seeTime = curPosition - time * 1000; // 限制 seekValue 在合法范围内 if (seeTime < 0) { seeTime = 0; } else if (seeTime > this.duration) { seeTime = this.duration; } Logger.info('onecold seeTime= ' + seeTime) this.setSeekToActionProgress(seeTime) this.seekTo(seeTime + "") }; private sessionSeekCallback = (seekTime: number) => { LogUtils.getInstance().LOGI(`播控中心请求进度调节: ${seekTime}ms`); this.seekTo(seekTime + ""); }; // 注意:基础播控回调方法(play, pause, playNext, playPrevious, seek) // 已经在UnifiedPlayerService中实现,这里不再重复定义,避免冲突 private async getFileSize(filePath: string): Promise { try { const stat = await fs.stat(filePath); return stat.size; } catch (err) { console.error('Failed to get file size: ' + err.message); return 0; } } // 启动投播长时任务 private startCastingBackgroundTask(): void { try { // 这里应该调用BackgroundTaskManager来申请长时任务 // 避免应用在投播时进入后台被系统冻结 LogUtils.getInstance().LOGI('申请投播长时任务'); // BackgroundTaskManager.startContinuousTask(this.context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK); } catch (error) { LogUtils.getInstance().LOGI(`申请投播长时任务失败: ${error}`); } } // 停止投播长时任务 private stopCastingBackgroundTask(): void { try { LogUtils.getInstance().LOGI('停止投播长时任务'); // BackgroundTaskManager.stopContinuousTask(this.context); } catch (error) { LogUtils.getInstance().LOGI(`停止投播长时任务失败: ${error}`); } } // 获取DRM许可证(示例实现,需要根据实际服务配置) private async getLicense(drmUrl: string, requestData: Uint8Array): Promise { try { // 这里需要根据实际的DRM服务实现许可证获取逻辑 LogUtils.getInstance().LOGI(`请求DRM许可证: ${drmUrl}`); // 示例:使用HTTP请求获取许可证 // const response = await fetch(drmUrl, { // method: 'POST', // headers: { // 'Content-Type': 'application/octet-stream' // }, // body: requestData // }); // // if (response.ok) { // const licenseData = await response.arrayBuffer(); // return new Uint8Array(licenseData); // } LogUtils.getInstance().LOGI('DRM许可证获取功能需要根据实际服务配置'); return undefined; } catch (error) { LogUtils.getInstance().LOGI(`获取DRM许可证失败: ${error}`); return undefined; } } // 检查当前是否正在投播 public isCurrentlyCasting(): boolean { return this.isCasting && this.castController !== undefined; } // 获取当前投播设备信息 public getCurrentCastDevice(): avSession.DeviceInfo | undefined { return this.currentCastDevice; } // 投播控制命令 public async sendCastControlCommand(command: string, parameter?: number): Promise { if (!this.castController) { LogUtils.getInstance().LOGI('投播控制器不可用'); return; } try { let avCommand: avSession.AVCastControlCommand = { command: command as avSession.AVCastControlCommandType }; if (parameter !== undefined) { avCommand.parameter = parameter; } await this.castController.sendControlCommand(avCommand); LogUtils.getInstance().LOGI(`投播控制命令发送成功: ${command}`); } catch (error) { LogUtils.getInstance().LOGI(`发送投播控制命令失败: ${error}`); ToastUtil.showToast(`投播控制失败: ${error}`); } } private async pause() { if (this.unifiedPlayerService.getIjkPlayer()?.isPlaying()){ this.savePlaybackPosition(); this.unifiedPlayerService.pause(); this.setProgress(); this.mDestroyPage = true; this.CONTROL_PlayStatus = PlayStatus.PAUSE; // 注意:AVSession状态更新由UnifiedPlayerService统一处理,避免冲突 this.playChange() if (this.pipController) { this.pipController.updatePiPControlStatus(PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE, PiPWindow.PiPControlStatus.PAUSE); } } // try { // // 使用UnifiedPlayerService暂停播放 // await this.unifiedPlayerService.pause(); // // // 保持原有的UI更新逻辑 // this.setProgress(); // this.mDestroyPage = true; // this.CONTROL_PlayStatus = PlayStatus.PAUSE; // this.setIsPlaying(false); // this.updateSessionPlayState(false); // this.playChange() // // if (this.pipController) { // this.pipController.updatePiPControlStatus(PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE, // PiPWindow.PiPControlStatus.PAUSE); // } // // LogUtils.getInstance().LOGI("LocalMusic: pause completed via UnifiedPlayerService"); // } catch (error) { // LogUtils.getInstance().LOGI(`LocalMusic pause error: ${error}`); // ToastUtil.showToast(`暂停失败: ${error}`); // } } private async stop() { try { // 使用UnifiedPlayerService停止播放 await this.unifiedPlayerService.stop(); // 保持原有的UI更新逻辑 - 重要:停止时清除所有播放状态 this.stopProgressTask(); this.CONTROL_PlayStatus = PlayStatus.INIT; // 初始状态,不是暂停 this.setIsPlaying(false); // 强制更新UI状态 this.playChange() this.watchStatus(); LogUtils.getInstance().LOGI("LocalMusic: stop completed - all states cleared to INIT"); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic stop error: ${error}`); ToastUtil.showToast(`停止播放失败: ${error}`); } } //保持记忆播放功能 private savePlaybackPosition() { try { // 通过UnifiedPlayerService获取当前播放位置 const currentState = this.unifiedPlayerService.getCurrentState(); const position = currentState.currentPosition; const duration = currentState.duration; const threshold = 5000; // 阈值,单位为毫秒(这里设为5秒) // 如果播放位置接近视频末尾,则保存 position 为 0 const playbackPosition = (duration - position < threshold) ? 0 : position; // 同时保存到PreferencesUtil和AppStorage,确保卡片也能访问 PreferencesUtil.putSync(this.videoUrl, playbackPosition); AppStorage.setOrCreate(`playback_${this.videoUrl}`, playbackPosition); LogUtils.getInstance().LOGI(`Saved playback position: ${playbackPosition}ms for ${this.name}`); } catch (error) { LogUtils.getInstance().LOGI(`savePlaybackPosition error: ${error}`); } } //获取记忆播放功能 private restorePlaybackPosition() { // 优先从AppStorage获取,如果没有则从PreferencesUtil获取 let position: number = AppStorage.get(`playback_${this.videoUrl}`) || 0; if (position === 0) { position = PreferencesUtil.getNumberSync(this.videoUrl, 0); } // ToastUtil.showToast('position = ' + position) if (position > 0) { // ToastUtil.showToast('seekTo = ' + position) this.seekTo(position + ""); LogUtils.getInstance().LOGI(`Restored playback position: ${position}ms for ${this.name}`); } } private async seekTo(value: string) { try { const position = parseInt(value); // 如果正在投播,使用投播进度控制 if (this.isCurrentlyCasting()) { await this.handleCastingSeek(position); return; } // 使用UnifiedPlayerService进行拖动 await this.unifiedPlayerService.seekTo(value); // 保持原有的UI更新逻辑 this.setProgress() LogUtils.getInstance().LOGI(`LocalMusic: seekTo ${value} completed via UnifiedPlayerService`); } catch (error) { LogUtils.getInstance().LOGI(`LocalMusic seekTo error: ${error}`); // 如果是WMA格式错误,显示特定提示 if (error.toString().includes('WMA format')) { ToastUtil.showToast('wma格式不支持拖动快进。'); return; } else { ToastUtil.showToast(`拖动失败: ${error}`); } } } // 添加到下一首播放 private addToNextPlay(song: VideoItem) { if (!this.debounce()) { return; } if (this.currentSong) { if (this.currentSong === song) { ToastUtil.showToast('当前已经在播放这首歌了!') return } } try { // 如果未开始播放或队列为空 if (this.curIndex === -1 || this.songList.length === 0) { this.songList = [song]; this.curIndex = 0; this.doPlay(song) return; } // 插入到当前索引+1的位置 const insertPos = this.curIndex + 1; // 使用UnifiedPlayerService添加到播放列表 this.unifiedPlayerService.addToPlaylist(song, insertPos); // 更新本地播放列表 this.songList.splice(insertPos, 0, song); this.songList = [...this.songList]; // 触发状态更新 this.sonDataSource.pushArrayData(this.songList); ToastUtil.showToast('已添加至下一首播放') } catch (error) { // 错误处理:回退到原有逻辑 const insertPos = this.curIndex + 1; this.songList.splice(insertPos, 0, song); this.songList = [...this.songList]; this.sonDataSource.pushArrayData(this.songList); ToastUtil.showToast('已添加至下一首播放') } } //下一个 private async playNext() { if (!this.debounce()) { return; } try { // 移除了随机播放的特殊处理,统一使用UnifiedPlayerService // 这样可以确保所有播放模式的状态都保持一致 if (ArrayUtil.isNotEmpty(this.songList)) { // 如果正在投播,需要特殊处理 if (this.isCurrentlyCasting()) { await this.handleCastingPlayNext(); return; } // 使用UnifiedPlayerService播放下一首,支持所有播放模式(包括随机播放) await this.unifiedPlayerService.playNext(); // 智能同步播放列表到UnifiedPlayerService const servicePlaylist = this.unifiedPlayerService.getPlaylist(); const serviceIndex = this.unifiedPlayerService.getCurrentIndex(); if (ArrayUtil.isEmpty(servicePlaylist)) { // 如果服务没有播放列表,使用LocalMusic的播放列表 this.unifiedPlayerService.setPlaylist(this.songList, this.curIndex); } else { // 如果服务已有播放列表,检查是否需要同步 const serviceSong = this.unifiedPlayerService.getCurrentSong(); if (serviceSong && serviceIndex >= 0 && serviceIndex < servicePlaylist.length) { // 同步服务的状态到LocalMusic,确保状态一致 this.songList = servicePlaylist; this.curIndex = serviceIndex; this.currentSong = serviceSong; this.sonDataSource.pushArrayData(this.songList); } } // 更新本地状态以保持UI同步 const currentIndex = this.unifiedPlayerService.getCurrentIndex(); const currentSong = this.unifiedPlayerService.getCurrentSong(); if (currentSong) { this.curIndex = currentIndex; this.currentSong = currentSong; this.videoUrl = currentSong.filePath; this.artist = currentSong.artist; this.name = currentSong.name; this.cover = currentSong.pixelMapPath; // 重置时间显示状态,防止新歌曲时间显示被上一首歌曲的时间"卡住" this.oldSeconds = 0; this.currentTime = "00:00"; this.lastSongPath = currentSong.filePath; this.justSwitched = true; // 标记歌曲刚刚切换 this.changeImageAnimation(); } } } catch (error) { ToastUtil.showToast(`切换下一首失败: ${error}`); // 错误处理:回退到原有逻辑 this.fallbackPlayNext(); } // 同步播放列表到UnifiedPlayerService this.syncPlaylistToService(); } // 回退到原有的playNext逻辑 private fallbackPlayNext() { if (ArrayUtil.isNotEmpty(this.songList)) { if (this.curIndex == this.songList.length - 1) { this.curIndex = 0; } else { this.curIndex++; } this.CONTROL_PlayStatus = PlayStatus.INIT; this.stop(); this.currentSong = this.songList[this.curIndex] this.videoUrl = this.songList[this.curIndex].filePath; this.artist = this.songList[this.curIndex].artist this.name = this.songList[this.curIndex].name this.changeImageAnimation() } } changeImageAnimation() { if(this.openSkipSongAnimate){ // 第一步:淡出动画 this.getUIContext()?.animateTo({ duration: 500, curve: Curve.EaseOut, }, () => { this.opacityValueImage = 0.1; if(this.isCoverRectangle){ this.scaleValueImage = 0.88 } }); // 第二步:切换图片后淡入 setTimeout(() => { this.cover = this.songList[this.curIndex].pixelMapPath this.getUIContext()?.animateTo({ duration: 500, curve: Curve.EaseIn, }, () => { this.opacityValueImage = 1; this.scaleValueImage = 1 // 图标恢复到原始大小 // 注意:不再手动调用 startPlayOrResumePlay(),因为 UnifiedPlayerService 已经处理了播放逻辑 }); }, 500); }else{ this.cover = this.songList[this.curIndex].pixelMapPath // 注意:不再手动调用 startPlayOrResumePlay(),因为 UnifiedPlayerService 已经处理了播放逻辑 } } // 存储已播放的歌曲索引 private playedIndices: Set = new Set(); //随机播放 private randomPlay() { // if (!this.debounce()) { // return; // } if (ArrayUtil.isNotEmpty(this.songList)) { if (this.songList.length > 3) { // 如果所有歌曲都播放过一次,清空已播放索引集合 if (this.playedIndices.size === this.songList.length) { this.playedIndices.clear(); } // 随机选择一个未播放的歌曲索引 let newIndex: number; // 添加超时机制防止无限循环 let attempt = 0; const maxAttempts = 100; // 根据需求调整 do { newIndex = RandomUtil.getRandomNumber(0, this.songList.length - 1); attempt++; } while (this.playedIndices.has(newIndex) && attempt < maxAttempts); if (attempt >= maxAttempts) { // 处理无法找到新索引的情况(如随机选择或按顺序播放) newIndex = Math.floor(Math.random() * this.songList.length); } this.curIndex = newIndex; this.playedIndices.add(newIndex); } else { // 当歌曲数量小于或等于3时,直接播放下一首 if (this.curIndex == this.songList.length - 1) { this.curIndex = 0; } else { this.curIndex++; } } this.CONTROL_PlayStatus = PlayStatus.INIT; this.stop(); this.currentSong = this.songList[this.curIndex]; this.videoUrl = this.songList[this.curIndex].filePath; this.name = this.songList[this.curIndex].name; this.artist = this.songList[this.curIndex].artist // this.cover = this.songList[this.curIndex].pixelMapPath this.changeImageAnimation() } } private async playIndex(index: number) { if (!this.debounce()) { return; } if (ArrayUtil.isNotEmpty(this.songList)) { try { // 使用UnifiedPlayerService播放指定索引的歌曲 await this.playSongAtIndexViaService(index); this.changeImageAnimation(); } catch (error) { // 错误处理:回退到原有逻辑 this.CONTROL_PlayStatus = PlayStatus.INIT; this.stop(); this.currentSong = this.songList[index] this.videoUrl = this.songList[index].filePath; this.name = this.songList[index].name this.artist = this.songList[index].artist this.curIndex = index; this.changeImageAnimation() } } } //上一个 private async playPrevious() { if (!this.debounce()) { return; } try { // 移除了随机播放的特殊处理,统一使用UnifiedPlayerService // 这样可以确保所有播放模式的状态都保持一致 if (ArrayUtil.isNotEmpty(this.songList)) { // 如果正在投播,需要特殊处理 if (this.isCurrentlyCasting()) { await this.handleCastingPlayPrevious(); return; } // 使用UnifiedPlayerService播放上一首,支持所有播放模式(包括随机播放) await this.unifiedPlayerService.playPrevious(); // 智能同步播放列表到UnifiedPlayerService const servicePlaylist = this.unifiedPlayerService.getPlaylist(); const serviceIndex = this.unifiedPlayerService.getCurrentIndex(); // 更新本地状态以保持UI同步 const currentIndex = this.unifiedPlayerService.getCurrentIndex(); const currentSong = this.unifiedPlayerService.getCurrentSong(); if (currentSong) { this.curIndex = currentIndex; this.currentSong = currentSong; this.videoUrl = currentSong.filePath; this.name = currentSong.name; this.artist = currentSong.artist; this.cover = currentSong.pixelMapPath; // 重置时间显示状态,防止新歌曲时间显示被上一首歌曲的时间"卡住" this.oldSeconds = 0; this.currentTime = "00:00"; this.lastSongPath = currentSong.filePath; this.justSwitched = true; // 标记歌曲刚刚切换 this.changeImageAnimation(); } } } catch (error) { ToastUtil.showToast(`切换上一首失败: ${error}`); // 错误处理:回退到原有逻辑 this.fallbackPlayPrevious(); } // 同步播放列表到UnifiedPlayerService this.syncPlaylistToService(); } // 回退到原有的playPrevious逻辑 private fallbackPlayPrevious() { if (this.curIndex == 0) { this.curIndex = this.songList.length - 1; } else { this.curIndex--; } this.CONTROL_PlayStatus = PlayStatus.INIT; this.stop(); this.currentSong = this.songList[this.curIndex] this.videoUrl = this.songList[this.curIndex].filePath; this.name = this.songList[this.curIndex].name this.artist = this.songList[this.curIndex].artist this.startPlayOrResumePlay() this.changeImageAnimation() } //随机播放模式下点击上一首: 上一首应该应该播放历史记录的第二首 private randomModePlayFromHistory() { if (this.historyList.length >= 2) { // 播放历史记录的第二首 const prevSong = this.historyList[1]; this.curIndex = this.songList.findIndex(song => song.filePath === prevSong.filePath); this.CONTROL_PlayStatus = PlayStatus.INIT; this.stop(); this.currentSong = prevSong; this.videoUrl = prevSong.filePath; this.cover = prevSong.pixelMapPath this.artist = prevSong.artist this.name = prevSong.name; this.startPlayOrResumePlay(); } else { // 如果历史记录不足两首,可根据需求处理,这里简单按普通逻辑处理 if (this.curIndex === 0) { this.curIndex = this.songList.length - 1; } else { this.curIndex--; } this.CONTROL_PlayStatus = PlayStatus.INIT; this.stop(); this.currentSong = this.songList[this.curIndex]; this.videoUrl = this.songList[this.curIndex].filePath; this.artist = this.songList[this.curIndex].artist this.name = this.songList[this.curIndex].name; this.changeImageAnimation() } } debounce() { const delay = 600; let cur = new Date().getTime(); if (cur - this.last > delay) { this.last = cur; return true; } return false; } /** * Sets whether the screen is a constant based on the playback status. */ watchStatus() { // let windowClass = GlobalContext.getContext().getObject('windowClass') as window.Window; if (this.windowClass) { if (this.CONTROL_PlayStatus === PlayStatus.PLAY) { this.windowClass.setWindowKeepScreenOn(true); } else { this.windowClass.setWindowKeepScreenOn(false); } } } /** * Volume gesture method onActionStart. * * @param event Gesture event. */ private customDialogComponentIdMM: number = 0 @Builder customDialogComponentMM() { Column() { Row() { Blank() Text('验证密码') .fontSize(22) .fontColor($r('app.color.pri_bg')) .margin({ top: 18 }) Blank() Image($r('app.media.close')) .fillColor(Color.White) .margin({ top: 18, right: 18 }) .width(20) .onClick(() => { promptAction.closeCustomDialog(this.customDialogComponentIdMM) }) } .width('100%') // 使Row占据整个宽度 .justifyContent(FlexAlign.SpaceBetween); // 在Row中创建空间 Verify({ onVerifyCompleted: this.handleVerifyCompleted.bind(this) }); }.height('60%') .backgroundColor($r('app.color.index_background')) .justifyContent(FlexAlign.SpaceBetween) } private handleVerifyCompleted(isSuccess: boolean) { if (isSuccess) { console.log('验证成功'); // 在这里处理验证成功后的逻辑 promptAction.closeCustomDialog(this.customDialogComponentIdMM) this.currentPath = this.lockPath this.getSortedFiles(this.currentPath) } else { // console.log('验证失败'); ToastUtil.showToast('验证失败') } } private showVerifyDialog() { promptAction.openCustomDialog({ transition: TransitionEffect.asymmetric( TransitionEffect.OPACITY.animation({ duration: 888 }).combine( TransitionEffect.translate({ y: 888 }).animation({ duration: 888 })) , TransitionEffect.OPACITY.animation({ delay: 888, duration: 888 }).combine( TransitionEffect.translate({ y: 888 }).animation({ duration: 888 })) ), builder: () => { this.customDialogComponentMM() }, isModal: true, autoCancel: true, maskColor: Color.Transparent, alignment: DialogAlignment.Bottom, onWillDismiss: (dismissDialogAction: DismissDialogAction) => { console.info("reason" + JSON.stringify(dismissDialogAction.reason)) console.log("dialog onWillDismiss") if (dismissDialogAction.reason == DismissReason.PRESS_BACK) { dismissDialogAction.dismiss() } if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) { dismissDialogAction.dismiss() } } }).then((dialogId: number) => { this.customDialogComponentIdMM = dialogId }) } //左滑操作 移动文件 重命名 删除等操作 @Builder DeleteButton(item: VideoItem, index: number, filePath: string) { Row() { //加入收藏 Button() { Image(Utility.getIsFav(this.favList, item) ? $r('app.media.add_fac_light2') : $r('app.media.add_fac')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { if (item) { this.doFav(item) } }) //加入下一首播放 Button() { Image($r('app.media.next_to_play')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { this.addToNextPlay(item); }) //复制文件 Button() { Image($r('app.media.copy')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .id(filePath + 'copy') .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { this.showCopyDialog(false, item, index, filePath + 'copy'); }) //移动文件 Button() { Image($r('app.media.cut')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .id(filePath) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { this.showCutDialog(false, item, index, filePath); }) Button() { Image($r('app.media.cut_current')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .id(filePath + 'current') .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : (this.isHasDir ? Visibility.Visible : Visibility.None)) .onClick(() => { this.showCutDialog(true, item, index, filePath + 'current'); }) //重命名 Button() { Image($r('app.media.rename2')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC ? Visibility.None : Visibility.Visible) : Visibility.Visible) .margin(5) .onClick(() => { this.showReNameDialog(item, index + '', filePath) }) Button() { Image($r('app.media.delete2')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC ? Visibility.None : Visibility.Visible) : Visibility.Visible) .margin(5) .onClick(() => { this.showWarnIsDeleteFile(item, index + '', filePath) }) Button() { Image($r('app.media.share2')) .fillColor(Color.White) .width(20) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor(this.themeColor) .margin(5) .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible) .onClick(() => { Utility.doShareMusic(item, getContext(this) as common.UIAbilityContext) }) } } /** * 穿山甲banner广告 */ // private declare bannerAd: CSJNativeExpressAd; // @State private status: string = "未加载" // @State private isShowAd: boolean = false // private declare mAdSlot: AdSlot; // private mBiddingAdm = '' //服务端bidding才需要设置 // private expressLoadAdListener: NativeExpressAdListener = { // /** // * 加载失败的回调 // * // * @param code // * @param message // */ // onError: (code: number, message: string) => { // console.error("加载广告失败,code=" + code + ",message=" + message); // this.status = "加载广告失败,code=" + code + ",message=" + message; // }, // // /** // * 广告加载成功的回调,接入方可以在这个回调中进行渲染 // * // * @param ads 返回的广告列表 // */ // // onNativeExpressAdLoad: (ads: ArrayList) => { // console.log("BannerExpressAdPage==onNativeExpressAdLoad......success"); // if (ads && ads.length > 0) { // ads.forEach((ad: CSJNativeExpressAd, idx: number) => { // ad.setExpressInteractionListener({ // /** // *广告的点击回调 // * @param type 广告的交互类型 // */ // onAdClicked: (type: number) => { // console.log("BannerExpressAdPage==onAdClicked......"); // }, // // /** // * 广告的展示回调 每个广告仅回调一次 // * @param type 广告的交互类型 // */ // onAdShow: (type: number) => { // console.log("BannerExpressAdPage==onAdShow......"); // }, // // /** // * 模板渲染失败 // */ // onRenderFail: (code: number, msg: string) => { // console.log("BannerExpressAdPage==onRenderFail...code=" + code + ",msg=" + msg); // }, // // /** // * 模板渲染成功 // * @param width 返回view的宽 单位 vp // * @param height 返回view的高 单位 vp // */ // onRenderSuccess: (width: number, height: number) => { // console.log("BannerExpressAdPage==onRenderSuccess......width=" + width + ",height=" + height); // this.bannerAd = ad; // this.status = "广告加载完成待展示"; // // this.showBannerAd() // } // }) // //渲染广告 // ad.setSlideIntervalTime(30 * 1000); //设置轮播时间长 // this.setDislikeCallback(ad); //设置dislike // ad.render(this.getUIContext()) // this.status = "广告加载中..."; // }); // } // } // } // // private setDislikeCallback(ad: CSJNativeExpressAd) { // ad.setDislikeCallback({ // /** // * dislike show // */ // onShow: () => { // console.log("BannerExpressAdPage==dislike......show"); // }, // // /** // * @param position 选择的位置 // * @param value 选择的内容 // * @param enforceRemove 是否强制关闭广告 // */ // onSelected: (position: number, value: string, enforceRemove: boolean) => { // this.isShowAd = false; // console.log("BannerExpressAdPage==dislike......onSelected:position=" + position + ",value:" + value + // ",enforce=" + enforceRemove); // }, // // /** // * 点击取消 // */ // onCancel: () => { // console.log("BannerExpressAdPage==dislike......onCancel"); // } // // }) // } /** * 加载Banner广告 */ loadBannerAd(rit: string) { // if (Utility.isNoble()) { // return // } // if (!Utility.isPassInstallTime(2)) { // return // } // this.isShowAd = false; // let adCreator: CSJAdCreator = CSJAdSdk.getAdCreator() // this.mAdSlot = new AdSlotBuilder() // .setCodeId(rit) // .setAcceptSize(CSJUtil.BANNER_WIGTH, CSJUtil.BANNER_HEIGHT) // .setAdCount(1) // .setBidAdm(this.mBiddingAdm) // .build() // PrintBiddingTokenUtils.printBiddingToken(this.mAdSlot, adCreator); // adCreator.loadBannerAd(this.mAdSlot, this.expressLoadAdListener) // this.status = "广告加载中..." } /** * 节流广播进度更新 */ private broadcastProgressIfNeeded(): void { const now = Date.now(); if (now - this.lastProgressBroadcastTime >= this.PROGRESS_BROADCAST_INTERVAL) { this.lastProgressBroadcastTime = now; this.broadcastPlayerProgress(); } } /** * 广播播放进度到卡片 */ private broadcastPlayerProgress(): void { try { const currentPos: number = this.unifiedPlayerService.getCurrentPosition() || 0; const progressData: PlayProgress = { currentPosition: currentPos, duration: this.duration || 0, percentage: this.duration > 0 ? (currentPos / this.duration) * 100 : 0, currentTimeText: this.currentTime || '00:00', totalTimeText: this.totalTime || '00:00' }; // 广播播放进度变化 const publishInfo: commonEventManager.CommonEventPublishData = { data: JSON.stringify(progressData) }; commonEventManager.publish(PLAYER_PROGRESS_CHANGED_EVENT, publishInfo, (err) => { if (err) { LogUtils.getInstance().error(`Failed to broadcast player progress: ${JSON.stringify(err)}`); } else { LogUtils.getInstance().LOGI(`Progress broadcasted: ${progressData.percentage.toFixed(1)}%`); } }); } catch (error) { LogUtils.getInstance().error(`Failed to broadcast player progress: ${error}`); } } } //视频气泡窗口的布局 @Builder function customPopupBuilder(dataBu: BubbleBean) { List() { LazyForEach(new LazyDataSource(dataBu.data), (item: string, index: number) => { ListItem() { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { Column() { Image(dataBu.imageRes[index]) .height(28) .fillColor(dataBu.color) .alignSelf(ItemAlign.Center) } .width(32) Text(item) .padding({ top: 10, bottom: 10, right: 10, left: 10 }) .fontColor(dataBu.modeType == index ? dataBu.color : $r('app.color.index_tab_font_color')) .textAlign(TextAlign.Start) .width("100%") } } .backgroundColor($r('app.color.settings_background_main')) .margin({ top: 6, bottom: 6 }) } .transition(TransitionEffect.asymmetric(TransitionEffect.move(TransitionEdge.BOTTOM) .animation({ duration: 500, curve: Curve.Ease, delay: 100 * index }), TransitionEffect.scale({ x: 0, y: 0 }))) // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }), // TransitionEffect.scale({ x: 0, y: 0 }) )) .onClick(() => { dataBu.onItemClick?.(index) }) }) } .width(120) .divider({ strokeWidth: 1, color: "#22FFFFFF" }) } // Function to calculate a hash for the file list function simpleHash(fileList: string[]): string { let hash = 0; const str = fileList.join(','); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash |= 0; // Convert to 32bit integer } return hash.toString(); } interface MoreItem { id: number; image: Resource; title: string; } //视频气泡窗口的布局 @Builder function cutPopupBuilder(dataBu: BubbleBean) { Column() { Text(dataBu.isCopy ? '将文件复制到' : '将文件移动到') .padding({ top: 10, bottom: 10, right: 10, left: 10 }) .textAlign(TextAlign.Start) .width(160) .fontWeight(666) .fontSize(16) List() { ForEach(dataBu.data, (item: string, index: number) => { ListItem() { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { Column() { Image($r('app.media.music_group')) .height(28) .fillColor(dataBu.color) .alignSelf(ItemAlign.Center) } .width(32) Text(item.startsWith('.') ? item.replace(/\./g, '') : item) .padding({ top: 10, bottom: 10, right: 10, left: 10 }) .textAlign(TextAlign.Start).width("100%")// .fontColor(Color.Gray) .fontSize(15) } } .backgroundColor(Color.Transparent) .margin({ top: 7, bottom: 7 }) } .transition(TransitionEffect.asymmetric(TransitionEffect.move(TransitionEdge.BOTTOM) .animation({ duration: 500 }), TransitionEffect.scale({ x: 0, y: 0 }))) // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }), // TransitionEffect.scale({ x: 0, y: 0 }) )) .onClick(() => { dataBu.onItemClick?.(index) }) }) } .width(150) .divider({ strokeWidth: 1, color: "#22FFFFFF" }) } .height(300) .padding({ bottom: 30 }) } function getFileDirName(filePath: string,rootPath:string): string{ if(filePath===rootPath){ return '首页' } let result = FileUtil.getFileName(filePath) if(StrUtil.isEmpty(result)) return '' if(result.startsWith('.')) result = result.replace(/\./g, '') return result }