import TitleBar from './TitleBar' import { curves, display, MenuModifier, PiPWindow, promptAction, router, SymbolGlyphModifier, window } from '@kit.ArkUI'; import { VideoItem } from '../viewmodel/VideoItem'; import { LengthMetrics, SegmentButton,SegmentButtonOptions } from '@kit.ArkUI'; import { getWebdavVideoItems, getWebdavCurrentPlayIndex } from '../pages/WebDavMainPage'; import { getNavidromeVideoItems, getNavidromeCurrentPlayIndex, getUnplayedSongs, getUnplayedCount, markAsPlayed, clearPlayedHistory, getPlayedCount, getNavidromeTotalCount, setNavidromeTotalCount } from '../common/util/NavidromePlaylistStore'; import { FindPlaylistMeta, getFindVideoItems, getFindCurrentPlayIndex, getFindPlaylistMeta, resolveFindPagedLocalQueueMetaForPlayback } from '../common/util/FindPlaylistStore'; import { triggerLoadMoreSongs, hasMoreData } from '../common/util/NavidromeRandomLoader'; import { DEFAULT_LOCAL_RANDOM_QUEUE_PAGE_SIZE, isFindLocalRandomPagedPlaylist } from '../common/util/LocalRandomPagedQueueHelper'; import { ImplOnBufferingUpdateListener, ImplOnCompletionListener, ImplOnErrorListener, ImplOnInfoListener, ImplOnPreparedListener, ImplOnSeekCompleteListener, ImplOnTimedTextListener, ImplOnVideoSizeChangedListener } from '../common/IjkPlayerListenerImpls'; import { ImageFancyModifier, } from '../common/util/AttributeModifierUtil' import { AppUtil, ArrayUtil, Base64Util, DateUtil, DeviceUtil, FileUtil, GlobalContext, LogUtil, MD5, PreferencesUtil, RandomUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'; import { imagePathToPixelMap } from '../common/util/CommUtils'; import LyricUtil from '../common/util/LyricUtil'; import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@kit.ArkData'; import { CommonConstants } from '../common/constants/CommonConstants'; import { BaiduConstants } from '../common/constants/BaiduConstants'; import { EventConstants } from '../common/constants/EventConstants'; import Logger from '../common/util/Logger'; import { filterPlaylistSongsByKeyword, findSongIndexByFilePath } from '../common/util/PlaylistSearchHelper'; import { http } from '@kit.NetworkKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { BusinessError, emitter } from '@kit.BasicServicesKit'; 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, JSON } from '@kit.ArkTS'; import { Verify } from './Verify'; import { taskpool } from '@kit.ArkTS'; import { RotatingCover } from './RotatingCover'; import { SpectrumView } from './spectrum/SpectrumView'; import { PlayingIndicator } from './PlayingIndicator'; import { PointLightButton } from './PointLight/PointLightButton'; import { TitleBarPointLightButton } from './PointLight/TitleBarPointLightButton' import { PlayConstants } from '../common/constants/PlayConstants'; import { MusicCardActionConstants } from '../common/player/MusicCardActionConstants'; import { MusicCardManager, MusicCardPlaybackStateOptions } from '../common/player/MusicCardManager' import { effectKit } from '@kit.ArkGraphics2D'; import { ColorConversion } from '../common/util/ColorConversion'; import { Lyric, LyricController, LyricParser, LyricView2 } from '@seagazer/cclyric'; import { AvSessionController } from '../controller/AvSessionController'; import { EqualizerManager } from '../common/util/EqualizerManager'; import { EqualizerViewWithCallback } from './EqualizerView'; import { CastController, CastControllerCallbacks } from '../controller/CastController'; import { repairAudioMetadata, convertDsfToWav, getApiLyric,changeMusicCover} from '../common/util/MusicTagUtils'; import { extractHwMediaMetadata, FFMpegTags,Utility } from '../common/util/Utility'; import { TagsContentCover } from '../view/TagsContentCover'; import { DeviceChangeReason, IjkMediaPlayer, InterruptEvent, InterruptHintType, LogUtils, OnBufferingUpdateListener, OnCompletionListener, OnErrorListener, OnInfoListener, OnPreparedListener, OnSeekCompleteListener, OnTimedTextListener, OnVideoSizeChangedListener } 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 { media } from '@kit.MediaKit'; import { UniversalDetector } from '@ohos/juniversalchardet'; import { SettingPage } from '../pages/SettingPage'; import { secondToTime,getTransverterText } from '../common/util/CommUtils'; import { CommonConstants2 } from '../common/util/CommonConstants2'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { LazyDataSource } from '../common/util/LazyDataSource'; import MediaTable, { PageQueryOptions, PageQueryResult, RandomSongQueryOptions } from '../common/util/MediaTable'; import { requestPlaybackPlayerOpen, setPlaybackRuntimeReady } from '../playback/PlaybackHostState'; import { BackgroundAudioPlaybackHost } from '../playback/BackgroundAudioPlaybackHost' import FileDeletionWatcher from '../common/util/FileDeletionWatcher'; import NetAxiosUtil from '../common/util/NetAxiosUtil'; import { ringtone } from '@kit.RingtoneKit'; import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem'; import { resourceManager } from '@kit.LocalizationKit'; import { deviceInfo } from '@kit.BasicServicesKit'; import '../common/network/RemoteCacheRegistry'; import { shouldWrapLocalMusicMenuInScroll } from '../common/util/LocalMusicMenuScrollHelper'; import { ServerLogUtil } from '../common/util/ServerLogUtil'; import { MusicPlaybackController } from '../controller/MusicPlaybackController'; import { PlaybackRuntime } from '../controller/PlaybackCoordinator' import { PlaybackSnapshot as PlaybackStateBridgeSnapshot, PlaybackStateBridge } from '../controller/PlaybackStateBridge' import { clearRegisteredPlaybackRuntime, registerPlaybackRuntime } from '../playback/PlaybackRuntimeRegistry'; import { consumePendingPlaybackActivation } from '../playback/PlaybackActivationStore'; import { PlaybackSnapshotStore } from '../playback/PlaybackSnapshotStore' import { resolvePlaybackSnapshotProgressValue } from '../playback/model/PlaybackSnapshot' import { lyricService, SongData } from '../common/service/LyricService'; import { shouldResolveRemoteLyricOnPlay, tryResolveRemotePlaybackLyric } from '../common/util/RemotePlaybackLyricUtil'; import { resolvePlayerDismissMorphTarget } from '../common/util/PlayerDismissHelper'; import { isBaiduStreamingPlaybackUrl, resolveSeekGuardDecision } from '../common/util/PlaybackSeekGuard'; import { cloneVideoItem, preloadNextSongIfNeeded, setVideoUrlForSong, getTypeOrder, isWebDavType, isSmbType, isNavidromeType, isFtpType, isBaiduType, isJellyfinType, isEmbyType, isAudioStationType, isPlexType, isRemoteCloudType, mergeCoverWithPriority, WorkerEditMusicResult, WebDavMetadataUpdatePayload, isDaoLiYuType } from '../common/util/RemotePlayerUtil'; import { PlaylistPlayRequest, clearPendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore'; import { resolvePlaylistDisplayCount } from '../common/util/PlaylistPlayDispatchHelper' 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 { DeleteComptent } from '../view/DeleteComptent'; import { ABLoopComptent } from '../view/ABLoopComptent'; import { FixMessyView } from '../view/FixMessyView'; import { parseCueFile, CueTrack, CueInfo, getCueTrackDuration, getCueTrackEndOffset, getCueTrackStartOffset, importCueTracks, isCueSplitItem } from '../common/util/CueUtils'; import { CueComptent } from '../view/CueComptent'; import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil'; import { IsoExtractComptent } from '../view/IsoExtractComptent'; import PlaylistTable from '../common/util/PlaylistTable'; import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog'; import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog'; import { convertPlaylistSongsToVideoItems, emptyView,updateAllSongsSortOrder } from '../pages/PlaylistDetailPage'; import { Playlist, PlaylistSong } from '../viewmodel/Playlist'; import { Song } from '../viewmodel/Song'; import { WebDavAccount } from '../viewmodel/WebDavAccount'; import { SongType } from '../common/enums/SongType'; import { RemoteDriveManager, WebDavAuthInfo as WebDavManagerAuthInfo, buildHttpHeadersWithWebDav, WebDavAuthItem} from '../common/util/RemoteDriveManager'; import PermissionUtil from '../common/util/PermissionUtil' import { EditAudio, setRingTone } from './EditAudio'; import { ExtractAccompaniment } from './ExtractAccompaniment'; import { ExtractAudioFromVideo } from './ExtractAudioFromVideo'; import { AudioFormatConverter } from './AudioFormatConverter'; import { MergeAudio } from './MergeAudio'; import { AIVoiceSeparation } from './AIVoiceSeparation'; import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil'; import { ChangeTone } from './ChangeTone'; import { hdsEffect, HdsVisualComponent, HdsSceneController, HdsSceneType, HdsVisualComponentAttribute, } from '@kit.UIDesignKit'; import { GlobalAlphabetIndexerModifier } from '../Styels/GlobalStyles' import { getFirstLetter } from '../Constants'; import { audio } from '@kit.AudioKit'; import { PointLightDefaultButton } from './PointLight/PointLightDeFaultButton'; import { CoverThumbCache } from '../common/util/CoverThumbCache'; import { LyricCopySheet } from './LyricCopySheet'; import { IsoArchiveAudioEntry, IsoExtractFailureDetail, IsoExtractProgressMessage, IsoExtractTaskResult, listIsoAudioEntriesTask, extractIsoAudioEntriesTask } from '../common/util/IsoArchiveUtil'; const TAG = 'LocalMusic'; const HICAR_LOG_TAG = 'HiCarWindow'; const PREF_PLAY_COVER_SCALE_GUIDE_SHOWN = 'play_cover_scale_guide_show'; function logHiCarWindow(message: string): void { void ServerLogUtil.info(HICAR_LOG_TAG, message); } export interface WorkerMetadataPayload { originFilePath: string; originId?: string; bit_rate?: string; duration?: string; sampleRate?: string; mimeType?: string; md5Str?: string; videoSize?: number; size?: string; pixelMapPath?: string; artist?: string; album?: string; name?: string; lyricContent?: string; bits_per_raw_sample?: string; channels?: string; channel_layout?: string; start_time?: string; genre?: string; year?: string; probe_score?: number; track?: string; disc?: string; ALBUMARTIST?: string; COMPOSER?: string; COMMENT?: string; LYRICIST?: string; nb_streams?: number; nb_programs?: number; error?: string; } interface WorkerMessageScanDone { code: 101; data: WorkerMessageScanDoneData; } interface WorkerMessageScanDoneData { } interface WorkerMessageMediaList { code: 102; data: Array; totalCount: number; } interface WorkerMessageArtistList { code: 103; data1: Map>; data2: Array; totalCount: number; } interface WorkerMessageAlbumList { code: 104; data1: Map>; data2: Array; totalCount: number; } interface WorkerMessageMetadata { code: 105; data: WorkerMetadataPayload; } interface WorkerMessageEditResult { code: 106; data: WorkerEditMusicResult; } interface AlphaTaskItem { type: number; name?: string; pyStr?: string; fileName?: string; artist?: string; album?: string; } interface SongPlaybackPreparationResult { success: boolean; loadingShown: boolean; } interface OpenLocalSpecialListEventData { target?: string; } type LocalMusicWorkerMessage = WorkerMessageScanDone | WorkerMessageMediaList | WorkerMessageArtistList | WorkerMessageAlbumList | WorkerMessageMetadata | WorkerMessageEditResult; 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'] const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets"); // 元数据提取状态追踪 const metadataExtractionInFlight: Map> = new Map(); // setVideoUrlForSong 函数已移至 MusicPlayerUtil.ets // 定义接口 interface HiCarAspectRatio { ratio: number; name: string; } interface GeneratedObjectLiteralInterface_1 { assetId: string; title: string; artist: string; // 网络资源投播,设置mediaUri; 本地资源投播,将本地文件打开后,相关的文件描述符设置到fdSrc mediaUri: string; // 该字段大写,音频'AUDIO',视频'VIDEO' mediaType: string; mediaSize: number; //startPosition为投播当前进度,设置该字段可将本机播放进度同步到远端 startPosition: number; // 投播资源播放时长,设置该字段可将本机播放时长同步到远端显示 duration: number; albumCoverUri: string; albumTitle: string; appName: string; // DRM资源,需要配置支持的DRM类型, 以chinaDRM为例。 drmScheme: string; } const ITEM_HEIGHT_SMALL: number = 48; // 列表项小高度 const ITEM_HEIGHT: number = 65; // 列表项中高度 const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度 /** * 排序模式枚举 * 定义各种排序方式的升序基准值 */ export enum SortMode { ByArtist = 0, // 按艺术家升序 ByAlbum = 2, // 按专辑升序 ByName = 4, // 按名称升序 ByTime = 6, // 按时间升序 ByAddTime = 7, // 按添加时间(只有降序) ByCount = 8, // 按数量升序 ByDiscTrack = 10 // 按碟片编号-音轨号升序 } //功能强大的音乐播放器 @Preview @Component export struct LocalMusic { @State default_layout:number = 0 @State isGeKongPlayNext: boolean = true//是否开启隔空手势播放下一首 @State bluetoothDisconnectPause: boolean = false // 蓝牙设备断开暂停 @State isBluetoothConnected: boolean = false // 蓝牙连接状态 @State bluetoothLyricEnabled: boolean = false // 蓝牙歌词开关 @State bluetoothLyricMode: number = 0 // 蓝牙歌词显示模式 private lastBluetoothLyricLine: string = '' // 上一次发送的歌词行 private audioRoutingManager?: audio.AudioRoutingManager private outputDeviceChangeCallback?: Callback private audioInterruptResumePending: boolean = false @State showFindLocation: boolean = true @StorageProp("EnablePointLight") enablePointLight: boolean = true @State showSimple: boolean = true//开启沉浸模式 @State isPaging: boolean = true;//是否分页加载机制 @State isCoverRectangle: boolean = true; @State showZMIndex: boolean = false //是否右侧显示字母索引 private prevOffsetY: number = 0; // 记录上一次的Y轴偏移量 @State autoHideTitle: boolean = true //滚动自动隐藏标题栏 // 字母表 @State alphaBet: string[] = ['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'] @State alphaBetStyle: GlobalAlphabetIndexerModifier = new GlobalAlphabetIndexerModifier() @State selectAlphaBetIndex: number = 0 @State isShowAlphaBet: boolean = false @State isScrolling: boolean = false @State isTouchingAlphaBet: boolean = false @State alphabetHeight: number = 0 public enableShowAlphabet: boolean = true // 是否显示字母表 private alphaBetHideTimer: number = 0 private alphaBetRebuildTimer: number = 0 private alphaBetDirty: boolean = false private alphaBetCacheKey: string = '' private alphaBetBuildVersion: number = 0 @State isShowPrecious: boolean = false //播控条显示上一首按钮 //背景两侧流光控制器 @State sceneBgController: HdsSceneController|undefined = deviceInfo.sdkApiVersion>=20 &&canIUse("SystemCapability.UIDesign.HDSComponent.Core")? new HdsSceneController() .setSceneParams({ backgroundMaskColors: [Color.Green, Color.Red], firstEdgeFlowLight: { startPos: 0, endPos: 0.5, color: Color.Red }, secondEdgeFlowLight: { startPos: 0, endPos: -0.5, color: Color.Green } }) : undefined; //背景流光控制器 @State bgController: hdsEffect.ShaderEffectController|undefined = deviceInfo.sdkApiVersion>=20 &&canIUse("SystemCapability.UIDesign.HDSComponent.Core")? new hdsEffect.ShaderEffectController():undefined; @State showAddSheet:boolean = false private loadingDialogId: string = '' private loadingProgressDialogId: string = '' @State isoAudioEntries: IsoArchiveAudioEntry[] = [] @State selectedIsoAudioPaths: string[] = [] @State isoSourceFileName: string = '' @State isIsoExtracting: boolean = false @State isoExtractProgress: number = 0 @State isoExtractProgressText: string = '' @State isoExtractCurrentEntryName: string = '' private isoSourceFilePath: string = '' private isoExtractDestinationDir: string = '' private isoComponentId: number = 0 private isoExtractProgressTimer: number = 0 private isoExtractExpectedOutputPaths: Array = [] private isoExtractTotalBytes: number = 0 private isoExtractDisplayedProgress: number = 0 private isoExtractReceivedTaskProgress: boolean = false private isoExtractTask: taskpool.Task | undefined = undefined private isoExtractLastActivityTime: number = 0 private readonly isoExtractStallTimeoutMs: number = 15000 private isoExtractCanceled: boolean = false private isoExtractCancelMessage: string = '' searchController: SearchController = new SearchController() @State rightTopImage:Resource = $r('sys.symbol.sort') @State isEditAudio:boolean = false @State isExtractAcc:boolean = false @State isConverter:boolean = false @State isAIVoiceSeparation:boolean = false @State isExtractAudio:boolean = false @State isMergeAudio:boolean = false @State isCopyFileToDownLoad: boolean = false @State isChangeTone:boolean = false @State offHeight: number = 75 @State currentSongList: Array = []//当前歌单 @Consume currentSongListName:string //当前歌单名称 @Consume @Watch('onIDChange') currentSongListID:string //当前歌单ID @State currentPlaylistSongFilePaths: string[] = [] //当前歌单的歌曲路径列表(用于随机播放) @State isRefreshing: boolean = false; @State maxRefreshingHeight: number = 100.0; @State isPlayerLoading: boolean = false; // 播放器加载状态 private contentNode?: ComponentContent = undefined; private skipNextPlaylistPersist: boolean = false private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance() private playbackStateBridge: PlaybackStateBridge = PlaybackStateBridge.getInstance() private playbackSnapshotStore: PlaybackSnapshotStore = new PlaybackSnapshotStore() private pendingInternalLoopModeReport: number = -1 private playbackRuntime: PlaybackRuntime = { playQueue: async (queue: VideoItem[], startIndex: number, source: string, playType?: number) => { const safeIndex = queue.length <= 0 ? -1 : Math.max(0, Math.min(startIndex, queue.length - 1)) if (safeIndex < 0) { return } const findPagedLocalMeta = resolveFindPagedLocalQueueMetaForPlayback(queue, safeIndex) if (playType !== undefined) { this.applyRequestedPlayType(playType) } if (findPagedLocalMeta?.isPagedLocalQueue) { this.playQueueScope = 'find_paged_local' this.applyFindPagedLocalQueueState(findPagedLocalMeta, findPagedLocalMeta.totalCount) this.currentPlaylistSongFilePaths = [] } else { this.resetFindPagedLocalQueueState() } this.songList = [...queue] this.currentSongList = [...queue] this.sonDataSource.pushArrayData(this.songList) this.curIndex = safeIndex this.currentSong = queue[safeIndex] AppStorage.setOrCreate('currentSong', this.currentSong) if (source === 'controller-open') { requestPlaybackPlayerOpen() } await this.doPlay(queue[safeIndex], safeIndex, true) this.playbackStateBridge.replaceQueue(this.songList, this.curIndex) this.playbackStateBridge.updatePlaybackState( this.CONTROL_PlayStatus === PlayStatus.PLAY, this.mIjkMediaPlayer.getCurrentPosition(), this.getActiveDuration() ) }, playOrPause: async () => { await this.playOrPause() }, playNext: async () => { await this.playNext() }, playPrevious: async () => { await this.playPrevious() }, seekTo: async (value: string, source?: string) => { await this.seekTo(value, source ?? 'coordinator') } } // WebDAV相关实例变量(现在改为从内存读取,不再需要存储) @State ratioL: number = 1; @State mediaKuCount: number = 0; @State albumCount: number = 0; @State artistCount: number = 0; @State fastForwardSeconds: string = '10' @State isShowBackFast: boolean = true//快进快退按钮 @State isCanAuto:boolean = true @State opacityValueImage: number = 1; @State tipPopup:boolean = false @State preloadNextSong: boolean = PreferencesUtil.getBooleanSync('preload_next_song', true) // 是否提前缓存下一首 @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)) private playlistTable: PlaylistTable | null = null private deletionWatcher: FileDeletionWatcher | null = null @State allPlaylists: Playlist[] = [] @State isZero: boolean = false @State fileList: Array = [] @State dirList: Array = [] @Consume videoLocalList:VideoItem[] @State dataSource: LazyDataSource = new LazyDataSource(this.videoLocalList) @State listRefreshKey: number = 0 @State mediaKuList: Array = []; //媒体库文件 @State artistList: Array = []; //艺术家文件 @State artistMap: Map = new Map(); //艺术家Map @State artistSongCountMap: Map = new Map(); @State albumList: Array = []; //专辑文件 @State albumMap: Map = new Map(); //专辑Map @State albumSongCountMap: Map = new 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 isShowSingleLineLyric: boolean = false //是否显示单行歌词 @State isEnableWordByWordLyric: boolean = true //是否开启逐字歌词 @State isShowHistory: boolean = true //是否显示最近播放 @State @Watch('onColorModeChange') isCustomizeBg: boolean = false //自定义背景界面 @State isHomeBgFollowMusicCover: boolean = true //主页背景随当前播放封面 @State isGridMusic: boolean = false //是否网格布局 @State isCircleBtn: boolean = false //是否圆形播放按钮 @State twoFingerType: number = 3 //双支放大缩小的类型 @State isScrollHide: boolean = false //是否滚动隐藏 @State isSameTimePlay: boolean = false //是否和其他app同时播放 @State isStartAutoPlay: boolean = false //启动后自动播放 @State isShowPlayPageBack: boolean = false //是否显示播放页返回键 @State isShowHeader: boolean = true @State isPlayListBgGrass: boolean = true //是否播放列表玻璃透明效果 @State isMemoryLastPlay: boolean = false //是否启用应用退出记忆最后一首的播放进度 @State isCoverTopBig: boolean = false //顶部大封面部分手机显示会和播放控制页重叠 @State rectangleCoverScale: number = 1 @State rectangleCoverPinchValue: number = 1 @State isSwipe: boolean = false //listItem的左滑开关 @State openSkipSongAnimate: boolean = true//切歌动画效果 @State isPlayPageSwipeNext: boolean = true // 播放页上滑切歌 @State isLightText: boolean = true //是否开启歌词发光 @State customizeBgPath: string | undefined = ''; @StorageProp('customizeMusicBgPath') customizeMusicBgPath: string = ''; @State isDarkMode: boolean = false @State lyricTextWeight: number = 400 @State lyricTextWeightPip: number = 400 @State currentSwiperIndex: number = 0 @StorageLink('playbackHostOpenRequestToken') @Watch('onPlaybackHostOpenRequestChange') playbackHostOpenRequestToken: number = 0 @StorageLink('playbackHostPlaylistRequestToken') @Watch('onPlaybackHostPlaylistRequestChange') playbackHostPlaylistRequestToken: number = 0 @State blurValue: number = 0 //背景模糊 @State bgBrightness: number = 0 //背景亮度 private listScroller: ListScroller = new ListScroller() private playListScroller: Scroller = new Scroller() @State isOneKeyTags:boolean = false @State isFixMessy:boolean = false // 分页相关状态 @State private currentPage: number = 0 // 当前页码 @State private pageSize: number = 53 // 每页数量 @State private totalCount: number = 0 // 总记录数 @State private hasMoreData: boolean = true // 是否还有更多数据 @State private isLoadingPage: boolean = false // 是否正在加载分页数据 private pageCache: Map = new Map() // 页面缓存(最多保留3页) private pageHasMoreCache: Map = new Map() private pageTotalCountCache: Map = new Map() private prefetchingPages: Set = new Set() private preloadStartRatio: number = 0.18 private appendStartRatio: number = 0.30 private earlyAppendReserveCount: number = 28 private lastScrollTriggerPage: number = -1 private lastVisibleEndIndex: number = -1 private isPagingTransition: boolean = false private pagingScheduleTimer: number = 0 private listScrollAlphaCheckAt: number = 0 private listScrollPagingCheckAt: number = 0 private listScrollLastAlphaStart: number = -1 private listScrollLastPagingEnd: number = -1 private gridScrollPagingCheckAt: number = 0 private gridScrollLastPagingEnd: number = -1 private waterScrollPagingCheckAt: number = 0 private waterScrollLastPagingEnd: number = -1 private gridWaterScrollIdleTimer: number = 0 private gridWaterLastActiveTs: number = 0 private listUpdateTimer: number = 0 private listUpdateVersion: number = 0 private detailBackTransitioning: boolean = false private mediaLibraryEnterTs: number = 0 private mediaLibraryWarmupPrefetchTimer: number = 0 private listVisibleStart: number = -1 private listVisibleEnd: number = -1 private listThumbWarmupCheckAt: number = 0 private listThumbWarmupStart: number = -1 private listThumbWarmupEnd: number = -1 private gridThumbWarmupCheckAt: number = 0 private gridThumbWarmupStart: number = -1 private gridThumbWarmupEnd: number = -1 private waterThumbWarmupCheckAt: number = 0 private waterThumbWarmupStart: number = -1 private waterThumbWarmupEnd: number = -1 private gridVisibleStart: number = -1 private gridVisibleEnd: number = -1 private waterVisibleStart: number = -1 private waterVisibleEnd: number = -1 private lastHandledPlaybackHostOpenRequestToken: number = 0 private lastHandledPlaybackHostPlaylistRequestToken: number = 0 private titleBarToggleMinDelta: number = 12 private titleBarLastCheckTs: number = 0 @State private coverThumbVersion: number = 0 private coverThumbCache: CoverThumbCache = new CoverThumbCache(1, 960) private coverThumbCacheReady: boolean = false private pendingCoverThumbRefresh: boolean = false private playbackSnapshotHydrated: boolean = false private isHostSnapshotDisplayOnly: boolean = false private playQueueScope: 'view' | 'history' | 'fav' | 'playlist' | 'single' | 'find_paged_local' = 'view' private isHydratingPlayList: boolean = false private preserveExistingQueueOnNextPlay: boolean = false private nextPlayQueueScope: 'view' | 'history' | 'fav' | 'playlist' | 'single' | 'find_paged_local' = 'view' private findPagedLocalQueuePageIndex: number = 0 private findPagedLocalQueuePageSize: number = DEFAULT_LOCAL_RANDOM_QUEUE_PAGE_SIZE private findPagedLocalQueueTotalCount: number = 0 private findPagedLocalQueueHasMore: boolean = false private findPagedLocalQueueSortType: number = 4 private isLoadingFindPagedLocalQueue: boolean = false @StorageProp('isLandscape') @Watch('onIsLandscapeChange') isLandscape: boolean = false; @StorageProp('topSafeHeight') topSafeHeight: number = 0; @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0; @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: {} } @State private pointLightOptions: hdsEffect.PointLightOptions | undefined = undefined @State private activePointLightItemKey: string = '' @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET; // WebDAV认证信息缓存(作为实例变量,比全局上下文更可靠) private currentWebDavAuthInfo: WebDavAuthItem | null = null; // 当前实际播放中的记忆key,避免切歌时 currentSong 已切换导致串写 private activePlaybackMemoryKey: string = ''; // 网盘流地址记忆恢复token,切歌后用于取消旧的延迟seek任务 private remoteRestoreSeekToken: number = 0; // 本次是否需要先seek到记忆点再开始播放 private waitInitialSeekBeforeStart: boolean = false; // 本次播放记忆点(毫秒) private pendingMemorySeekPosition: number = 0; // 记忆播放seek兜底,避免一直转圈 private initialSeekGuardTimer: number = 0; private readonly initialSeekGuardTimeoutMs: number = 2000; private readonly remoteMemorySeekInitialDelayMs: number = 120; private readonly remoteMemorySeekRetryIntervalMs: number = 80; private readonly remoteMemorySeekMaxRetryCount: number = 6; private lastSeekRequestMs: number = -1; private lastSeekSource: string = ''; private seekTraceUntilTs: number = 0; private lastSeekTraceLogTs: number = 0; private readonly seekTraceLogWindowMs: number = 4000; private readonly seekTraceLogIntervalMs: number = 400; //瀑布流的列数横竖屏动态切换 onIsLandscapeChange() { if(this.isLandscape){ this.columns = 4 }else{ this.columns = PreferencesUtil.getNumberSync('lastColumns', 2); } } onColorModeChange() { this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK } //点击某个歌单id发生变化的时候回调 onIDChange(){ console.log('onecold 歌单id发生变化this.currentSongListID='+this.currentSongListID) console.log('onecold 歌单id发生变化this.currentSongListName='+this.currentSongListName) if(this.modeType==4){ this.titleName = this.currentSongListName this.rightTopImage = $r('sys.symbol.sort') //获取缓存 const cacheSongs:string =PreferencesUtil.getStringSync(this.currentSongListID+'currentSongList', '') console.log('onecold 歌单id发生变化获取缓存2 cacheSongs = '+cacheSongs) if(StrUtil.isNotEmpty(cacheSongs)){ this.currentSongList = JSON.parse(cacheSongs) as VideoItem[] this.updateListData(this.currentSongList, true) } this.doSongListTask()//获取歌单信息 } } async doSongListTask() { // 初始化歌单数据库 const playlistTable = new PlaylistTable(this.context); // 查询歌单中的所有歌曲 const playlistSongs: PlaylistSong[] = await playlistTable.queryPlaylistSongs(this.currentSongListID); console.log('onecold 歌单id发生变化获取缓存7 playlistSongs='+playlistSongs.length) if (ArrayUtil.isEmpty(playlistSongs)) { this.updateListData([], true) // 清空歌单路径列表 this.currentPlaylistSongFilePaths = []; } else { this.currentSongList = await convertPlaylistSongsToVideoItems(this.context, playlistSongs); console.log('onecold 从数据库读取的歌曲为 playlistSongs='+this.currentSongList.length) this.updateListData(this.currentSongList, true) // 保存歌单的歌曲路径列表,用于随机播放 this.currentPlaylistSongFilePaths = playlistSongs.map(song => song.songFilePath); Logger.info('heanup doSongListTask', `保存歌单歌曲路径列表,歌单ID=${this.currentSongListID}, 数量=${this.currentPlaylistSongFilePaths.length}`); if (ArrayUtil.isNotEmpty(this.currentSongList)&&this.currentSongList.length > 100) { // 只保存前100条记录到缓存中 let canceList = this.currentSongList.slice(0, 100); PreferencesUtil.putSync(this.currentSongListID+'currentSongList', JSON.stringify(canceList)) } else { PreferencesUtil.putSync(this.currentSongListID+'currentSongList', JSON.stringify(this.currentSongList)); } } this.isRefreshing = false } async onModeChange(preserveList: boolean = false): Promise { this.isFavMusic = false this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true) this.refreshDirVisibilitySettings() if (this.mediaLibraryWarmupPrefetchTimer) { clearTimeout(this.mediaLibraryWarmupPrefetchTimer) this.mediaLibraryWarmupPrefetchTimer = 0 } Logger.info('heanup LocalMusic', `onModeChange: 切换模式 modeType=${this.modeType}`); switch (this.modeType) { case 0: // 首页模式已在 resetAndLoadFirstPage 中处理 // 切换模式时加载对应的排序类型 this.titleName = Utility.resourceToString(this.context,$r('app.string.home')) this.sortType = this.loadSortTypeForCurrentMode() this.mediaLibraryEnterTs = 0 break case 1: this.rightTopImage = $r('sys.symbol.sort') this.titleName = Utility.resourceToString(this.context,$r('app.string.media_ku')) // 切换模式时加载对应的排序类型 this.sortType = this.loadSortTypeForCurrentMode() this.mediaLibraryEnterTs = Date.now() break case 2: this.rightTopImage = $r('sys.symbol.sort') this.titleName = Utility.resourceToString(this.context,$r('app.string.artist')) // 切换到艺术家模式时,加载艺术家模式的排序类型(默认按数量降序) this.sortType = this.loadSortTypeForCurrentMode() this.mediaLibraryEnterTs = 0 break case 3: this.rightTopImage = $r('sys.symbol.sort') this.titleName = Utility.resourceToString(this.context,$r('app.string.album')) // 切换到专辑模式时,加载专辑模式的排序类型(默认按数量降序) this.sortType = this.loadSortTypeForCurrentMode() this.mediaLibraryEnterTs = 0 break } // 模式切换时重置分页状态并加载第一页 await this.resetAndLoadFirstPage(!preserveList); Logger.info('heanup onModeChange', `doSortType: 切换排序方式 sortType=${this.sortType}, mode=${this.modeType}`); } // @Consume currentSong: VideoItem | undefined; @StorageLink('currentSong') @Watch('onHomeBgCurrentSongChange') currentSong: VideoItem | undefined = undefined; // @StorageLink('isPlay') isPlay: boolean = false; @StorageLink('isShowPlay') isShowPlay: boolean = false; @State isShowCoverScaleGuide: boolean = false @StorageLink('songList') songList: Array = []; @State translateY: number = 0; @State playDismissScale: number = 1; @State playDismissScaleY: number = 1; @State playDismissOpacity: number = 1; @State playDismissTranslateX: number = 0; @State playDismissBorderRadius: number = 0; @State playDismissProxySize: number = 0; @State playDismissProxyOpacity: number = 0; @State isShowSheet: boolean = false; @State isShowSheetView: boolean = false; @State isShowLyricCopySheet: boolean = false; @State lyricCopyLines: string[] = []; @State lyricSelectedIndexes: number[] = []; @State isShowTimeCloseView: boolean = false; private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Vertical }); private playDismissTimer: number = -1; private readonly playDismissContentDuration: number = 260; private readonly playDismissDotDuration: number = 160; private playOpenFromMiniBarPending: boolean = false; private playOpenTimer: number = -1; private readonly playOpenDotDuration: number = 120; private readonly playOpenExpandDuration: number = 320; @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 scrollerForlist: ListScroller = new ListScroller(); private knockController: KnockController | undefined = undefined; @StorageProp('animationState') 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 parsedCache: Object | null = JSON.parse(cacheString); if (parsedCache && Array.isArray(parsedCache)) { const cacheArray: [string, Array][] = parsedCache; this.cache = new Map(cacheArray); } } if (stringCacheString) { const parsedStringCache: Object | null = JSON.parse(stringCacheString); if (parsedStringCache && Array.isArray(parsedStringCache)) { const stringCacheArray: [string, string][] = parsedStringCache; 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() } // 删除字符串缓存 private deleteStringCache(key: string) { this.stringCache.delete(key); } // 多选机制 @State selectedFiles: Array = []; @Consume isMultiSelect: boolean; @State isAllSelected: boolean = false; @State appName: string = '' @State isHasDir: boolean = false @State packName: string = '' context = this.getUIContext().getHostContext() as common.UIAbilityContext doSwipBack() { if(this.isSearchMode){ console.info('onecold doSwipBack = ' +this.isSearchMode) this.isSearchMode = false if (this.modeType==0) {//如果是首页 直接getSortedFiles,如果不是就走this.onSearchInput('') this.searchText = '' this.getSortedFiles(this.currentPath) return } this.onSearchInput('') return } this.isZero = false if (this.modeType !== 0 && this.isCanBack) { if (this.detailBackTransitioning) { return } this.detailBackTransitioning = true // 先设置 isCanBack=false,确保 onModeChange 读取的是列表页的排序设置 this.isCanBack = false this.rightTopImage = $r('sys.symbol.sort') if (this.modeType == 2) { this.titleName = Utility.resourceToString(this.context,$r('app.string.artist')) if (ArrayUtil.isNotEmpty(this.artistList)) { this.updateListData(this.artistList, true, true) } } else if (this.modeType == 3) { this.titleName = Utility.resourceToString(this.context,$r('app.string.album')) if (ArrayUtil.isNotEmpty(this.albumList)) { this.updateListData(this.albumList, true, true) } } // 返回列表页时保留当前列表,避免先清空导致闪现 void this.onModeChange(true).then(() => { if (this.modeType == 2) { if (this.isGridMusic||this.twoFingerType==4) { let lastOffset = this.mScrollMap.get('lastGridArtistScrollOffset') ?? 0 let lastWaterOffset = this.mScrollMap.get('lastWaterArtistScrollOffset') ?? 0 if(this.twoFingerType==4){ this.waterScroller.scrollTo({ xOffset: 0, yOffset: lastWaterOffset }) }else{ 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||this.twoFingerType==4) { let lastOffset = this.mScrollMap.get('lastGridAlBumScrollOffset') ?? 0 let lastWaterOffset = this.mScrollMap.get('lastWaterAlBumScrollOffset') ?? 0 if(this.twoFingerType==4){ this.waterScroller.scrollTo({ xOffset: 0, yOffset: lastWaterOffset }) }else{ this.scroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } } else { let lastOffset = this.mScrollMap.get('lastListAlBumScrollOffset') ?? 0 this.listScroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } } this.detailBackTransitioning = false }).catch((error: Error) => { Logger.error('heanup LocalMusic', `doSwipBack onModeChange error: ${error.message}`) this.detailBackTransitioning = false }) return } this.mediaLibraryEnterTs = 0 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||this.twoFingerType==4) { let lastOffset = this.mScrollMap.get('lastGridDirScrollOffset') ?? 0 let lastWaterOffset = this.mScrollMap.get('lastWaterDirScrollOffset') ?? 0 if(this.twoFingerType==4){ this.waterScroller.scrollTo({ xOffset: 0, yOffset: lastWaterOffset }) }else{ this.scroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } } else { let lastOffset = this.mScrollMap.get('lastListDirScrollOffset') ?? 0 this.listScroller.scrollTo({ xOffset: 0, yOffset: lastOffset }) } } }, 100) this.currentPath = Utility.getParentDirectory(this.currentPath) Logger.info('this.currentPath2 = ' + this.currentPath) this.getSortedFiles(this.currentPath, false, undefined, undefined, undefined, true) } private triggerSwipeBackWithAnimation(): void { animateTo({ duration: 555 }, () => { this.doSwipBack() }) } private onPlaybackHostOpenRequestChange(): void { if (this.playbackHostOpenRequestToken === this.lastHandledPlaybackHostOpenRequestToken) { return } this.lastHandledPlaybackHostOpenRequestToken = this.playbackHostOpenRequestToken if (!this.isShowPlay) { this.setShowPlayTrue() } this.showPlayerView() } private onPlaybackHostPlaylistRequestChange(): void { if (this.playbackHostPlaylistRequestToken === this.lastHandledPlaybackHostPlaylistRequestToken) { return } this.lastHandledPlaybackHostPlaylistRequestToken = this.playbackHostPlaylistRequestToken this.openPlayList() } private consumePendingPlaybackActivationAfterRestore(): void { const activation = consumePendingPlaybackActivation() if (!activation) { return } if (activation.actionType === 'resume_play') { void this.playbackController.playOrPause() return } if (activation.actionType === 'play_previous') { void this.playbackController.playPrevious() return } if (activation.actionType === 'play_next') { void this.playbackController.playNext() return } if (activation.actionType === 'open_player') { requestPlaybackPlayerOpen() } } // 组件生命周期 aboutToAppear() { registerPlaybackRuntime(this.playbackRuntime) setPlaybackRuntimeReady(true) this.playbackSnapshotHydrated = this.hydrateFromPlaybackSnapshot() this.onPlaybackHostOpenRequestChange() this.onPlaybackHostPlaylistRequestChange() console.info('onecold aboutToAppear sdkApiVersion = '+deviceInfo.sdkApiVersion) if(ArrayUtil.isNotEmpty(this.videoLocalList)&&this.modeType==0){ this.updateListData(this.videoLocalList) } // 初始化 PlaylistTable this.playlistTable = new PlaylistTable(getContext(this)) this.sonTwoFingerType = PreferencesUtil.getNumberSync('sonTwoFingerType', 3); this.columns = PreferencesUtil.getNumberSync('lastColumns', 2); if(PreferencesUtil.getBooleanSync('isFirstTiped',true)){ this.tipPopup = !this.tipPopup PreferencesUtil.putSync('isFirstTiped',false) } this.loadSearchHistory() this.initSetting() this.syncEqualizerUi(); this.UNKONWN = Utility.resourceToString(this.context, $r('app.string.unknown')); Utility.getAppName(getContext(this)).then((appName: string) => { this.appName = appName }) this.packName = AppUtil.getBundleName() this.tabSelectedIndexes[0] = this.modeType//SegmentButton的索引 this.loadCacheFromStorage(); // 加载文件夹缓存 // 在首页初始化时读取媒体库,艺术家,专辑等缓存数据 this.initLoadCache() this.mkDownLoadDir() let eventMusic: emitter.InnerEvent = { eventId: EventConstants.EVENT_AUDIO_OPEN } // 监听广播事件(打开其他应用处理) emitter.on(eventMusic, (eventData: emitter.EventData) => { this.saveVideoDatas([eventData.data?.message], true) }); //侧滑广播接收时间 let eventUpdateSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_UPDATE } // 监听广播事件(手势返回处理) emitter.on(eventUpdateSwipeBack, (eventData: emitter.EventData) => { this.triggerSwipeBackWithAnimation() }); //ScanFilePage广播接收时间 let eventScanUpdate: emitter.InnerEvent = { eventId: EventConstants.EVENT_SCAN_UPDATE } // 监听ScanFilePage广播事件(更新数据库) emitter.on(eventScanUpdate, (eventData: emitter.EventData) => { this.doUpdateData() }); let eventEqualizerChanged: emitter.InnerEvent = { eventId: EventConstants.EVENT_EQUALIZER_CHANGED } emitter.on(eventEqualizerChanged, () => { this.syncEqualizerUi(); if (this.mIjkMediaPlayer) { const equalizerManager = EqualizerManager.getInstance(); equalizerManager.applyToPlayer(this.mIjkMediaPlayer); this.markEqualizerApplied(); } }); let eventRefreshSort: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_REFRESH_SORT } // 监听广播事件(通用设置配置更新) emitter.on(eventRefreshSort, (eventData: emitter.EventData) => { if(this.modeType ==4){ this.doSongListTask()//获取歌单信息 } }); let eventQueueRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_QUEUE_REFRESH } emitter.on(eventQueueRefresh, () => { this.syncWebdavQueueWithoutRestart(); }); let eventOpenLocalSpecialList: emitter.InnerEvent = { eventId: EventConstants.EVENT_OPEN_LOCAL_SPECIAL_LIST } emitter.on(eventOpenLocalSpecialList, (eventData: emitter.EventData) => { const data = eventData.data as OpenLocalSpecialListEventData | undefined const target = data?.target ?? '' this.openSpecialLocalList(target) }); let eventSetting: emitter.InnerEvent = { eventId: EventConstants.EVENT_SETTING_UPDATE } // 监听广播事件(通用设置配置更新) emitter.on(eventSetting, (eventData: emitter.EventData) => { this.initSetting() }); // 监听 WebDAV 元数据更新事件 let eventWebDavMetadata: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED } emitter.on(eventWebDavMetadata, (eventData: emitter.EventData) => { const payloads = eventData?.data as WebDavMetadataUpdatePayload[] | undefined; if (!payloads || payloads.length === 0) { Logger.info(TAG, 'LocalMusic: WebDav metadata event received but payload empty'); return; } Logger.info(TAG, `LocalMusic: WebDav metadata event payload count: ${payloads.length}`); // 处理每个元数据更新 for (let i = 0; i < payloads.length; i++) { const payload = payloads[i]; if (!payload || !payload.filePath) { continue; } // 重要:只更新当前正在播放的歌曲 // 如果 filePath 不匹配,说明是其他歌曲的元数据更新,忽略不做处理 if (this.currentSong && this.currentSong.filePath === payload.filePath) { Logger.info(TAG, `LocalMusic: 更新当前播放歌曲的元数据: ${payload.filePath}`); if (payload.name && payload.name.length > 0) { this.name = payload.name; if (this.currentSong) { this.currentSong.name = payload.name; } } if (payload.artist && payload.artist.length > 0) { this.artist = payload.artist; if (this.currentSong) { this.currentSong.artist = payload.artist; } } if (this.currentSong) { const coverMutated = mergeCoverWithPriority(this.currentSong, payload.pixelMapPath, payload.isCustomCover ?? 0); if (coverMutated && this.currentSong.pixelMapPath) { this.cover = this.currentSong.pixelMapPath; Logger.info(TAG, `LocalMusic: 封面已更新: ${this.currentSong.pixelMapPath}`); } if (payload.md5Str && payload.md5Str.length > 0) { this.currentSong.md5Str = payload.md5Str; } } // 同步更新 AppStorage AppStorage.setOrCreate('currentSong', this.currentSong); } else { Logger.info(TAG, `LocalMusic: 跳过非当前播放歌曲的元数据更新: ${payload.filePath}, 当前: ${this.currentSong?.filePath}`); } } }); // 监听广播事件(通用设置配置更新) emitter.on(eventSetting, (eventData: emitter.EventData) => { this.doChangeSetting() }); this.mIjkMediaPlayer.on('audioInterrupt', (event: InterruptEvent) => { this.handleAudioInterrupt(event) }); this.mIjkMediaPlayer.on('deviceChange', (event: InterruptEvent) => { this.handlePlayerDeviceChange(event) }); const eventPause: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_PAUSE } // 监听广播事件(hicar断开暂停播放) emitter.on(eventPause, (eventData: emitter.EventData) => { console.info('heanup onecold hicar断开暂停播放, 暂停播放') this.audioInterruptResumePending = false this.pause(); }); let audioManager = audio.getAudioManager(); // 需要先创建AudioManager实例。 this.audioRoutingManager = audioManager.getRoutingManager(); // 再调用AudioManager的方法创建AudioRoutingManager实例。 this.outputDeviceChangeCallback = (deviceChanged: audio.DeviceChangeAction) => { this.handleOutputDeviceChange(deviceChanged) } this.audioRoutingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG, this.outputDeviceChangeCallback); 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) this.rectangleCoverScale = PreferencesUtil.getNumberSync(SettingPage.RECTANGLE_COVER_SCALE, 1) if (this.rectangleCoverScale < 0.7 || this.rectangleCoverScale > 1.4) { this.rectangleCoverScale = 1 } // 读取 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() this.windowClass.on('windowSizeChange', (size: window.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) => { this.isAppForeground = fg; if (fg && this.curState === 'STARTED') { this.stopPip(); this.syncLyricPositionNow(); } }); } //载入媒体库,艺术家,专辑等缓存 initLoadCache(){ this.mediaKuCount = PreferencesUtil.getNumberSync('mediaKuCount', 0) this.artistCount = PreferencesUtil.getNumberSync('artistCount', 0) this.albumCount = PreferencesUtil.getNumberSync('albumCount', 0) let cachedListStr:string = PreferencesUtil.getStringSync('mediaKuList', '[]'); let cachedList = JSON.parse(cachedListStr) as Array; if(ArrayUtil.isNotEmpty(cachedList)){ this.mediaKuList = cachedList if(this.modeType==1){ this.updateListData(this.mediaKuList) } } let artistListStr:string = PreferencesUtil.getStringSync('artistList', '[]'); let artistCachedList = JSON.parse(artistListStr) as Array; if(ArrayUtil.isNotEmpty(artistCachedList)){ this.artistList = artistCachedList let artistMapStr:string = PreferencesUtil.getStringSync('artistMap', '[]'); const parsedArray = JSON.parse(artistMapStr) as [string, VideoItem[]][]; this.artistMap = new Map(parsedArray); // this.artistMap = JSON.parse(artistMapStr) as Map; if(this.modeType==2){ this.updateListData(this.artistList) } } let albumListStr:string = PreferencesUtil.getStringSync('albumList', '[]'); let albumCachedList = JSON.parse(albumListStr) as Array; if(ArrayUtil.isNotEmpty(albumCachedList)){ this.albumList = albumCachedList let albumMapStr:string = PreferencesUtil.getStringSync('albumMap', '[]'); const parsedArray = JSON.parse(albumMapStr) as [string, VideoItem[]][]; this.albumMap = new Map(parsedArray); if(this.modeType==3){ this.updateListData(this.albumList) } } } 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) } private isAudioPlaybackActive(): boolean { return this.CONTROL_PlayStatus === PlayStatus.PLAY || this.mIjkMediaPlayer.isPlaying() || (!!this.castControllerWrapper && this.isCastPlaying) } private isBluetoothOutputDeviceType(deviceType: audio.DeviceType): boolean { return deviceType === audio.DeviceType.BLUETOOTH_A2DP || deviceType === audio.DeviceType.BLUETOOTH_SCO } private isWiredOutputDeviceType(deviceType: audio.DeviceType): boolean { return deviceType === audio.DeviceType.WIRED_HEADSET || deviceType === audio.DeviceType.WIRED_HEADPHONES || deviceType === audio.DeviceType.USB_HEADSET } private handleAudioInterrupt(event: InterruptEvent): void { console.info(`heanup onecold 与其他应用音频冲突 被截断点event: ${JSON.stringify(event)}`) if (this.isSameTimePlay) { Logger.info(TAG, `音频中断已忽略,当前为混音模式: ${JSON.stringify(event)}`) return } const wasPlayingBeforeInterrupt = this.isAudioPlaybackActive() switch (event.hintType) { case InterruptHintType.INTERRUPT_HINT_PAUSE: this.audioInterruptResumePending = wasPlayingBeforeInterrupt if (wasPlayingBeforeInterrupt) { this.pause(false) } break case InterruptHintType.INTERRUPT_HINT_RESUME: if (this.audioInterruptResumePending && this.CONTROL_PlayStatus === PlayStatus.PAUSE) { console.info(`heanup onecold audioInterrupt startPlayOrResumePlay: ${JSON.stringify(event)}`) this.audioInterruptResumePending = false this.resumePlaybackAfterAudioInterrupt() } break case InterruptHintType.INTERRUPT_HINT_STOP: Logger.info(TAG, `收到音频中断 STOP,改为暂停等待用户手动恢复: ${JSON.stringify(event)}`) this.audioInterruptResumePending = false if (wasPlayingBeforeInterrupt) { this.pause(false) } break default: Logger.info(TAG, `忽略未处理的音频中断类型: ${JSON.stringify(event)}`) break } } private resumePlaybackAfterAudioInterrupt(): void { if (this.castControllerWrapper && this.isCastPlaying) { this.castControllerWrapper.setPlaying().then(() => { this.CONTROL_PlayStatus = PlayStatus.PLAY this.setIsPlaying(true) this.updateSessionPlayState(true) this.playChange() }).catch((error: BusinessError | Error) => { Logger.error(TAG, `音频中断后恢复投播失败: ${error}`) }) return } this.startPlayOrResumePlay() } private handlePlayerDeviceChange(event: InterruptEvent): void { console.info(`onecold Heanup deviceChange event: ${JSON.stringify(event)}`); if (event.reason === DeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE) { Logger.info(TAG, 'ijk deviceChange 检测到新设备接入,交由 AudioRoutingManager 处理') } else if (event.reason === DeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) { Logger.info(TAG, 'ijk deviceChange 检测到旧设备移除,等待 AudioRoutingManager 精确判断是否暂停') } } private handleOutputDeviceChange(deviceChanged: audio.DeviceChangeAction): void { const descriptors = deviceChanged?.deviceDescriptors ?? [] if (descriptors.length <= 0) { Logger.warn(TAG, `音频输出设备变化缺少描述信息: ${JSON.stringify(deviceChanged)}`) return } Logger.info(TAG, `音频输出设备变化: type=${deviceChanged.type}, count=${descriptors.length}`) const outputDescriptors = descriptors.filter((descriptor) => descriptor.deviceRole === audio.DeviceRole.OUTPUT_DEVICE) if (outputDescriptors.length <= 0) { Logger.info(TAG, `忽略非输出设备变化: ${JSON.stringify(deviceChanged)}`) return } const hasBluetoothOutput = outputDescriptors.some((descriptor) => this.isBluetoothOutputDeviceType(descriptor.deviceType)) const hasWiredOutput = outputDescriptors.some((descriptor) => this.isWiredOutputDeviceType(descriptor.deviceType)) if (deviceChanged.type === audio.DeviceChangeType.CONNECT) { if (hasBluetoothOutput) { this.isBluetoothConnected = true console.info('onecold 蓝牙设备已连接') } return } if (deviceChanged.type !== audio.DeviceChangeType.DISCONNECT) { return } if (hasBluetoothOutput) { this.isBluetoothConnected = false this.lastBluetoothLyricLine = '' console.info('onecold 蓝牙设备已断开') if (this.currentSong) { this.avSessionController.restoreOriginalMetadata( this.currentSong.name || '', this.currentSong.artist || '' ) } if (this.bluetoothDisconnectPause && this.isAudioPlaybackActive()) { this.audioInterruptResumePending = false console.info('heanup onecold 蓝牙音响断开, 暂停播放') this.pause() } return } if (hasWiredOutput && this.isAudioPlaybackActive()) { this.audioInterruptResumePending = false console.info('heanup onecold 有线/USB耳机断开, 暂停播放') this.pause() } } initSetting() { this.default_layout = PreferencesUtil.getNumberSync('default_layout', 0) this.isGeKongPlayNext = PreferencesUtil.getBooleanSync('isGeKongPlayNext', false) this.bluetoothDisconnectPause = PreferencesUtil.getBooleanSync(SettingPage.BLUETOOTH_DISCONNECT_PAUSE, false) this.bluetoothLyricEnabled = PreferencesUtil.getBooleanSync(SettingPage.BLUETOOTH_LYRIC_ENABLED, false) this.bluetoothLyricMode = PreferencesUtil.getNumberSync(SettingPage.BLUETOOTH_LYRIC_MODE, 0) this.isShowSpectrum = PreferencesUtil.getBooleanSync('isShowSpectrum', true) this.spectrumModeIndex = PreferencesUtil.getNumberSync('spectrumModeIndex', 0) this.enablePointLight = PreferencesUtil.getBooleanSync('enablePointLight', true) AppStorage.setOrCreate('EnablePointLight', this.enablePointLight); this.showSimple = PreferencesUtil.getBooleanSync('showSimple', true) this.showFindLocation = PreferencesUtil.getBooleanSync('showFindLocation', true) this.showZMIndex = PreferencesUtil.getBooleanSync('showZMIndex', false) this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true) this.isShowPrecious = PreferencesUtil.getBooleanSync('isShowPrecious', false) this.sortType = PreferencesUtil.getNumberSync(SettingPage.SORT_TYPE, 4) this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false) this.isHomeBgFollowMusicCover = PreferencesUtil.getBooleanSync(SettingPage.IS_HOME_BG_FOLLOW_MUSIC_COVER, true) 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.syncCustomizeMusicBgPath() this.twoFingerType = PreferencesUtil.getNumberSync(SettingPage.TWO_FINGER_TYPE, 3) this.isCircleBtn = PreferencesUtil.getBooleanSync(SettingPage.IS_CIRCLE_BTN, true) this.isShowPlayPageBack = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_PLAYPAGE_BACK, false) this.isCoverTopBig = PreferencesUtil.getBooleanSync(SettingPage.IS_COVER_TOP_BIG, false) this.isShowSingleLineLyric = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_SLLYRIC, false) this.isEnableWordByWordLyric = PreferencesUtil.getBooleanSync(SettingPage.IS_ENABLE_WORD_BY_WORD_LYRIC, true) this.isLightText = PreferencesUtil.getBooleanSync(SettingPage.IS_LIGHT_TEXT, true) this.syncWordByWordLyricMode() this.syncLyricLightTextMode() 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) const volumeBoostPercent = PreferencesUtil.getNumberSync(this.volumeBoostPreferenceKey, 0) this.volumeBoostPercent = Math.max(0, Math.min(volumeBoostPercent, this.volumeBoostMaxPercent)) this.isSwipe = PreferencesUtil.getBooleanSync(SettingPage.IS_SWIPE, true) this.openSkipSongAnimate = PreferencesUtil.getBooleanSync(SettingPage.OPEN_SKIPSONG_ANIMATE, true) this.isPlayPageSwipeNext = PreferencesUtil.getBooleanSync(SettingPage.IS_PLAYPAGE_SWIPE_NEXT, true) this.volumeSmall = PreferencesUtil.getBooleanSync('volumeSmall', false) this.autoParseMusicName = PreferencesUtil.getBooleanSync('autoParseMusicName', true) this.isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false) this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true) this.refreshDirVisibilitySettings() this.fastForwardSeconds = PreferencesUtil.getStringSync('fastForwardSeconds', '10') this.isShowBackFast = PreferencesUtil.getBooleanSync('isShowBackFast', false) this.isCopyFileToDownLoad = PreferencesUtil.getBooleanSync(SettingPage.IS_COPYFILE_TO_DOWNLOAD, false) let audioManager = audio.getAudioManager(); let audioSessionManager: audio.AudioSessionManager = audioManager.getSessionManager(); if(this.isSameTimePlay){ //与其他应用同时播放,关键代码 let strategy: audio.AudioSessionStrategy = { concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS }; audioSessionManager.activateAudioSession(strategy).then(() => { console.info('onecold Succeeded in doing activateAudioSession.'); }).catch((err: BusinessError) => { console.error(`Failed to activateAudioSession. Code: ${err.code}, message: ${err.message}`); }); }else{ let isActivated = audioSessionManager.isAudioSessionActivated(); if(isActivated){ audioSessionManager.deactivateAudioSession().then(() => { console.info('Succeeded in doing deactivateAudioSession.'); }).catch((err: BusinessError) => { console.error(`Failed to deactivateAudioSession. Code: ${err.code}, message: ${err.message}`); }); } } //如果是hicar连接状态,这些都是false if(this.isHiCar()){ this.autoHideTitle = false this.isCoverRectangle= true this.columns=4 if(!(this.windowWidth>1800&&this.windowHeight>=1200))//小屏幕不可以Grid { this.isGridMusic= false//用list不用网格 this.isCoverTopBig = false } this.isShowPlayPageBack = true //播放页返回键要显示,方便驾驶员操作 } if (this.isSavePlayMode) { this.playType = PreferencesUtil.getNumberSync('musicPlayType', 0) } } private onHomeBgCurrentSongChange(): void { this.syncCustomizeMusicBgPath() } private syncCustomizeMusicBgPath(): void { if (!this.isHomeBgFollowMusicCover) { return } const coverPath: string = this.currentSong?.pixelMapPath ?? '' AppStorage.setOrCreate('customizeMusicBgPath', coverPath) } private isMusicCoverHomeBackgroundActive(): boolean { return this.isHomeBgFollowMusicCover && this.customizeMusicBgPath.length > 0 } 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); this.refreshDirVisibilitySettings(); Logger.info('onecold onPageShow currentBreakpoint= ' + this.currentBreakpoint) } //开启线程查看各个数据库 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', ''), // data5: PreferencesUtil.getBooleanSync('autoParseMusicName', true), // }); PreferencesUtil.putSync('isFirstApp', false) } else { workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName }); workerInstance.postMessage({ code: 3, data: this.context }); workerInstance.postMessage({ code: 4, data: this.context }); this.getHistoryList(false) } }, 1000) workerInstance.onmessage = (e: MessageEvents): void => { const message = e.data as LocalMusicWorkerMessage; switch (message.code) { case 101: //第一次扫描文件夹入库 ToastUtil.showToast('刷新同步数据库成功!') Logger.info('onecold 扫描数据库结束 this.mediaKuList length= ' + this.mediaKuList.length) this.doAllUpdateData() break; case 102: //查询媒体库列表 this.isRefreshing = false this.mediaKuList = message.data // 只包含前100条 const mediaTotalCount: number = message.totalCount ?? this.mediaKuList.length; this.mediaKuCount = mediaTotalCount; Logger.info(`heanup 查询媒体库列表 缓存数=${this.mediaKuList.length}, 总数=${mediaTotalCount}`) Utility.doSortListAscending(this.mediaKuList) AppStorage.setOrCreate('mediaKuList', this.mediaKuList); // 媒体库模式下重新加载第一页(分页查询) if (this.modeType === 1) { this.resetAndLoadFirstPage(); } PreferencesUtil.putSync('mediaKuCount', mediaTotalCount) // 只保存前100条记录到缓存中 PreferencesUtil.putSync('mediaKuList', JSON.stringify(this.mediaKuList)); break; case 103: //收到查询艺术家列表 this.isRefreshing = false this.artistMap = message.data1; // 只包含前60个艺术家的歌曲 this.artistList = message.data2; // 只包含前100个艺术家 const artistTotalCount: number = message.totalCount ?? this.artistList.length; this.artistCount = artistTotalCount; const nextArtistCountMap = new Map(this.artistSongCountMap); this.artistMap.forEach((songs, artist) => { nextArtistCountMap.set(artist, songs.length); }); this.artistSongCountMap = nextArtistCountMap; Logger.info(`heanup 查询艺术家列表 缓存数=${this.artistList.length}, 总数=${artistTotalCount}`) PreferencesUtil.putSync('artistCount', artistTotalCount) // 艺术家模式下重新加载第一页(分页查询) if (this.modeType === 2) { this.resetAndLoadFirstPage(); } // 保存前100条艺术家列表 PreferencesUtil.putSync('artistList', JSON.stringify(this.artistList)); // 保存前60条艺术家的歌曲Map const sortedEntries = Array.from(this.artistMap.entries()); PreferencesUtil.putSync('artistMap', JSON.stringify(sortedEntries)); break; case 104: //收到查询专辑列表 this.isRefreshing = false this.albumMap = message.data1; this.albumList = message.data2 const albumTotalCount: number = message.totalCount ?? this.albumList.length; this.albumCount = albumTotalCount; const nextAlbumCountMap = new Map(this.albumSongCountMap); this.albumMap.forEach((songs, album) => { nextAlbumCountMap.set(album, songs.length); }); this.albumSongCountMap = nextAlbumCountMap; Logger.info(`heanup 查询专辑列表 缓存数=${this.albumList.length}, 总数=${albumTotalCount}`) // 专辑模式下重新加载第一页(分页查询) if (this.modeType === 3) { this.resetAndLoadFirstPage(); } //查询完毕在启动是否自动播放 if (this.isStartAutoPlay&&this.isCanAuto) { this.isCanAuto = false console.info('onecold startPlayOrResumePlay 757') this.startPlayOrResumePlay() } PreferencesUtil.putSync('albumCount', albumTotalCount) // 保存前100条专辑列表(Worker已经只发送了100条) PreferencesUtil.putSync('albumList', JSON.stringify(this.albumList)); // 保存前60条专辑Map(Worker已经只发送了60条) const mapArrayAlbum = Array.from(this.albumMap.entries()); PreferencesUtil.putSync('albumMap', JSON.stringify(mapArrayAlbum)); break; case 105: // 缓存完成后返回的元数据 this.handleWorkerMetadataPayload(message.data); break; case 106: // 编辑音乐元数据和封面完成 this.handleEditMusicResult(message.data); break; } }; // 在调用terminate后,执行onexit workerInstance.onexit = (code) => { metadataExtractionInFlight.clear(); Logger.info(TAG, 'workerInstance 线程已退出,code: ' + code); } workerInstance.onerror = (err: ErrorEvent) => { metadataExtractionInFlight.clear(); console.error('workerInstance 线程执行发生异常: ' + err.message) } } private handleWorkerMetadataPayload(payload: WorkerMetadataPayload | undefined): void { if (!payload || !payload.originFilePath) { return; } metadataExtractionInFlight.delete(payload.originFilePath); if (payload.error) { Logger.warn(TAG, `缓存文件元数据解析失败: ${payload.error}`); return; } this.applyMetadataToCollections(payload.originFilePath, payload); } private applyMetadataToCollections(filePath: string, payload: WorkerMetadataPayload): void { const targets: Array = []; let mainListUpdated = false; let playlistUpdated = false; if (this.currentSong && this.currentSong.filePath === filePath) { targets.push(this.currentSong); } const collectFromList = (list?: Array, markUpdated?: () => void) => { if (!list || list.length === 0) { return; } const found = Utility.getItemByFilePath(list, filePath); if (found) { markUpdated?.(); targets.push(found); } }; collectFromList(this.videoLocalList, () => { mainListUpdated = true; }); collectFromList(this.songList, () => { playlistUpdated = true; }); collectFromList(this.historyList); collectFromList(this.favList); const remoteManager = RemoteDriveManager.getInstance(); collectFromList(remoteManager.webDavSongs); const updated = new Set(); const remoteMetadataUpdates: Map = new Map(); for (let i = 0; i < targets.length; i++) { const target = targets[i]; if (!target || updated.has(target)) { continue; } updated.add(target); this.mergeVideoItemWithPayload(target, payload); if (isRemoteCloudType(target.type) && StrUtil.isNotEmpty(target.filePath)) { remoteMetadataUpdates.set(target.filePath, { filePath: target.filePath, pixelMapPath: target.pixelMapPath, isCustomCover: target.isCustomCover, md5Str: target.md5Str, name: target.name, artist: target.artist }); } } if (this.currentSong && this.currentSong.filePath === filePath) { this.mergeVideoItemWithPayload(this.currentSong, payload); if (payload.name) { this.name = payload.name; } if (payload.artist) { this.artist = payload.artist; } if (payload.pixelMapPath && this.currentSong?.pixelMapPath) { this.cover = this.currentSong.pixelMapPath; } if (payload.lyricContent) { this.applyInlineLyricContent(payload.lyricContent); } let avSessionDurationMs: number = this.duration; if (payload.duration) { const durationSeconds = Number(payload.duration); if (!Number.isNaN(durationSeconds) && durationSeconds > 0) { avSessionDurationMs = durationSeconds * 1000; this.duration = avSessionDurationMs; this.durationTime = Math.floor(avSessionDurationMs / 1000); this.durationStringTime = secondToTime(this.durationTime); } } if (avSessionDurationMs > 0) { this.avSessionController.setAVMetadataMusic(this.currentSong, avSessionDurationMs, this.lyricContent); } this.currentSong = cloneVideoItem(this.currentSong); AppStorage.setOrCreate('currentSong', this.currentSong); } const primaryTarget = targets.length > 0 ? targets[0] : undefined; if (primaryTarget) { this.persistRemoteMetadataToDb(primaryTarget).catch((error: Error) => { Logger.error(TAG, `缓存元数据持久化失败: ${error.message}`); }); } if (remoteMetadataUpdates.size > 0) { const eventUpdate: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED }; emitter.emit(eventUpdate, { data: Array.from(remoteMetadataUpdates.values()) }); } this.refreshLazyListViews(mainListUpdated, playlistUpdated); } private handleEditMusicResult(result: WorkerEditMusicResult): void { // 关闭进度条 DialogHelper.closeDialog(this.loadingDialogId) if (result.success) { console.log("heanup 编辑信息成功,数据库已同步"); if (result.coverResult) { ToastUtil.showToast('内嵌封面成功'); } ToastUtil.showToast('内嵌音乐标签成功'); this.isShowMoreView = false; this.name = this.titleStr; // 同步当前播放的歌曲信息 if (this.editingItem && this.currentSong && this.editingItem.filePath == this.currentSong.filePath) { this.currentSong.name = this.titleStr; this.artist = this.artistStr; this.currentSong.artist = this.artistStr; this.currentSong.album = this.ablumStr; this.currentSong.year = this.yearStr; this.currentSong.genre = this.genreStr; this.currentSong.track = this.trackStr; this.currentSong.lyricContent = this.lyricConStr; this.currentSong.ALBUMARTIST = this.albumArtistStr; this.currentSong.COMPOSER = this.composerStr; this.currentSong.LYRICIST = this.lyricistStr; this.currentSong.COMMENT = this.commentStr; } // if (result.needRefreshList && this.modeType == 0) { // this.deleteCache(this.currentPath); // this.getSortedFiles(this.currentPath); // } // 同步编辑字段到本地列表 if (this.editingItem) { this.syncEditedFields(this.editingItem); this.syncListsAfterEdit(this.editingItem.filePath); this.doUpateEditedFields(this.editingItem); } // 清理临时变量 this.longItemFilePath = ''; this.tempLyricContent = ''; this.isShowEdit = false; this.imagePathStr = ''; } else { console.error("heanup 编辑信息失败: " + result.error); ToastUtil.showToast('编辑失败: ' + (result.error || '未知错误')); // 清理临时变量 this.longItemFilePath = ''; this.tempLyricContent = ''; this.isShowEdit = false; this.imagePathStr = ''; } } private assignStringMetadata(target: VideoItem, key: keyof VideoItem, value?: string | null, allowEmpty = false): void { if (value === undefined || value === null) { return; } const text = value.toString(); if (!allowEmpty && text.trim().length === 0) { return; } Reflect.set(target, key as string, value); } private assignNumberMetadata(target: VideoItem, key: keyof VideoItem, value?: number | null): void { if (value === undefined || value === null || Number.isNaN(value)) { return; } Reflect.set(target, key as string, value); } private mergeVideoItemWithPayload(target: VideoItem, payload: WorkerMetadataPayload): void { if (!target || !payload) { return; } this.assignStringMetadata(target, 'name', payload.name); this.assignStringMetadata(target, 'artist', payload.artist); this.assignStringMetadata(target, 'album', payload.album); this.assignStringMetadata(target, 'bit_rate', payload.bit_rate); this.assignStringMetadata(target, 'duration', payload.duration); this.assignStringMetadata(target, 'sampleRate', payload.sampleRate); this.assignStringMetadata(target, 'mimeType', payload.mimeType); this.assignStringMetadata(target, 'md5Str', payload.md5Str); this.assignStringMetadata(target, 'size', payload.size); mergeCoverWithPriority(target, payload.pixelMapPath); this.assignStringMetadata(target, 'lyricContent', payload.lyricContent, true); this.assignStringMetadata(target, 'genre', payload.genre); this.assignStringMetadata(target, 'track', payload.track); this.assignStringMetadata(target, 'bits_per_raw_sample', payload.bits_per_raw_sample); this.assignStringMetadata(target, 'channels', payload.channels); this.assignStringMetadata(target, 'channel_layout', payload.channel_layout); this.assignStringMetadata(target, 'start_time', payload.start_time); this.assignStringMetadata(target, 'year', payload.year); this.assignStringMetadata(target, 'disc', payload.disc); this.assignStringMetadata(target, 'ALBUMARTIST', payload.ALBUMARTIST); this.assignStringMetadata(target, 'COMPOSER', payload.COMPOSER); this.assignStringMetadata(target, 'LYRICIST', payload.LYRICIST); this.assignStringMetadata(target, 'COMMENT', payload.COMMENT, true); this.assignNumberMetadata(target, 'videoSize', payload.videoSize); this.assignNumberMetadata(target, 'probe_score', payload.probe_score); this.assignNumberMetadata(target, 'nb_streams', payload.nb_streams); this.assignNumberMetadata(target, 'nb_programs', payload.nb_programs); } private mergeVideoItemFromSource(target: VideoItem, source: VideoItem): void { if (!target || !source) { return; } this.assignStringMetadata(target, 'name', source.name); this.assignStringMetadata(target, 'artist', source.artist); this.assignStringMetadata(target, 'album', source.album); this.assignStringMetadata(target, 'bit_rate', source.bit_rate); this.assignStringMetadata(target, 'duration', source.duration); this.assignStringMetadata(target, 'sampleRate', source.sampleRate); this.assignStringMetadata(target, 'md5Str', source.md5Str); this.assignStringMetadata(target, 'trackCount', source.trackCount); this.assignStringMetadata(target, 'mimeType', source.mimeType); this.assignStringMetadata(target, 'size', source.size); mergeCoverWithPriority(target, source.pixelMapPath, source.isCustomCover ?? 0); this.assignStringMetadata(target, 'lyricContent', source.lyricContent, true); this.assignStringMetadata(target, 'genre', source.genre); this.assignStringMetadata(target, 'track', source.track); this.assignStringMetadata(target, 'bits_per_raw_sample', source.bits_per_raw_sample); this.assignStringMetadata(target, 'channels', source.channels); this.assignStringMetadata(target, 'channel_layout', source.channel_layout); this.assignStringMetadata(target, 'start_time', source.start_time); this.assignStringMetadata(target, 'year', source.year); this.assignStringMetadata(target, 'disc', source.disc); this.assignStringMetadata(target, 'ALBUMARTIST', source.ALBUMARTIST); this.assignStringMetadata(target, 'COMPOSER', source.COMPOSER); this.assignStringMetadata(target, 'LYRICIST', source.LYRICIST); this.assignStringMetadata(target, 'COMMENT', source.COMMENT, true); this.assignNumberMetadata(target, 'videoSize', source.videoSize); this.assignNumberMetadata(target, 'probe_score', source.probe_score); this.assignNumberMetadata(target, 'nb_streams', source.nb_streams); this.assignNumberMetadata(target, 'nb_programs', source.nb_programs); if (source.remote_rel_path && StrUtil.isEmpty(target.remote_rel_path)) { target.remote_rel_path = source.remote_rel_path; } } private async hydrateRemoteSongFromDb(item: VideoItem | undefined): Promise { if (!item || !isRemoteCloudType(item.type)) { return; } const storagePath = item.remote_rel_path || WebDavUrlUtil.toStoragePath(item.filePath); if (!storagePath) { return; } try { const cached = await this.table.queryVideoByFilePath(storagePath); if (!cached) { return; } this.mergeVideoItemFromSource(item, cached); if (StrUtil.isEmpty(item.remote_rel_path)) { item.remote_rel_path = storagePath; } if (item === this.currentSong) { if (StrUtil.isNotEmpty(item.name)) { this.name = item.name; } if (StrUtil.isNotEmpty(item.artist)) { this.artist = item.artist!; } if (StrUtil.isNotEmpty(item.pixelMapPath)) { this.cover = item.pixelMapPath!; } if (StrUtil.isNotEmpty(item.lyricContent) && StrUtil.isEmpty(this.lyricContent)) { this.applyInlineLyricContent(item.lyricContent as string); } } } catch (error) { Logger.error(TAG, `hydrateRemoteSongFromDb 失败: ${(error as Error).message}`); } } private applyInlineLyricContent(content: string): void { if (StrUtil.isEmpty(content)) { return; } this.lyricContent = content; const lines: string[] = content.split('\n').map((line: string) => line.trim()); const lyric: Lyric = this.parser.parse(lines); this.lyricController.setLyric(lyric); this.lyricControllerXF.setLyric(lyric); this.lyricControllerSingle.setLyric(lyric); } private extractParentPathFromStoragePath(path: string): string { if (!path) { return ''; } const lastSlash = path.lastIndexOf('/'); if (lastSlash <= 0) { return ''; } return path.substring(0, lastSlash); } private resolveRemoteStoragePath(target: VideoItem): string | null { if (!target || !isRemoteCloudType(target.type)) { return null; } if (isWebDavType(target.type)) { const storagePath = target.remote_rel_path || WebDavUrlUtil.toStoragePath(target.filePath); if (storagePath) { target.remote_rel_path = storagePath; return storagePath; } return null; } if (target.remote_rel_path && target.remote_rel_path.length > 0) { return target.remote_rel_path; } return null; } private async persistRemoteMetadataToDb(target: VideoItem): Promise { if (!this.context || !target || !isRemoteCloudType(target.type)) { return; } const storagePath = this.resolveRemoteStoragePath(target); if (!storagePath) { return; } const table = new MediaTable(this.context); await new Promise((resolve) => { table.getRdbStore(this.context as Context, () => resolve()); }); const cloneItem = cloneVideoItem(target); cloneItem.filePath = storagePath; cloneItem.remote_rel_path = storagePath; cloneItem.parentPath = this.extractParentPathFromStoragePath(storagePath); await table.saveOrUpdateWebDavItem(cloneItem); } onPageHide() { this.knockController?.immersiveDisableListening(); } @State isFirstStartPlay: boolean = false private async startFileDeletionWatcher(): Promise { if (!this.rootPath) { return; } if (!this.deletionWatcher) { this.deletionWatcher = new FileDeletionWatcher(this.context, this.table); } try { await this.deletionWatcher.watchDirectories([ this.rootPath, this.lockPath, this.favPath, this.historyPath ]); } catch (error) { Logger.error(TAG, `初始化文件删除监听失败: ${(error as Error).message}`); } } //获取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 PreferencesUtil.putSync('download_path', this.rootPath+'/') this.currentPath = this.rootPath 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) if (!FileUtil.accessSync(this.favPath)) { FileUtil.mkdirSync(this.favPath) } if (!FileUtil.accessSync(this.historyPath)) { FileUtil.mkdirSync(this.historyPath) } // await this.startFileDeletionWatcher(); this.getSortedFiles(this.rootPath).then(async () => { this.isFavMusic = false //穿山甲 // this.loadBannerAd(CSJUtil.getBannerID()) this.restoreLastPlayContext(); const hydratedFromSnapshot = this.playbackSnapshotHydrated || this.hydrateFromPlaybackSnapshot() if (!hydratedFromSnapshot) { this.songList = PreferencesUtil.getSync('LastMusicList', []) as Array this.currentSong = PreferencesUtil.getSync('LastMusicInfo', undefined) as VideoItem if (ArrayUtil.isEmpty(this.songList)) { this.songList = this.videoLocalList.filter(item => item.type === CommonConstants.TYPE_LOCAL && !this.isIsoLocalFile(item)) } else if (this.currentSong !== undefined) { this.curIndex = Utility.getIndexFromList(this.songList, this.currentSong.filePath) } else { this.curIndex = 0 } 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] } const restoredUrl = await this.resolvePlaybackUrlForCurrentSong('restore-last-song'); if (!restoredUrl) { return; } this.videoUrl = restoredUrl; Logger.info(TAG, `WebDAV URL同步构建完成: ${this.videoUrl}`); this.name = this.currentSong.name this.cover = this.currentSong.pixelMapPath this.playbackStateBridge.replaceQueue(this.songList, this.curIndex) AppStorage.setOrCreate('currentSong',this.currentSong) ; this.consumePendingPlaybackActivationAfterRestore() } else { this.name = '空空如也' } } }) 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, true) } }) } 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'); clearRegisteredPlaybackRuntime(this.playbackRuntime) setPlaybackRuntimeReady(false) this.playbackSnapshotHydrated = false this.pendingInternalLoopModeReport = -1 if (this.alphaBetHideTimer) { clearTimeout(this.alphaBetHideTimer); this.alphaBetHideTimer = 0; } if (this.alphaBetRebuildTimer) { clearTimeout(this.alphaBetRebuildTimer); this.alphaBetRebuildTimer = 0; } if (this.pagingScheduleTimer) { clearTimeout(this.pagingScheduleTimer); this.pagingScheduleTimer = 0; } if (this.gridWaterScrollIdleTimer) { clearTimeout(this.gridWaterScrollIdleTimer); this.gridWaterScrollIdleTimer = 0; } if (this.listUpdateTimer) { clearTimeout(this.listUpdateTimer); this.listUpdateTimer = 0; } if (this.mediaLibraryWarmupPrefetchTimer) { clearTimeout(this.mediaLibraryWarmupPrefetchTimer); this.mediaLibraryWarmupPrefetchTimer = 0; } this.clearInitialSeekGuardTimer(); this.alphaBetBuildVersion++; this.gridWaterLastActiveTs = 0; this.coverThumbCache.dispose(); this.coverThumbCacheReady = false; this.pendingCoverThumbRefresh = false; this.deletionWatcher?.stop(); this.knockController?.immersiveDisableListening(); this.curIndex = 0 this.isAppForeground = false; emitter.off(EventConstants.EVENT_AUDIO_OPEN); emitter.off(EventConstants.EVENT_SCAN_UPDATE); emitter.off(EventConstants.EVENT_SWIPE_BACK_UPDATE); emitter.off(EventConstants.EVENT_SETTING_UPDATE); emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED); emitter.off(EventConstants.EVENT_EQUALIZER_CHANGED); emitter.off(EventConstants.EVENT_PLAYLIST_REFRESH_SORT); emitter.off(EventConstants.EVENT_PLAY_QUEUE_REFRESH); emitter.off(EventConstants.EVENT_OPEN_LOCAL_SPECIAL_LIST); emitter.off(EventConstants.EVENT_PLAY_PAUSE); this.mDestroyPage = true; this.audioInterruptResumePending = false; this.mIjkMediaPlayer.setScreenOnWhilePlaying(false); if (this.CONTROL_PlayStatus != PlayStatus.INIT && !this.isHostSnapshotDisplayOnly) { this.stop(); } this.isHostSnapshotDisplayOnly = false; this.mIjkMediaPlayer.off('audioInterrupt'); this.mIjkMediaPlayer.off('deviceChange'); if (this.audioRoutingManager && this.outputDeviceChangeCallback) { try { this.audioRoutingManager.off('deviceChange', this.outputDeviceChangeCallback); } catch (error) { Logger.warn(TAG, `注销音频输出设备监听失败: ${(error as BusinessError).message ?? error}`); } } this.outputDeviceChangeCallback = undefined; this.audioRoutingManager = undefined; if (workerInstance) { workerInstance.terminate(); } this.stopPip(); this.destroyPipController() this.eventHub.off('onStateChange'); } async getSortedFiles(curPath: string, isWorkerPost?: boolean, destPath?: string, isOpen?: boolean, openItem?: VideoItem, immediateListUpdate?: boolean, scrollTopAfterUpdate?: boolean) { this.isFavMusic = false console.info('onecold getSortedFiles 1 curPath '+curPath) if (isWorkerPost) { workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName }); } if (this.modeType !== 0) { // 第三方打开场景:非文件夹模式也需要继续执行自动播放 if (isOpen && destPath) { this.openMusic(true, destPath, openItem) } return } console.info('onecold getSortedFiles 2') this.isCanBack = false this.currentPath = curPath console.info('onecold getSortedFiles this.currentPath ='+this.currentPath) this.titleName = getFileDirName(this.currentPath,this.rootPath) if (curPath === this.rootPath) { this.rightTopImage = $r('sys.symbol.sort') } else { this.currentTitleName = getFileDirName(this.currentPath, this.rootPath) if (this.rootPath === Utility.getParentDirectory(curPath)) { this.rightTopImage = $r('sys.symbol.chevron_left') } } // 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.isRefreshing) { // this.videoLocalList = cachedValue; Logger.info('onecold 有缓存 cachedValue = ' + cachedValue) if (this.currentPath != this.rootPath) { //第一个封面赋值给currentTitleCover this.currentTitleCover = Utility.getFirstCoverFromList(cachedValue) } this.isRefreshing = false this.updateListData(cachedValue, undefined, immediateListUpdate) if (scrollTopAfterUpdate) { this.scrollToTopForCurrentLayout() } } if (cachedDirList !== undefined) { this.dirList = cachedDirList } //通过当前hash值和缓存hash判断list有没有变化 if (cachedValue && cachedHash === currentHash) { return; } // this.fileList = FileUtil.listFileSync(curPath) let directories: Array = []; let isoFiles: Array = this.getLocalIsoItems(curPath, this.fileList); 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.filter(item => !this.isIsoLocalFile(item)); } files.sort((a, b) => a.cTime.localeCompare(b.cTime)); Utility.doSortListAscending(files) Utility.doSortListAscending(isoFiles) 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(isoFiles).concat(files); this.updateListData(this.videoLocalList, undefined, immediateListUpdate) if (scrollTopAfterUpdate) { this.scrollToTopForCurrentLayout() } if (ArrayUtil.isNotEmpty(files)) { if (this.currentPath != this.rootPath) { if (files) { this.currentTitleCover = Utility.getFirstCoverFromList(files) } } } if (ArrayUtil.isNotEmpty(this.videoLocalList)) { 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].name}`); } // this.setButtonStatus() this.openMusic(isOpen, destPath,openItem) this.isRefreshing = false }); } //第三方app打开天天静听导入音乐后播放 openMusic(isOpen?: boolean, destPath?: string,openItem?:VideoItem){ if (isOpen&&destPath) { 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 } if(openItem){ this.doPlay(openItem,0,false,true) // this.isShowPlay = true this.setShowPlayTrue() } } } //扫描文件夹入库 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, this.autoParseMusicName); mediaItems.push(mediaItem); this.table.insert(mediaItem, (id: number) => { }); } } } return mediaItems; } updateListData(mList: Array, noSort?: boolean, immediate?: boolean) { this.isShowTitleBar = true const shouldAnimate = !immediate if (this.listUpdateTimer) { clearTimeout(this.listUpdateTimer) this.listUpdateTimer = 0 } const updateVersion = ++this.listUpdateVersion if (shouldAnimate) { this.getUIContext().animateTo({ duration: 666 }, () => { this.opacityItem = 0.5; }); } const applyListData = () => { if (updateVersion !== this.listUpdateVersion) { return } 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 (shouldAnimate) { this.getUIContext().animateTo({ duration: 666 }, () => { this.opacityItem = 1; }); } else { this.opacityItem = 1; } this.setButtonStatus() //添加到字母索引表 this.markAlphaBetDirty() }; if (immediate) { applyListData(); return; } this.listUpdateTimer = setTimeout(() => { this.listUpdateTimer = 0 applyListData(); }, 200); } getCurFileList(): Array { const source = this.isSearchMode ? this.filteredList : this.videoLocalList let files: Array = []; if (ArrayUtil.isNotEmpty(source)) { for (let i = 0; i < source.length; i++) { if (this.isSelectableLocalItem(source[i])) { files.push(source[i]); } } } return files } private isProtectedLocalDirectory(item: VideoItem): boolean { return item.type === CommonConstants.TYPE_IS_DIR && (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC) } private isIsoLocalFile(item?: VideoItem | null): boolean { if (!item || item.type !== CommonConstants.TYPE_LOCAL) { return false } return Utility.isIsoByExtension(item.filePath || item.fileName || item.name || '') } private supportsLocalAudioActions(item: VideoItem): boolean { return !!item && item.type !== CommonConstants.TYPE_IS_DIR && !this.isIsoLocalFile(item) } private buildLocalIsoItem(filePath: string, fileName?: string): VideoItem { const displayName = fileName || FileUtil.getFileName(filePath) || filePath let fileSize = 0 try { const stat = fs.statSync(filePath) as fs.Stat if (typeof stat.size === 'number') { fileSize = stat.size } } catch (_error) { } const item = new VideoItem( displayName, filePath, filePath, CommonConstants.TYPE_LOCAL, fileSize, Utility.getFormatDateStr(Date.now(), 'yyyy-MM-dd HH:mm'), Utility.formatFSize(fileSize), '', 'ISO 镜像', '可提取音频', displayName ) item.mimeType = 'application/x-iso9660-image' item.duration = '' return item } private getLocalIsoItems(curPath: string, fileNames?: Array): Array { let sourceNames: Array = [] if (fileNames !== undefined && fileNames.length > 0) { sourceNames = fileNames } else { const listedNames = FileUtil.listFileSync(curPath) sourceNames = listedNames ? listedNames : [] } const isoItems: Array = [] for (let i = 0; i < sourceNames.length; i++) { const name = sourceNames[i] const fullPath = `${curPath}/${name}` if (FileUtil.isDirectory(fullPath) || !Utility.isIsoByExtension(fullPath)) { continue } isoItems.push(this.buildLocalIsoItem(fullPath, name)) } Utility.doSortListAscending(isoItems) return isoItems } private filterOutLocalIsoItems(items: Array): Array { return items.filter(item => !this.isIsoLocalFile(item)) } private isSelectableLocalItem(item: VideoItem): boolean { if (!item) { return false } if (item.type === CommonConstants.TYPE_LOCAL) { return true } if (item.type === CommonConstants.TYPE_IS_DIR) { return !this.isProtectedLocalDirectory(item) } return false } private isLocalItemSelected(item: VideoItem): boolean { return this.selectedFiles.some(x => x.filePath === item.filePath) } private hasSelectedDirectories(): boolean { return this.selectedFiles.some(item => item.type === CommonConstants.TYPE_IS_DIR) } private getSelectedPlayableFiles(): Array { return this.selectedFiles.filter(item => item.type === CommonConstants.TYPE_LOCAL && !this.isIsoLocalFile(item)) } private syncLocalSelectionState(): void { const selectableItems = this.getCurFileList() const selectableKeys: Set = new Set(selectableItems.map(item => item.filePath)) const nextSelected = this.selectedFiles.filter(item => selectableKeys.has(item.filePath)) if (nextSelected.length !== this.selectedFiles.length) { this.selectedFiles = [...nextSelected] } this.isAllSelected = nextSelected.length > 0 && nextSelected.length === selectableItems.length } private enterLocalMultiSelect(item: VideoItem): void { if (!this.isSelectableLocalItem(item)) { return } if (!this.isMultiSelect) { this.isMultiSelect = true } if (!this.isLocalItemSelected(item)) { this.selectedFiles = [...this.selectedFiles, item] } this.syncLocalSelectionState() } private selectSingleItemForDelete(item: VideoItem): void { if (!this.isSelectableLocalItem(item)) { return } this.selectedFiles = [item] this.isAllSelected = false } 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 } } private getPlayListDisplayCount(): number { if (this.playQueueScope === 'history') { return this.historyList.length; } if (this.playQueueScope === 'fav') { return this.favList.length; } if (this.playQueueScope === 'find_paged_local') { const pagedCount = this.findPagedLocalQueueTotalCount > 0 ? this.findPagedLocalQueueTotalCount : this.totalCount; if (pagedCount > 0) { return pagedCount; } } if (this.playQueueScope === 'playlist') { // 检测是否是流媒体播放(Navidrome/Jellyfin/Emby/AudioStation/Plex/道理鱼等) // 如果是,优先返回服务端总数 const navidromeTotalCount = getNavidromeTotalCount(); Logger.info('heanup getPlayListDisplayCount', `playQueueScope=playlist, navidromeTotalCount=${navidromeTotalCount}, currentSong=${this.currentSong?.name}, type=${this.currentSong?.type}`); if (navidromeTotalCount > 0 && this.currentSong && ( isNavidromeType(this.currentSong.type) || isJellyfinType(this.currentSong.type) || isEmbyType(this.currentSong.type) || isAudioStationType(this.currentSong.type) || isPlexType(this.currentSong.type) || isDaoLiYuType(this.currentSong.type) )) { Logger.info('heanup getPlayListDisplayCount', `返回服务端总数: ${navidromeTotalCount}`); return navidromeTotalCount; } const displayCount = resolvePlaylistDisplayCount(this.currentSongList.length, this.songList.length); Logger.info('heanup getPlayListDisplayCount', `返回歌单数量: currentSongList=${this.currentSongList.length}, songList=${this.songList.length}, display=${displayCount}`); return displayCount; } if (this.playQueueScope === 'view' && this.modeType === 1 && this.totalCount > 0) { return this.totalCount; } Logger.info('heanup getPlayListDisplayCount', `返回 songList.length: ${this.songList.length}, playQueueScope=${this.playQueueScope}`); return this.songList.length; } private restoreLastPlayContext(): void { const scope = PreferencesUtil.getStringSync('LastPlayQueueScope', 'view') as string; if (scope === 'history' || scope === 'fav' || scope === 'playlist' || scope === 'single' || scope === 'find_paged_local') { this.playQueueScope = scope; } else { this.playQueueScope = 'view'; } const lastModeType = PreferencesUtil.getNumberSync('LastPlayModeType', this.modeType); const lastTotalCount = PreferencesUtil.getNumberSync('LastPlayTotalCount', 0); if (lastModeType === 1 && this.totalCount === 0 && lastTotalCount > 0) { this.totalCount = lastTotalCount; } if (this.playQueueScope === 'find_paged_local' && lastTotalCount > 0) { this.findPagedLocalQueueTotalCount = lastTotalCount; this.findPagedLocalQueueHasMore = this.songList.length < lastTotalCount; } } private hydrateFromPlaybackSnapshot(): boolean { if (this.playbackSnapshotHydrated) { return true } const snapshot = this.playbackSnapshotStore.read() if (!snapshot) { return false } const recovered = this.playbackSnapshotStore.recoverQueueState(snapshot) if (ArrayUtil.isEmpty(recovered.queue) || recovered.currentSong === undefined) { return false } this.songList = recovered.queue this.currentSongList = [...recovered.queue] this.curIndex = recovered.currentIndex this.currentSong = recovered.currentSong if (snapshot.cover && StrUtil.isEmpty(this.currentSong.pixelMapPath)) { this.currentSong.pixelMapPath = snapshot.cover } this.playType = snapshot.playType this.isFirstStartPlay = true this.videoUrl = this.currentSong.filePath this.name = snapshot.currentSongName !== '' ? snapshot.currentSongName : this.currentSong.name this.artist = snapshot.currentArtist ?? this.currentSong.artist ?? '' this.cover = snapshot.cover ?? this.currentSong.pixelMapPath this.CONTROL_PlayStatus = snapshot.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE this.progressValue = resolvePlaybackSnapshotProgressValue( snapshot.positionMs, snapshot.durationMs, this.PROGRESS_MAX_VALUE ) this.slideEnable = snapshot.durationMs > 0 this.currentTime = this.stringForTime(snapshot.positionMs) this.totalTime = this.stringForTime(snapshot.durationMs) this.duration = snapshot.durationMs this.durationTime = Math.floor(snapshot.durationMs / 1000) this.sonDataSource.pushArrayData(this.songList) const bridgeSnapshot: PlaybackStateBridgeSnapshot = { queue: this.songList, currentIndex: this.curIndex, currentSong: this.currentSong, isPlaying: snapshot.isPlaying, positionMs: snapshot.positionMs, durationMs: snapshot.durationMs } this.playbackStateBridge.restoreSnapshot(bridgeSnapshot) AppStorage.setOrCreate('songList', this.songList) AppStorage.setOrCreate('currIndex', this.curIndex) AppStorage.setOrCreate('currentSong', this.currentSong) AppStorage.setOrCreate('CONTROL_PlayStatus', this.CONTROL_PlayStatus) AppStorage.setOrCreate('progressValue', this.progressValue) AppStorage.setOrCreate('cover', this.cover ?? '') this.playbackSnapshotHydrated = true this.isHostSnapshotDisplayOnly = true Logger.info(TAG, `从后台宿主快照恢复页面状态 song=${this.currentSong.name}, index=${this.curIndex}, ` + `isPlaying=${snapshot.isPlaying}, position=${snapshot.positionMs}`) return true } private resetFindPagedLocalQueueState(): void { // 退出“随心所欲”分页队列时,避免旧分页状态污染后续普通播放队列。 this.findPagedLocalQueuePageIndex = 0; this.findPagedLocalQueuePageSize = DEFAULT_LOCAL_RANDOM_QUEUE_PAGE_SIZE; this.findPagedLocalQueueTotalCount = 0; this.findPagedLocalQueueHasMore = false; this.findPagedLocalQueueSortType = 4; this.isLoadingFindPagedLocalQueue = false; } private applyFindPagedLocalQueueState(meta?: FindPlaylistMeta, totalCount?: number): void { // 播放器只持有当前页数据,但总数、页码和排序要单独记住,后面才能继续按页补队列。 this.findPagedLocalQueuePageIndex = meta?.pageIndex ?? 0; this.findPagedLocalQueuePageSize = meta?.pageSize && meta.pageSize > 0 ? Math.max(1, Math.floor(meta.pageSize)) : DEFAULT_LOCAL_RANDOM_QUEUE_PAGE_SIZE; this.findPagedLocalQueueSortType = meta?.sortType !== undefined ? meta.sortType : 4; const actualTotalCount = meta?.totalCount && meta.totalCount > 0 ? meta.totalCount : (totalCount ?? 0); this.findPagedLocalQueueTotalCount = actualTotalCount; this.findPagedLocalQueueHasMore = (this.findPagedLocalQueuePageIndex + 1) * this.findPagedLocalQueuePageSize < actualTotalCount; if (actualTotalCount > 0) { this.totalCount = actualTotalCount; } } private isFindPagedLocalQueueActive(): boolean { return this.playQueueScope === 'find_paged_local'; } private shouldHydrateFullMediaPlayListOnOpen(): boolean { // 特殊分页队列禁止打开播放列表时自动补全全量歌曲,否则会回到 3000+ 首卡顿问题。 return !!this.currentSong && this.currentSong.type === CommonConstants.TYPE_LOCAL && !this.isFindPagedLocalQueueActive(); } private async hydrateFullMediaPlayListIfNeeded(isSonList:boolean = true): Promise { if (this.isHydratingPlayList) { return; } if (this.playQueueScope !== 'view' || this.modeType !== 1) { return; } if (this.totalCount === 0 || this.songList.length >= this.totalCount) { return; } this.isHydratingPlayList = true; try { const fullList: VideoItem[] = []; let pageIndex = 0; let hasMore = true; while (hasMore) { const result = await this.table.queryMediaLibraryPaged({ pageIndex, pageSize: this.pageSize, sortType: this.sortType, searchKeyword: this.isSearchMode ? this.searchText : undefined, isShowFileName: this.isShowFileName }); if (result.items.length === 0) { break; } fullList.push(...result.items); hasMore = result.hasMore; pageIndex += 1; if (fullList.length >= result.totalCount) { hasMore = false; } } if (fullList.length > 0) { if(isSonList){ this.songList = fullList; this.sonDataSource.pushArrayData(this.songList); }else{ this.videoLocalList = fullList; this.dataSource.pushArrayData(this.videoLocalList); } } } finally { this.isHydratingPlayList = false; } } @Builder SortMenuBuilder() { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.person')), content: "按艺术家", builder: this.SubSortBuilder(SortMode.ByArtist), symbolEndIcon: this.isSortTypeInRange(SortMode.ByArtist) ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortModeItemBackground(this.isSortTypeInRange(SortMode.ByArtist))) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.textformat_size_square')), content: "按专辑", builder: this.SubSortBuilder(SortMode.ByAlbum), symbolEndIcon: this.isSortTypeInRange(SortMode.ByAlbum) ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortModeItemBackground(this.isSortTypeInRange(SortMode.ByAlbum))) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.textformat')), content: "按名称", builder: this.SubSortBuilder(SortMode.ByName), symbolEndIcon: this.isSortTypeInRange(SortMode.ByName) ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortModeItemBackground(this.isSortTypeInRange(SortMode.ByName))) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')), content: $r('app.string.sort_by_time'), builder: this.SubSortBuilder(SortMode.ByTime), symbolEndIcon: this.isSortTypeInRange(SortMode.ByTime) ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortModeItemBackground(this.isSortTypeInRange(SortMode.ByTime))) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music')), content: '按碟片音轨号', builder: this.SubSortBuilder(SortMode.ByDiscTrack), symbolEndIcon: this.isSortTypeInRange(SortMode.ByDiscTrack) ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortModeItemBackground(this.isSortTypeInRange(SortMode.ByDiscTrack))) .visibility((this.modeType==0||(this.modeType==2&&this.isCanBack)|| (this.modeType==3&&this.isCanBack))?Visibility.Visible:Visibility.None) }.attributeModifier(new MenuModifier()) } @Builder SortMenuForArtistAblumBuilder() { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.textformat')), content: "按名称", builder: this.SubSortBuilder(SortMode.ByName), symbolEndIcon: this.isSortTypeInRange(SortMode.ByName) ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortModeItemBackground(this.isSortTypeInRange(SortMode.ByName))) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')), content: '按数量', builder: this.SubSortBuilder(SortMode.ByCount), symbolEndIcon: this.isSortTypeInRange(SortMode.ByCount) ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortModeItemBackground(this.isSortTypeInRange(SortMode.ByCount))) }.attributeModifier(new MenuModifier()) } @Builder SubSortBuilder(sortType: number){ Menu(){ MenuItem({ content: "升序", symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_up')), symbolEndIcon: this.sortType === sortType ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortOrderItemBackground(this.sortType === sortType, true)) .onClick(async () => { await this.doSortType(sortType) }) MenuItem({ content: "降序", symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_down')), symbolEndIcon: this.sortType === sortType + 1 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortOrderItemBackground(this.sortType === sortType + 1, false)) .onClick(async () => { await this.doSortType(sortType + 1) // 降序的sortType = 升序的sortType + 1 }) } } async doSortType(index: number): Promise { Logger.info('heanup LocalMusic', `doSortType: 切换排序方式 sortType=${index}, mode=${this.modeType}, isCanBack=${this.isCanBack}`); this.sortType = index; this.saveSortTypeForCurrentMode(index); // 详情页模式(modeType=2/3 且 isCanBack=true): 重新查询当前项目的歌曲 if (this.isCanBack && (this.modeType === 2 || this.modeType === 3)) { await this.reloadCurrentDetailSongs(); } else { // 分页模式下,排序需要重新从数据库查询 await this.resetAndLoadFirstPage(); } } /** * 重新加载当前详情页(专辑/艺术家)的歌曲数据 * 用于详情页切换排序方式时重新查询并排序 */ private async reloadCurrentDetailSongs(): Promise { Logger.info('heanup LocalMusic', `reloadCurrentDetailSongs: modeType=${this.modeType}, titleName=${this.titleName}, sortType=${this.sortType}`); try { if (this.modeType === 2) { // 艺术家详情页: 重新查询该艺术家的歌曲 const mList = await this.table.querySongsByArtist(this.titleName, this.sortType, this.isShowFileName); if (ArrayUtil.isNotEmpty(mList)) { // 使用带排序类型的缓存键 const cacheKey = `${this.titleName}_sort_${this.sortType}`; this.artistMap.set(cacheKey, mList); this.totalCount = mList.length; // 使用 noSort=true 避免在 updateListData 中重新排序导致死循环 this.updateListData(mList, true); Logger.info('heanup LocalMusic', `reloadCurrentDetailSongs: 艺术家详情页重新加载完成, 歌曲数=${mList.length}`); } } else if (this.modeType === 3) { // 专辑详情页: 重新查询该专辑的歌曲 const albumList = await this.table.querySongsByAlbum(this.titleName, this.sortType, this.isShowFileName); if (ArrayUtil.isNotEmpty(albumList)) { // 使用带排序类型的缓存键 const cacheKey = `${this.titleName}_sort_${this.sortType}`; this.albumMap.set(cacheKey, albumList); this.totalCount = albumList.length; // 使用 noSort=true 避免在 updateListData 中重新排序导致死循环 this.updateListData(albumList, true); Logger.info('heanup LocalMusic', `reloadCurrentDetailSongs: 专辑详情页重新加载完成, 歌曲数=${albumList.length}`); } } } catch (err) { const error = err as Error; Logger.error('heanup LocalMusic', `reloadCurrentDetailSongs error: ${error.message}`); ToastUtil.showToast('重新加载数据失败'); } } /** * 根据当前模式保存排序类型到不同的存储键 * 区分列表页和详情页的排序设置 */ private saveSortTypeForCurrentMode(sortType: number): void { let storageKey = SettingPage.SORT_TYPE; if (this.modeType === 2) { // 艺术家模式 if (this.isCanBack) { storageKey = 'SORT_TYPE_ARTIST_DETAIL'; // 艺术家详情页排序 } else { storageKey = 'SORT_TYPE_ARTIST'; // 艺术家列表页排序 } } else if (this.modeType === 3) { // 专辑模式 if (this.isCanBack) { storageKey = 'SORT_TYPE_ALBUM_DETAIL'; // 专辑详情页排序 } else { storageKey = 'SORT_TYPE_ALBUM'; // 专辑列表页排序 } } else { storageKey = SettingPage.SORT_TYPE; } PreferencesUtil.putSync(storageKey, sortType); Logger.info('heanup LocalMusic', `saveSortTypeForCurrentMode: mode=${this.modeType}, isCanBack=${this.isCanBack}, sortType=${sortType}, key=${storageKey}`); } /** * 根据当前模式加载排序类型 * 区分列表页和详情页的排序设置 */ private loadSortTypeForCurrentMode(): number { let storageKey = SettingPage.SORT_TYPE; let defaultSortType = 4; if (this.modeType === 2) { // 艺术家模式 if (this.isCanBack) { storageKey = 'SORT_TYPE_ARTIST_DETAIL'; // 艺术家详情页排序 defaultSortType = 4; // 详情页默认按名称升序 } else { storageKey = 'SORT_TYPE_ARTIST'; // 艺术家列表页排序 defaultSortType = 9; // 列表页默认按数量降序 } } else if (this.modeType === 3) { // 专辑模式 if (this.isCanBack) { storageKey = 'SORT_TYPE_ALBUM_DETAIL'; // 专辑详情页排序 defaultSortType = 4; // 详情页默认按名称升序 } else { storageKey = 'SORT_TYPE_ALBUM'; // 专辑列表页排序 defaultSortType = 9; // 列表页默认按数量降序 } } return PreferencesUtil.getNumberSync(storageKey, defaultSortType); } /** * 检查当前sortType是否在指定排序类型的范围内 * 用于判断是否显示打勾图标 * @param baseSortType 基础排序类型(升序的值) * @returns 如果当前sortType是该排序类型的升序或降序,返回true */ private isSortTypeInRange(baseSortType: number): boolean { return this.sortType === baseSortType || this.sortType === baseSortType + 1; } private getSortModeItemBackground(isSelected: boolean): ResourceColor { if (!isSelected) { return Color.Transparent; } return this.isDarkMode ? '#334967' : '#EAF2FF'; } private getSortOrderItemBackground(isSelected: boolean, isAsc: boolean): ResourceColor { if (!isSelected) { return Color.Transparent; } if (isAsc) { return this.isDarkMode ? '#295B8A' : '#DCEEFF'; } return this.isDarkMode ? '#7A4D22' : '#FFE9D5'; } showSheelDialog() { // if(PreferencesUtil.getBooleanSync('isFirstMusic',true)) { // this.showTips() // return // } this.showAddDialog() } showAddDialog() { if (this.modeType === 2 || this.modeType === 3) { return } DialogHelper.showActionSheetDialog({ title: "请选择添加方式(建议歌词文件一起导入)", 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 if( deviceInfo.sdkApiVersion>=21){//如果api大于21 ,可以支持1w首歌以上 documentSelectOptions.maxSelectNumber = 20000 } 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() { try { this.getUIContext().getPromptAction().showDialog({ title: '导入指南', message:'用系统自带的文件管理器将:\n\n1、音频文件放入' + '\n\n我的设备/Download/' + this.appName + '\n\n2、无需繁琐的勾选和分类步骤。\n\n3、每个子文件夹将被组成一个分组。' + '\n\n4、歌词文件的文件名必须和目标歌曲文件名相同(不包含后缀名)。\n\n5、歌词文件和目标歌曲文件必须同个目录。' + '\n\n6、电脑上Download的目录是\nDownload/' + this.packName + '。\n\n7、放好文件后去文件扫描一下。\n', buttons: [ { text: '知道了', color: '#000000' }, ] }, (err, data) => { if (err) { console.error('showDialog err: ' + err); return; } console.info('showDialog success callback, click button: ' + data.index); }); } catch (error) { let message = (error as BusinessError).message; let code = (error as BusinessError).code; console.error(`showDialog args error code is ${code}, message is ${message}`); }; } 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; } console.info('onecold this.isCopyFileToDownLoad = ' + this.isCopyFileToDownLoad); if(this.isCopyFileToDownLoad||this.currentPath==this.lockPath||isOpen){ this.saveVideoDatasToDownLoad(uris,isOpen) return } // 初始化进度条 this.progress = 0; this.loadingProgressDialogId = DialogHelper.showLoadingProgress({ progress: this.progress, backCancel: false, autoCancel: false, loadColor: $r('app.color.title_bar_bg'), fontColor: $r('app.color.title_bar_bg') }); // 计算处理总数用于进度计算 const totalItems = uris.length; let processedItems = 0; for (let i = 0; i < uris.length; i++) { let filePath = uris[i]; try { 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, this.autoParseMusicName); console.info('onecold saveVideoDatas filePath = ' + filePath) if (!filePath.includes(this.rootPath)) { console.info('onecold saveVideoDatas mediaItem.parentPath111 = ' + mediaItem.parentPath) mediaItem.parentPath = this.currentPath; } console.info('onecold saveVideoDatas mediaItem.parentPath = ' + mediaItem.parentPath) this.table.insert(mediaItem, (id: number) => { // 插入完成回调 }); } } // 更新进度 processedItems++; this.progress = Math.floor((processedItems / totalItems) * 100); DialogHelper.updateLoading(this.loadingProgressDialogId,` 正在处理 ${this.progress}%`, this.progress); } catch (error) { Logger.error(TAG, 'saveVideoDatas failed with err: ' + JSON.stringify(error)); // 即使出错也更新进度 processedItems++; this.progress = Math.floor((processedItems / totalItems) * 100); DialogHelper.updateLoading(this.loadingProgressDialogId,` 正在处理 ${this.progress}%`, this.progress); } } // 关闭进度条 DialogHelper.closeDialog(this.loadingProgressDialogId) this.isZero = false // 删除目标路径缓存 setTimeout(() => { this.cache.delete(this.currentPath); this.getSortedFiles(this.currentPath,false,uris[0],isOpen) this.setButtonStatus() }, 500); } async saveVideoDatasToDownLoad(uris: string[], isOpen?: boolean) { if (ArrayUtil.isEmpty(uris)) { return; } let newUris: string[] = []; this.progress = 0 this.loadingProgressDialogId = 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.loadingProgressDialogId,`正在导入 ${this.progress}%`, 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, this.autoParseMusicName); this.table.insert(mediaItem, (id: number) => { // 删除目标路径缓存 this.cache.delete(this.currentPath); this.getSortedFiles(this.currentPath, true, destPath, isOpen,mediaItem) }); } } } catch (error) { Logger.error(TAG, 'saveVideoDatas failed with err: ' + JSON.stringify(error)); } } DialogHelper.closeDialog(this.loadingProgressDialogId); this.isZero = false 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)); // } // // // // } async goSelectImage(item: VideoItem): Promise { if (!item) { return undefined; } try { let selectUris: Array = []; let photoPicker = new photoAccessHelper.PhotoViewPicker(); let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; photoSelectOptions.maxSelectNumber = 1; const photoSelectResult = await photoPicker.select(photoSelectOptions); selectUris = photoSelectResult.photoUris; console.info('photoViewPicker.select to file succeed and uris are:' + selectUris); let file = fs.openSync(selectUris[0], fs.OpenMode.READ_ONLY); console.info('file fd: ' + file.fd); let name = await MD5.digestSync(this.videoUrl)+'.jpg' 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 (this.currentSong) { this.currentSong.isCustomCover = 1; } } if (item) { item.pixelMapPath = imagePath item.isCustomCover = 1; } if (isRemoteCloudType(item.type)) { return FileUtil.getFilePath(imagePath); } this.table.updateCustomCoverPath(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) return FileUtil.getFilePath(imagePath); // Return the image path here } catch (err) { console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`); return undefined; } } private async pickCoverImageToSandbox(): Promise { try { const photoPicker = new photoAccessHelper.PhotoViewPicker(); const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; photoSelectOptions.maxSelectNumber = 1; const photoSelectResult = await photoPicker.select(photoSelectOptions); const selectUris = photoSelectResult.photoUris; if (!selectUris || selectUris.length === 0) { return undefined; } const coverDir = this.context.filesDir + FileUtil.separator + 'ttmusic_cover'; if (!FileUtil.accessSync(coverDir)) { FileUtil.mkdirSync(coverDir); } const sourceUri = selectUris[0]; const sourceName = FileUtil.getFileName(sourceUri) || ''; let suffix = '.jpg'; const dotIndex = sourceName.lastIndexOf('.'); if (dotIndex >= 0) { suffix = sourceName.substring(dotIndex); } const targetName = `${Date.now()}_${await MD5.digestSync(sourceUri)}${suffix}`; const targetPath = coverDir + FileUtil.separator + targetName; const sourceFile = fileIo.openSync(sourceUri, fileIo.OpenMode.READ_ONLY); const targetFile = fileIo.openSync(targetPath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); fileIo.copyFileSync(sourceFile.fd, targetFile.fd); fileIo.closeSync(sourceFile.fd); fileIo.closeSync(targetFile.fd); return fileUri.getUriFromPath(targetPath); } catch (error) { const err = error as BusinessError; Logger.error(TAG, `pickCoverImageToSandbox failed, code: ${err.code}, message: ${err.message}`); return undefined; } } private async replaceCurrentSongCoverByAlbum(): Promise { if (!this.currentSong || StrUtil.isEmpty(this.currentSong.filePath)) { ToastUtil.showToast('当前歌曲无效'); return; } const sandboxCoverUri = await this.pickCoverImageToSandbox(); if (StrUtil.isEmpty(sandboxCoverUri)) { return; } const coverUri = sandboxCoverUri as string; this.loadingDialogId = DialogHelper.showLoadingDialog(); try { if (isRemoteCloudType(this.currentSong.type)) { this.cover = coverUri; this.currentSong.pixelMapPath = coverUri; this.currentSong.isCustomCover = 1; this.syncRemoteManagerSong(this.currentSong); await this.persistRemoteMetadataToDb(this.currentSong); this.emitRemoteMetadataUpdated(this.currentSong); this.currentSong = cloneVideoItem(this.currentSong); AppStorage.setOrCreate('currentSong', this.currentSong); ToastUtil.showToast('更换封面成功'); return; } if (this.currentSong.type !== CommonConstants.TYPE_LOCAL) { ToastUtil.showToast('当前歌曲类型暂不支持更换封面'); return; } const task = new taskpool.Task( replaceLocalMusicCoverTask, this.context, this.currentSong.filePath, FileUtil.getFilePath(coverUri) ); const result = await taskpool.execute(task, taskpool.Priority.HIGH) as boolean; if (!result) { ToastUtil.showToast('更换封面失败'); return; } this.cover = coverUri; this.currentSong.pixelMapPath = coverUri; this.currentSong.isCustomCover = 1; await this.doUpateEditedFields(this.currentSong); ToastUtil.showToast('更换封面成功'); } catch (error) { Logger.error(TAG, `replaceCurrentSongCoverByAlbum failed: ${JSON.stringify(error)}`); ToastUtil.showToast('更换封面失败'); } finally { DialogHelper.closeDialog(this.loadingDialogId); } } // 拉起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)); } } //重命名的对话框 showReNameDialog(item: VideoItem, index: string, filePath: string) { if(!item.filePath.includes(this.packName)){ ToastUtil.showToast('请把该音频导入到本应用的DownLoad文件下,其他路径音频无权限重命名') return } DialogHelper.showTextInputDialog({ title: '重命名', text: item.type === CommonConstants.TYPE_IS_DIR ? item.name : (item.fileName || item.name), 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 targetName = item.fileName || item.name const newPath = rPath + '/' + mList[i] + '/' + targetName Logger.info('newPath = ' + newPath) FileUtil.moveFile(item.filePath, newPath, 1).then(async () => { ToastUtil.showToast('移动成功') this.table.deleteData(item, () => { }); if (!Utility.isIsoByExtension(newPath)) { let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName); 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 targetName = item.fileName || item.name const newPath = rPath + '/' + mList[i] + '/' + targetName Logger.info('newPath = ' + newPath) FileUtil.copyFile(item.filePath, newPath, 0).then(async () => { ToastUtil.showToast('复制成功') this.cache.delete(rPath + '/' + mList[i]); // 删除目标路径缓存 this.getSortedFiles(this.currentPath) if (!Utility.isIsoByExtension(newPath)) { let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName); 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)) { let refreshedItem: VideoItem = info 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) refreshedItem = new VideoItem(newName, newName, newPath, CommonConstants.TYPE_IS_DIR, 0, '') } else if (info.type === CommonConstants.TYPE_LOCAL) { if (Utility.isIsoByExtension(oldPath) || Utility.isIsoByExtension(newPath)) { refreshedItem = this.buildLocalIsoItem(newPath, newName) } else { this.table.updateRename(newName, oldPath, newPath, (success: boolean, error?: string) => { if (success) { console.log(" onecold 重命名成功,数据库已同步"); } else { console.error(" onecold 重命名数据库失败原因: " + error); } }); refreshedItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName); } } else { return } this.videoLocalList[index] = refreshedItem; } this.cache.delete(this.currentPath) this.cache.delete(oldPath) this.cache.delete(newPath) this.getSortedFiles(this.currentPath, true) }).catch((error: BusinessError) => { console.error(error.message); }); } build() { Stack() { this.ContentBuild() Stack() { Column() .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.getPlayListDisplayCount()}首)` } }) } .bindContentCover($$this.isShowPlay, this.MusicPlayBuilder(), { modalTransition: ModalTransition.DEFAULT, onWillDisappear: () => { this.setShowPlayFalse() }, }) } .alignContent(Alignment.Bottom) .width('100%') .height('100%') } @Builder ContentBuild() { Scroll() { Column() { Stack() { //libraryname加上去后onKeyEvent监听不了,因为目前的解决方案是上面的XComponent,去掉libraryname XComponent({ id: 'for_onKeyEvent',type: XComponentType.SURFACE, controller: this.xcomponentController }) .onLoad(async (event?: object) => { }) .focusable(true) .height(0) .focusOnTouch(true) .defaultFocus(true) // 自动获取键盘焦点 .onKeyEvent((event?: KeyEvent) => { console.info('onecold点击 onKeyEvent') // 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; } console.info('onecold点击 onKeyEvent event.keyCode: ' + event.keyCode) //空格键 Space key controls pause/play if (event.keyCode === KeyCode.KEYCODE_SPACE|| event.keyCode === KeyCode.KEYCODE_MEDIA_PLAY_PAUSE) { this.playbackController.playOrPause() } if (event.keyCode === KeyCode.KEYCODE_DPAD_UP) { void this.playbackController.playPrevious() } if (event.keyCode === KeyCode.KEYCODE_DPAD_DOWN) { void this.playbackController.playNext() } // Right-click to fast forward if (event.keyCode === KeyCode.KEYCODE_DPAD_RIGHT) { this.fastForward() } // Left-click to rewind if (event.keyCode === KeyCode.KEYCODE_DPAD_LEFT) { this.backForward() } }) Column() { XComponent({ id: 'xcomponentId', type: 'surface', libraryname: 'ijkplayer_napi', controller: this.xcomponentController }) .onLoad((event?: object) => { if (!!event) { this.initDelayPlay(event); } }) .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) .bindSheet($$this.isABSheet, this.ABLoopSheet(), { height:this.isLandscape?'80%': '43%', dragBar: true, showClose: true, preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM, blurStyle:BlurStyle.Thin, backgroundColor:Color.Transparent, title: { title: 'AB循环' } }) Stack() { Column() { if(this.twoFingerType==4){ this.getWaterView() }else{ if(this.isGridMusic){ this.getGridView() }else { this.getListView() } } if (this.modeType ==0||this.modeType==4) { Column() { emptyView(this.themeColor) } .padding({ right:6 }) .onClick(()=>{ this.asyncCurrentPathOnlyFile() }) .alignItems(HorizontalAlign.Center) .position({ top: 200 }) .justifyContent(FlexAlign.Center) .opacity(this.isZero ? 1 : 0) .visibility(this.isZero&&!this.isLoadingPage && !this.isFavMusic && !this.isHistory ? Visibility.Visible : Visibility.None) .animation({ duration: 500, curve: Curve.Friction // 可选动画曲线 }) .width('100%') } } .layoutWeight(1) .height('100%') //拖拽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}`); } }) // 右侧居中的字母表索引 this.indexBuilder() this.topTitleBar() } .alignContent(Alignment.Top) .layoutWeight(1) .height('100%') Row() { Button(this.isAllSelected ? '反选' : '全选', { type: ButtonType.Circle, stateEffect: true }) .width(45) .height(45) .fontSize(10) .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.syncLocalSelectionState(); }) Button('内嵌标签', { type: ButtonType.Capsule, stateEffect: true }) .width(72) .height(45) .fontSize(10) .margin({ top: 10, bottom: 10, right: 6 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .backgroundColor(this.themeColor) .enabled(!this.hasSelectedDirectories() && this.getSelectedPlayableFiles().length > 0) .bindContentCover($$this.isOneKeyTags, this.TagsContentCoverBuilder(), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) .onClick(() => { if (this.hasSelectedDirectories()) { ToastUtil.showToast('文件夹不支持批量内嵌标签') return } this.isOneKeyTags = !this.isOneKeyTags }) Button('合并', { type: ButtonType.Circle, stateEffect: true }) .width(45) .height(45) .fontSize(10) .margin({ top: 10, bottom: 10, right: 6 }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .backgroundColor(this.themeColor) .enabled(!this.hasSelectedDirectories() && this.getSelectedPlayableFiles().length > 0) .bindContentCover($$this.isMergeAudio, this.MergeAudioBuilder(), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) .onClick(() => { const selectedMediaFiles = this.getSelectedPlayableFiles() if (this.hasSelectedDirectories()) { ToastUtil.showToast('文件夹不支持合并') return } if(ArrayUtil.isNotEmpty(selectedMediaFiles)&&Utility.isMerge(selectedMediaFiles,this.packName)){ this.isMergeAudio = !this.isMergeAudio }else{ ToastUtil.showToast('选中的文件包含了不是本应用的DownLoad文件下,其他路径音频无权限合并') } }) Button('+歌单', { type: ButtonType.Circle, stateEffect: true }) .width(45) .height(45) .fontSize(10) .margin({ top: 10, bottom: 10, right: 6 }) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .enabled(!this.hasSelectedDirectories() && this.getSelectedPlayableFiles().length > 0) .onClick(async () => { this.longItemFilePath = '' const selectedMediaFiles = this.getSelectedPlayableFiles() if (ArrayUtil.isEmpty(selectedMediaFiles)) { ToastUtil.showToast('请先选择音频文件') return } await this.openAddToPlaylistDialog(selectedMediaFiles) }) // Button('乱码', { type: ButtonType.Circle, stateEffect: true }) // .width(45) // .height(45) // .fontSize(10) // .margin({ top: 10, bottom: 10, right: 6 }) // .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) // .backgroundColor(this.themeColor) // .bindContentCover($$this.isFixMessy, this.FixMessyViewBuilder(), { // transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) // }) // .onClick(() => { // this.isFixMessy = !this.isFixMessy // }) // 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(45) .height(45) .fontSize(10) .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(45) .height(45) .fontSize(10) .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({ bottom: 20 }) // 将 Row 固定在底部 } .width('100%') .height('100%') .alignContent(Alignment.Top) } .width('100%') .height('100%') .backgroundImage(this.isMusicCoverHomeBackgroundActive() ? this.customizeMusicBgPath : (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.isMusicCoverHomeBackgroundActive() || this.isCustomizeBg ? 0.1 : 0, lightUpDegree: this.bgBrightness }) .backgroundBlurStyle(this.isMusicCoverHomeBackgroundActive() ? BlurStyle.BACKGROUND_ULTRA_THICK : BlurStyle.NONE) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) } } //字母表索引 @Builder indexBuilder(){ Column({space: 5}){ if(this.showZMIndex&&this.enableShowAlphabet){ if(this.isShowAlphaBet&&!(this.modeType==2&&!this.isCanBack) &&!(this.modeType==3&&!this.isCanBack)&&(this.sortType==4||this.sortType==5)){ AlphabetIndexer({arrayValue: this.alphaBet,selected: this.selectAlphaBetIndex}) .attributeModifier(this.alphaBetStyle) .usingPopup(this.isTouchingAlphaBet) .onAreaChange((old_value,new_value)=>{ this.alphabetHeight = new_value.height as number }) .popupPosition({ x: 40, y: this.alphabetHeight / 2 }) .onSelect((index: number) => { void this.onSelectIndexItem(this.alphaBet[index]) }) .onTouch((event: TouchEvent) =>{ this.showAlphabetIndex() if(event.type === TouchType.Up || event.type === TouchType.Cancel){ this.isTouchingAlphaBet = false this.hideAlphabetIndexAfterDelay(1500) }else{ this.isTouchingAlphaBet = true } }) .transition( TransitionEffect.translate({x: 10}) .combine(TransitionEffect.opacity(0.1)) .animation({ duration: 150, curve: Curve.EaseInOut, }) ) } } if(this.showFindLocation){ if(!(this.showZMIndex&&this.enableShowAlphabet)){ Blank() .layoutWeight(1) } PointLightDefaultButton({ isSysBol:true, pointColor:Color.Gray, imageResource:$r('sys.symbol.local_fill'), isPx: false, builderHeight: 30, builderWidth: 30, }) .onClick(() =>{ void this.locateCurrentSong(); }) .clickEffect({level:ClickEffectLevel.LIGHT,scale: 0.5}) .margin({ bottom: this.bottomSafeHeight+100,right:10 }) } } .padding({ bottom: 0 }) .margin({right:5}) .position({x:'92%'}) .height('100%') .justifyContent(FlexAlign.Center) .bindSheet($$this.isEqualizerSheet, this.EqualizerSheet(), { height: '100%', dragBar: true, showClose: true, preferType: SheetType.CENTER , blurStyle: BlurStyle.Thin, title: { title: '均衡器' }, onDisappear: () => { this.isEqualizerSheet = false } }) } async onSelectIndexItem(letter: string) { await this.ensureFullListForAlphabetIndex(); const findTargetIndex = (): number => { const songs = this.videoLocalList; for (let i = 0; i < songs.length; i++) { const song = songs[i]; let text = this.isShowFileName ? song.fileName : (song.type != CommonConstants.TYPE_IS_DIR && song.pyStr) ? song.pyStr : song.name; if (this.sortType === 0 || this.sortType == 1) { text = song.artist; } else if (this.sortType === 2 || this.sortType == 3) { text = song.album; } if (text) { const firstPinyin = getFirstLetter(text); if (firstPinyin && firstPinyin === letter) { return i; } } } return -1; }; const targetIndex = findTargetIndex(); if (targetIndex !== -1) { if (this.isGridMusic || this.twoFingerType == 4) { this.scroller.scrollToIndex(targetIndex, false); } else { this.listScroller.scrollToIndex(targetIndex); } } } private showAlphabetIndex(): void { this.isShowAlphaBet = true; if (this.alphaBetHideTimer) { clearTimeout(this.alphaBetHideTimer); this.alphaBetHideTimer = 0; } } private hideAlphabetIndexAfterDelay(delayMs: number): void { if (this.alphaBetHideTimer) { clearTimeout(this.alphaBetHideTimer); } this.alphaBetHideTimer = setTimeout(() => { if (!this.isTouchingAlphaBet) { this.isShowAlphaBet = false; } this.alphaBetHideTimer = 0; }, delayMs) as number; } private scheduleAlphaBetRebuild(delayMs: number = 120): void { if (!this.showZMIndex) { return; } if (this.alphaBetRebuildTimer) { clearTimeout(this.alphaBetRebuildTimer); } this.alphaBetRebuildTimer = setTimeout(() => { this.alphaBetRebuildTimer = 0; if (this.isScrolling) { this.alphaBetDirty = true; return; } this.alphaBetDirty = false; void this.setAlphaBet(); }, delayMs) as number; } private markAlphaBetDirty(): void { if (!this.showZMIndex) { return; } if (this.isMediaLibraryWarmupWindow()) { this.alphaBetDirty = true; this.scheduleAlphaBetRebuild(900); return; } this.alphaBetDirty = true; if (!this.isScrolling) { this.scheduleAlphaBetRebuild(80); } } private async ensureFullListForAlphabetIndex(): Promise { if (this.isLoadingPage) { return; } if (this.isCanBack || this.isHistory || this.isFavMusic) { return; } if (this.totalCount === 0 || this.videoLocalList.length >= this.totalCount) { return; } if (this.modeType !== 0 && this.modeType !== 1) { return; } this.isLoadingPage = true; try { const options: PageQueryOptions = { pageIndex: 0, pageSize: this.totalCount, sortType: this.sortType, searchKeyword: this.isSearchMode ? this.searchText : undefined, isShowFileName: this.isShowFileName }; if (this.modeType === 0) { options.parentPath = this.currentPath; } let result: PageQueryResult; if (this.modeType === 0) { result = await this.table.queryByParentPathPaged(options); const dbItems = this.filterOutLocalIsoItems(result.items); if (!this.isSearchMode) { const directories: VideoItem[] = await this.loadDirectories(this.currentPath); const isoItems: VideoItem[] = this.getLocalIsoItems(this.currentPath); result.items = [...directories, ...isoItems, ...dbItems]; } else { result.items = dbItems; } } else { result = await this.table.queryMediaLibraryPaged(options); } this.videoLocalList = result.items; this.dataSource.pushArrayData(this.videoLocalList); this.pageCache.clear(); this.pageHasMoreCache.clear(); this.pageTotalCountCache.clear(); this.prefetchingPages.clear(); this.currentPage = 0; this.totalCount = this.modeType === 0 ? result.items.length : result.totalCount; this.hasMoreData = false; this.lastScrollTriggerPage = -1; this.lastVisibleEndIndex = -1; this.isPagingTransition = false; if (this.pagingScheduleTimer) { clearTimeout(this.pagingScheduleTimer); this.pagingScheduleTimer = 0; } if (this.modeType === 1) { this.mediaKuCount = result.totalCount; } this.setButtonStatus(); this.markAlphaBetDirty(); } catch (err) { const error = err as Error; Logger.warn('heanup LocalMusic', `ensureFullListForAlphabetIndex error: ${error.message}`); } finally { this.isLoadingPage = false; } } // 找到对应字母表的索引 findAlphaBetIndex(song: VideoItem):number{ let text = this.isShowFileName?song.fileName: (song.type!=CommonConstants.TYPE_IS_DIR&&song.pyStr)?song.pyStr:song.name if(this.sortType === 0||this.sortType==1){ text = song.artist }else if(this.sortType === 2||this.sortType==3){ text = song.album } if(text){ let firstLetter = getFirstLetter(text) for(let i=0;i): Array { const items: Array = [] for (const song of songs) { items.push({ type: song.type, name: song.name, pyStr: song.pyStr, fileName: song.fileName, artist: song.artist, album: song.album }) } return items } private async buildAlphaBetByTask(songs: Array, version: number): Promise { const taskItems = this.buildAlphaTaskItems(songs) const task = new taskpool.Task(buildAlphabetIndexTask, taskItems, this.sortType, this.isShowFileName) const result = await taskpool.execute(task, taskpool.Priority.MEDIUM) as Array if (version !== this.alphaBetBuildVersion) { return } this.alphaBet = result if (this.videoLocalList.length > 0) { this.selectAlphaBetIndex = 0 } } async setAlphaBet(){ if(!this.showZMIndex){ return } if (this.modeType === 1 && this.totalCount > this.videoLocalList.length) { await this.refreshAlphaBetFromMediaLibrary() return } try { const songs = this.videoLocalList if (songs.length > 40000) { this.alphaBet = [] return } const version = ++this.alphaBetBuildVersion await this.buildAlphaBetByTask(songs, version) } catch (e) { console.error('testTag','排序字母表出错',JSON.stringify(e)) } } private async refreshAlphaBetFromMediaLibrary(): Promise { if (this.totalCount <= 0) { return } if (this.totalCount > 40000) { this.alphaBet = [] return } const keyword = this.isSearchMode ? this.searchText : '' const cacheKey = `${this.modeType}-${this.sortType}-${this.isShowFileName}-${keyword}-${this.totalCount}` if (this.alphaBetCacheKey === cacheKey && this.alphaBet.length > 0) { return } const result = await this.table.queryMediaLibraryPaged({ pageIndex: 0, pageSize: this.totalCount, sortType: this.sortType, searchKeyword: this.isSearchMode ? this.searchText : undefined, isShowFileName: this.isShowFileName }) const version = ++this.alphaBetBuildVersion await this.buildAlphaBetByTask(result.items, version) this.alphaBetCacheKey = cacheKey } @Builder topTitleBar(){ Column() { Row({ space: 15 }) { if (!this.isSearchMode ) { //左侧滑动按钮 TitleBarPointLightButton({ iconResource: this.rightTopImage, iconSize: 25, pointColor: this.themeColor, clickScale: 0.6, clickHandler: () => { this.triggerSwipeBackWithAnimation() } }) }else{ //左侧搜索返回按钮 TitleBarPointLightButton({ iconResource: $r('sys.symbol.chevron_left'), iconSize: 25, pointColor: this.themeColor, clickScale: 0.6, clickHandler: () => { this.isSearchMode = false if (this.modeType==0) {//如果是首页 直接getSortedFiles,如果不是就走this.onSearchInput('') this.searchText = '' this.getSortedFiles(this.currentPath) return } this.onSearchInput('') } }) .margin({bottom:5}) } if (!this.isSearchMode) { Text(this.modeType==1?`${this.titleName}(${this.mediaKuCount}首)`:this.titleName) .margin({left:3,right:10}) .fontColor($r('app.color.text_color')) .fontSize(this.twoFingerType==3?17:16) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) } //搜索框 Search({ controller: this.searchController,value: this.searchText, placeholder: '搜索标题、艺术家...' }) .searchButton('搜索',{fontColor:this.themeColor}) .searchIcon({ src: $r('sys.media.ohos_ic_public_search_filled') }) .cancelButton({ style: CancelButtonStyle.CONSTANT, icon: { src: $r('sys.media.ohos_ic_public_cancel_filled') } }) .layoutWeight(1) .height(40) .maxLength(20) .backgroundColor(this.isDarkMode?Color.Black:'#F5F5F5') .placeholderColor(Color.Grey) .placeholderFont({ size: 14, weight: 400 }) .textFont({ size: 14, weight: 400 }) .onSubmit((value: string) => { console.log('onecold onSubmit ='+value) this.searchController.stopEditing() if(this.modeType==0){ this.modeType =1//如果是首页 切换到媒体库搜索 } this.commitSearchHistory(value) this.onSearchInput(this.searchText); }) .onChange((value: string) => { console.log('onecold onChange ='+value) if(this.modeType==0){ this.modeType =1//如果是首页 切换到媒体库搜索 } this.onSearchInput(value); }) .visibility(this.isSearchMode?Visibility.Visible:Visibility.None) .animation({ duration: 300, curve: Curve.Ease }) //搜索按钮 if (!this.isSearchMode) { TitleBarPointLightButton({ iconResource: $r('sys.symbol.magnifyingglass'), iconSize: 25, pointColor: this.themeColor, clickScale: 0.6, clickHandler: () => { this.isSearchMode = true this.loadSearchHistory() } }) //排序按钮 TitleBarPointLightButton({ iconResource: $r('sys.symbol.list_number'), iconSize: 25, pointColor: this.themeColor, clickScale: 0.6, menuBuilder: () => { if ((this.modeType==2&&!this.isCanBack)||(this.modeType==3&&!this.isCanBack)) { this.SortMenuForArtistAblumBuilder() return } this.SortMenuBuilder() } }) //添加按钮 TitleBarPointLightButton({ iconResource: $r('sys.symbol.plus'), iconSize: 25, pointColor: this.themeColor, clickScale: 0.6, clickHandler: () => { this.showAddSheet = !this.showAddSheet; } }) .visibility(this.modeType==0||this.modeType==1?Visibility.Visible:Visibility.None) .bindSheet($$this.showAddSheet, this.addSheet(), { height: SheetSize.FIT_CONTENT, dragBar: true, showClose: true, preferType: SheetType.CENTER , title: { title: '导入音乐' } }) } } } .padding({ top: this.topSafeHeight+10, left: 10, right: 10 }) .width('100%') .translate({ x: 0, y: (this.isShowTitleBar || !this.autoHideTitle) ? 0 : -80 }) .opacity((this.isShowTitleBar || !this.autoHideTitle) ? 1 : 0) .animation({ duration: 240, curve: Curve.EaseInOut }) } @Builder addSheet() { Scroll() { Column() { Column() { Row() { SymbolGlyph($r('sys.symbol.info_circle')) .fontSize(22) .fontColor([this.themeColor]) .alignSelf(ItemAlign.Center) .margin({ left: 8 }) Text('导入音乐复制到DownLoad目录') .margin({ left: 8 }) .fontSize(14) .fontColor($r('app.color.text_color')) .fontWeight(480) .layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.isCopyFileToDownLoad }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 2 }) .onChange((checked: boolean) => { this.isCopyFileToDownLoad = checked; PreferencesUtil.put(SettingPage.IS_COPYFILE_TO_DOWNLOAD, this.isCopyFileToDownLoad) }) .width(50) .height(30); } .height(72) .padding({ left:8,right:8 }) } .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .backgroundColor($r('app.color.start_window_background')) .borderRadius(15) .width('85%') .margin({ top: 10, bottom: 10 }) .padding(0) .border({ color: Color.White, width: 1.8 }) Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.folder_badge_plus')), content: "新建文件夹" }) .backgroundColor($r('app.color.silvery')) .border({ width: { bottom: 1 }, color: '#12ec5c87' }) .margin(10) .height(60) .padding({left:15,right:15}) .onClick(async () => { this.showMkDialog() this.showAddSheet = !this.showAddSheet }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.folder')), content: $r('app.string.import_from_files') }) .padding({left:15,right:15}) .backgroundColor($r('app.color.silvery')) .border({ width: { bottom: 1 }, color: '#12ec5c87' }) .margin(10) .height(60) .onClick(async () => { this.callFilePickerSelectFile() this.showAddSheet = !this.showAddSheet }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square')), content: '全选导入' // content: '从下载文件夹导入(路径:Download/' + // PathUtils.appContext!.resourceManager.getStringSync($r('app.string.EntryAbility_label').id) + ')' }) .padding({left:15,right:15}) .backgroundColor($r('app.color.silvery')) .border({ width: { bottom: 1 }, color: '#12ec5c87' }) .margin(10) .height(60) .onClick(async () => { this.goSelectMusic() this.showAddSheet = !this.showAddSheet }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.save')), content: '自动导入教程' }) .padding({left:15,right:15}) .backgroundColor($r('app.color.silvery')) .border({ width: { bottom: 1 }, color: '#12ec5c87' }) .margin(10) .height(60) .onClick(async () => { this.showTips() this.showAddSheet = !this.showAddSheet }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.questionmark_circle')), content: '导入疑问' }) .height(60) .padding({left:15,right:15}) .backgroundColor($r('app.color.silvery')) .border({ width: { bottom: 1 }, color: '#12ec5c87' }) .margin(10) .onClick(async () => { this.showProblomTips() this.showAddSheet = !this.showAddSheet }) }.attributeModifier(new MenuModifier()) .margin({bottom:30}) .width('90%') .borderRadius(15) } .layoutWeight(1) } } async showProblomTips() { try { this.getUIContext().getPromptAction().showDialog({ title: '导入疑问', message:'为什么有些歌曲导入成功后,内嵌标签或者其他功能不能用?:\n\n1、由于鸿蒙系统的安全策略,文件权限不够,请将文件移动到Download/天天静听目录下。' + '这样内嵌标签功能就没有权限问题。\n\n2、导入可以也选择不复制,这样可以省略空间。\n\n', buttons: [ { text: '知道了', color: '#000000' }, ] }, (err, data) => { if (err) { console.error('showDialog err: ' + err); return; } console.info('showDialog success callback, click button: ' + data.index); }); } catch (error) { let message = (error as BusinessError).message; let code = (error as BusinessError).code; console.error(`showDialog args error code is ${code}, message is ${message}`); }; } @Builder TagsContentCoverBuilder() { Scroll() { Column() { TagsContentCover( { selectedFiles:this.selectedFiles, isOneKeyTags:this.isOneKeyTags, onSeePath:(path:string)=>{ this.modeType = 0 this.deleteCache(path) this.getSortedFiles(path) }, onTagsResult:(result: boolean,deleteCache_path?:string)=>{ this.isMultiSelect = false if(result){//如果嵌入成功,更新数据 this.listRefreshKey++ setTimeout(()=>{ this.doUpdateData() },500) } console.info('onecold onTagsResult deleteCache_path =' +deleteCache_path) if(deleteCache_path){ this.deleteCache(deleteCache_path) } } } ) } } .width('100%') .height('100%') } @Builder FixMessyViewBuilder() { Scroll() { Column() { FixMessyView( { selectedFiles:this.selectedFiles, isFixMessy:this.isFixMessy, onFixResult:(result: boolean)=>{ this.isMultiSelect = false if(result){//如果嵌入成功,更新数据 this.deleteCache(this.currentPath) this.doUpdateData() } } } ) } } .width('100%') .height('100%') } //剪辑音频 @Builder EditAudioBuilder(item:VideoItem) { Scroll() { Column() { EditAudio( { mVideoItem:item, currentPath:this.currentPath, videoUrl:this.videoUrl, CONTROL_PlayStatus:this.CONTROL_PlayStatus, currentTime:this.currentTime, progressValue:this.progressValue, onBack:()=>{ this.isEditAudio = !this.isEditAudio }, onSeeOutputPath:(outPutPath:string)=>{ this.isEditAudio = !this.isEditAudio this.longItemFilePath = '' this.modeType = 0 this.isShowFileName = true this.getSortedFiles(FileUtil.getParentPath(outPutPath)) }, onPlayOrPause:()=>{ this.dispatchTogglePlayForItem(item) }, onSliderChange:(value:number)=>{ LogUtils.getInstance().LOGI("slider-->seekValue start:" + value); const seekValue = MusicPlaybackController.resolveSeekValueFromPercent(value, this.getActiveDuration()) this.dispatchSeekFromEditorSlider(value, item.filePath) LogUtils.getInstance().LOGI("slider-->seekValue end:" + seekValue); }, onResult:(result: boolean,outPath:string)=>{ if(result){//如果剪辑成功,更新数据 this.doUpdateData() }else{ ToastUtil.showToast('剪辑失败') } } } ) } } .width('100%') .height('100%') } //提取伴奏 @Builder ConverterBuilder(item:VideoItem) { Scroll() { Column() { AudioFormatConverter( { mVideoItem:item, videoUrl:this.videoUrl, currentPath:this.currentPath, CONTROL_PlayStatus:this.CONTROL_PlayStatus, currentTime:this.currentTime, progressValue:this.progressValue, onBack:()=>{ this.isConverter = !this.isConverter this.longItemFilePath = '' }, onPlayOrPause:()=>{ this.dispatchTogglePlayForItem(item) }, onSeeOutputPath:(outPutPath:string)=>{ this.isConverter = !this.isConverter this.longItemFilePath = '' this.modeType = 0 this.isShowFileName = true this.getSortedFiles(FileUtil.getParentPath(outPutPath)) }, onSliderChange:(value:number)=>{ this.dispatchSeekFromEditorSlider(value, item.filePath) }, onResult:(result: boolean,outPath:string)=>{ if(result){//如果成功,更新数据 this.doUpdateData() }else{ ToastUtil.showToast('格式转化失败') } } } ) } } .width('100%') .height('100%') } //提取伴奏 @Builder ExtractAccBuilder(item:VideoItem) { Scroll() { Column() { ExtractAccompaniment( { mVideoItem:item, currentPath:this.currentPath, videoUrl:this.videoUrl, CONTROL_PlayStatus:this.CONTROL_PlayStatus, currentTime:this.currentTime, progressValue:this.progressValue, onBack:()=>{ this.isExtractAcc = !this.isExtractAcc this.longItemFilePath = '' }, onPlayOrPause:()=>{ this.dispatchTogglePlayForItem(item) }, onSeeOutputPath:(outPutPath:string)=>{ this.isExtractAcc = !this.isExtractAcc this.longItemFilePath = '' this.modeType = 0 this.isShowFileName = true this.getSortedFiles(FileUtil.getParentPath(outPutPath)) }, onSliderChange:(value:number)=>{ this.dispatchSeekFromEditorSlider(value, item.filePath) }, onResult:(result: boolean,outPath:string)=>{ if(result){//如果成功,更新数据 this.doUpdateData() }else{ ToastUtil.showToast('提取伴奏失败') } } } ) } } .width('100%') .height('100%') } //AI人声分离 @Builder AiBuilder(item:VideoItem) { Scroll() { Column() { AIVoiceSeparation( { mVideoItem:item, videoUrl:this.videoUrl, currentPath:this.currentPath, CONTROL_PlayStatus:this.CONTROL_PlayStatus, currentTime:this.currentTime, progressValue:this.progressValue, onBack:()=>{ this.isAIVoiceSeparation = !this.isAIVoiceSeparation this.longItemFilePath = '' }, onPlayOrPause:()=>{ this.dispatchTogglePlayForItem(item, true) }, onPlayPath:(path:string)=>{ this.videoUrl==path const itemP = Utility.getItemByFilePath(this.videoLocalList,path) if(itemP) this.doPlay(itemP,0,false,true) }, onSeeOutputPath:(outPutPath:string)=>{ this.isAIVoiceSeparation = !this.isAIVoiceSeparation this.longItemFilePath = '' this.modeType = 0 this.isShowFileName = true this.getSortedFiles(FileUtil.getParentPath(outPutPath)) }, onSliderChange:(value:number)=>{ this.dispatchSeekFromEditorSlider(value, item.filePath) }, onResult:(result: boolean,outPath:string)=>{ if(result){//如果成功,更新数据 this.doUpdateData() }else{ ToastUtil.showToast('Ai人声分离失败') } } } ) } } .width('100%') .height('100%') } //变速变调 @Builder ChangeToneBuilder(item:VideoItem) { Scroll() { Column() { ChangeTone( { mVideoItem:item, currentPath:this.currentPath, videoUrl:this.videoUrl, CONTROL_PlayStatus:this.CONTROL_PlayStatus, currentTime:this.currentTime, progressValue:this.progressValue, onBack:()=>{ this.isChangeTone = !this.isChangeTone this.longItemFilePath = '' }, onPlayOrPause:()=>{ this.dispatchTogglePlayForItem(item) }, onSeeOutputPath:(outPutPath:string)=>{ this.isChangeTone = !this.isChangeTone this.longItemFilePath = '' this.modeType = 0 this.isShowFileName = true this.getSortedFiles(FileUtil.getParentPath(outPutPath)) }, onSliderChange:(value:number)=>{ this.dispatchSeekFromEditorSlider(value, item.filePath) }, onResult:(result: boolean,outPath:string)=>{ if(result){//如果成功,更新数据 this.doUpdateData() }else{ ToastUtil.showToast('变速变调失败') } } } ) } } .width('100%') .height('100%') } //提取视频中的音频 @Builder extractAudioFromVideoBuilder(item:VideoItem) { Scroll() { Column() { ExtractAudioFromVideo( { mVideoItem:item, currentPath:this.currentPath, videoUrl:this.videoUrl, CONTROL_PlayStatus:this.CONTROL_PlayStatus, currentTime:this.currentTime, progressValue:this.progressValue, onBack:()=>{ this.isExtractAudio = !this.isExtractAudio this.longItemFilePath = '' }, onPlayOrPause:()=>{ this.dispatchTogglePlayForItem(item) }, onSeeOutputPath:(outPutPath:string)=>{ this.isExtractAudio = !this.isExtractAudio this.longItemFilePath = '' this.modeType = 0 this.isShowFileName = true this.getSortedFiles(FileUtil.getParentPath(outPutPath)) }, onSliderChange:(value:number)=>{ this.dispatchSeekFromEditorSlider(value, item.filePath) }, onResult:(result: boolean,outPath:string)=>{ if(result){//如果成功,更新数据 this.doUpdateData() }else{ ToastUtil.showToast('提取音频失败') } } } ) } } .width('100%') .height('100%') } //合并音频 @Builder MergeAudioBuilder() { Scroll() { Column() { MergeAudio( { videoUrl:this.videoUrl, CONTROL_PlayStatus:this.CONTROL_PlayStatus, currentPath:this.currentPath, selectedFiles:this.selectedFiles, currentTime:this.currentTime, progressValue:this.progressValue, onBack:()=>{ this.isMergeAudio = !this.isMergeAudio this.longItemFilePath = '' }, // onPlayPath:(path:string)=>{ // if(this.videoUrl==item.filePath){ // this.playOrPause() // }else{//如果不是当前播放的,则播放该文件 // this.videoUrl==item.filePath // this.doPlay(item,0,false,true) // } // // }, onSeeOutputPath:(outPutPath:string)=>{ this.isMergeAudio = !this.isMergeAudio this.longItemFilePath = '' this.modeType = 0 this.isShowFileName = true this.getSortedFiles(FileUtil.getParentPath(outPutPath)) }, onResult:(result: boolean,outPath:string)=>{ if(result){//如果成功,更新数据 this.doUpdateData() }else{ if(this.selectedFiles.length>=3){ ToastUtil.showToast('合并数量太多导致失败') }else{ ToastUtil.showToast('合并失败') } } } } ) } } .width('100%') .height('100%') } asyncCurrentPathData() { if (this.modeType == 0) { workerInstance.postMessage({ code: 1, data1: this.context, data2: this.currentPath, data3: this.lockPath, data4: PreferencesUtil.getStringSync('COVER_API', ''), data5: PreferencesUtil.getBooleanSync('autoParseMusicName', true), }); } } // 处理文件选择的函数 handleFileSelection(item: VideoItem, checked: boolean) { if (!this.isSelectableLocalItem(item)) { return } const exists = this.isLocalItemSelected(item) if (checked) { if (!exists) { this.selectedFiles = [...this.selectedFiles, item]; } } else if (exists) { this.selectedFiles = this.selectedFiles.filter(selected => selected.filePath !== item.filePath); if (this.selectedFiles.length === 0) { this.isAllSelected = false } } this.syncLocalSelectionState() } private deleteComponentId: number = 0 private closeDeleteDialog(afterClose?: () => void): void { if (this.deleteComponentId > 0) { try { this.getUIContext().getPromptAction().closeCustomDialog(this.deleteComponentId) } catch (_error) { } this.deleteComponentId = 0 } if (afterClose) { setTimeout(() => { afterClose() }, 16) } } private resetDeleteSelectionState(): void { this.isMultiSelect = false this.selectedFiles = [] this.isAllSelected = false } private removeDisplayedLocalItemsByKeys(deletedKeys: Set): void { for (let index = this.dataSource.dataArray.length - 1; index >= 0; index--) { const item = this.dataSource.dataArray[index] if (item && deletedKeys.has(item.filePath)) { this.dataSource.deleteData(index) } } } private async removeDeletedLocalItemsLocally(deletedItems: Array): Promise { if (ArrayUtil.isEmpty(deletedItems) || this.modeType !== 0) { return } const deletedKeys: Set = new Set() for (let i = 0; i < deletedItems.length; i++) { const item = deletedItems[i] if (StrUtil.isNotEmpty(item?.filePath)) { deletedKeys.add(item.filePath) } } if (deletedKeys.size <= 0) { return } const nextVideoLocalList = this.videoLocalList.filter((item: VideoItem) => !deletedKeys.has(item.filePath)) const nextDirList = this.dirList.filter((item: VideoItem) => !deletedKeys.has(item.filePath)) const nextFilteredList = this.filteredList.filter((item: VideoItem) => !deletedKeys.has(item.filePath)) const removedCount = this.videoLocalList.length - nextVideoLocalList.length this.videoLocalList = nextVideoLocalList this.dirList = nextDirList this.filteredList = nextFilteredList this.removeDisplayedLocalItemsByKeys(deletedKeys) if (removedCount > 0 && this.totalCount > 0) { this.totalCount = Math.max(0, this.totalCount - removedCount) } this.pageCache.clear() this.pageHasMoreCache.clear() this.pageTotalCountCache.clear() this.setButtonStatus() this.markAlphaBetDirty() this.syncLocalSelectionState() this.addCache(this.currentPath, nextVideoLocalList) if (this.currentPath === this.rootPath) { this.addCache(this.currentPath + '_dirList', nextDirList) } this.deleteStringCache(this.currentPath + '_hash') await this.saveCacheToStorage() } //多选删除文件 showWarnIsDelete(){ this.getUIContext().getPromptAction().openCustomDialog({ builder: () => { this.deleteComptentBuilder() }, isModal:true, showInSubWindow:false, maskColor: Color.Transparent, dialogTransition: // 设置弹窗内容显示的过渡效果 TransitionEffect.translate({ x: 0, y: 120, z: 0 }) .combine(TransitionEffect.opacity(0.01)) .animation({ duration: 260, curve: Curve.EaseOut }), maskTransition: // 设置蒙层显示的过渡效果 TransitionEffect.opacity(0) .animation({ duration: 220, curve: Curve.EaseOut }) }).then((dialogId: number) => { this.deleteComponentId = dialogId }) .catch((error: BusinessError) => { console.error(`openCustomDialog error code is ${error.code}, message is ${error.message}`) }) } @Builder deleteComptentBuilder(){ DeleteComptent({ selectedFiles:this.selectedFiles, isPlaylistMode: this.modeType == 4, playlistId: this.currentSongListID, onCancel:()=>{ this.closeDeleteDialog(() => { this.resetDeleteSelectionState() }) }, onDeleteResult:(result: boolean)=>{ const deletedItems = [...this.selectedFiles] this.closeDeleteDialog(() => { this.resetDeleteSelectionState() if(result){//如果删除成功,更新数据 if (this.modeType == 4) { this.doSongListTask() } else { void this.removeDeletedLocalItemsLocally(deletedItems) } } }) } }) } //多选移动文件 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 targetName = item.fileName || item.name; const newPath = newDirPath + '/' + targetName; 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) if (!Utility.isIsoByExtension(newPath)) { let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName); 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(); } } /** * 加载所有歌单 */ async loadAllPlaylists() { try { this.allPlaylists = await this.playlistTable?.queryAllPlaylists()! LogUtil.info('heanup Loaded playlists: ' + this.allPlaylists.length) } catch (error) { LogUtil.error('heaunp Failed to load playlists: ' + error.message) } } /** * 显示添加到歌单对话框 */ async openAddToPlaylistDialog(items: VideoItem | VideoItem[]) { const songs = Array.isArray(items) ? items : [items] const musicItems = songs.filter(song => song.type !== CommonConstants.TYPE_IS_DIR && !this.isIsoLocalFile(song)) if (musicItems.length === 0) { ToastUtil.showToast('请选择要添加的歌曲') return } // 加载最新的歌单列表 await this.loadAllPlaylists() showAddToPlaylistDialog( musicItems, this.allPlaylists, async (playlistId: string) => { // 添加歌曲到歌单 const success = await this.playlistTable?.addSongsToPlaylist(playlistId, musicItems.map(song => song.filePath)) if (success) { const count = musicItems.length ToastUtil.showToast(count > 1 ? `已添加 ${count} 首歌曲` : '已添加到歌单') // 重新加载歌单列表 await this.loadAllPlaylists() } else { ToastUtil.showToast(musicItems.length > 1 ? '选中的歌曲已在该歌单中' : '歌曲已在该歌单中') } this.longItemFilePath = '' if (this.isMultiSelect) { this.isMultiSelect = false this.isAllSelected = false this.selectedFiles = [] } }, () => { LogUtil.info('取消添加到歌单') }, () => { // 创建新歌单 showCreatePlaylistDialog( async (name: string, description: string, coverPath?: string) => { const success = await this.playlistTable?.createPlaylist(name, description, coverPath) if (success) { ToastUtil.showToast('歌单创建成功') // 重新加载歌单列表并打开添加对话框 await this.loadAllPlaylists() await this.openAddToPlaylistDialog(musicItems) } else { ToastUtil.showToast('歌单创建失败') } }, () => { // 取消创建歌单 } ) } ) } private syncFavStateLocal(item: VideoItem, isFav: number): void { const nextFavList = [...this.favList]; const itemFavKey = Utility.getFavMatchKey(item); const favIndex = nextFavList.findIndex((favItem: VideoItem) => Utility.getFavMatchKey(favItem) === itemFavKey); if (isFav === 1) { if (favIndex >= 0) { nextFavList[favIndex].isFav = 1; } else { const favItem = cloneVideoItem(item); favItem.isFav = 1; nextFavList.push(favItem); } } else if (favIndex >= 0) { nextFavList.splice(favIndex, 1); } this.favList = nextFavList; item.isFav = isFav; if (this.currentSong && Utility.getFavMatchKey(this.currentSong) === itemFavKey) { this.currentSong.isFav = isFav; this.isFac = isFav === 1; } } private syncFavoriteStateForPlainList(list: Array | undefined, targetKey: string, isFav: number): boolean { if (!list || ArrayUtil.isEmpty(list) || StrUtil.isEmpty(targetKey)) { return false; } let changed = false; for (let index = 0; index < list.length; index += 1) { if (Utility.getFavMatchKey(list[index]) !== targetKey) { continue; } list[index].isFav = isFav; changed = true; } return changed; } private syncFavoriteStateForVisibleList(list: Array | undefined, dataSource: LazyDataSource, targetKey: string, isFav: number, removeOnCancel: boolean): boolean { if (!list || ArrayUtil.isEmpty(list) || StrUtil.isEmpty(targetKey)) { return false; } let changed = false; for (let index = list.length - 1; index >= 0; index -= 1) { if (Utility.getFavMatchKey(list[index]) !== targetKey) { continue; } changed = true; if (removeOnCancel) { list.splice(index, 1); if (index < dataSource.dataArray.length) { dataSource.deleteData(index); } } else { list[index].isFav = isFav; if (index < dataSource.dataArray.length) { dataSource.dataArray[index].isFav = isFav; dataSource.notifyDataChange(index); } } } return changed; } private syncFavoriteStateInCaches(targetKey: string, isFav: number): void { if (StrUtil.isEmpty(targetKey) || this.cache.size === 0) { return; } let changed = false; this.cache.forEach((items: Array) => { for (let index = 0; index < items.length; index += 1) { if (Utility.getFavMatchKey(items[index]) !== targetKey) { continue; } items[index].isFav = isFav; changed = true; } }); if (changed) { void this.saveCacheToStorage(); } } private syncFavoriteStateAcrossCollections(item: VideoItem, isFav: number): void { const targetKey = Utility.getFavMatchKey(item); if (StrUtil.isEmpty(targetKey)) { return; } const removeFromCurrentView = this.isFavMusic && isFav === 0; const currentViewChanged = this.syncFavoriteStateForVisibleList(this.videoLocalList, this.dataSource, targetKey, isFav, removeFromCurrentView); this.syncFavoriteStateForVisibleList(this.songList, this.sonDataSource, targetKey, isFav, false); this.syncFavoriteStateForPlainList(this.currentSongList, targetKey, isFav); this.syncFavoriteStateForPlainList(this.historyList, targetKey, isFav); this.syncFavoriteStateForPlainList(this.mediaKuList, targetKey, isFav); this.syncFavoriteStateForPlainList(this.artistList, targetKey, isFav); this.syncFavoriteStateForPlainList(this.albumList, targetKey, isFav); this.syncFavoriteStateForPlainList(this.filteredList, targetKey, isFav); this.syncFavoriteStateInCaches(targetKey, isFav); if (this.isFavMusic) { this.currentTitleCover = ArrayUtil.isNotEmpty(this.favList) ? Utility.getFirstCoverFromList(this.favList) : $r('app.media.alt'); } if (currentViewChanged) { this.setButtonStatus(); if (removeFromCurrentView) { this.markAlphaBetDirty(); } } } private syncRemoteFavoriteState(item: VideoItem, isFav: number): void { if (!item || !isRemoteCloudType(item.type) || StrUtil.isEmpty(item.filePath)) { return; } const remoteManager = RemoteDriveManager.getInstance(); const target = Utility.getItemByFilePath(remoteManager.webDavSongs, item.filePath); if (target) { target.isFav = isFav; } } doFav(item: VideoItem) { LogUtil.info('onecold doFav isFav=' + item.isFav) let isFav = 0 if (Utility.getIsFav(this.favList, item)) { isFav = 0 } else { isFav = 1 } const targetFilePath = isRemoteCloudType(item.type) ? (this.resolveRemoteStoragePath(item) || item.filePath) : item.filePath; const onUpdateSuccess = (): void => { this.syncFavStateLocal(item, isFav); this.syncRemoteFavoriteState(item, isFav); this.syncFavoriteStateAcrossCollections(item, isFav); if (isFav === 1) { ToastUtil.showToast('收藏成功'); } else { ToastUtil.showToast('取消收藏成功'); } }; const updateFavorite = (): void => { this.table.updateIsFavByFilePath(targetFilePath, isFav, async (result: boolean, error?: string) => { if (result) { onUpdateSuccess(); return; } if (isRemoteCloudType(item.type)) { Logger.warn(TAG, `远程歌曲收藏更新失败,准备补入库后重试: ${targetFilePath}, error=${error}`); try { await this.persistRemoteMetadataToDb(item); this.table.updateIsFavByFilePath(targetFilePath, isFav, (retryResult: boolean, retryError?: string) => { if (retryResult) { onUpdateSuccess(); } else { Logger.error(TAG, `远程歌曲收藏补入库后仍失败: ${targetFilePath}, error=${retryError}`); ToastUtil.showToast(isFav === 1 ? '收藏失败' : '取消收藏失败'); } }); } catch (persistError) { Logger.error(TAG, `远程歌曲收藏补入库失败: ${targetFilePath}, error=${(persistError as Error).message}`); ToastUtil.showToast(isFav === 1 ? '收藏失败' : '取消收藏失败'); } return; } ToastUtil.showToast(isFav === 1 ? '收藏失败' : '取消收藏失败'); }) }; updateFavorite() } //多选复制文件 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 targetName = item.fileName || item.name; const newPath = newDirPath + '/' + targetName; 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) if (!Utility.isIsoByExtension(newPath)) { let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newPath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName); 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.Capsule, stateEffect: true }) { Row() { if(this.modeType !== 0 && StrUtil.isNotEmpty(item.pixelMapPath)){ Image(this.getListImageSource(item, index ?? -1, this.coverThumbVersion)) .height(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 50 : 40) .width(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 50 : 40) .fillColor(this.themeColor)// .fontSize(33) .alt($r('app.media.music_group')) .borderRadius(this.isCoverRectangle?9:'100%') .clip(true) .shadow({ radius: 2, type: ShadowType.BLUR, color: 'on_primary' }) .sourceSize({width:38, height:38}) .margin({ left: 20, top: 8, bottom: 8 }) }else{ SymbolGlyph($r('sys.symbol.folder')) .fontSize(this.twoFingerType==3?55:this.twoFingerType==2?50:45) .fontColor([this.themeColor]) .alignSelf(ItemAlign.Center) .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%') .textOverflow({ overflow: TextOverflow.Ellipsis })//列表模式固定省略,避免滚动跑马灯开销 .margin({ left: 10, right: 20 }) .fontColor($r('app.color.text_color')) Row() { Text(`${this.getArtistSongCount(item.name)}首`) .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.getAlbumSongCount(item.name)}首`) .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%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) Blank() Checkbox({ name: 'dir_checkbox' + (index ?? -1) }) .select(this.isLocalItemSelected(item)) .selectedColor(this.themeColor) .shape(CheckBoxShape.CIRCLE) .opacity(this.isMultiSelect && this.isSelectableLocalItem(item) ? 1 : 0) .visibility(this.isMultiSelect && this.isSelectableLocalItem(item) ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => this.handleFileSelection(item, checked)) .margin({ left: 20, top: 8, bottom: 8, right: 18 }) .width(22) .height(22) Image($r('app.media.arrow_right')) .height(25) .alignSelf(ItemAlign.Center) .id(item.filePath) .visibility(this.isShowAlphaBet || (this.isMultiSelect && this.isSelectableLocalItem(item)) ? Visibility.None : Visibility.Visible) } } .backgroundColor(Color.Transparent) .height(this.twoFingerType == 3 ? ITEM_HEIGHT_BIG : this.twoFingerType == 2 ? ITEM_HEIGHT : ITEM_HEIGHT_SMALL) .width('98%') .reuseId('dir_item') .visibility(this.isShowDir(item.name) ? Visibility.Visible : Visibility.None) .opacity(this.opacityItem) // 绑定透明度 .onTouch((event: TouchEvent) => { this.handlePointLightTouch(this.getPointLightItemKey('list_dir', item, index), event) }) .visualEffect(deviceInfo.sdkApiVersion >= 20 ? new hdsEffect.HdsEffectBuilder() .pointLight({ options: this.getPointLightOptions(this.getPointLightItemKey('list_dir', item, index)), illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT }) .buildEffect() : undefined) } private refreshDirVisibilitySettings(): void { 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) } isShowDir(dirName: string) { 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 } private getPointLightItemKey(prefix: string, item: VideoItem, index?: number): string { const itemPath = StrUtil.isNotEmpty(item.filePath) ? item.filePath : `${item.name}_${index ?? -1}` return `${prefix}_${itemPath}` } private handlePointLightTouch(itemKey: string, event: TouchEvent): void { if (deviceInfo.sdkApiVersion < 20) { return } if (event.type === TouchType.Down) { this.activePointLightItemKey = itemKey this.pointLightOptions = { color: this.themeColor, intensity: 1, height: 60 } return } if ((event.type === TouchType.Up || event.type === TouchType.Cancel) && this.activePointLightItemKey === itemKey) { this.activePointLightItemKey = '' this.pointLightOptions = undefined } } private getPointLightOptions(itemKey: string): hdsEffect.PointLightOptions | undefined { if (this.activePointLightItemKey !== itemKey) { return undefined } return this.pointLightOptions } @State opacityItem: number = 1; // 控制透明度的状态变量 @Builder private MusicItem(item: VideoItem, index?: number) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { Stack() { Image(this.getListImageSource(item, index ?? -1, this.coverThumbVersion)) .fillColor(this.themeColor) .height(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 50 : 40) .width(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 50 : 40) .alt($r('app.media.alt')) .borderRadius(this.isCoverRectangle?9:'100%') .clip(true) .draggable(false) .sourceSize({width:38, height:38}) // .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿 // .autoResize(true) // 重采样,可减少内存占用 .opacity(this.opacityItem)// 绑定透明度 .margin({ left: 20 }) .shadow({ radius: this.isScrolling ? 0 : 10, type: ShadowType.BLUR, color: 'on_primary' }) .onClick(()=>{ if (this.isMultiSelect) { const isChecked = this.isLocalItemSelected(item); this.handleFileSelection(item, !isChecked); } else { this.doPlay(item,index) this.setShowPlayTrue() } }) } Column() { Column() { Text(this.isShowFileName?item.fileName :item.name) .fontSize(this.twoFingerType == 1 ? 12 : this.twoFingerType == 2 ? 15 : 18) .maxLines(1) .textOverflow({ overflow: this.isLongNameRoLL&&!this.isScrolling?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动 .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .margin({ left: this.twoFingerType == 3 ? 6 : 8 }) .padding({right:25}) .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 = '' } } }) 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.duration :this.twoFingerType==2? item.artist +' ' + item.duration : item.artist + ' ' + item?.duration) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 ,right:25}) .visibility(this.isShowFileName?Visibility.None:Visibility.Visible) .maxLines(1) .opacity(0.85) .textOverflow({ overflow: TextOverflow.Ellipsis })//列表模式固定省略 .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) .margin({ left: this.twoFingerType == 3 ? 6 : 8 }) Blank() Text(this.isShowFileName?item.duration:item.size) .fontSize(this.twoFingerType == 1 ? 11 : this.twoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .maxLines(1) .visibility(this.isShowFileName?Visibility.Visible:StrUtil.isEmpty(item.artist) ? Visibility.Visible : Visibility.None) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : Color.Gray) .margin({ left: StrUtil.isEmpty(item.artist) ?0:10 }) } } .height('100%') .width('100%') .margin({ right: 10 }) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') .margin({ left: 5 }) .layoutWeight(1) Column() { Checkbox({ name: 'checkbox' + index }) .select(this.isLocalItemSelected(item)) .selectedColor(this.themeColor) .shape(CheckBoxShape.CIRCLE) .opacity(this.isMultiSelect ? 1 : 0) .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => this.handleFileSelection(item, checked)) .margin({ left: 20, top: 8, bottom: 8,right:18 }) .width(22) .height(22) PlayingIndicator({ isActive: this.videoUrl === item.filePath && !this.isMultiSelect, indicatorSize: 18, marginRight: 20, marginTop: 8, marginBottom: 8 }) } } } .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 = this.supportsLocalAudioActions(item) ? await this.getLyricContent(item) : '' LogUtil.info('长按this.tempLyricContent = '+this.tempLyricContent) }) .onActionEnd((event: GestureEvent) => { })) .backgroundColor(Color.Transparent) .width('100%') .reuseId('file_item') .height(this.twoFingerType == 3 ? ITEM_HEIGHT_BIG : this.twoFingerType == 2 ? ITEM_HEIGHT : ITEM_HEIGHT_SMALL) .onTouch((event: TouchEvent) => { this.handlePointLightTouch(this.getPointLightItemKey('list_music', item, index), event) }) .visualEffect(deviceInfo.sdkApiVersion >= 20 ? new hdsEffect.HdsEffectBuilder() .pointLight({ options: this.getPointLightOptions(this.getPointLightItemKey('list_music', item, index)), illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT }) .buildEffect() : undefined) } private readonly tabs: string[] = ['文件夹', '媒体库', '艺术家', '专辑'] @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.tab({ buttons: [{ text: '文件夹' }, { text: '媒体库' },{ text: '艺术家' }, { text: '专辑' }], direction: Direction.Ltr, buttonPadding:{top:12,bottom:12}, backgroundColor: Color.Transparent, fontSize:14, selectedFontSize:15, selectedFontColor:Color.White, selectedBackgroundColor: this.themeColor, localizedTextPadding: { end: LengthMetrics.vp(20), start: LengthMetrics.vp(20) } }) @State @Watch('capsuleSelectedIndexesChanged')tabSelectedIndexes: number[] = [0] //当胶囊按钮的选择发生变化时调用此函数 capsuleSelectedIndexesChanged() { this.modeType = this.tabSelectedIndexes[0] } @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 } 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 } // 第二步: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 @State searchHistoryItems: string[] = [] private readonly searchHistoryScope: string = 'local_music' private loadSearchHistory(): void { this.searchHistoryItems = SearchHistoryUtil.load(this.searchHistoryScope) } private saveSearchHistory(keyword: string): void { this.searchHistoryItems = SearchHistoryUtil.save(this.searchHistoryScope, keyword) } private commitSearchHistory(keyword: string): void { const normalized = keyword.trim() if (normalized.length === 0) { return } this.saveSearchHistory(normalized) } private applySearchHistory(keyword: string): void { this.searchText = keyword this.searchController.stopEditing() void this.onSearchInput(keyword) } // 实时搜索逻辑(带防抖) private async onSearchInput(value: string): Promise { this.searchText = value.trim(); Logger.info('heanup LocalMusic', `onSearchInput: keyword="${this.searchText}", mode=${this.modeType}`); // 歌单模式不走媒体库分页查询,直接在当前歌单中筛选,避免混入非歌单歌曲并打乱顺序。 if (this.modeType === 4) { if (this.searchText === '') { this.loadSearchHistory() this.filteredList = [] this.updateListData(this.currentSongList, true, true) Logger.info('heanup LocalMusic', `onSearchInput: restore playlist list, count=${this.currentSongList.length}`) return } if (!this.isSearchMode) { this.isSearchMode = true } const filteredSongs = filterPlaylistSongsByKeyword(this.currentSongList, this.searchText) this.filteredList = filteredSongs this.updateListData(filteredSongs, true, true) Logger.info('heanup LocalMusic', `onSearchInput: playlist filter applied, keyword="${this.searchText}", count=${filteredSongs.length}`) return } // 清空搜索文本时保持搜索态,只重置为不带关键词的结果列表 if (this.searchText === '') { this.loadSearchHistory() await this.resetAndLoadFirstPage(); return; } // 进入搜索模式 if (!this.isSearchMode) { this.isSearchMode = true; } // 使用数据库分页查询(带搜索条件) await this.resetAndLoadFirstPage(); } @Builder private SearchHistoryView(): void { Column({ space: 10 }) { Row() { Text('搜索历史') .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.text_color')) Blank() Button('清空') .fontSize(12) .fontColor(this.themeColor) .backgroundColor(Color.Transparent) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .onClick(() => { SearchHistoryUtil.clear(this.searchHistoryScope) this.searchHistoryItems = [] }) } .width('100%') Flex({ wrap: FlexWrap.Wrap }) { ForEach(this.searchHistoryItems, (keyword: string) => { Button(keyword) .fontSize(12) .fontColor($r('app.color.text_color')) .backgroundColor(this.isDarkMode ? '#222222' : '#F2F3F5') .borderRadius(16) .margin({ right: 8, bottom: 8 }) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .onClick(() => { this.applySearchHistory(keyword) }) }) } .width('100%') } .width('100%') .padding({ left: 16, right: 16, top: 10, bottom: 10 }) } @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) if(this.modeType==4&&this.playlistTable){ updateAllSongsSortOrder(this.playlistTable, this.videoLocalList,this.currentSongListID) } } //瀑布流布局 支持双指缩放 @State columns: number = 2; @State waterFlowScale: number = 1; @State imageScale: number = 1; @State waterFlowOpacity: number = 1; @State waterFlowSnapshot: image.PixelMap | undefined = undefined; private columnChanged: boolean = false; private oldColumn: number = this.columns; private pinchTime: number = 0; // 根据缩放阈值改变列数,触发WaterFlow重新布局 changeColumns(scale: number): void { if (scale > (this.columns / (this.columns - 0.5)) && this.columns > 1) { this.columns--; this.columnChanged = true; } else if (scale < 1 && this.columns < 4) { this.columns++; this.columnChanged = true; } } @Builder getWaterView(){ Scroll(this.scroller) { Column() { Blank().height(this.topSafeHeight + 55) WaterFlow({ scroller:this.waterScroller, layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW }) { LazyForEach(this.dataSource, (item: VideoItem, index: number) => { FlowItem() { this.MusicWaterCardItem(item,index) } .width('100%') .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 }) .bindContextMenu(this.ContextMenuBuilder(item, index, item.filePath), ResponseType.LongPress, { preview: MenuPreviewMode.IMAGE }) .bindContextMenu(this.ContextMenuBuilder(item, index, item.filePath), ResponseType.RightClick, { preview: MenuPreviewMode.IMAGE }) }, (item: VideoItem) => item.filePath + '_' + this.listRefreshKey) } // .columnsTemplate('repeat(auto-fit, 160)') .id('waterflow') // 设置id用于截图 .columnsTemplate('1fr '.repeat(this.columns)) // 动态生成列模板,如:'1fr 1fr 1fr'表示3列等宽 .columnsGap(12) // 列间距 .rowsGap(this.columns >= 4 ? 8 : (this.columns == 3 ? 10 : 14)) // 行间距 .cachedCount(this.columns >= 4 ? 2 : (this.columns == 3 ? 3 : 4)) // .padding({bottom:this.bottomBarHeight+10}) .scrollBar(BarState.Off) // .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(38) }) // 效果:只有向上或者向下滑动到顶部时才会有弹簧回弹效果 .edgeEffect(EdgeEffect.None) .width('90%') .height('100%') .opacity(this.waterFlowOpacity) .scale({ x: this.waterFlowScale, y: this.waterFlowScale, centerX: 0, centerY: 0 }) .onReachEnd(() => { if(this.modeType === 4 ) return; // 触底加载下一页 Logger.info('heanup LocalMusic', `onReachEnd: hasMore=${this.hasMoreData}, isLoading=${this.isLoadingPage}`); if (this.isCanBack && (this.modeType === 2 || this.modeType === 3)) { return; } if (this.hasMoreData && !this.isLoadingPage && !this.isPagingTransition) { this.scheduleLoadNextPage(); } }) .onAppear(() => { this.scroller.scrollEdge(Edge.Top) }) .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST }) .priorityGesture( PinchGesture() .onActionStart((event: GestureEvent) => { // 双指捏合手势识别成功时截图 this.pinchTime = event.timestamp; this.columnChanged = false; this.oldColumn = this.columns; this.getUIContext().getComponentSnapshot().get('waterflow', (error: Error, pixmap: image.PixelMap) => { if (error) { console.info('error:' + JSON.stringify(error)); return; } this.waterFlowSnapshot = pixmap; if ((this.oldColumn === 1 ) && event.scale > 1|| (this.oldColumn === 4 && event.scale < 1)) { console.info("onecold onActionStart oldColumn:" + this.oldColumn) if (this.oldColumn ===4) { this.twoFingerType =3 this.isGridMusic = true }else if (this.oldColumn ===1) { this.twoFingerType =1 this.isGridMusic = false } PreferencesUtil.put(SettingPage.TWO_FINGER_TYPE, this.twoFingerType) PreferencesUtil.put(SettingPage.IS_GRID_MUSIC, this.isGridMusic) return; } }) }) .onActionUpdate((event: GestureEvent) => { // 手势更新:处理缩放逻辑和视觉效果 // 边界限制:防止超出列数范围时继续缩放 if ((this.oldColumn === 1 && event.scale > 1) || (this.oldColumn === 4 && event.scale < 1)) { return; } // 节流处理:避免过于频繁的更新,提升性能 if (event.timestamp - this.pinchTime < 10000000) { return; } this.pinchTime = event.timestamp; this.waterFlowScale = event.scale; this.imageScale = event.scale; // 根据缩放比例设置WaterFlow透明度 this.waterFlowOpacity = (this.waterFlowScale > 1) ? (this.waterFlowScale - 1) : (1 - this.waterFlowScale); this.waterFlowOpacity *= 3; if (!this.columnChanged) { this.changeColumns(event.scale); } // 列数改变后的缩放比例调整:避免出现空白区域 if (this.columnChanged) { this.waterFlowScale = this.imageScale * this.columns / this.oldColumn; // 限制缩放范围,确保视觉效果自然 if (event.scale < 1) { this.waterFlowScale = this.waterFlowScale > 1 ? this.waterFlowScale : 1; } else { this.waterFlowScale = this.waterFlowScale < 1 ? this.waterFlowScale : 1; } } }) .onActionEnd((event: GestureEvent) => { // 手势结束:执行归位动画并保存状态 // 执行归位动画:平滑过渡到正常状态 this.getUIContext()?.animateTo({ duration: 300 }, () => { this.waterFlowScale = 1; this.waterFlowOpacity = 1; }) // 持久化保存当前列数:下次启动时恢复 PreferencesUtil.putSync('lastColumns', this.columns); }) ) .onScrollFrameBegin((offset: number) => { this.markGridWaterScrollingActive() // 获取当前滚动偏移量 const currentOffsetY = this.waterScroller.currentOffset().yOffset; this.tryTriggerLoadForWaterByOffset(currentOffsetY) this.updateTitleBarByScrollOffset(currentOffsetY) return { offsetRemain: offset }; }) } } .scrollBar(BarState.Off) } //瀑布流卡片布局 @Builder private MusicWaterCardItem(item: VideoItem, index: number) { Button({ type: ButtonType.Normal, stateEffect: false }) { Stack() { Column() { Image(this.getGridWaterImageSource(item, index, true, this.coverThumbVersion)) .backgroundImageSize(ImageSize.Auto) .borderRadius({ topLeft: 12, topRight : 12, bottomLeft: 0, bottomRight: 0 }) .draggable(false)// 禁止长按手势拖动 .autoResize(true) // 重采样,可减少内存占用 .sourceSize({ width: this.getCoverThumbTargetSize(true), height: this.getCoverThumbTargetSize(true) }) .width('100%') .height('auto') .alt(item.type==CommonConstants.TYPE_IS_DIR?$r('app.media.dir_alt'):$r('app.media.alt')) .objectFit(ImageFit.Cover) .bindSheet(this.longItemFilePath== item.filePath, this.editSheet(item), { height: '99%' , dragBar: true, onDisappear: () => { this.longItemFilePath = ''; this.tempLyricContent = '' }, showClose: true, preferType: SheetType.CENTER , title: { title: '编辑标签' } }) .draggable(false) .opacity(this.opacityItem)// 绑定透明度 .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.columns == 4 ||this.columns==3? 12 : this.columns == 2 ? 15 : 18) .maxLines(1) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: this.isLongNameRoLL&&!this.isScrolling?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动 .bindSheet(this.longItemFilePathDetail== item.filePath, this.detailSheet(item), { height: '95%', dragBar: true, showClose: true, onDisappear: () => { this.longItemFilePathDetail = ''; }, blurStyle:BlurStyle.Thin, backgroundColor:Color.Transparent, preferType: SheetType.CENTER , title: { title: '详情' } }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) Column() { Text(`${this.getArtistSongCount(item.name)}首`) .fontSize(11) .fontSize(this.columns == 4 ||this.columns==3? 10 : this.columns == 2 ? 12 : 14) .visibility(this.modeType === 2 && !this.isCanBack ? Visibility.Visible : Visibility.None) .fontColor($r('app.color.text_color')) Text(`${this.getAlbumSongCount(item.name)}首`) .fontSize(11) .fontSize(this.columns == 4 ||this.columns==3? 10 : this.columns == 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.columns == 4 ||this.columns==3? 8 : this.columns == 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)||this.twoFingerType==1?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.columns == 4 ||this.columns==3? 10 : this.columns == 2 ? 12 : 13) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis })//瀑布流固定省略 .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) // 关键:使内容水平居中 } .height(this.columns == 4 ||this.columns==3? item.type==CommonConstants.TYPE_IS_DIR?30:40 : this.columns == 2 ? (item.type==CommonConstants.TYPE_IS_DIR?38:48) : (item.type==CommonConstants.TYPE_IS_DIR?48:58)) .width('100%') .justifyContent(FlexAlign.Center) .margin({ left: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20, right: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20 }) } .width('100%') .borderRadius(12) .backgroundColor($r('app.color.com_bg')) Checkbox({ name: 'checkbox' + index }) .select(this.isLocalItemSelected(item)) .selectedColor(this.themeColor) .shape(CheckBoxShape.CIRCLE) .opacity(this.isMultiSelect && this.isSelectableLocalItem(item) ? 1 : 0) .visibility(this.isMultiSelect && this.isSelectableLocalItem(item) ? 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%') .height('auto') .padding({top:2}) .shadow({ radius: this.isScrolling ? 0 : 8, type: ShadowType.BLUR, color: 'on_primary' }) .reuseId('card_item') .onTouch((event: TouchEvent) => { this.handlePointLightTouch(this.getPointLightItemKey('water_music', item, index), event) }) .visualEffect(deviceInfo.sdkApiVersion >= 20 ? new hdsEffect.HdsEffectBuilder() .pointLight({ options: this.getPointLightOptions(this.getPointLightItemKey('water_music', item, index)), illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT }) .buildEffect() : undefined) .onClick(() => { if (this.isMultiSelect) { if (this.isSelectableLocalItem(item)) { const isChecked = this.isLocalItemSelected(item); this.handleFileSelection(item, !isChecked); } return } this.doPlay(item, index) }) } // 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; } } 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(); private waterScroller: Scroller = new Scroller(); private startIndex: number = 0 private 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() { Column(){ Blank().height(this.topSafeHeight + 45) } } LazyForEach(this.dataSource, (item: VideoItem, index: number) => { GridItem() { Stack() { this.MusicItemGrid(item, index) } } .bindContextMenu(this.ContextMenuBuilder(item, index, item.filePath), ResponseType.LongPress, { preview: MenuPreviewMode.IMAGE }) .bindContextMenu(this.ContextMenuBuilder(item, index, item.filePath), ResponseType.RightClick, { preview: MenuPreviewMode.IMAGE }) // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 0.9, y: 0.9 }).animation({ duration: 300 }), // 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 }) }, (item: VideoItem) => item.filePath + '_' + this.listRefreshKey) } .width('94%') .height('100%') .editMode(true) .padding({bottom:this.bottomSafeHeight+10}) .reuseId('grid_item') .layoutWeight(1) .scrollBar(BarState.Off) .supportAnimation(false) .onReachEnd(() => { if(this.modeType === 4 ) return; // 触底加载下一页 Logger.info('heanup LocalMusic', `onReachEnd: hasMore=${this.hasMoreData}, isLoading=${this.isLoadingPage}`); if (this.isCanBack && (this.modeType === 2 || this.modeType === 3)) { return; } if (this.hasMoreData && !this.isLoadingPage && !this.isPagingTransition) { this.scheduleLoadNextPage(); } }) .cachedCount(this.twoFingerType==1?1:this.twoFingerType==2?2:2) .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 this.gridVisibleStart = start this.gridVisibleEnd = end this.lastVisibleEndIndex = end this.warmupGridWaterVisibleThumbs(start, end, false) const now = Date.now() const pagingDelta = this.gridScrollLastPagingEnd >= 0 ? Math.abs(end - this.gridScrollLastPagingEnd) : 999 if (pagingDelta >= 2 || now - this.gridScrollPagingCheckAt >= 40) { this.gridScrollPagingCheckAt = now this.gridScrollLastPagingEnd = end this.tryTriggerLoadNextPageByScroll(end) } }) .onScrollFrameBegin((offset: number) => { this.markGridWaterScrollingActive() const currentOffsetY = this.scroller.currentOffset().yOffset; this.updateTitleBarByScrollOffset(currentOffsetY) 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 }; }) } @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 >= 4){ this.twoFingerType = 4//切换成瀑布流 this.columns = 4 } } else { this.twoFingerType-- Logger.info('this.twoFingerType2 = ' + this.twoFingerType); if (this.twoFingerType < 1) { this.twoFingerType = 3 this.isGridMusic = false PreferencesUtil.put(SettingPage.IS_GRID_MUSIC, this.isGridMusic) } } PreferencesUtil.put(SettingPage.TWO_FINGER_TYPE, this.twoFingerType) PreferencesUtil.put(SettingPage.IS_GRID_MUSIC, this.isGridMusic) this.pinchValue = 1; this.scaleValue = 1; } @State longItemFilePath: string = '' @State tempLyricContent: string = '' @State editingItem: VideoItem | null = null @Builder private MusicItemGrid(item: VideoItem, index: number) { Button({ type: ButtonType.Normal, stateEffect: false }) { Stack() { Column() { Image(this.getGridWaterImageSource(item, index, false, this.coverThumbVersion)) .height(this.getGridHeight()) .width(this.getGridWight()) .draggable(false) .autoResize(true) // 重采样,可减少内存占用 .sourceSize({ width: this.getCoverThumbTargetSize(false), height: this.getCoverThumbTargetSize(false) }) .alt(item.type==CommonConstants.TYPE_IS_DIR?$r('app.media.dir_alt'):$r('app.media.alt')) .clip(true) .borderRadius({ topLeft: 12, topRight: 12, bottomLeft: 0, bottomRight: 0 }) .bindSheet(this.longItemFilePath== item.filePath, this.editSheet(item), { height: '99%' , dragBar: true, onDisappear: () => { this.longItemFilePath = ''; this.tempLyricContent = '' }, showClose: true, preferType: SheetType.CENTER , title: { title: '编辑标签' } }) .draggable(false) .opacity(this.opacityItem)// 绑定透明度 .margin({ left: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20, right: this.currentBreakpoint !== BreakpointTypeEnum.SM ? 40 : 20 }) Column() { Text(this.isShowFileName&&this.modeType !== 2&&this.modeType !== 3&& item.type!=CommonConstants.TYPE_IS_DIR?item.fileName :item.name.startsWith('.') ? item.name.replace(/\./g, '') : item.name) .fontSize(this.twoFingerType == 1 ? 12 : this.twoFingerType == 2 ? 15 : 18) .maxLines(1) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: this.isLongNameRoLL&&!this.isScrolling?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动 .bindSheet(this.longItemFilePathDetail== item.filePath, this.detailSheet(item), { height: '100%', dragBar: true, showClose: true, onDisappear: () => { this.longItemFilePathDetail = ''; }, blurStyle:BlurStyle.Thin, backgroundColor:Color.Transparent, preferType: SheetType.CENTER , title: { title: '详情' } }) .fontColor(this.currentSong?.filePath === item.filePath ? this.themeColor : $r('app.color.text_color')) Column() { Text(`${this.getArtistSongCount(item.name)}首`) .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.getAlbumSongCount(item.name)}首`) .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)||this.twoFingerType==1?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(this.isShowFileName?item.duration:StrUtil.isEmpty(item.artist) ? item.duration: item.artist) .fontSize(this.twoFingerType == 1 ? 10 : this.twoFingerType == 2 ? 12 : 13) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis })//Grid固定省略 .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) // 关键:使内容水平居中 } .height(this.twoFingerType == 3 ? 60 : this.twoFingerType == 2 ? 48 : 35) .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) .backgroundColor($r('app.color.com_bg')) Checkbox({ name: 'checkbox' + index }) .select(this.isLocalItemSelected(item)) .selectedColor(this.themeColor) .shape(CheckBoxShape.CIRCLE) .opacity(this.isMultiSelect && this.isSelectableLocalItem(item) ? 1 : 0) .visibility(this.isMultiSelect && this.isSelectableLocalItem(item) ? 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()) .onTouch((event: TouchEvent) => { this.handlePointLightTouch(this.getPointLightItemKey('grid_music', item, index), event) }) .visualEffect(deviceInfo.sdkApiVersion >= 20 ? new hdsEffect.HdsEffectBuilder() .pointLight({ options: this.getPointLightOptions(this.getPointLightItemKey('grid_music', item, index)), illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT }) .buildEffect() : undefined) .onClick(() => { if (this.isMultiSelect) { if (this.isSelectableLocalItem(item)) { const isChecked = this.isLocalItemSelected(item); this.handleFileSelection(item, !isChecked); } return } this.doPlay(item, index) }) } getGridAllHeight() { let heightG = 222 switch (this.twoFingerType) { case 1: heightG = 143 break; case 2: heightG = 178 break; case 3: heightG = 222 break; } return heightG } getGridHeight() { let heightG = 150 switch (this.twoFingerType) { case 1: heightG = 80 break; case 2: heightG = 110 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 @State longItemFilePathDetail:string ='' private canShowItemContextMenu(item: VideoItem): boolean { return !(((this.modeType===2||this.modeType==3)&&!this.isCanBack) ||(item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO || item.name === LocalMusic.STR_HISTORY_MUSIC)) } @Builder private LocalMusicMenuContent(item: VideoItem, index: number, filePath: string) { 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(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(() => { if (item) { this.doFav(item) } this.longItemFilePath = '' }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.forward_end_fill')), content: '下一首播放' }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(() => { this.addToNextPlay(item); this.longItemFilePath = '' }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')), content: '添加到歌单' }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(async () => { this.longItemFilePath = '' await this.openAddToPlaylistDialog(item) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')), content: '编辑标签' }) .bindSheet($$this.isShowEdit, this.editSheet(item), { height: '99%', dragBar: true, showClose: true, preferType: SheetType.CENTER , title: { title: '编辑标签' } }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(async() => { this.initEditFields(item) this.tempLyricContent = await this.getLyricContent(item); if (StrUtil.isNotEmpty(this.tempLyricContent)) { this.lyricConStr = this.tempLyricContent } if(this.isGridMusic||this.twoFingerType==4){ this.longItemFilePath = item.filePath }else{ this.isShowEdit = !this.isShowEdit; } }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square_on_square')), content: $r('app.string.select_all') }) .onClick(() => { this.longItemFilePath = '' this.enterLocalMultiSelect(item) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')), content: '提取ISO文件' }) .visibility(this.isIsoLocalFile(item) ? Visibility.Visible : Visibility.None) .onClick(async () => { this.longItemFilePath = '' await this.openIsoArchiveDialog(item) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.cut')), content: $r('app.string.edit_audio') }) .bindContentCover($$this.isEditAudio, this.EditAudioBuilder(item), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(() => { this.isEditAudio = !this.isEditAudio }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music')), content: $r('app.string.extract_accompaniment') }) .bindContentCover($$this.isExtractAcc, this.ExtractAccBuilder(item), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(() => { if (!Utility.isNoble()) { this.showVipDialog() return } this.isExtractAcc = !this.isExtractAcc }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.repeat')), content: $r('app.string.converter_audio') }) .bindContentCover($$this.isConverter, this.ConverterBuilder(item), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(() => { if (!Utility.isNoble()) { this.showVipDialog() return } this.isConverter = !this.isConverter }) // MenuItem({ // symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.ai_edit')), // content: $r('app.string.vocal_separation') // }) // .bindContentCover($$this.isAIVoiceSeparation, this.AiBuilder(item), { // transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) // }) // .onClick(() => { // this.isAIVoiceSeparation = !this.isAIVoiceSeparation // }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_counterclockwise_clock')), content: $r('app.string.change_tone') }) .bindContentCover($$this.isChangeTone, this.ChangeToneBuilder(item), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(() => { this.isChangeTone = !this.isChangeTone }) if(Utility.isVideoByExtension(item.filePath)){ MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.identify_song')), content: $r('app.string.extract_audio') }) .bindContentCover($$this.isExtractAudio, this.extractAudioFromVideoBuilder(item), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) .onClick(() => { this.isExtractAudio = !this.isExtractAudio }) } MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.phone')), content: $r('app.string.set_ring') }) .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .onClick(async () => { await PermissionUtil.activatePermission(item.filePath) // if(item.filePath.includes(this.packName)){ setRingTone(this.context, item.filePath,item.name) // }else{ // ToastUtil.showToast('请把该音频导入到本应用的DownLoad文件下,其他路径音频无权限设置铃声') // } this.longItemFilePath = '' }) 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.selectSingleItemForDelete(item) this.showWarnIsDelete() this.longItemFilePath = '' }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.info_circle')), content: $r('app.string.detail') }) .bindSheet($$this.isShowDetail, this.detailSheet(item), { height:'100%', 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(() => { if(this.isGridMusic||this.twoFingerType==4){ this.longItemFilePathDetail = item.filePath }else{ 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: 14, weight: FontWeight.Normal }) } .width(180) } @Builder MenuBuilder(item: VideoItem, index: number, filePath: string) { if (this.canShowItemContextMenu(item)) { if (shouldWrapLocalMusicMenuInScroll(false)) { Scroll() { this.LocalMusicMenuContent(item, index, filePath) } .height('auto') .enableScrollInteraction(true) .scrollBar(this.isGridMusic?BarState.On:BarState.Off) } else { this.LocalMusicMenuContent(item, index, filePath) } } } @Builder ContextMenuBuilder(item: VideoItem, index: number, filePath: string) { if (this.canShowItemContextMenu(item)) { if (shouldWrapLocalMusicMenuInScroll(true)) { Scroll() { this.LocalMusicMenuContent(item, index, filePath) } .height('auto') .enableScrollInteraction(true) .scrollBar(this.isGridMusic?BarState.On:BarState.Off) } else { this.LocalMusicMenuContent(item, index, filePath) } } } private listMaxScrollOffsetY: number = 0 private 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 PreferencesUtil.put(SettingPage.IS_GRID_MUSIC, this.isGridMusic) } } else { this.twoFingerType-- Logger.info('this.twoFingerType2 = ' + this.twoFingerType); if (this.twoFingerType < 1) { this.twoFingerType = 4 this.columns = 1 } } PreferencesUtil.put(SettingPage.TWO_FINGER_TYPE, this.twoFingerType) PreferencesUtil.put(SettingPage.IS_GRID_MUSIC, this.isGridMusic) this.pinchValue = 1; this.scaleValue = 1; } @Builder getList(){ Stack() { Refresh({ refreshing: $$this.isRefreshing, refreshingContent: this.contentNode }) { this.getListView() } .pullDownRatio(this.ratioL) .pullToRefresh(true) .refreshOffset(0) .onOffsetChange((offset: number) => { // 越接近最大距离,下拉跟手系数越小。 this.ratioL = 1 - Math.pow((offset / this.maxRefreshingHeight), 3); }) .onStateChange((refreshStatus: RefreshStatus) => { console.info('onecold Refresh onStatueChange state is ' + refreshStatus); }) .onRefreshing(async () => { if(this.modeType==0){ this.getHistoryList(false) this.asyncCurrentPathOnlyFile() }else if(this.modeType == 1){ workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName }); }else if(this.modeType == 2){ workerInstance.postMessage({ code: 3, data: this.context }); }else if(this.modeType == 3){ workerInstance.postMessage({ code: 4, data: this.context }); }else if(this.modeType == 4){ this.doSongListTask() } console.log('onRefreshing test'); }) if (this.isLoadingPage) { Row() { LoadingProgress() .width(24) .height(24) .color(this.themeColor) Text('加载中...') .margin({ left: 8 }) .fontSize(13) .fontColor($r('app.color.text_color')) } .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .backgroundColor(this.isDarkMode ? '#66000000' : '#66FFFFFF') .borderRadius(16) .align(Alignment.Bottom) .margin({ bottom: this.bottomBarHeight + this.bottomSafeHeight + 14 }) } } } // 扫描当前目录下的文件,不扫描子目录 asyncCurrentPathOnlyFile(){ if(this.modeType==0){ const task = new taskpool.Task(scanCurrentDirectoryTask, getContext(this), this.currentPath, this.lockPath,PreferencesUtil.getStringSync('COVER_API','')); taskpool.execute(task, taskpool.Priority.HIGH).then(()=>{ if(this.modeType==0){ setTimeout(()=>{ this.isRefreshing =false this.deleteCache(this.currentPath) this.getSortedFiles(this.currentPath) },500) } }).catch((e:object)=>{ this.isRefreshing =false console.info("onecold task1 catch e: " + e); }) } } @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 { this.MusicItem(item, index) } } } .clickEffect({ level: ClickEffectLevel.LIGHT }) .swipeAction(( ((this.isSwipe && !this.isScrolling) && (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), edgeEffect: SwipeEdgeEffect.None } : {}) //左滑 .onClick(() => { if (this.isMultiSelect) { if (this.isSelectableLocalItem(item)) { const isChecked = this.isLocalItemSelected(item); this.handleFileSelection(item, !isChecked); } return } this.doPlay(item, index) }) }, (item: VideoItem) => item.filePath + '_' + this.listRefreshKey) } .onScrollStart(() => { this.isScrolling = true this.listVisibleStart = -1 this.listVisibleEnd = -1 this.listScrollAlphaCheckAt = 0 this.listScrollPagingCheckAt = 0 this.listScrollLastAlphaStart = -1 this.listScrollLastPagingEnd = -1 if (this.alphaBetRebuildTimer) { clearTimeout(this.alphaBetRebuildTimer) this.alphaBetRebuildTimer = 0 } this.showAlphabetIndex() }) .onScrollIndex((start: number,center:number,end:number) => { this.selectedIndex = start this.listVisibleStart = start this.listVisibleEnd = end this.lastVisibleEndIndex = end if (!this.isMediaLibraryWarmupWindow()) { this.warmupListVisibleThumbs(start, end) } const now = Date.now() const alphaDelta = this.listScrollLastAlphaStart >= 0 ? Math.abs(start - this.listScrollLastAlphaStart) : 999 if (!this.isMediaLibraryWarmupWindow() && start >= 0 && start < this.videoLocalList.length && (alphaDelta >= 2 || now - this.listScrollAlphaCheckAt >= 48)) { this.listScrollAlphaCheckAt = now this.listScrollLastAlphaStart = start const nextAlphaIndex = this.findAlphaBetIndex(this.videoLocalList[start]) if (nextAlphaIndex !== this.selectAlphaBetIndex) { this.selectAlphaBetIndex = nextAlphaIndex } } const pagingDelta = this.listScrollLastPagingEnd >= 0 ? Math.abs(end - this.listScrollLastPagingEnd) : 999 if (pagingDelta >= 2 || now - this.listScrollPagingCheckAt >= 32) { this.listScrollPagingCheckAt = now this.listScrollLastPagingEnd = end this.tryTriggerLoadNextPageByScroll(end) } }) .onScrollStop(() => { if(this.isScrolling){ this.isScrolling = false } this.flushPendingCoverThumbRefresh() if (this.alphaBetDirty) { this.scheduleAlphaBetRebuild(0) } this.hideAlphabetIndexAfterDelay(1500) }) .onReachEnd(() => { if(this.modeType === 4 )//歌单不能加载下一页 return; // 触底加载下一页 Logger.info('heanup LocalMusic', `onReachEnd: hasMore=${this.hasMoreData}, isLoading=${this.isLoadingPage}`); if (this.isCanBack && (this.modeType === 2 || this.modeType === 3)) { return; } if (this.hasMoreData && !this.isLoadingPage && !this.isPagingTransition) { this.scheduleLoadNextPage(); } }) .scrollBar(BarState.Off) .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 => { })) .contentStartOffset(this.topSafeHeight + 55) .scrollBar(BarState.Off) .contentEndOffset(this.bottomBarHeight+this.bottomSafeHeight) .cachedCount(this.modeType == 1 ? 4 : 8) // .reuseId('list_item') .borderRadius(20) .layoutWeight(1) .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) => { const currentOffsetY = this.listScroller.currentOffset().yOffset; this.updateTitleBarByScrollOffset(currentOffsetY) return { offsetRemain: offset }; }) } setBarHeightNormal() { this.hideDone = false; this.getUIContext().animateTo({ duration: 300 }, () => { this.bottomBarHeight = 68 + 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 = 68 + 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 async doPlay(item: VideoItem, index?: number, isFromSonPlayList?: boolean,isOpen?:boolean) { Logger.info(`heanup doPlay被调用 - 歌曲: ${item.name}, 文件路径: ${item.filePath}, 索引: ${index}, 类型: ${item.type}, isFromSonPlayList: ${isFromSonPlayList}`) switch (item.type) { case CommonConstants.TYPE_IS_DIR: // 进入歌单前安全保存当前滚动偏移量,支持列表和网格 let shouldScrollTopNow = true let offsetB = 0 if(this.twoFingerType==4){ if (this.waterScroller && typeof this.waterScroller.currentOffset === 'function') { let cur = this.waterScroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offsetB = cur.yOffset } } this.mScrollMap.set('lastWaterDirScrollOffset', offsetB) }else 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.alt') } else if (item.name === LocalMusic.STR_HISTORY_MUSIC) { this.titleName = '最近播放' this.currentTitleCover = $r('app.media.alt') 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 } else if (item.name === LocalMusic.STR_FAC_VIDEO) { this.titleName = '我的收藏' this.currentTitleName = '我的收藏' this.currentTitleCover = $r('app.media.alt') //加载动画效果 this.updateListData(this.favList, true) if (ArrayUtil.isEmpty(this.favList)) { ToastUtil.showToast('无音乐收藏记录') } else { this.currentTitleCover = Utility.getFirstCoverFromList(this.favList) } this.isFavMusic = true } else { this.currentTitleName = item.name this.currentPath = item.filePath this.currentTitleCover = $r('app.media.alt') shouldScrollTopNow = false this.getSortedFiles(this.currentPath, false, undefined, undefined, undefined, true, true) } //进入歌单的时候的滚动位置在顶部 if (shouldScrollTopNow) { if(this.twoFingerType==4){ setTimeout(() => { this.waterScroller.scrollEdge(Edge.Top) this.scroller.scrollEdge(Edge.Top) }, 200) }else{ if (this.isGridMusic) { this.scroller.scrollToIndex(0,false) }else{ this.listScroller.scrollToIndex(0,false) } } } this.rightTopImage = $r('sys.symbol.chevron_left') break; case CommonConstants.TYPE_IS_ARTIST: // 进入艺术家前安全保存当前滚动偏移量,支持列表和网格 let offsetA = 0 if(this.twoFingerType==4){ if (this.waterScroller && typeof this.waterScroller.currentOffset === 'function') { let cur = this.waterScroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offsetA = cur.yOffset } } this.mScrollMap.set('lastWaterArtistScrollOffset', offsetA) }else 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) } // 进入艺术家详情页前,先加载详情页的排序类型 // 注意:需要临时设置isCanBack=true,以便loadSortTypeForCurrentMode读取详情页的排序设置 const artistOriginalIsCanBack = this.isCanBack; this.isCanBack = true; const artistDetailSortType = this.loadSortTypeForCurrentMode(); this.isCanBack = artistOriginalIsCanBack; // 恢复原值 Logger.info('heanup LocalMusic', `进入艺术家详情页: 加载详情页排序 sortType=${artistDetailSortType}`); // 缓存键需要包含排序类型,因为不同的排序类型会产生不同的数据 const artistCacheKey = `${item.name}_sort_${artistDetailSortType}`; let mList = this.artistMap.get(artistCacheKey) if (mList === undefined || ArrayUtil.isEmpty(mList)) { // 使用详情页的排序类型查询 mList = await this.table.querySongsByArtist(item.name, artistDetailSortType, this.isShowFileName); if (ArrayUtil.isNotEmpty(mList)) { this.artistMap.set(artistCacheKey, mList); } } // 设置当前排序类型为详情页排序 this.sortType = artistDetailSortType; if (mList !== undefined && ArrayUtil.isNotEmpty(mList)) { // 注意:不再调用 Utility.doSortListAscending,因为数据已经按 artistDetailSortType 排序了 const nextArtistCountMap = new Map(this.artistSongCountMap); nextArtistCountMap.set(item.name, mList.length); this.artistSongCountMap = nextArtistCountMap; this.isCanBack = true this.hasMoreData = false this.isLoadingPage = false this.pageCache.clear() this.pageHasMoreCache.clear() this.pageTotalCountCache.clear() this.prefetchingPages.clear() this.lastScrollTriggerPage = -1 this.lastVisibleEndIndex = -1 this.isPagingTransition = false if (this.pagingScheduleTimer) { clearTimeout(this.pagingScheduleTimer) this.pagingScheduleTimer = 0 } this.totalCount = mList.length this.titleName = item.name this.currentTitleName = '艺术家:' + item.name if (item.pixelMapPath) { this.currentTitleCover = item.pixelMapPath } this.updateListData(mList, true, true) // 详情页立即替换列表,避免旧列表顶部闪现 } else { ToastUtil.showToast('未找到该艺术家的歌曲'); return; } this.rightTopImage = $r('sys.symbol.chevron_left') //进入歌单的时候的滚动位置在顶部 if(this.twoFingerType==4){ setTimeout(() => { this.waterScroller.scrollEdge(Edge.Top) this.scroller.scrollEdge(Edge.Top) }, 380) }else{ if (this.isGridMusic) { this.scroller.scrollEdge(Edge.Top) }else{ this.listScroller.scrollEdge(Edge.Top) } } break; case CommonConstants.TYPE_IS_ALBUM: // 进入专辑前安全保存当前滚动偏移量,支持列表和网格 let offset = 0 if(this.twoFingerType==4){ if (this.waterScroller && typeof this.waterScroller.currentOffset === 'function') { let cur = this.waterScroller.currentOffset() if (cur && typeof cur.yOffset === 'number') { offset = cur.yOffset } } this.mScrollMap.set('lastWaterAlBumScrollOffset', offset) }else 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) } // 进入专辑详情页前,先加载详情页的排序类型 // 注意:需要临时设置isCanBack=true,以便loadSortTypeForCurrentMode读取详情页的排序设置 const albumOriginalIsCanBack = this.isCanBack; this.isCanBack = true; const albumDetailSortType = this.loadSortTypeForCurrentMode(); this.isCanBack = albumOriginalIsCanBack; // 恢复原值 Logger.info('heanup LocalMusic', `进入专辑详情页: 加载详情页排序 sortType=${albumDetailSortType}`); // 缓存键需要包含排序类型,因为不同的排序类型会产生不同的数据 const albumCacheKey = `${item.name}_sort_${albumDetailSortType}`; let albumList = this.albumMap.get(albumCacheKey) if (albumList === undefined || ArrayUtil.isEmpty(albumList)) { // 使用详情页的排序类型查询 albumList = await this.table.querySongsByAlbum(item.name, albumDetailSortType, this.isShowFileName); if (ArrayUtil.isNotEmpty(albumList)) { this.albumMap.set(albumCacheKey, albumList); } } // 设置当前排序类型为详情页排序 this.sortType = albumDetailSortType; if (albumList !== undefined && ArrayUtil.isNotEmpty(albumList)) { // 注意:不再调用 Utility.doSortListAscending,因为数据已经按 albumDetailSortType 排序了 const nextAlbumCountMap = new Map(this.albumSongCountMap); nextAlbumCountMap.set(item.name, albumList.length); this.albumSongCountMap = nextAlbumCountMap; this.isCanBack = true this.hasMoreData = false this.isLoadingPage = false this.pageCache.clear() this.pageHasMoreCache.clear() this.pageTotalCountCache.clear() this.prefetchingPages.clear() this.lastScrollTriggerPage = -1 this.lastVisibleEndIndex = -1 this.isPagingTransition = false if (this.pagingScheduleTimer) { clearTimeout(this.pagingScheduleTimer) this.pagingScheduleTimer = 0 } this.totalCount = albumList.length this.titleName = item.name this.currentTitleName = '专辑:' + item.name if (item.pixelMapPath) { this.currentTitleCover = item.pixelMapPath } this.updateListData(albumList, true, true) // 详情页立即替换列表,避免旧列表顶部闪现 this.mScrollMap.set(item.name, this.selectedIndex) this.rightTopImage = $r('sys.symbol.chevron_left') } else { ToastUtil.showToast('未找到该专辑的歌曲'); return; } //进入歌单的时候的滚动位置在顶部 if(this.twoFingerType==4){ setTimeout(() => { this.waterScroller.scrollEdge(Edge.Top) this.scroller.scrollEdge(Edge.Top) }, 380) }else{ if (this.isGridMusic) { this.scroller.scrollEdge(Edge.Top) }else{ this.listScroller.scrollEdge(Edge.Top) } } break; case CommonConstants.TYPE_IS_CSJAD: return break; case CommonConstants.TYPE_LOCAL: if (this.isIsoLocalFile(item)) { await this.openIsoArchiveDialog(item) return } if(item&&item.filePath.toLowerCase().endsWith('.cue')){ const cueinfo:CueInfo = await parseCueFile(item.filePath) const cueTracks:CueTrack[] = cueinfo.tracks console.log('onecold cue cueTracks =', JSON.stringify(cueTracks)); if(ArrayUtil.isNotEmpty(cueTracks)){ this.showCueDialog(cueTracks,cueinfo) } // this.showCueSheet = !this.showCueSheet return } console.info('kkMusic doPlay this.CONTROL_PlayStatus ='+this.CONTROL_PlayStatus ) if (this.CONTROL_PlayStatus !== PlayStatus.INIT) { console.info('kkMusic doPlay stop =' ) this.stop(); } const useExistingQueueForPlayback = this.preserveExistingQueueOnNextPlay; const nextQueueScope = this.nextPlayQueueScope; this.preserveExistingQueueOnNextPlay = false; this.nextPlayQueueScope = 'view'; if (isOpen) { this.resetFindPagedLocalQueueState(); this.currentSong = item if (index !== undefined) { this.curIndex = index } this.songList = [] this.songList.push(item) this.sonDataSource.pushArrayData(this.songList) this.playQueueScope = 'single' }else if(isFromSonPlayList){ //点击来自右下角的播放列表 this.currentSong = item if (index !== undefined) { this.curIndex = index } Logger.info(`heanup isFromSonPlayList分支 - 当前播放列表长度: ${this.songList.length}, 当前索引: ${this.curIndex}`) if (this.isFindPagedLocalQueueActive()) { // 这类队列来自发现页分页随机,点击播放列表里的歌也要继续保持“分页队列”身份。 this.playQueueScope = 'find_paged_local' this.currentPlaylistSongFilePaths = []; } else { this.resetFindPagedLocalQueueState(); this.playQueueScope = 'playlist' if (this.currentPlaylistSongFilePaths.length === 0 && this.songList.length > 0) { this.currentPlaylistSongFilePaths = this.songList.map(song => song.filePath); Logger.info(`heanup isFromSonPlayList - 重新构建歌单路径列表,数量=${this.currentPlaylistSongFilePaths.length}`); } } }else if (useExistingQueueForPlayback) { // 特殊分页队列初始化时,直接复用 finishLoadingPlaylist 准备好的当前页,不再按视图重建全量队列。 this.currentSong = item if (index !== undefined) { this.curIndex = index } this.playQueueScope = nextQueueScope this.currentPlaylistSongFilePaths = []; this.sonDataSource.pushArrayData(this.songList) }else { this.resetFindPagedLocalQueueState(); const globalVideoList = this.buildPlayQueueFromView(); this.curIndex = findSongIndexByFilePath(globalVideoList, item.filePath) if (this.curIndex < 0) { globalVideoList.push(item); this.curIndex = globalVideoList.length - 1; } this.songList = globalVideoList this.sonDataSource.pushArrayData(this.songList) this.currentSong = this.songList[this.curIndex] // 只在非歌单模式下清空歌单路径列表 if (this.modeType !== 4) { this.currentPlaylistSongFilePaths = []; Logger.info(`heanup doPlay设置播放列表 - 总歌曲数: ${this.songList.length}, 当前索引: ${this.curIndex}, 当前歌曲: ${this.currentSong?.name}, 已清空歌单路径列表(modeType=${this.modeType})`) } else { // 歌单模式:确保歌单路径列表已设置 if (this.currentPlaylistSongFilePaths.length === 0 && this.songList.length > 0) { this.currentPlaylistSongFilePaths = this.songList.map(song => song.filePath); Logger.info(`heanup doPlay设置播放列表 - 从songList重新构建歌单路径列表,数量=${this.currentPlaylistSongFilePaths.length}`); } else { Logger.info(`heanup doPlay设置播放列表 - 歌单模式,保持歌单路径列表,数量=${this.currentPlaylistSongFilePaths.length}`); } } } AppStorage.setOrCreate('currentSong',this.currentSong) ; try { const localUrl = await this.resolvePlaybackUrlForCurrentSong('doPlay-local'); this.videoUrl = localUrl || this.currentSong.filePath } catch (error) { Logger.error(TAG, `本地歌曲构建播放地址失败: ${(error as Error).message}`); this.videoUrl = this.currentSong.filePath } this.name = this.currentSong.name this.cover = this.currentSong.pixelMapPath this.artist = this.currentSong.artist LogUtil.info('onecold this.currentSong.cover =' + this.currentSong.pixelMapPath) LogUtil.info('this.currentSongmimeType =' + this.currentSong.mimeType) this.startPlayOrResumePlay() break; case CommonConstants.TYPE_WEBDAV: case CommonConstants.TYPE_SMB: case CommonConstants.TYPE_NAVIDROME: case CommonConstants.TYPE_FTP: case CommonConstants.TYPE_BAIDU: case CommonConstants.TYPE_JELLYFIN: case CommonConstants.TYPE_EMBY: case CommonConstants.TYPE_AUDIOSTATION: case CommonConstants.TYPE_PLEX: case CommonConstants.TYPE_DAOLIYU: // 处理网络音频播放(WebDAV/SMB/Navidrome/Jellyfin/Emby/AudioStation/Plex/道理鱼/Baidu) Logger.info(`heanup 处理云端音频播放: ${item.name}, URL: ${item.filePath}`) if (this.CONTROL_PlayStatus !== PlayStatus.INIT) { this.stop(); } this.resetFindPagedLocalQueueState(); this.preserveExistingQueueOnNextPlay = false; this.nextPlayQueueScope = 'view'; if (isOpen) { this.currentSong = item if (index !== undefined) { this.curIndex = index } this.songList = [] this.songList.push(item) this.sonDataSource.pushArrayData(this.songList) } else if (isFromSonPlayList) { // 点击来自右下角的播放列表 this.currentSong = item if (index !== undefined) { this.curIndex = index } // 设置播放队列作用域为歌单 this.playQueueScope = 'playlist' // 确保播放列表是完整的歌单歌曲列表(已在finishLoadingPlaylist中设置) Logger.info(`heanup 网络音频 isFromSonPlayList分支 - 当前播放列表长度: ${this.songList.length}, 当前索引: ${this.curIndex}, playQueueScope=${this.playQueueScope}`) } else { // 从全局网络音频列表中查找 let globalVideoList = this.videoLocalList.filter(video => isRemoteCloudType(video.type)) as VideoItem[]; this.curIndex = globalVideoList.findIndex(video => video.filePath === item.filePath); if (this.curIndex === -1 && index !== undefined) { this.curIndex = index; } this.songList = globalVideoList.length > 0 ? globalVideoList : [item]; this.currentSong = this.songList[this.curIndex]; this.sonDataSource.pushArrayData(this.songList) } await this.hydrateRemoteSongFromDb(this.currentSong); await this.hydrateRemoteSongFromDb(this.currentSong); AppStorage.setOrCreate('currentSong', this.currentSong); const remotePlayLoading = this.beginRemoteSongSwitchLoading(this.currentSong); const playbackUrl = await this.resolvePlaybackUrlForCurrentSong('doPlay-remote'); if (!playbackUrl) { if (remotePlayLoading) { this.hideLoadIng(); } return; } this.videoUrl = playbackUrl; Logger.info(TAG, `WebDAV URL同步构建完成(分支2): ${this.videoUrl}`); this.name = this.currentSong.name this.cover = this.currentSong.pixelMapPath this.artist = this.currentSong.artist Logger.info(`heanup 准备播放网络音频 - 歌名: ${this.name}, 艺术家: ${this.artist}, 完整URL: ${this.videoUrl}`) this.startPlayOrResumePlay() break; } } private async openIsoArchiveDialog(item: VideoItem): Promise { if (!item || StrUtil.isEmpty(item.filePath)) { Logger.error(TAG, `heanup ISO open dialog invalid path=${item ? item.filePath : ''}`) ToastUtil.showToast('ISO 文件无效') return } if (this.isIsoExtracting) { if (ArrayUtil.isNotEmpty(this.isoAudioEntries)) { if (this.isoSourceFilePath !== item.filePath) { ToastUtil.showToast('当前有 ISO 提取任务正在后台执行,已打开提取进度窗口') } this.presentIsoArchiveDialog() return } ToastUtil.showToast('当前有 ISO 提取任务正在后台执行') return } Logger.info(TAG, `heanup ISO open dialog path=${item.filePath}, name=${item.fileName || item.name}`) this.closeIsoDialog() this.isoSourceFilePath = item.filePath this.isoSourceFileName = item.fileName || item.name this.loadingProgressDialogId = DialogHelper.showLoadingProgress({ progress: 0, backCancel: false, autoCancel: false, loadColor: this.themeColor, fontColor: this.themeColor }) try { const task = new taskpool.Task(listIsoAudioEntriesTask, item.filePath) const entries = await taskpool.execute(task, taskpool.Priority.HIGH) as IsoArchiveAudioEntry[] this.isoAudioEntries = Array.isArray(entries) ? entries : [] Logger.info(TAG, `heanup ISO list entries done count=${this.isoAudioEntries.length}, path=${item.filePath}`) if (ArrayUtil.isEmpty(this.isoAudioEntries)) { Logger.error(TAG, `heanup ISO list entries empty path=${item.filePath}`) ToastUtil.showToast('ISO 内未发现可提取的媒体文件') this.closeIsoDialog() return } this.selectedIsoAudioPaths = [] } catch (error) { Logger.error(TAG, `heanup 解析 ISO 失败: ${(error as Error).message}`) ToastUtil.showToast('解析 ISO 失败') this.closeIsoDialog() return } finally { if (this.loadingProgressDialogId) { DialogHelper.closeDialog(this.loadingProgressDialogId) this.loadingProgressDialogId = '' } } this.presentIsoArchiveDialog() } private presentIsoArchiveDialog(): void { if (this.isoComponentId > 0) { return } this.getUIContext().getPromptAction().openCustomDialog({ builder: () => { this.isoComptentBuilder() }, isModal: true, showInSubWindow: false, maskColor: Color.Transparent, dialogTransition: TransitionEffect.translate({ x: 0, y: 390, z: 0 }) .animation({ duration: 600, curve: Curve.Smooth }), maskTransition: TransitionEffect.opacity(0.5) .animation({ duration: 600, curve: Curve.Smooth }) }).then((dialogId: number) => { this.isoComponentId = dialogId }).catch((error: BusinessError) => { Logger.error(TAG, `heanup 打开 ISO 弹窗失败: ${error.message}`) this.closeIsoDialog() }) } private toggleIsoEntrySelection(path: string, checked: boolean): void { const selectedPathSet: Set = new Set(this.selectedIsoAudioPaths) if (checked) { selectedPathSet.add(path) } else { selectedPathSet.delete(path) } this.selectedIsoAudioPaths = this.isoAudioEntries .filter((entry: IsoArchiveAudioEntry) => selectedPathSet.has(entry.path)) .map((entry: IsoArchiveAudioEntry) => entry.path) Logger.info(TAG, `heanup ISO toggle selection path=${path}, checked=${checked}, total=${this.selectedIsoAudioPaths.length}`) } private toggleIsoEntrySelectAll(): void { if (this.selectedIsoAudioPaths.length === this.isoAudioEntries.length) { this.selectedIsoAudioPaths = [] Logger.info(TAG, 'heanup ISO toggle select all cleared') return } this.selectedIsoAudioPaths = this.isoAudioEntries.map((entry: IsoArchiveAudioEntry) => entry.path) Logger.info(TAG, `heanup ISO toggle select all count=${this.selectedIsoAudioPaths.length}`) } private startIsoExtractProgressWatcher(entries: Array, destinationDir: string): void { this.stopIsoExtractProgressWatcher() this.isoExtractExpectedOutputPaths = this.buildIsoExtractExpectedOutputPaths(entries, destinationDir) this.isoExtractTotalBytes = entries.reduce((total: number, entry: IsoArchiveAudioEntry) => { const size = entry && entry.size > 0 ? entry.size : 0 return total + size }, 0) this.isoExtractReceivedTaskProgress = false this.isoExtractDisplayedProgress = 0 this.isoExtractLastActivityTime = Date.now() if (this.isoExtractTotalBytes <= 0 || ArrayUtil.isEmpty(this.isoExtractExpectedOutputPaths)) { return } this.isoExtractProgressTimer = setInterval(() => { this.updateIsoExtractProgressFromFiles() }, 500) } private stopIsoExtractProgressWatcher(): void { if (this.isoExtractProgressTimer) { clearInterval(this.isoExtractProgressTimer) this.isoExtractProgressTimer = 0 } this.isoExtractExpectedOutputPaths = [] this.isoExtractTotalBytes = 0 this.isoExtractReceivedTaskProgress = false this.isoExtractLastActivityTime = 0 } private markIsoExtractActivity(): void { this.isoExtractLastActivityTime = Date.now() } private updateIsoExtractProgressFromFiles(): void { if (this.isoExtractTotalBytes <= 0 || ArrayUtil.isEmpty(this.isoExtractExpectedOutputPaths) || this.isoExtractReceivedTaskProgress) { return } let writtenBytes = 0 let existingCount = 0 for (let i = 0; i < this.isoExtractExpectedOutputPaths.length; i++) { const outputPath = this.isoExtractExpectedOutputPaths[i] if (!FileUtil.accessSync(outputPath)) { continue } existingCount++ try { const stat = fs.statSync(outputPath) as fs.Stat if (typeof stat.size === 'number' && stat.size > 0) { writtenBytes += stat.size } } catch (_error) { } } let progress = 0 if (writtenBytes > 0) { progress = Math.ceil((writtenBytes / this.isoExtractTotalBytes) * 100) if (progress > 99) { progress = 99 } this.markIsoExtractActivity() } else if (existingCount > 0) { progress = 1 this.markIsoExtractActivity() } if (progress > 0) { this.updateIsoExtractProgressState(progress, `正在提取 ${progress}% (${existingCount}/${this.isoExtractExpectedOutputPaths.length})`) return } if (this.isoExtractTask && this.isoExtractLastActivityTime > 0 && Date.now() - this.isoExtractLastActivityTime > this.isoExtractStallTimeoutMs) { Logger.warn(TAG, `heanup ISO 提取进度长时间无变化: timeout=${this.isoExtractStallTimeoutMs}`) this.markIsoExtractActivity() this.updateIsoExtractProgressState(1, '准备提取 1%') } } private updateIsoExtractProgressState(progress: number, message: string, currentName: string = '', force: boolean = false): void { let safeProgress = progress if (safeProgress < 0) { safeProgress = 0 } if (safeProgress > 100) { safeProgress = 100 } if (!force && safeProgress < this.isoExtractDisplayedProgress) { return } this.isoExtractDisplayedProgress = safeProgress this.isoExtractProgress = safeProgress this.isoExtractProgressText = message if (force || StrUtil.isNotEmpty(currentName)) { this.isoExtractCurrentEntryName = currentName } } private resetIsoExtractProgressState(): void { this.isoExtractProgress = 0 this.isoExtractProgressText = '' this.isoExtractCurrentEntryName = '' this.isoExtractDisplayedProgress = 0 } private buildIsoExtractExpectedOutputPaths(entries: Array, destinationDir: string): Array { const outputPaths: Array = [] const occupiedPaths: Set = new Set() for (let i = 0; i < entries.length; i++) { const entry = entries[i] const fileName = entry.name || this.getIsoLastPathSegment(entry.path) const outputPath = this.buildIsoExpectedOutputPath(destinationDir, fileName, occupiedPaths) occupiedPaths.add(outputPath) outputPaths.push(outputPath) } return outputPaths } private buildIsoExpectedOutputPath(destinationDir: string, fileName: string, occupiedPaths: Set): string { const cleanName = this.sanitizeIsoOutputFileName(fileName) const extension = this.getIsoOutputFileExtension(cleanName) const baseName = extension.length > 0 ? cleanName.substring(0, cleanName.length - extension.length) : cleanName let candidatePath = `${this.trimIsoOutputTrailingSeparator(destinationDir)}/${cleanName}` let suffix = 0 while (occupiedPaths.has(candidatePath) || FileUtil.accessSync(candidatePath)) { suffix++ candidatePath = `${this.trimIsoOutputTrailingSeparator(destinationDir)}/${baseName} (${suffix})${extension}` } return candidatePath } private sanitizeIsoOutputFileName(fileName: string): string { const lastSegment = this.getIsoLastPathSegment(fileName) let sanitized = '' for (let i = 0; i < lastSegment.length; i++) { const char = lastSegment[i] if (char === '/' || char === '\\' || char === ':' || char === '*' || char === '?' || char === '"' || char === '<' || char === '>' || char === '|') { sanitized += '_' } else { sanitized += char } } if (sanitized.trim().length === 0) { return 'iso_audio.bin' } return sanitized } private getIsoLastPathSegment(path: string): string { const normalizedPath = path.replace(/\\/g, '/') const lastSeparator = normalizedPath.lastIndexOf('/') if (lastSeparator < 0) { return normalizedPath } return normalizedPath.substring(lastSeparator + 1) } private getIsoOutputFileExtension(fileName: string): string { const lastDot = fileName.lastIndexOf('.') if (lastDot < 0) { return '' } return fileName.substring(lastDot) } private trimIsoOutputTrailingSeparator(path: string): string { if (path.endsWith('/')) { return path.substring(0, path.length - 1) } return path } private cancelIsoExtraction(reason?: string): void { const currentTask = this.isoExtractTask Logger.info(TAG, `heanup ISO cancel request reason=${reason || ''}, hasTask=${currentTask ? 'true' : 'false'}`) this.isoExtractCanceled = true this.isoExtractCancelMessage = reason ? reason : '' this.isoExtractTask = undefined this.updateIsoExtractProgressState(this.isoExtractProgress, '正在停止提取...', this.isoExtractCurrentEntryName, true) if (currentTask) { try { taskpool.terminateTask(currentTask) } catch (error) { Logger.error(TAG, `heanup 终止 ISO 提取任务失败: ${(error as Error).message}`) } } } private moveIsoExtractionToBackground(): void { this.closeIsoDialog(false) ToastUtil.showToast('ISO 提取已转入后台') } private async extractSelectedIsoAudioFiles(): Promise { if (this.isIsoExtracting) { return } if (ArrayUtil.isEmpty(this.selectedIsoAudioPaths)) { ToastUtil.showToast('请先选择要提取的文件') return } const selectedPathSet: Set = new Set(this.selectedIsoAudioPaths) const selectedEntries = this.isoAudioEntries .filter((entry: IsoArchiveAudioEntry) => selectedPathSet.has(entry.path)) if (ArrayUtil.isEmpty(selectedEntries)) { ToastUtil.showToast('未找到要提取的文件') return } const hasSacdTrack: boolean = selectedEntries.some((entry: IsoArchiveAudioEntry) => entry.entryType === 'sacd_track') const destinationDir: string = this.currentPath this.isIsoExtracting = true this.isoExtractCanceled = false this.isoExtractCancelMessage = '' this.isoExtractDestinationDir = destinationDir Logger.info(TAG, `heanup ISO extract start file=${this.isoSourceFilePath}, selected=${selectedEntries.length}, ` + `hasSacd=${hasSacdTrack}, destinationDir=${destinationDir}`) Logger.info(TAG, `heanup ISO selected entries: ${selectedEntries.map((entry: IsoArchiveAudioEntry) => entry.path).join(', ')}`) this.resetIsoExtractProgressState() this.updateIsoExtractProgressState(1, '准备提取 1%', '', true) this.startIsoExtractProgressWatcher(selectedEntries, destinationDir) try { const extractStartedAt = Date.now() const outputPaths: string[] = this.buildIsoExtractExpectedOutputPaths(selectedEntries, destinationDir) const payloadJson = JSON.stringify({ isoPath: this.isoSourceFilePath, destinationDir, entries: selectedEntries, outputPaths }) const task = new taskpool.Task(extractIsoAudioEntriesTask, payloadJson) this.isoExtractTask = task task.onReceiveData((progressData: IsoExtractProgressMessage) => { this.markIsoExtractActivity() const progress = typeof progressData.progress === 'number' ? Math.floor(progressData.progress) : 0 Logger.info(TAG, `heanup ISO task receive stage=${progressData.stage || ''}, progress=${progress}, name=${progressData.name || ''}`) if (progress > 1 && !this.isoExtractReceivedTaskProgress) { this.isoExtractReceivedTaskProgress = true if (this.isoExtractProgressTimer) { clearInterval(this.isoExtractProgressTimer) this.isoExtractProgressTimer = 0 } Logger.info(TAG, `heanup ISO 提取切换为任务进度: progress=${progress}, stage=${progressData.stage || ''}`) } const stageText = progressData.stage === 'prepare' ? '准备提取' : (progressData.stage === 'done' ? '提取完成' : (progressData.stage === 'error' ? '提取异常' : '正在提取')) if (progress > this.isoExtractDisplayedProgress || progressData.stage !== 'extract') { Logger.info(TAG, `heanup ISO 提取任务进度: stage=${progressData.stage || ''}, progress=${progress}, ` + `name=${progressData.name || ''}`) } this.updateIsoExtractProgressState(progress, `${stageText} ${progress}%`, progressData.name || '') }) const result = await taskpool.execute(task, taskpool.Priority.HIGH) as IsoExtractTaskResult const extractElapsedMs = Date.now() - extractStartedAt this.isoExtractTask = undefined Logger.info(TAG, `heanup ISO extract task completed canceled=${this.isoExtractCanceled}, elapsedMs=${extractElapsedMs}`) if (this.isoExtractCanceled) { return } const extractedPaths = result?.extractedPaths || [] const failedEntries = result?.failedEntries || [] const failureDetails = result?.failureDetails || [] Logger.info(TAG, `heanup ISO extract result extracted=${extractedPaths.length}, failed=${failedEntries.length}, details=${failureDetails.length}`) let importErrorMessage: string = '' let importedCount: number = 0 if (ArrayUtil.isNotEmpty(extractedPaths)) { const importStartedAt = Date.now() try { await this.postProcessExtractedIsoFiles(extractedPaths, selectedEntries, outputPaths) importedCount = await this.importExtractedFilesToDirectory(extractedPaths, destinationDir) Logger.info(TAG, `heanup ISO import extracted files success elapsedMs=${Date.now() - importStartedAt}, ` + `importedCount=${importedCount}`) } catch (importError) { importErrorMessage = (importError as Error).message || '导入媒体库失败' Logger.error(TAG, `heanup ISO import extracted files failed elapsedMs=${Date.now() - importStartedAt}, ` + `message=${importErrorMessage}`) } } if (extractedPaths.length > 0 && failedEntries.length > 0) { if (StrUtil.isNotEmpty(importErrorMessage)) { ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个,导入失败`) } else if (importedCount > 0) { ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个,已入库 ${importedCount} 个音频文件`) } else { ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个`) } } else if (extractedPaths.length > 0) { if (StrUtil.isNotEmpty(importErrorMessage)) { ToastUtil.showToast(`提取完成,但导入媒体库失败: ${importErrorMessage}`) } else if (importedCount > 0) { ToastUtil.showToast(`提取完成,已提取 ${extractedPaths.length} 个文件,入库 ${importedCount} 个音频文件`) } else { ToastUtil.showToast(`提取完成,已提取 ${extractedPaths.length} 个文件`) } } else if (ArrayUtil.isNotEmpty(failureDetails)) { const firstFailure: IsoExtractFailureDetail = failureDetails[0] const reasonText = firstFailure.reason && firstFailure.reason.length > 0 ? firstFailure.reason : '提取失败' ToastUtil.showToast(reasonText) } else if (hasSacdTrack) { ToastUtil.showToast('SACD ISO 轨道提取失败') } else { ToastUtil.showToast('提取失败') } } catch (error) { this.isoExtractTask = undefined if (this.isoExtractCanceled) { return } Logger.error(TAG, `heanup 提取 ISO 文件失败: ${(error as Error).message}`) ToastUtil.showToast('提取 ISO 文件失败') } finally { const shouldResetDialogState: boolean = this.isoComponentId <= 0 this.stopIsoExtractProgressWatcher() this.isoExtractTask = undefined this.isIsoExtracting = false Logger.info(TAG, `heanup ISO extract finally canceled=${this.isoExtractCanceled}, cancelMessage=${this.isoExtractCancelMessage}`) if (this.isoExtractCanceled && this.isoExtractCancelMessage.length > 0) { ToastUtil.showToast(this.isoExtractCancelMessage) } this.isoExtractCanceled = false this.isoExtractCancelMessage = '' if (shouldResetDialogState) { this.resetIsoDialogState() } else { this.selectedIsoAudioPaths = [] this.resetIsoExtractProgressState() } } } private async postProcessExtractedIsoFiles(extractedPaths: Array, selectedEntries: Array, outputPaths: Array): Promise { if (ArrayUtil.isEmpty(extractedPaths) || ArrayUtil.isEmpty(selectedEntries) || ArrayUtil.isEmpty(outputPaths)) { return } const entryByOutputPath: Map = new Map() for (let i = 0; i < outputPaths.length && i < selectedEntries.length; i++) { entryByOutputPath.set(outputPaths[i], selectedEntries[i]) } for (let i = 0; i < extractedPaths.length; i++) { const extractedPath = extractedPaths[i] const entry = entryByOutputPath.get(extractedPath) if (!entry) { continue } await this.postProcessExtractedIsoEntry(extractedPath, entry) } } private async postProcessExtractedIsoEntry(filePath: string, entry: IsoArchiveAudioEntry): Promise { if (!Utility.isMusicByExtension(filePath)) { return } const hasEmbeddedMetadata: boolean = StrUtil.isNotEmpty(entry.title) || StrUtil.isNotEmpty(entry.album) || StrUtil.isNotEmpty(entry.artist) || StrUtil.isNotEmpty(entry.albumArtist) || StrUtil.isNotEmpty(entry.comment) || !!(entry.trackNumber && entry.trackNumber > 0) if (entry.entryType !== 'sacd_track' && !hasEmbeddedMetadata) { return } const tags: FFMpegTags = this.buildIsoExtractedMetadata(entry) if (Object.keys(tags).length > 0) { try { const metadataResult = await repairAudioMetadata(filePath, '', tags, true, '') Logger.info(TAG, `heanup ISO metadata post-process path=${filePath}, success=${metadataResult}, type=${entry.entryType || ''}`) } catch (error) { Logger.error(TAG, `heanup ISO metadata post-process failed path=${filePath}, message=${(error as Error).message}`) } } if (!filePath.toLowerCase().endsWith('.dsf')) { return } const coverUrl = await this.resolveIsoExtractCoverUrl(entry) if (StrUtil.isEmpty(coverUrl)) { return } try { const coverResult = await changeMusicCover(this.context, filePath, coverUrl, true, '') Logger.info(TAG, `heanup ISO cover post-process path=${filePath}, success=${coverResult}`) } catch (error) { Logger.error(TAG, `heanup ISO cover post-process failed path=${filePath}, message=${(error as Error).message}`) } } private buildIsoExtractedMetadata(entry: IsoArchiveAudioEntry): FFMpegTags { const metadata: FFMpegTags = {} const title = entry.title || this.getIsoEntryBaseName(entry.name || entry.path) if (StrUtil.isNotEmpty(title)) { metadata.title = title metadata.TITLE = title } if (StrUtil.isNotEmpty(entry.artist)) { metadata.artist = entry.artist metadata.ARTIST = entry.artist } if (StrUtil.isNotEmpty(entry.album)) { metadata.album = entry.album metadata.ALBUM = entry.album } if (entry.trackNumber && entry.trackNumber > 0) { metadata.track = `${entry.trackNumber}` metadata.TRACK = `${entry.trackNumber}` } if (StrUtil.isNotEmpty(entry.albumArtist)) { metadata.ALBUMARTIST = entry.albumArtist metadata.album_artist = entry.albumArtist metadata.TPE2 = entry.albumArtist } if (StrUtil.isNotEmpty(entry.comment)) { metadata.comment = entry.comment metadata.COMMENT = entry.comment metadata.COMM = entry.comment } return metadata } private getIsoEntryBaseName(fileName: string): string { const lastSegment = this.getIsoLastPathSegment(fileName) const lastDot = lastSegment.lastIndexOf('.') if (lastDot <= 0) { return lastSegment } return lastSegment.substring(0, lastDot) } private async resolveIsoExtractCoverUrl(entry: IsoArchiveAudioEntry): Promise { const title = entry.title || this.getIsoEntryBaseName(entry.name || entry.path) const artist = entry.artist || entry.albumArtist || '' if (StrUtil.isEmpty(title) || PreferencesUtil.getStringSync('COVER_API', '') === '') { return '' } try { const coverUrl = await NetAxiosUtil.getLyricCover(title, artist, PreferencesUtil.getStringSync('COVER_API', '')) if (StrUtil.isNotEmpty(coverUrl) && coverUrl !== 'unknown') { return coverUrl } } catch (error) { Logger.error(TAG, `heanup ISO resolve cover failed title=${title}, artist=${artist}, message=${(error as Error).message}`) } return '' } private async importExtractedFilesToDirectory(paths: Array, destinationDir: string): Promise { if (ArrayUtil.isEmpty(paths)) { return 0 } let importedCount: number = 0 for (let i = 0; i < paths.length; i++) { const filePath = paths[i] if (destinationDir === this.lockPath || !Utility.isMeidaByExtension(filePath) || filePath.endsWith('.lrc') || filePath.endsWith('.srt') || Utility.isIsoByExtension(filePath)) { continue } const mediaItem = await Utility.uriGetMusicAssetsFromFile(this.context, filePath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName) await new Promise((resolve) => { this.table.insert(mediaItem, (_id: number) => resolve()) }) importedCount++ } this.cache.delete(destinationDir) if (this.currentPath === destinationDir) { this.pageCache.clear() this.pageHasMoreCache.clear() this.pageTotalCountCache.clear() if (destinationDir === this.lockPath) { this.getSortedFiles(destinationDir, true) } else { await this.resetAndLoadFirstPage() } } return importedCount } private resetIsoDialogState(): void { this.isoAudioEntries = [] this.selectedIsoAudioPaths = [] this.isoSourceFileName = '' this.isoSourceFilePath = '' this.isoExtractDestinationDir = '' this.isIsoExtracting = false this.stopIsoExtractProgressWatcher() this.isoExtractTask = undefined this.resetIsoExtractProgressState() } private closeIsoDialog(reset: boolean = true): void { if (this.isoComponentId > 0) { try { this.getUIContext().getPromptAction().closeCustomDialog(this.isoComponentId) } catch (_error) { } this.isoComponentId = 0 } if (!reset) { return } this.resetIsoDialogState() } @Builder isoComptentBuilder() { IsoExtractComptent({ isoFileName: this.isoSourceFileName, entries: this.isoAudioEntries, selectedPaths: this.selectedIsoAudioPaths, isLongNameRoLL: this.isLongNameRoLL, isExtracting: this.isIsoExtracting, extractProgress: this.isoExtractProgress, extractProgressText: this.isoExtractProgressText, extractCurrentName: this.isoExtractCurrentEntryName, onToggleSelection: (path: string, checked: boolean) => { this.toggleIsoEntrySelection(path, checked) }, onToggleSelectAll: () => { this.toggleIsoEntrySelectAll() }, onExtract: async () => { await this.extractSelectedIsoAudioFiles() }, onRunInBackground: () => { this.moveIsoExtractionToBackground() }, onStopExtract: () => { this.cancelIsoExtraction('已取消 ISO 提取') }, onCancel: () => { if (this.isIsoExtracting) { this.moveIsoExtractionToBackground() return } this.closeIsoDialog() } }) } private cueComponentId: number = 0 private async handleCueTrackImport(cueinfo: CueInfo): Promise { try { let sourceItem = Utility.getItemByFilePath(this.videoLocalList, cueinfo.filePath); if (!sourceItem) { sourceItem = await this.table.queryVideoByFilePath(cueinfo.filePath) || undefined; } const importedItems = await importCueTracks(this.context, this.table, cueinfo, sourceItem); if (ArrayUtil.isEmpty(importedItems)) { ToastUtil.showToast('未导入到任何分轨'); return; } this.getUIContext().getPromptAction().closeCustomDialog(this.cueComponentId); await this.resetAndLoadFirstPage(); ToastUtil.showToast(`分轨完成,已入库 ${importedItems.length} 首`); } catch (error) { Logger.error(TAG, `CUE 分轨入库失败: ${(error as Error).message}`); ToastUtil.showToast('分轨失败'); } } //显示cue分轨列表对话框 showCueDialog(cueTracks:CueTrack[],cueinfo:CueInfo){ this.getUIContext().getPromptAction().openCustomDialog({ builder: () => { this.cueComptentBuilder(cueTracks,cueinfo) }, isModal:true, showInSubWindow:false, maskColor: Color.Transparent, dialogTransition: // 设置弹窗内容显示的过渡效果 TransitionEffect.translate({ x: 0, y: 390, z: 0 }) .animation({ duration: 600, curve: Curve.Smooth }), maskTransition: // 设置蒙层显示的过渡效果 TransitionEffect.opacity(0.5) .animation({ duration: 600, curve: Curve.Smooth }) }).then((dialogId: number) => { this.cueComponentId = dialogId }) .catch((error: BusinessError) => { console.error(`openCustomDialog error code is ${error.code}, message is ${error.message}`) }) } @Builder cueComptentBuilder(cueTracks:CueTrack[]=[],cueinfo:CueInfo){ CueComptent({ title:cueinfo.title, cueTracks:cueTracks, isLongNameRoLL:this.isLongNameRoLL, onCancel:()=>{ this.getUIContext().getPromptAction().closeCustomDialog(this.cueComponentId) }, onSplitTracks: async ()=>{ await this.handleCueTrackImport(cueinfo) }, onDoItemClick: async (item: CueTrack, index: number)=>{ //获取cueinfo对应的filePath let globalVideoList = Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL) as VideoItem[]; this.curIndex = Utility.getCurIndexFromGlobalList(globalVideoList, cueinfo.filePath) this.currentSong = Utility.getItemByFilePath(globalVideoList, cueinfo.filePath) console.info('onecold cueinfo.filePath=', cueinfo.filePath) console.info('onecold this.currentSong =', JSON.stringify(this.currentSong)) if (this.currentSong){ if (this.CONTROL_PlayStatus !== PlayStatus.INIT) { this.stop();//播放之前先停止之前播放。 } this.songList = globalVideoList this.sonDataSource.pushArrayData(this.songList) if (this.currentSong) { try { const cueUrl = await this.resolvePlaybackUrlForCurrentSong('cue-track-play'); if (!cueUrl) { return; } this.videoUrl = cueUrl; } catch (error) { Logger.error(TAG, `CUE 点播构建播放地址失败: ${(error as Error).message}`); this.videoUrl = this.currentSong.filePath; } } this.name = item.title||this.currentSong.name this.cover = this.currentSong.pixelMapPath this.artist = item.performer||this.currentSong.artist this.currentSong.name = this.name this.currentSong.artist = this.artist AppStorage.setOrCreate('currentSong',this.currentSong) ; this.startPlayOrResumePlay(item.startOffset)//从cue的某个位置开始播放 } } }) } @State isFrontWhite: boolean = false private showCoverScaleGuideIfNeeded(): void { const hasShown = PreferencesUtil.getBooleanSync(PREF_PLAY_COVER_SCALE_GUIDE_SHOWN, false) if (hasShown) { return } this.isShowCoverScaleGuide = true setTimeout(() => { if (this.isShowCoverScaleGuide) { this.dismissCoverScaleGuide() } }, 9000) } private dismissCoverScaleGuide(): void { this.isShowCoverScaleGuide = false PreferencesUtil.put(PREF_PLAY_COVER_SCALE_GUIDE_SHOWN, true) } private resolveMiniBarDismissTarget() { const viewportWidthVp = this.windowWidth > 0 ? px2vp(this.windowWidth) : 360 const viewportHeightVp = this.windowHeight > 0 ? px2vp(this.windowHeight) : 800 const useFixedBottomMargin = DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_2IN1 || this.curDisplayIsHiCar return resolvePlayerDismissMorphTarget(viewportWidthVp, viewportHeightVp, this.bottomSafeHeight, useFixedBottomMargin) } private clearPlayDismissTimer(): void { if (this.playDismissTimer >= 0) { clearTimeout(this.playDismissTimer) this.playDismissTimer = -1 } } private clearPlayOpenTimer(): void { if (this.playOpenTimer >= 0) { clearTimeout(this.playOpenTimer) this.playOpenTimer = -1 } } private resetMusicPlayDismissState(): void { this.clearPlayDismissTimer() this.clearPlayOpenTimer() this.playOpenFromMiniBarPending = false this.translateY = 0 this.playDismissTranslateX = 0 this.playDismissScale = 1 this.playDismissScaleY = 1 this.playDismissOpacity = 1 this.playDismissBorderRadius = 0 this.playDismissProxySize = 0 this.playDismissProxyOpacity = 0 this.scaleValueImage = 1 this.scaleValueText = 1 this.playDragUiOpacity = 1 } private preparePlayerViewOpenFromMiniBar(): void { const target = this.resolveMiniBarDismissTarget() this.playOpenFromMiniBarPending = true this.translateY = target.translateY this.playDismissTranslateX = 0 this.playDismissScale = 0.9 this.playDismissScaleY = 0.8 this.playDismissOpacity = 0 this.playDismissBorderRadius = 48 this.playDismissProxySize = 0 this.playDismissProxyOpacity = 0 this.playDragUiOpacity = 0 this.scaleValueImage = 0.82 this.scaleValueText = 0.88 } private startPlayerViewOpenAnimationIfNeeded(): void { if (!this.playOpenFromMiniBarPending) { return } this.playOpenFromMiniBarPending = false this.getUIContext()?.animateTo({ duration: this.playOpenExpandDuration, curve: Curve.Friction }, () => { this.playDismissProxySize = 0 this.playDismissProxyOpacity = 0 this.translateY = 0 this.playDismissTranslateX = 0 this.playDismissScale = 1 this.playDismissScaleY = 1 this.playDismissOpacity = 1 this.playDismissBorderRadius = 0 this.playDragUiOpacity = 1 this.scaleValueImage = 1 this.scaleValueText = 1 }) } setShowPlayTrue(){ this.getUIContext()?.animateTo({ duration: 500, curve: Curve.Friction }, () => { this.isShowPlay = true; this.showCoverScaleGuideIfNeeded() }); } setShowPlayFalse(){ this.getUIContext()?.animateTo({ duration: 500, curve: Curve.Friction }, () => { this.isShowPlay = false; }); this.isShowCoverScaleGuide = false } //打开播放页 showPlayerView() { console.info('onecold showPlayerView this.songList'+ JSON.stringify(this.songList)) if (ArrayUtil.isNotEmpty(this.songList)) { // this.setShowPlayTrue() let lyricPath = this.videoUrl.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc'; this.initLyric(lyricPath); this.startAutoHide(); } else { ToastUtil.showToast('当前播放列表为空,请先导入音乐。'); } } //打开播放列表 openPlayList(){ if (ArrayUtil.isNotEmpty(this.songList)) { this.isShowSheet = !this.isShowSheet; this.isFrontWhite = false LogUtil.info('onecold scrollToIndex = ' + this.curIndex) this.scrollIndex() if (this.shouldHydrateFullMediaPlayListOnOpen()) { void this.hydrateFullMediaPlayListIfNeeded(); } } else { ToastUtil.showToast('当前播放列表为空,请先导入音乐。') } } scrollIndex(){ try { setTimeout(() => { this.getUIContext()?.animateTo({ duration: 555 }, () => { if(this.curIndex>60){ this.playListScroller.scrollToIndex(this.curIndex,false, ScrollAlign.CENTER) }else{ this.playListScroller.scrollToIndex(this.curIndex,true, ScrollAlign.CENTER) } }) }, 123) }catch (error) { console.error(`onecold scrollIndex出错: ${error}`); } } scrollIndexMain(dialogId?:string){ try { setTimeout(() => { this.getUIContext()?.animateTo({ duration: 555 }, () => { console.info('heanup scrollIndexMain this.curIndex='+this.curIndex) if(dialogId) DialogHelper.closeDialog(dialogId) let isAni:boolean = false if(this.curIndex<50){ isAni = true } if(this.twoFingerType==4){ this.waterScroller.scrollToIndex(this.curIndex,isAni, ScrollAlign.CENTER) }else{ if(this.isGridMusic){ this.scroller.scrollToIndex(this.curIndex,isAni, ScrollAlign.CENTER) }else{ this.listScroller.scrollToIndex(this.curIndex,isAni, ScrollAlign.CENTER) } } }) }, 123) }catch (error) { console.error(`onecold scrollIndexMain出错: ${error}`); } } // 智能定位当前播放歌曲,如果当前页没有则加载完整列表 async locateCurrentSong(): Promise { if (!this.currentSong) { console.warn("heanup locateCurrentSong: 当前没有播放的歌曲"); return; } // 检查当前播放歌曲是否在已加载的列表中 const targetIndex = Utility.getIndexFromList(this.videoLocalList, this.currentSong.filePath); if (targetIndex > 0) { // 歌曲在当前列表中,直接滚动 console.info("heanup locateCurrentSong: 找到歌曲,索引=" + targetIndex); this.curIndex = targetIndex; this.scrollIndexMain(); } else { // 歌曲不在当前列表中,加载当前歌曲再第几页完整列表后再定位 console.info("heanup locateCurrentSong: 歌曲不在当前列表,开始加载完整列表"); const dialogID = DialogHelper.showLoadingDialog() await this.hydrateFullMediaPlayListIfNeeded(false); this.curIndex = Utility.getIndexFromList(this.videoLocalList, this.currentSong.filePath); this.scrollIndexMain(dialogID); } } //播放列表布局 @Builder PlayListSheet() { Column() { this.PlayList() } .width('100%') .height('100%') } @State titleStr: string = '' @State ablumStr: string = '' @State artistStr: string = '' @State lyricConStr: string = '' @State currentEditItem: string = '' @State yearStr: string = '' @State trackStr: string = '' @State genreStr: string = '' @State imagePathStr: string = '' @State albumArtistStr: string = '' @State composerStr: string = '' @State lyricistStr: string = '' @State commentStr: string = '' @State discStr: string = '' doUpdateData() { setTimeout(async () => { if (this.modeType === 0) { this.deleteCache(this.currentPath) await this.resetAndLoadFirstPage(); }else if (this.modeType === 1){ await this.resetAndLoadFirstPage(); workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName }); } }, 500) } doAllUpdateData() { setTimeout(async () => { if (this.modeType === 0) { this.deleteCache(this.currentPath) await this.resetAndLoadFirstPage(); }else{ await this.resetAndLoadFirstPage(); } workerInstance.postMessage({ code: 2, data: this.context,data2:this.packName }); workerInstance.postMessage({ code: 3, data: this.context }); workerInstance.postMessage({ code: 4, data: this.context }); }, 999) } private delayMs(ms: number): Promise { return new Promise(resolve => { setTimeout(() => resolve(), ms); }); } private async appendItemsSmoothly(items: VideoItem[]): Promise { if (!items || items.length === 0) { return; } const isMediaLibraryListMode = this.isMediaLibraryListMode(); const isHeavyGridWaterMode = this.isGridMusic || this.twoFingerType == 4; const directAppendThreshold = isHeavyGridWaterMode ? 8 : (isMediaLibraryListMode ? 6 : 12); if (!this.isScrolling || items.length <= directAppendThreshold) { this.videoLocalList.push(...items); this.dataSource.appendArrayData(items); return; } const chunkSize = isHeavyGridWaterMode ? Math.max(3, Math.min(6, Math.floor(this.pageSize * 0.15))) : (isMediaLibraryListMode ? Math.max(2, Math.min(4, Math.floor(this.pageSize * 0.08))) : Math.max(4, Math.min(8, Math.floor(this.pageSize * 0.18)))); const chunkDelay = isHeavyGridWaterMode ? 12 : (isMediaLibraryListMode ? 10 : 8); for (let index = 0; index < items.length; index += chunkSize) { const chunk = items.slice(index, Math.min(index + chunkSize, items.length)); this.videoLocalList.push(...chunk); this.dataSource.appendArrayData(chunk); if (index + chunkSize < items.length) { await this.delayMs(chunkDelay); } } } /** * 加载指定页的数据 */ private async loadPage(pageIndex: number, append: boolean = true): Promise { try { // 1. 检查缓存 if (this.pageCache.has(pageIndex)) { Logger.info('heanup LocalMusic', `loadPage: 从缓存加载 page=${pageIndex}`); const inMediaLibraryWarmup = this.modeType === 1 && pageIndex === 0 && !append && this.isMediaLibraryWarmupWindow(); const cachedData = this.pageCache.get(pageIndex)!; const cachedHasMore = this.pageHasMoreCache.get(pageIndex); const cachedTotal = this.pageTotalCountCache.get(pageIndex); if (cachedHasMore !== undefined) { this.hasMoreData = cachedHasMore; } if (cachedTotal !== undefined) { this.totalCount = cachedTotal; if (this.modeType === 1) { this.mediaKuCount = cachedTotal; } } if (append) { this.prefetchListThumbsFromItems(cachedData, 26); this.prefetchGridWaterThumbsFromItems(cachedData, this.twoFingerType == 4 ? 56 : 42); await this.appendItemsSmoothly(cachedData); } else { this.videoLocalList = cachedData; this.dataSource.pushArrayData(this.videoLocalList); if (inMediaLibraryWarmup) { this.prefetchListThumbsFromItems(cachedData, 8); } else { this.prefetchListThumbsFromItems(cachedData, 30); this.prefetchGridWaterThumbsFromItems(cachedData, this.twoFingerType == 4 ? 56 : 42); } } this.setButtonStatus(); if (inMediaLibraryWarmup) { this.scheduleAlphaBetRebuild(900); } else { this.markAlphaBetDirty(); } if (this.hasMoreData) { if (inMediaLibraryWarmup) { if (this.mediaLibraryWarmupPrefetchTimer) { clearTimeout(this.mediaLibraryWarmupPrefetchTimer); } this.mediaLibraryWarmupPrefetchTimer = setTimeout(() => { this.mediaLibraryWarmupPrefetchTimer = 0; if (this.modeType === 1 && !this.isCanBack) { this.prefetchFollowingPages(pageIndex + 1); } }, 900) as number; } else { this.prefetchFollowingPages(pageIndex + 1); } } return; } this.isLoadingPage = true; Logger.info('heanup LocalMusic', `loadPage: 开始加载 page=${pageIndex}, size=${this.pageSize}, mode=${this.modeType}`); // 2. 构造查询参数 const options: PageQueryOptions = { pageIndex: pageIndex, pageSize: this.pageSize, sortType: this.sortType, searchKeyword: this.isSearchMode ? this.searchText : undefined, isShowFileName: this.isShowFileName }; // 根据 modeType 补充查询条件 if (this.modeType === 0) { options.parentPath = this.currentPath; } // 3. 执行查询 let result: PageQueryResult = { items: [], totalCount: 0, hasMore: false }; switch (this.modeType) { case 0: result = await this.table.queryByParentPathPaged(options); result.items = this.filterOutLocalIsoItems(result.items); // modeType=0时,第一页需要添加文件夹列表 if (pageIndex === 0 && !this.isSearchMode) { const directories: VideoItem[] = await this.loadDirectories(this.currentPath); const isoItems: VideoItem[] = this.getLocalIsoItems(this.currentPath); result.items = [...directories, ...isoItems, ...result.items]; result.totalCount += directories.length + isoItems.length; } break; case 1: result = await this.table.queryMediaLibraryPaged(options); break; case 2: result = await this.table.queryArtistsPaged(options); break; case 3: result = await this.table.queryAlbumsPaged(options); break; default: result = await this.table.queryMediaLibraryPaged(options); break; } if (!result || result.items.length === 0) { Logger.info('heanup LocalMusic', 'loadPage: 查询结果为空'); this.isLoadingPage = false; this.hasMoreData = false; // 仅首页为空时清空,后续页为空只终止继续分页 if (pageIndex === 0) { this.totalCount = 0; this.videoLocalList = []; this.dataSource.pushArrayData(this.videoLocalList); this.setButtonStatus(); this.markAlphaBetDirty(); } return; } // 4. 更新状态和缓存 this.totalCount = result.totalCount; this.hasMoreData = result.hasMore; if (this.modeType === 1) { this.mediaKuCount = result.totalCount; } if (this.modeType === 2 && result.countMap) { this.mergeArtistCountMap(result.countMap); } else if (this.modeType === 3 && result.countMap) { this.mergeAlbumCountMap(result.countMap); } Logger.info('heanup LocalMusic', `loadPage: 查询完成 items=${result.items.length}, total=${result.totalCount}, hasMore=${result.hasMore}`); // 缓存管理 - 只保留最近3页 this.pageCache.set(pageIndex, result.items); this.pageHasMoreCache.set(pageIndex, result.hasMore); this.pageTotalCountCache.set(pageIndex, result.totalCount); this.cleanOldCache(pageIndex); // 5. 更新显示列表 const inMediaLibraryWarmup = this.modeType === 1 && pageIndex === 0 && !append && this.isMediaLibraryWarmupWindow(); if (append) { this.prefetchListThumbsFromItems(result.items, 26); this.prefetchGridWaterThumbsFromItems(result.items, this.twoFingerType == 4 ? 56 : 42); await this.appendItemsSmoothly(result.items); } else { this.videoLocalList = result.items; this.dataSource.pushArrayData(this.videoLocalList); if (inMediaLibraryWarmup) { this.prefetchListThumbsFromItems(result.items, 8); } else { this.prefetchListThumbsFromItems(result.items, 30); this.prefetchGridWaterThumbsFromItems(result.items, this.twoFingerType == 4 ? 56 : 42); } } this.setButtonStatus(); if (inMediaLibraryWarmup) { this.scheduleAlphaBetRebuild(900); } else { this.markAlphaBetDirty(); } if (inMediaLibraryWarmup) { if (this.mediaLibraryWarmupPrefetchTimer) { clearTimeout(this.mediaLibraryWarmupPrefetchTimer); } this.mediaLibraryWarmupPrefetchTimer = setTimeout(() => { this.mediaLibraryWarmupPrefetchTimer = 0; if (this.modeType === 1 && !this.isCanBack) { this.prefetchFollowingPages(pageIndex + 1); } }, 900) as number; } else { this.prefetchFollowingPages(pageIndex + 1); } this.isLoadingPage = false; } catch (err) { const error = err as Error; Logger.error('heanup LocalMusic', `loadPage error: ${error.message}`); // ToastUtil.showToast('加载数据失败'); this.isLoadingPage = false; this.hasMoreData = false; } } /** * 清理旧缓存 - 只保留当前页前后各1页 */ private cleanOldCache(currentPage: number): void { const pagesToKeep = new Set([currentPage - 1, currentPage, currentPage + 1]); const keysToDelete: number[] = []; this.pageCache.forEach((_, page) => { if (!pagesToKeep.has(page)) { keysToDelete.push(page); } }); keysToDelete.forEach(page => { this.pageCache.delete(page); this.pageHasMoreCache.delete(page); this.pageTotalCountCache.delete(page); }); if (keysToDelete.length > 0) { Logger.info('heanup LocalMusic', `cleanOldCache: 清理了 ${keysToDelete.length} 页缓存`); } } private async prefetchNextPage(nextPage: number): Promise { if (this.isCanBack || this.isHistory || this.isFavMusic) { return; } if (!this.hasMoreData || this.pageCache.has(nextPage) || this.prefetchingPages.has(nextPage)) { return; } const expectedModeType = this.modeType; const expectedSortType = this.sortType; const expectedIsShowFileName = this.isShowFileName; const expectedSearchKeyword = this.isSearchMode ? this.searchText : ''; const expectedParentPath = this.currentPath; this.prefetchingPages.add(nextPage); try { const options: PageQueryOptions = { pageIndex: nextPage, pageSize: this.pageSize, sortType: this.sortType, searchKeyword: this.isSearchMode ? this.searchText : undefined, isShowFileName: this.isShowFileName }; if (this.modeType === 0) { options.parentPath = this.currentPath; } let result: PageQueryResult = { items: [], totalCount: 0, hasMore: false }; switch (this.modeType) { case 0: result = await this.table.queryByParentPathPaged(options); result.items = this.filterOutLocalIsoItems(result.items); break; case 1: result = await this.table.queryMediaLibraryPaged(options); break; case 2: result = await this.table.queryArtistsPaged(options); if (result.countMap) { this.mergeArtistCountMap(result.countMap); } break; case 3: result = await this.table.queryAlbumsPaged(options); if (result.countMap) { this.mergeAlbumCountMap(result.countMap); } break; default: result = await this.table.queryMediaLibraryPaged(options); break; } const currentSearchKeyword = this.isSearchMode ? this.searchText : ''; if (expectedModeType !== this.modeType || expectedSortType !== this.sortType || expectedIsShowFileName !== this.isShowFileName || expectedSearchKeyword !== currentSearchKeyword || (expectedModeType === 0 && expectedParentPath !== this.currentPath)) { Logger.info('heanup LocalMusic', `prefetchNextPage: 上下文已变化,放弃写入 page=${nextPage}`); return; } this.pageHasMoreCache.set(nextPage, result.hasMore); this.pageTotalCountCache.set(nextPage, result.totalCount); if (result.items.length > 0) { this.pageCache.set(nextPage, result.items); this.prefetchGridWaterThumbsFromItems(result.items, this.twoFingerType == 4 ? 48 : 36); this.cleanOldCache(nextPage); if (nextPage === this.currentPage + 1 && !this.isLoadingPage && !this.isPagingTransition && this.lastVisibleEndIndex >= 0 && this.videoLocalList.length > 0) { const seenRatio = (this.lastVisibleEndIndex + 1) / Math.max(this.videoLocalList.length, 1); if (seenRatio >= Math.max(0.14, this.getDynamicAppendStartRatio() - 0.12)) { Logger.info('heanup LocalMusic', `prefetchNextPage: 下一页缓存已就绪,提前衔接 page=${nextPage}, ratio=${seenRatio}`); this.scheduleLoadNextPage(8); } } } } catch (err) { const error = err as Error; Logger.warn('heanup LocalMusic', `prefetchNextPage error: ${error.message}`); } finally { this.prefetchingPages.delete(nextPage); } } private mergeArtistCountMap(countMap: Map): void { const nextMap = new Map(this.artistSongCountMap); countMap.forEach((count, artist) => { nextMap.set(artist, count); }); this.artistSongCountMap = nextMap; } private mergeAlbumCountMap(countMap: Map): void { const nextMap = new Map(this.albumSongCountMap); countMap.forEach((count, album) => { nextMap.set(album, count); }); this.albumSongCountMap = nextMap; } private getArtistSongCount(artist: string): number { return this.artistSongCountMap.get(artist) ?? this.artistMap.get(artist)?.length ?? 0; } private getAlbumSongCount(album: string): number { return this.albumSongCountMap.get(album) ?? this.albumMap.get(album)?.length ?? 0; } /** * 加载文件夹列表(仅用于modeType=0的首页模式) */ private async loadDirectories(curPath: string): Promise { try { Logger.info('heanup LocalMusic', `loadDirectories: 开始加载文件夹 path=${curPath}`); // 检查缓存 const cacheKey = curPath + "_dirList"; const cachedDirList = this.findCache(cacheKey); if (cachedDirList !== undefined && Array.isArray(cachedDirList)) { Logger.info('heanup LocalMusic', `loadDirectories: 从缓存加载 count=${cachedDirList.length}`); return cachedDirList as VideoItem[]; } // 从文件系统读取 const fileList = FileUtil.listFileSync(curPath); const directories: VideoItem[] = []; for (let i = 0; i < fileList.length; i++) { const path = curPath + '/' + fileList[i]; if (FileUtil.isDirectory(path)) { const item: VideoItem = new VideoItem( fileList[i].toString(), fileList[i].toString(), path, CommonConstants.TYPE_IS_DIR, 0, '' ); if (this.isShowDir(item.name)) { directories.push(item); } } } // 排序 Utility.doSortListAscending(directories); // 缓存文件夹列表 if (curPath === this.rootPath) { this.dirList = directories; this.addCache(cacheKey, directories); } Logger.info('heanup LocalMusic', `loadDirectories: 加载完成 count=${directories.length}`); return directories; } catch (err) { const error = err as Error; Logger.error('heanup LocalMusic', `loadDirectories error: ${error.message}`); return []; } } /** * 构建播放队列(避免一次性加载全量) */ private buildPlayQueueFromView(): Array { if (this.modeType === 4) { this.playQueueScope = 'playlist'; return this.currentSongList; } if (this.isHistory) { this.playQueueScope = 'history'; return this.historyList; } if (this.isFavMusic) { this.playQueueScope = 'fav'; return this.favList; } this.playQueueScope = 'view'; return Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL); } private openSpecialLocalList(target: string): void { this.mType = 0 this.isShowDrawer = false this.offsetX = 0 this.isHistory = false this.isFavMusic = false if (target === 'recent') { this.titleName = '最近播放' this.currentTitleName = '最近播放' this.currentTitleCover = $r('app.media.alt') this.updateListData(this.historyList, true) if (ArrayUtil.isNotEmpty(this.historyList)) { this.currentTitleCover = Utility.getFirstCoverFromList(this.historyList) } this.isHistory = true return } if (target === 'favorite') { this.titleName = '我的收藏' this.currentTitleName = '我的收藏' this.currentTitleCover = $r('app.media.alt') this.updateListData(this.favList, true) if (ArrayUtil.isNotEmpty(this.favList)) { this.currentTitleCover = Utility.getFirstCoverFromList(this.favList) } this.isFavMusic = true } } private async tryLoadNextPageForPlayback(): Promise { if (this.playQueueScope === 'find_paged_local') { // 发现页的本地随机分页队列单独补页,不去动主媒体库列表状态。 await this.loadMoreFindPagedLocalSongs(); return; } if (this.playQueueScope !== 'view') { return; } if (this.isCanBack || this.isHistory || this.isFavMusic) { return; } if (!this.hasMoreData || this.isLoadingPage || this.isPagingTransition) { return; } await this.loadNextPage(); const nextList = Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL); if (nextList.length > this.songList.length) { this.songList = nextList; this.sonDataSource.pushArrayData(this.songList); } } private async loadMoreFindPagedLocalSongs(): Promise { if (!this.findPagedLocalQueueHasMore || this.isLoadingFindPagedLocalQueue) { return; } this.isLoadingFindPagedLocalQueue = true; const nextPage = this.findPagedLocalQueuePageIndex + 1; Logger.info(TAG, `loadMoreFindPagedLocalSongs: page=${nextPage}, size=${this.findPagedLocalQueuePageSize}, ` + `loaded=${this.songList.length}, total=${this.findPagedLocalQueueTotalCount}`); try { // 继续按发现页建立队列时的排序规则查询下一页,保证后续页和初始页顺序一致。 const result = await this.table.queryMediaLibraryPaged({ pageIndex: nextPage, pageSize: this.findPagedLocalQueuePageSize, sortType: this.findPagedLocalQueueSortType, isShowFileName: this.isShowFileName }); this.findPagedLocalQueuePageIndex = nextPage; this.findPagedLocalQueueHasMore = result.hasMore; if (result.totalCount > 0) { this.findPagedLocalQueueTotalCount = result.totalCount; this.totalCount = result.totalCount; } if (!result.items || result.items.length === 0) { return; } const existingPaths = new Set(this.songList.map((item: VideoItem) => item.filePath)); // 随机播放过程中可能已经把“未加载页里的歌曲”临时插进队列,这里要去重后再追加。 const newItems = result.items.filter((item: VideoItem) => !existingPaths.has(item.filePath)); if (newItems.length === 0) { return; } this.songList.push(...newItems); this.currentSongList = this.songList; this.sonDataSource.appendArrayData(newItems); PreferencesUtil.putSync('LastMusicList', this.songList); } catch (error) { Logger.error(TAG, `loadMoreFindPagedLocalSongs failed: ${(error as Error).message}`); } finally { this.isLoadingFindPagedLocalQueue = false; } } /** * 加载更多流媒体歌曲(Navidrome/Jellyfin/Emby/AudioStation/Plex) * 当播放列表弹窗滚动到底部时调用 */ private async loadMoreNavidromeSongs(): Promise { const beforeCount = this.songList.length; const hasMore = hasMoreData(); Logger.info('heanup', `开始加载更多流媒体歌曲, 当前数量=${beforeCount}, hasMoreData=${hasMore}`); if (!hasMore) { Logger.info('heanup', '没有更多数据可加载(hasMoreData=false)'); return; } // 触发加载更多(会更新NavidromePlaylistStore中的数据) const success = await triggerLoadMoreSongs(1); Logger.info('heanup', `triggerLoadMoreSongs返回: ${success}`); if (success) { // 获取更新后的完整列表 const updatedList = getNavidromeVideoItems(); Logger.info('heanup', `更新后列表长度: ${updatedList.length}`); if (updatedList.length > beforeCount) { Logger.info('heanup', `流媒体歌曲加载成功: ${beforeCount} -> ${updatedList.length}`); this.songList = updatedList; this.sonDataSource.pushArrayData(this.songList); } else { Logger.info('heanup', '列表长度未增加'); } } else { Logger.info('heanup', '流媒体歌曲加载未返回新数据(triggerLoadMoreSongs=false)'); } } /** * 重置分页状态并加载首页 */ private async resetAndLoadFirstPage(clearExistingList: boolean = true): Promise { Logger.info('heanup LocalMusic', 'resetAndLoadFirstPage: 重置分页状态'); this.currentPage = 0; if (clearExistingList) { this.videoLocalList = []; } this.pageCache.clear(); this.pageHasMoreCache.clear(); this.pageTotalCountCache.clear(); this.prefetchingPages.clear(); this.hasMoreData = true; this.totalCount = 0; this.lastScrollTriggerPage = -1; this.lastVisibleEndIndex = -1; this.listScrollAlphaCheckAt = 0; this.listScrollPagingCheckAt = 0; this.listScrollLastAlphaStart = -1; this.listScrollLastPagingEnd = -1; this.gridScrollPagingCheckAt = 0; this.gridScrollLastPagingEnd = -1; this.waterScrollPagingCheckAt = 0; this.waterScrollLastPagingEnd = -1; this.listVisibleStart = -1; this.listVisibleEnd = -1; this.listThumbWarmupCheckAt = 0; this.listThumbWarmupStart = -1; this.listThumbWarmupEnd = -1; this.gridThumbWarmupCheckAt = 0; this.gridThumbWarmupStart = -1; this.gridThumbWarmupEnd = -1; this.waterThumbWarmupCheckAt = 0; this.waterThumbWarmupStart = -1; this.waterThumbWarmupEnd = -1; this.gridVisibleStart = -1; this.gridVisibleEnd = -1; this.waterVisibleStart = -1; this.waterVisibleEnd = -1; this.gridWaterLastActiveTs = 0; this.isPagingTransition = false; if (this.pagingScheduleTimer) { clearTimeout(this.pagingScheduleTimer); this.pagingScheduleTimer = 0; } if (this.gridWaterScrollIdleTimer) { clearTimeout(this.gridWaterScrollIdleTimer); this.gridWaterScrollIdleTimer = 0; } this.coverThumbCache.clearPending(); this.pendingCoverThumbRefresh = false; if (this.modeType === 2) { this.artistSongCountMap = new Map(); } else if (this.modeType === 3) { this.albumSongCountMap = new Map(); } await this.loadPage(0, false); } private markGridWaterScrollingActive(): void { if (!this.isScrolling) { this.isScrolling = true; } const now = Date.now(); if (now - this.gridWaterLastActiveTs < 80) { return; } this.gridWaterLastActiveTs = now; if (this.gridWaterScrollIdleTimer) { clearTimeout(this.gridWaterScrollIdleTimer); } this.gridWaterScrollIdleTimer = setTimeout(() => { this.gridWaterScrollIdleTimer = 0; if (this.isScrolling) { this.isScrolling = false; } this.flushPendingCoverThumbRefresh(); }, 120); } private updateTitleBarByScrollOffset(currentOffsetY: number): void { if (!this.autoHideTitle) { return; } const now = Date.now(); if (now - this.titleBarLastCheckTs < 36) { return; } this.titleBarLastCheckTs = now; const delta = currentOffsetY - this.prevOffsetY; if (Math.abs(delta) < this.titleBarToggleMinDelta) { return; } if (delta > 0) { if (this.isShowTitleBar) { this.isShowTitleBar = false; } } else { if (!this.isShowTitleBar) { this.isShowTitleBar = true; } } this.prevOffsetY = currentOffsetY; } private shouldPrefetchSecondPage(): boolean { if (this.isMediaLibraryListMode()) { return this.pageSize <= 120; } if (this.pageSize > 60) { return false; } return this.isGridMusic || this.twoFingerType == 4; } private isMediaLibraryListMode(): boolean { return this.modeType === 1 && !this.isGridMusic && this.twoFingerType != 4; } private isMediaLibraryWarmupWindow(): boolean { if (this.modeType !== 1 || this.isCanBack) { return false; } if (this.mediaLibraryEnterTs <= 0) { return false; } return Date.now() - this.mediaLibraryEnterTs < 1800; } private getDynamicPreloadStartRatio(): number { if (this.twoFingerType == 4) { return 0.10; } if (this.isGridMusic) { return 0.14; } if (this.isMediaLibraryListMode()) { return 0.12; } return this.preloadStartRatio; } private getDynamicAppendStartRatio(): number { if (this.twoFingerType == 4) { return 0.18; } if (this.isGridMusic) { return 0.22; } if (this.isMediaLibraryListMode()) { return 0.22; } return this.appendStartRatio; } private getDynamicEarlyAppendReserveCount(): number { if (this.twoFingerType == 4) { return Math.max(this.earlyAppendReserveCount, Math.floor(this.pageSize * 0.9)); } if (this.isGridMusic) { return Math.max(this.earlyAppendReserveCount, Math.floor(this.pageSize * 0.82)); } if (this.isMediaLibraryListMode()) { return Math.max(this.earlyAppendReserveCount, Math.floor(this.pageSize * 0.75)); } return this.earlyAppendReserveCount; } private prefetchFollowingPages(nextPage: number): void { void this.prefetchNextPage(nextPage); if (this.shouldPrefetchSecondPage()) { void this.prefetchNextPage(nextPage + 1); } } private scrollToTopForCurrentLayout(): void { if (this.twoFingerType == 4) { this.waterScroller.scrollEdge(Edge.Top); this.scroller.scrollEdge(Edge.Top); return; } if (this.isGridMusic) { this.scroller.scrollEdge(Edge.Top); return; } this.listScroller.scrollEdge(Edge.Top); } private getListImageSource(item: VideoItem, index: number, coverVersion: number = 0): ResourceStr { const fallback = item.type == CommonConstants.TYPE_IS_DIR ? $r('app.media.dir_alt') : $r('app.media.alt'); if (coverVersion < -1) { return fallback; } if (StrUtil.isEmpty(item.pixelMapPath)) { return fallback; } const sourceUri = item.pixelMapPath as string; this.initCoverThumbCacheIfNeeded(); const targetSize = this.getListThumbTargetSize(); const cachedThumb = this.coverThumbCache.getThumbUri(sourceUri, targetSize); if (cachedThumb) { return cachedThumb; } if (!this.coverThumbCache.isRemoteImageSource(sourceUri)) { const preloadBuffer = Math.max(8, Math.floor(this.pageSize * 0.2)); const hasRange = this.listVisibleStart >= 0 && this.listVisibleEnd >= 0; const isNearVisible = !hasRange || (index >= this.listVisibleStart - preloadBuffer && index <= this.listVisibleEnd + preloadBuffer); if (isNearVisible) { this.coverThumbCache.ensureThumbRequested(sourceUri, targetSize); } } return sourceUri; } private getListThumbTargetSize(): number { if (this.twoFingerType == 1) { return 96; } if (this.twoFingerType == 2) { return 112; } return 128; } private prefetchListThumbsFromItems(items: VideoItem[], limit: number = 24): void { if (!this.isMediaLibraryListMode() || !items || items.length === 0) { return; } this.initCoverThumbCacheIfNeeded(); const targetSize = this.getListThumbTargetSize(); const maxCount = Math.min(limit, items.length); for (let i = 0; i < maxCount; i++) { const item = items[i]; if (StrUtil.isEmpty(item.pixelMapPath)) { continue; } const sourceUri = item.pixelMapPath as string; if (!this.coverThumbCache.isRemoteImageSource(sourceUri)) { this.coverThumbCache.ensureThumbRequested(sourceUri, targetSize); } } } private warmupListVisibleThumbs(start: number, end: number): void { if (!this.isMediaLibraryListMode()) { return; } if (start < 0 || end < 0 || this.videoLocalList.length <= 0) { return; } const now = Date.now(); const startDelta = this.listThumbWarmupStart >= 0 ? Math.abs(start - this.listThumbWarmupStart) : 999; const endDelta = this.listThumbWarmupEnd >= 0 ? Math.abs(end - this.listThumbWarmupEnd) : 999; if (startDelta < 2 && endDelta < 2 && now - this.listThumbWarmupCheckAt < 90) { return; } this.listThumbWarmupCheckAt = now; this.listThumbWarmupStart = start; this.listThumbWarmupEnd = end; this.initCoverThumbCacheIfNeeded(); const targetSize = this.getListThumbTargetSize(); const preloadAhead = Math.max(14, Math.floor(this.pageSize * 0.4)); const begin = Math.max(0, start - 4); const last = Math.min(this.videoLocalList.length - 1, end + preloadAhead); for (let index = begin; index <= last; index++) { const item = this.videoLocalList[index]; if (!item || StrUtil.isEmpty(item.pixelMapPath)) { continue; } const sourceUri = item.pixelMapPath as string; if (!this.coverThumbCache.isRemoteImageSource(sourceUri)) { this.coverThumbCache.ensureThumbRequested(sourceUri, targetSize); } } } private prefetchGridWaterThumbsFromItems(items: VideoItem[], limit: number = 42): void { if (!(this.isGridMusic || this.twoFingerType == 4) || !items || items.length === 0) { return; } this.initCoverThumbCacheIfNeeded(); const isWater = this.twoFingerType == 4; const targetSize = this.getCoverThumbTargetSize(isWater); const maxCount = Math.min(limit, items.length); for (let i = 0; i < maxCount; i++) { const item = items[i]; if (!item || StrUtil.isEmpty(item.pixelMapPath)) { continue; } const sourceUri = item.pixelMapPath as string; if (!this.coverThumbCache.isRemoteImageSource(sourceUri)) { this.coverThumbCache.ensureThumbRequested(sourceUri, targetSize); } } } private warmupGridWaterVisibleThumbs(start: number, end: number, isWater: boolean): void { if (!(this.isGridMusic || this.twoFingerType == 4)) { return; } if (start < 0 || end < 0 || this.videoLocalList.length <= 0) { return; } const now = Date.now(); let lastStart = -1; let lastEnd = -1; let lastCheckAt = 0; if (isWater) { lastStart = this.waterThumbWarmupStart; lastEnd = this.waterThumbWarmupEnd; lastCheckAt = this.waterThumbWarmupCheckAt; } else { lastStart = this.gridThumbWarmupStart; lastEnd = this.gridThumbWarmupEnd; lastCheckAt = this.gridThumbWarmupCheckAt; } const startDelta = lastStart >= 0 ? Math.abs(start - lastStart) : 999; const endDelta = lastEnd >= 0 ? Math.abs(end - lastEnd) : 999; if (startDelta < 2 && endDelta < 2 && now - lastCheckAt < 70) { return; } if (isWater) { this.waterThumbWarmupCheckAt = now; this.waterThumbWarmupStart = start; this.waterThumbWarmupEnd = end; } else { this.gridThumbWarmupCheckAt = now; this.gridThumbWarmupStart = start; this.gridThumbWarmupEnd = end; } this.initCoverThumbCacheIfNeeded(); const columns = isWater ? Math.max(this.columns, 1) : (this.twoFingerType == 1 ? 4 : this.twoFingerType == 2 ? 3 : 2); const preloadAhead = Math.max(columns * (isWater ? 8 : 6), isWater ? 24 : 18); const begin = Math.max(0, start - Math.max(columns * 2, 4)); const last = Math.min(this.videoLocalList.length - 1, end + preloadAhead); const targetSize = this.getCoverThumbTargetSize(isWater); for (let index = begin; index <= last; index++) { const item = this.videoLocalList[index]; if (!item || StrUtil.isEmpty(item.pixelMapPath)) { continue; } const sourceUri = item.pixelMapPath as string; if (!this.coverThumbCache.isRemoteImageSource(sourceUri)) { this.coverThumbCache.ensureThumbRequested(sourceUri, targetSize); } } } private getGridWaterImageSource(item: VideoItem, index: number, isWater: boolean, coverVersion: number = 0): ResourceStr { const fallback = item.type == CommonConstants.TYPE_IS_DIR ? $r('app.media.dir_alt') : $r('app.media.alt'); if (coverVersion < -1) { return fallback; } if (StrUtil.isEmpty(item.pixelMapPath)) { return fallback; } const sourceUri = item.pixelMapPath as string; this.initCoverThumbCacheIfNeeded(); const isRemoteCover = this.coverThumbCache.isRemoteImageSource(sourceUri); const targetSize = this.getCoverThumbTargetSize(isWater); const cachedThumb = this.coverThumbCache.getThumbUri(sourceUri, targetSize); if (cachedThumb) { return cachedThumb; } const visibleStart = isWater ? this.waterVisibleStart : this.gridVisibleStart; const visibleEnd = isWater ? this.waterVisibleEnd : this.gridVisibleEnd; const columns = isWater ? Math.max(this.columns, 1) : (this.twoFingerType == 1 ? 4 : this.twoFingerType == 2 ? 3 : 2); const preloadBuffer = Math.max(columns * 2, 8); const isVisibleRangeReady = visibleStart >= 0 && visibleEnd >= 0; const isNearVisible = !isVisibleRangeReady || (index >= visibleStart - preloadBuffer && index <= visibleEnd + preloadBuffer); if (!isRemoteCover && isNearVisible) { this.coverThumbCache.ensureThumbRequested(sourceUri, targetSize); } if (!this.isScrolling) { return sourceUri; } if (isNearVisible) { return sourceUri; } return fallback; } private flushPendingCoverThumbRefresh(): void { if (!this.pendingCoverThumbRefresh) { return; } this.pendingCoverThumbRefresh = false; this.coverThumbVersion++; } private initCoverThumbCacheIfNeeded(): void { if (this.coverThumbCacheReady) { return; } this.coverThumbCache.setContext(this.context); this.coverThumbCache.setOnThumbReady(() => { if (this.isScrolling) { this.pendingCoverThumbRefresh = true; return; } this.coverThumbVersion++; }); this.coverThumbCacheReady = true; } private getCoverThumbTargetSize(isWater: boolean): number { if (isWater) { if (this.columns >= 4) { return 160; } if (this.columns == 3) { return 200; } return 260; } const baseSize = Math.max(this.getGridWight(), this.getGridHeight()); if (this.twoFingerType == 1) { return baseSize; } if (this.twoFingerType == 2) { return Math.floor(baseSize * 1.1); } return Math.floor(baseSize * 1.15); } private tryTriggerLoadForWaterByOffset(currentOffsetY: number): void { if (this.videoLocalList.length <= 0) { return; } const columns = Math.max(this.columns, 1); const viewportHeight = this.windowHeight > 0 ? this.windowHeight : 900; const estimatedCardHeight = this.columns >= 3 ? 196 : 228; const startRow = Math.max(0, Math.floor(Math.max(currentOffsetY, 0) / estimatedCardHeight) - 1); const estimatedRows = Math.ceil((Math.max(currentOffsetY, 0) + viewportHeight) / estimatedCardHeight); const startIndex = Math.min(this.videoLocalList.length - 1, startRow * columns); const estimatedEnd = Math.min(this.videoLocalList.length - 1, estimatedRows * columns + columns); this.waterVisibleStart = startIndex; this.waterVisibleEnd = estimatedEnd; this.lastVisibleEndIndex = estimatedEnd; this.warmupGridWaterVisibleThumbs(startIndex, estimatedEnd, true); const now = Date.now(); const pagingDelta = this.waterScrollLastPagingEnd >= 0 ? Math.abs(estimatedEnd - this.waterScrollLastPagingEnd) : 999; if (pagingDelta >= 3 || now - this.waterScrollPagingCheckAt >= 80) { this.waterScrollPagingCheckAt = now; this.waterScrollLastPagingEnd = estimatedEnd; this.tryTriggerLoadNextPageByScroll(estimatedEnd); } } private tryTriggerLoadNextPageByScroll(endIndex: number): void { if (this.modeType === 4) { return; } if (this.isCanBack && (this.modeType === 2 || this.modeType === 3)) { return; } if (this.isHistory || this.isFavMusic) { return; } if (!this.hasMoreData || this.isLoadingPage || this.isPagingTransition) { return; } if (endIndex < 0 || endIndex >= this.videoLocalList.length) { return; } const nextPage = this.currentPage + 1; const next2Page = this.currentPage + 2; const totalLoaded = this.videoLocalList.length; const seenRatio = (endIndex + 1) / Math.max(totalLoaded, 1); const remainCount = this.videoLocalList.length - endIndex - 1; const preloadStartRatio = this.getDynamicPreloadStartRatio(); const appendStartRatio = this.getDynamicAppendStartRatio(); const earlyAppendReserveCount = this.getDynamicEarlyAppendReserveCount(); const emergencyDistance = Math.max(12, Math.floor(this.pageSize * 0.5)); if (seenRatio >= preloadStartRatio) { if (!this.pageCache.has(nextPage)) { void this.prefetchNextPage(nextPage); } if (this.shouldPrefetchSecondPage() && !this.pageCache.has(next2Page)) { void this.prefetchNextPage(next2Page); } } const shouldAppendEarly = this.pageCache.has(nextPage) && (seenRatio >= appendStartRatio || remainCount <= earlyAppendReserveCount); const shouldGapAppend = this.pageCache.has(nextPage) && seenRatio >= (appendStartRatio - 0.08) && remainCount <= Math.max(earlyAppendReserveCount, Math.floor(this.pageSize * 0.9)); const shouldEmergencyLoad = remainCount <= emergencyDistance; if (!shouldAppendEarly && !shouldGapAppend && !shouldEmergencyLoad) { return; } if (this.lastScrollTriggerPage === this.currentPage) { return; } this.lastScrollTriggerPage = this.currentPage; Logger.info('heanup LocalMusic', `tryTriggerLoadNextPageByScroll: end=${endIndex}, remain=${remainCount}, ratio=${seenRatio}, page=${this.currentPage}`); this.scheduleLoadNextPage(8); } private scheduleLoadNextPage(delayMs: number = 0): void { if (this.pagingScheduleTimer) { return; } this.pagingScheduleTimer = setTimeout(() => { this.pagingScheduleTimer = 0; if (!this.hasMoreData || this.isLoadingPage || this.isPagingTransition) { return; } void this.loadNextPage(); }, delayMs); } /** * 加载下一页 */ private async loadNextPage(): Promise { if (!this.hasMoreData || this.isLoadingPage || this.isPagingTransition) { return; } this.isPagingTransition = true; this.currentPage++; Logger.info('heanup LocalMusic', `loadNextPage: 加载第 ${this.currentPage} 页`); try { await this.loadPage(this.currentPage, true); } finally { this.isPagingTransition = false; } } //编辑信息 async doEdit(item: VideoItem) { if (!item) { return } if (StrUtil.isEmpty(this.titleStr)) { ToastUtil.showToast('标题不能为空') return } //内嵌音乐标签不能开启监听 // this.deletionWatcher?.stop(); this.loadingDialogId = DialogHelper.showLoadingDialog() if (isRemoteCloudType(item.type)) { await this.saveRemoteSongEdit(item); return; } await PermissionUtil.activatePermission(item.filePath) let tempOutPath = '' // 如果不是 packName 包下的文件,则直接路径用this.currentPath if(!item.filePath.toLowerCase().includes(this.packName)){ tempOutPath = this.currentPath + '/' + item.fileName } console.info('onecold tempOutPath ='+tempOutPath) //用户更改了封面,那就内嵌下封面 if(StrUtil.isNotEmpty(this.imagePathStr)){ const resultCover:boolean = await changeMusicCover(this.context, item.filePath,this.imagePathStr,true,tempOutPath); this.imagePathStr = '' if(resultCover){ ToastUtil.showToast('内嵌封面成功') } } const metadata:FFMpegTags = { title: this.titleStr, artist: this.artistStr, album: this.ablumStr, TYER:this.yearStr, genre:this.genreStr, track:this.trackStr, album_artist:this.albumArtistStr, TPE2:this.albumArtistStr,//ID3v2 中通常没有专门的 "专辑艺术家" 字段,但有些软件会使用 TPE2 来表示专辑艺 COMPOSER:this.composerStr, lyricist:this.lyricistStr, TEXT:this.lyricistStr,//ID3v2 使用 TEXT 字段来表示作词者 comment:this.commentStr, disc:this.discStr, }; let newInput = item.filePath //内嵌标签歌路径如果不是包含包名,输入newInput要赋值tempOutPath,然后tempOutPath为空 if(!item.filePath.toLowerCase().includes(this.packName)&&FileUtil.accessSync(tempOutPath)){ newInput = tempOutPath tempOutPath = '' } const isWav:boolean = newInput.toLowerCase().endsWith('.wav') ||newInput.toLowerCase().endsWith('.ape') ||newInput.toLowerCase().endsWith('.dsf') ||newInput.toLowerCase().endsWith('.dff'); //内嵌下标签的值,设置overwrite为true会直接修改原文件 const result:boolean = await repairAudioMetadata( newInput, this.lyricConStr, metadata, true, tempOutPath ); // ToastUtil.showToast('result ='+ result) if (result||isWav) { if(!item.filePath.includes(this.packName)){ //内嵌成功的歌路径如果不是包含包名,则要入库 let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newInput, CommonConstants.TYPE_LOCAL, this.autoParseMusicName); this.table.insert(newItem, (id: number) => { //加入数据库 if(this.modeType==0){ this.deleteCache(this.currentPath) this.getSortedFiles(this.currentPath) } }); } const task = new taskpool.Task( updateInfoDb, this.context, item.filePath, this.titleStr, this.artistStr, this.ablumStr, this.lyricConStr, this.yearStr,this.genreStr, this.trackStr,this.albumArtistStr, this.composerStr, this.lyricistStr, this.commentStr,this.discStr ); taskpool.execute(task, taskpool.Priority.HIGH).then((result) => { if (result) { console.log(" onecold 编辑信息成功,数据库已同步"); if(isWav){ ToastUtil.showToast('dsf、wav、dff或者ape格式有可能内嵌失败,但是编辑信息成功') }else{ ToastUtil.showToast('内嵌音乐标签成功') } this.isShowMoreView = false this.name = this.titleStr this.syncEditedFields(item) 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.currentSong.year = this.yearStr this.currentSong.genre = this.genreStr this.currentSong.track = this.trackStr this.currentSong.lyricContent = this.lyricConStr this.currentSong.ALBUMARTIST = this.albumArtistStr this.currentSong.COMPOSER = this.composerStr this.currentSong.LYRICIST = this.lyricistStr this.currentSong.COMMENT = this.commentStr } this.syncListsAfterEdit(item.filePath) this.doUpateEditedFields(item) //this.doUpdateData() } else { // ToastUtil.showToast('保存失败' + error?.toString()) console.error(" onecold 编辑信息数据库失败 "); } // 关闭进度条 DialogHelper.closeDialog(this.loadingDialogId) }).catch((error: Error) => { // 关闭进度条 DialogHelper.closeDialog(this.loadingDialogId) console.error('onecold Subtitle sync failed:', error); }); console.log(' onecold 元数据标签更新成功,'); } else { // 关闭进度条 DialogHelper.closeDialog(this.loadingDialogId) ToastUtil.showToast('内嵌音乐标签失败') console.log('onecold 内嵌音乐标签失败'); } this.longItemFilePath = '' this.tempLyricContent = '' this.isShowEdit = false } private normalizeEditedCoverPath(path: string): string { if (StrUtil.isEmpty(path)) { return ''; } const lowerPath = path.toLowerCase(); if (lowerPath.startsWith('http://') || lowerPath.startsWith('https://') || lowerPath.startsWith('file://') || lowerPath.startsWith('data:')) { return path; } return fileUri.getUriFromPath(path); } private emitRemoteMetadataUpdated(item: VideoItem): void { if (!item || StrUtil.isEmpty(item.filePath)) { return; } const payload: WebDavMetadataUpdatePayload = { filePath: item.filePath, pixelMapPath: item.pixelMapPath, isCustomCover: item.isCustomCover, md5Str: item.md5Str, name: item.name, artist: item.artist }; const eventUpdate: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED }; emitter.emit(eventUpdate, { data: [payload] }); } private syncRemoteManagerSong(item: VideoItem): void { if (!item || StrUtil.isEmpty(item.filePath)) { return; } const remoteManager = RemoteDriveManager.getInstance(); const target = Utility.getItemByFilePath(remoteManager.webDavSongs, item.filePath); if (!target) { return; } this.mergeVideoItemFromSource(target, item); } private async saveRemoteSongEdit(item: VideoItem): Promise { try { this.syncEditedFields(item); if (StrUtil.isNotEmpty(this.imagePathStr)) { item.pixelMapPath = this.normalizeEditedCoverPath(this.imagePathStr); item.isCustomCover = 1; } this.name = this.titleStr; if (this.currentSong && item.filePath === this.currentSong.filePath) { this.syncEditedFields(this.currentSong); if (StrUtil.isNotEmpty(item.pixelMapPath)) { mergeCoverWithPriority(this.currentSong, item.pixelMapPath, item.isCustomCover ?? 0); this.cover = this.currentSong.pixelMapPath || item.pixelMapPath; } this.artist = this.currentSong.artist ?? ''; this.currentSong = cloneVideoItem(this.currentSong); AppStorage.setOrCreate('currentSong', this.currentSong); } this.syncListsAfterEdit(item.filePath); this.syncRemoteManagerSong(item); await this.persistRemoteMetadataToDb(item); this.emitRemoteMetadataUpdated(item); ToastUtil.showToast(StrUtil.isNotEmpty(item.pixelMapPath) ? '网盘歌曲信息已保存' : '网盘歌曲标签已保存'); } catch (error) { Logger.error(TAG, `saveRemoteSongEdit 失败: ${(error as Error).message}`); ToastUtil.showToast('网盘歌曲编辑失败'); } finally { DialogHelper.closeDialog(this.loadingDialogId); this.longItemFilePath = ''; this.tempLyricContent = ''; this.isShowEdit = false; this.imagePathStr = ''; this.isShowMoreView = false; } } // 内嵌标签更新当前内嵌的Item async doUpateEditedFields(item: VideoItem) { // 找到当前项在 videoLocalList 中的索引 const index = this.videoLocalList.findIndex(video => video.filePath === item.filePath); if (index !== -1) { // 使用已初始化的MediaTable实例从数据库查询歌曲信息 const updatedItem = await this.table.queryVideoByFilePath(item.filePath) // 只有当查询结果不为 null 时才更新列表 if (updatedItem !== null) { // 更新 videoLocalList 中的项(通过创建新数组触发响应式更新) this.videoLocalList = [ ...this.videoLocalList.slice(0, index), updatedItem, ...this.videoLocalList.slice(index + 1) ]; // 同步更新 LazyDataSource this.dataSource.pushArrayData(this.videoLocalList) } else { ToastUtil.showToast('未找到更新的数据'); } } DialogHelper.closeDialog(this.loadingDialogId) } @Builder editSheet(item: VideoItem) { Scroll() { Column() { this.editDetail(item) } } .width('100%') .height('100%') } @Builder editDetail(item: VideoItem) { Column() { if (item) { Row() { Stack() { Image(StrUtil.isNotEmpty(this.imagePathStr)?this.imagePathStr.startsWith('http')? this.imagePathStr:fileUri.getUriFromPath(this.imagePathStr): StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.add_image2') : item.pixelMapPath) .fillColor(this.themeColor) .height(88) .width(88) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.6}) .borderRadius(12) .clip(true) .opacity(this.opacityItem)// 绑定透明度 .margin({ left: 25 }) } .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.6}) .onClick(async () => { const imagePath:string | undefined = await this.goSelectImage(item); if (imagePath) { //用户更改过图片 this.imagePathStr = imagePath } }) .width('28%') Row() { Column() { Text(item.name) .fontSize(17) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .animation({ duration: 555, curve: 'Linear', }) .fontColor(this.themeColor) .margin({ left: 8 }) Row() { Text(item.artist) .fontSize(14) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .padding({ top: 8 }) .fontColor(this.themeColor) .margin({ left: 8 }) } } .height('100%') .width(100) .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) Column() { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row(){ SymbolGlyph($r('sys.symbol.picture')) .fontSize(18) .fontColor([Color.White]) Text('获取封面') .margin({ left: 4 }) .fontSize(11) .fontColor(Color.White) .fontWeight(480) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) } .height(38) .width(100) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ right: 35 }) .onClick(async () => { if (PreferencesUtil.getStringSync('COVER_API', '') === '') { this.showTipsDialog(); return; // Return empty string when no API is configured } const imagePath:string | undefined = await this.doSearchCover(item, this.titleStr,this.artistStr); console.info('onecold imagePath = '+imagePath) if (StrUtil.isNotEmpty(imagePath)) { //用户更改过图片 this.imagePathStr = imagePath ToastUtil.showToast('获取封面成功') }else{ ToastUtil.showToast('获取封面失败,请重试') } }) Button({ type: ButtonType.Capsule, stateEffect: true }) { Row(){ SymbolGlyph($r('sys.symbol.input_mode')) .fontSize(18) .fontColor([Color.White]) Text('获取歌词') .margin({ left: 4 }) .fontSize(11) .fontColor(Color.White) .fontWeight(480) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) } .height(38) .width(100) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ top: 9, right: 35 }) .onClick(async () => { //判断有没有设置api let apiUrl = PreferencesUtil.getStringSync('LRC_API', '') if (StrUtil.isEmpty(apiUrl)) { this.isLyricSetting = false this.showTipsDialog() return } let res: string | undefined = await getApiLyric(apiUrl, this.titleStr, this.artistStr, false); if (res && StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') { this.lyricConStr = res; ToastUtil.showToast('获取歌词成功') }else{ ToastUtil.showToast('获取歌词失败,请重试') } }) } .justifyContent(FlexAlign.Start) } .height('100%') .justifyContent(FlexAlign.Start) .layoutWeight(1) } .width('100%') .height(98) Row() { Text('标题:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) TextInput({ text: this.titleStr }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 15, left: 10 }) .onChange((val: string) => { this.titleStr = val }) Button('修复乱码',{ type: ButtonType.Capsule, stateEffect: true}) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .backgroundColor(this.themeColor) .fontSize(10) .margin({ right: 20}) .onClick(async () => { let hwTags = await extractHwMediaMetadata(item.filePath) let artist = hwTags?.artist|| ''; let title = hwTags?.title|| ''; let album = hwTags?.album|| ''; console.info('onecold 乱码修正 artist='+artist); console.info('onecold 乱码修正 title='+title); console.info('onecold 乱码修正 album='+album); this.titleStr = title; this.artistStr = artist; this.ablumStr = album; }) } .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: this.artistStr }) .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: this.ablumStr}) .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() { Column(){ Column(){ Text('歌词:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ top:9,left: 22 }) Button({ type: ButtonType.Capsule, stateEffect: true }) { SymbolGlyph($r('sys.symbol.trash_fill')) .fontSize(25) .alignSelf(ItemAlign.Center) .margin({ left: 22,top:25}) } .backgroundColor(Color.Transparent) .visibility(StrUtil.isEmpty(this.tempLyricContent)&&StrUtil.isEmpty(this.lyricConStr)? Visibility.None:Visibility.Visible) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .onClick(()=>{ this.lyricConStr = '' this.lyricContent = '' this.tempLyricContent = '' }) Button({ type: ButtonType.Capsule, stateEffect: true }) { SymbolGlyph($r('sys.symbol.plus')) .fontSize(25) .alignSelf(ItemAlign.Center) .margin({ left: 22,top:25}) } .backgroundColor(Color.Transparent) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .onClick( ()=>{ this.callFilePickerSelectFileForLyric() }) Button({ type: ButtonType.Capsule, stateEffect: true }) { SymbolGlyph($r('sys.symbol.traditional_square')) .fontSize(25) .alignSelf(ItemAlign.Center) .margin({ left: 22,top:25}) } .backgroundColor(Color.Transparent) .visibility(StrUtil.isEmpty(this.tempLyricContent)&&StrUtil.isEmpty(this.lyricConStr)? Visibility.None:Visibility.Visible) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .onClick( ()=>{ let lyricC = StrUtil.isNotEmpty(this.lyricConStr)?this.lyricConStr: this.tempLyricContent this.lyricConStr = getTransverterText(lyricC,1)//1是简体转繁体 ,2是繁体转简体 }) Button({ type: ButtonType.Capsule, stateEffect: true }) { Image($r('app.media.jianti')) .height(25) .fillColor($r('app.color.text_color')) .alignSelf(ItemAlign.Center) .margin({ left: 22,top:25}) } .backgroundColor(Color.Transparent) .visibility(StrUtil.isEmpty(this.tempLyricContent)&&StrUtil.isEmpty(this.lyricConStr)? Visibility.None:Visibility.Visible) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .onClick( ()=>{ let lyricC = StrUtil.isNotEmpty(this.lyricConStr)?this.lyricConStr: this.tempLyricContent this.lyricConStr = getTransverterText(lyricC,2)//1是简体转繁体 ,2是繁体转简体 }) } .layoutWeight(1) } TextArea({ text: StrUtil.isNotEmpty(this.lyricConStr)?this.lyricConStr: this.tempLyricContent }) .type(TextAreaType.NORMAL) .height(StrUtil.isEmpty(this.lyricConStr)&&StrUtil.isEmpty(this.tempLyricContent)?100:'auto') .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.lyricConStr = val }) } .width('100%') .height(StrUtil.isEmpty(this.lyricConStr)&&StrUtil.isEmpty(this.tempLyricContent)?120:'auto') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('年份:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) TextInput({ text: item.year }) .height(40) .maxLines(1) .fontSize(14) .type(InputType.Number) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.yearStr = 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.track=='未知'?'': item.track }) .height(40) .maxLines(1) .fontSize(14) .type(InputType.Number) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.trackStr = 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.disc }) .height(40) .maxLines(1) .fontSize(14) .type(InputType.Number) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.discStr = 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.genre }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.genreStr = 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.ALBUMARTIST }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.albumArtistStr = 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.COMPOSER }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.composerStr = 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.LYRICIST }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.lyricistStr = 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.COMMENT }) .height(40) .maxLines(1) .fontSize(14) .layoutWeight(1) .fontColor($r('app.color.text_color')) .margin({ right: 20, left: 10 }) .onChange((val: string) => { this.commentStr = val }) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Button('保存') .fontColor(Color.White) .layoutWeight(1) .height(50) .width(100) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .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) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .backgroundColor(this.themeColor) .stateEffect(true) .margin({ left: 20, bottom: 20 }) .onClick(async () => { if(this.isGridMusic){ this.longItemFilePath ='' }else{ this.isShowEdit = false } this.setEditStrEmpty() if(this.isShowPlay){ this.isShowEdit = false } }) } .margin({ left: 38, right: 38, top: 10, bottom: 10 }) .width(250) .height(60) } } .margin({ bottom: 20 }) } setEditStrEmpty(){ this.imagePathStr = '' this.lyricConStr = '' this.artistStr = '' this.titleStr = '' this.ablumStr = '' this.genreStr = '' this.yearStr = '' this.trackStr = '' this.albumArtistStr = '' this.composerStr = '' this.lyricistStr = '' this.commentStr = '' this.discStr = '' } initEditFields(item?: VideoItem) { if (!item) { this.setEditStrEmpty() return } this.imagePathStr = '' this.titleStr = item.name ?? '' this.artistStr = item.artist ?? '' this.ablumStr = item.album ?? '' this.genreStr = item.genre ?? '' this.yearStr = item.year ?? '' this.trackStr = item.track ?? '' this.albumArtistStr = item.ALBUMARTIST ?? '' this.composerStr = item.COMPOSER ?? '' this.lyricistStr = item.LYRICIST ?? '' this.commentStr = item.COMMENT ?? '' this.discStr = item.disc ?? '' this.lyricConStr = item.lyricContent ?? '' } private syncEditedFields(target?: VideoItem) { if (!target) { return } target.name = this.titleStr target.artist = this.artistStr target.album = this.ablumStr target.lyricContent = this.lyricConStr target.year = this.yearStr target.genre = this.genreStr target.track = this.trackStr target.ALBUMARTIST = this.albumArtistStr target.COMPOSER = this.composerStr target.LYRICIST = this.lyricistStr target.COMMENT = this.commentStr target.disc = this.discStr } private replaceItemInList(list: Array | undefined, filePath: string, apply?: (next: Array) => void) { if (!list || ArrayUtil.isEmpty(list)) { return } const index = list.findIndex((video: VideoItem) => video.filePath === filePath) if (index < 0) { return } const nextList = [...list] this.syncEditedFields(nextList[index]) apply?.(nextList) } private syncListsAfterEdit(filePath: string) { this.replaceItemInList(this.videoLocalList, filePath, (next: Array) => { this.videoLocalList = next this.dataSource.pushArrayData(next) }) this.replaceItemInList(this.songList, filePath, (next: Array) => { this.songList = next this.sonDataSource.pushArrayData(next) }) this.replaceItemInList(this.favList, filePath, (next: Array) => { this.favList = next }) this.replaceItemInList(this.historyList, filePath, (next: Array) => { this.historyList = next }) this.replaceItemInList(this.currentSongList, filePath, (next: Array) => { this.currentSongList = next }) this.replaceItemInList(this.mediaKuList, filePath, (next: Array) => { this.mediaKuList = next }) this.replaceItemInList(this.filteredList, filePath, (next: Array) => { this.filteredList = next }) this.replaceItemInList(this.artistList, filePath, (next: Array) => { this.artistList = next }) this.replaceItemInList(this.albumList, filePath, (next: Array) => { this.albumList = next }) this.listRefreshKey++ } private refreshLazyListViews(mainListUpdated: boolean, playlistUpdated: boolean): void { if (!mainListUpdated && !playlistUpdated) { return; } this.listRefreshKey++; if (mainListUpdated) { this.dataSource.notifyDataReload(); } if (playlistUpdated) { this.sonDataSource.notifyDataReload(); } } @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.alt') : currentItem.pixelMapPath) .fillColor(this.themeColor) .height(80) .width(80) .borderRadius(10) .opacity(this.opacityItem)// 绑定透明度 .margin({ left: 22 }) } Column() { Column() { Text(currentItem.name) .fontSize(16) .maxLines(1) .animation({ duration: 555, curve: 'Linear', }) .fontColor($r('app.color.text_color')) .margin({ left: 15 }) } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') } .width('100%') .height(90) .justifyContent(FlexAlign.SpaceBetween) Row() { Text('艺术家:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem?.artist ? currentItem.artist : '未知歌手') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 10 }) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('专辑名:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem?.album ?currentItem.album : '未知专辑') .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('时长:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(StrUtil.isEmpty(currentItem.duration)? (this.isShowDetailMore?this.totalTime:'未知'):currentItem.duration) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('采样率:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(Utility.convertToKHz(currentItem.sampleRate)) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('比特率:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.bit_rate) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('位深:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(`${currentItem.bits_per_raw_sample} bits`) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('声道:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(`${currentItem.channels}`) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('声道布局:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(`${currentItem.channel_layout}`) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('起始播放点:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(`${currentItem.start_time}`) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('风格:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.genre) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('发行时间:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.year) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('质量评分:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.probe_score+"") .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('音轨号:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.track) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('碟号:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.disc) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('专辑艺术家:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.ALBUMARTIST) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('作曲家:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.COMPOSER) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('作词家:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.LYRICIST) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('注释:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.COMMENT) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('格式:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(Utility.formatMimeType(currentItem.mimeType)) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('播放次数:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.playCount?.toString()) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('流数量:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.nb_streams?.toString()) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('文件大小:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.size) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('文件名:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.fileName) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('修改时间:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.cTime) .fontSize(14) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .width('100%') .margin({ top: 10, bottom: 10 }) .justifyContent(FlexAlign.Start) Row() { Text('存放目录:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.filePath) .fontSize(12) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .layoutWeight(1) } .margin({ top: 10, bottom: 10 }) .width('100%') .justifyContent(FlexAlign.Start) Row() { Text('内嵌歌词:') .fontSize(14) .fontColor($r('app.color.text_color')) .margin({ left: 22 }) Text(currentItem.lyricContent) .fontSize(12) .copyOption(CopyOptions.InApp) .margin({ left: 10 }) .fontColor($r('app.color.text_color')) .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.songList.splice(Number(index), 1) this.sonDataSource.pushArrayData(this.songList) }) 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; } 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) if(this.modeType==4&&this.playlistTable){ updateAllSongsSortOrder(this.playlistTable, this.songList,this.currentSongListID) } } @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: 0.8, y: 0.8 }).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 + '_' + this.listRefreshKey) } .width('100%') .height('100%') .onReachEnd(() => { if(this.modeType === 4 && !this.isFindPagedLocalQueueActive()) return; // 检测是否是流媒体类型(Navidrome/Jellyfin/Emby/AudioStation/Plex),如果是则调用网盘加载更多逻辑 const isStreamingSong = this.currentSong && ( isNavidromeType(this.currentSong.type) || isJellyfinType(this.currentSong.type) || isEmbyType(this.currentSong.type) || isAudioStationType(this.currentSong.type) || isPlexType(this.currentSong.type) ); if (isStreamingSong && hasMoreData()) { Logger.info('heanup', '播放列表滚动到底部,加载更多流媒体歌曲'); void this.loadMoreNavidromeSongs(); } else { void this.tryLoadNextPageForPlayback(); } }) // .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.setSonListTwoFingers() }) .onActionUpdate(e => { this.scaleValue = this.pinchValue * e.scale }) .onActionStart(e => { })) .padding({ bottom: 0 }) } setSonListTwoFingers() { this.pinchValue = this.scaleValue; Logger.info('this.pinchValue = ' + this.pinchValue); if (this.pinchValue >= 1) { this.sonTwoFingerType++ Logger.info('this.twoFingerType1 = ' + this.sonTwoFingerType); if (this.sonTwoFingerType > 3) { this.sonTwoFingerType = 1 PreferencesUtil.put(SettingPage.IS_GRID_MUSIC, this.isGridMusic) } } else { this.sonTwoFingerType-- Logger.info('this.twoFingerType2 = ' + this.sonTwoFingerType); if (this.sonTwoFingerType < 1) { this.sonTwoFingerType = 1 } } PreferencesUtil.put('sonTwoFingerType', this.sonTwoFingerType) this.pinchValue = 1; this.scaleValue = 1; } @State sonTwoFingerType:number = 3 @Builder private MusicItemSon(item: VideoItem, index?: number) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row() { Stack() { Image(StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.alt') : item.pixelMapPath) .height(this.sonTwoFingerType == 3||this.sonTwoFingerType==4 ? 55 : this.sonTwoFingerType == 2 ? 48 : 40) .width(this.sonTwoFingerType == 3||this.sonTwoFingerType==4 ? 55 : this.sonTwoFingerType == 2 ? 48 : 40) .alt($r('app.media.alt')) .fillColor(this.themeColor) .borderRadius(this.isCoverRectangle?11:'100%') .clip(true) .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿 .draggable(false)// 禁止长按手势拖动 .autoResize(true) // 重采样,可减少内存占用 .margin({ left: 20 }) } .width('15%') Column() { Column() { Text(this.isShowFileName?item.fileName :item.name) .fontSize(this.sonTwoFingerType == 1 ? 12 : this.sonTwoFingerType == 2 ? 15 : 18) .fontColor(this.curIndex === index ? this.themeColor : this.isFrontWhite ? Color.White : $r('app.color.text_color')) .maxLines(1) .margin({ left: this.sonTwoFingerType == 3 ? 18 : 10 }) Row() { Column(){ Text(item.md5Str?.includes('Lossless')? Utility.resourceToString(this.context,$r('app.string.lossless')):item.md5Str)//音质 .fontSize(this.sonTwoFingerType == 1 ? 8 : this.sonTwoFingerType == 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.sonTwoFingerType == 3 ? 12 : 10 }) .backgroundColor('#FFC107') } .padding({ top: 8 }) Text(StrUtil.isEmpty(item.artist) ? item.duration : item.artist + ' ' + item?.duration) .fontSize(this.sonTwoFingerType == 1 ? 11 : this.sonTwoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .maxLines(1) .fontColor(this.curIndex === index ? this.themeColor : this.isFrontWhite ? $r('app.color.playlistText_color') : $r('app.color.text_color')) .margin({ left: StrUtil.isEmpty(item.md5Str)? 12 :this.sonTwoFingerType == 3 ? 5 : 3 }) Blank() Text(item.size) .fontSize(this.sonTwoFingerType == 1 ? 11 : this.sonTwoFingerType == 2 ? 12 : 14) .padding({ top: 8 }) .maxLines(1) .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() { PlayingIndicator({ isActive: this.videoUrl === item.filePath && !this.isMultiSelect, indicatorSize: 18 }) } .width('20%') } } .backgroundColor(Color.Transparent) .width('100%') .reuseId('son_item') .height(this.sonTwoFingerType == 3 ? ITEM_HEIGHT_BIG : this.sonTwoFingerType == 2 ? ITEM_HEIGHT+10 : ITEM_HEIGHT_SMALL+10) .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 private MusicPlayDismissProxyDot() { Stack({ alignContent: Alignment.Center }) { Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt')) .width('100%') .height('100%') .objectFit(ImageFit.Cover) .alt($r('app.media.alt')) } .width(this.playDismissProxySize) .height(this.playDismissProxySize) .borderRadius(this.playDismissProxySize) .clip(true) .opacity(this.playDismissProxyOpacity) .translate({ x: this.playDismissTranslateX, y: this.translateY }) .shadow({ radius: 18, type: ShadowType.BLUR, color: 'on_primary' }) } @Builder MusicPlayBuilder() { Column() { this.IjkMusicPlayerView() } .transition(TransitionEffect.asymmetric( TransitionEffect.opacity(1), TransitionEffect.OPACITY )) .visualEffect(deviceInfo.sdkApiVersion>=20&&this.bgController? new hdsEffect.HdsEffectBuilder() .shaderEffect({ effectType: hdsEffect.EffectType.UV_BACKGROUND_FLOW_LIGHT, animation: { duration: 10000, iterations: -1, autoPlay: true, onFinish: ()=> { console.info('Succeeded in finishing'); } }, controller: this.bgController, }) .buildEffect():null) .height('100%') .width('100%') .onDisAppear(() => { this.translateY = 0; this.playDragUiOpacity = 1 }) .translate({ y: this.translateY }) .gesture( PanGesture(this.panOption) .onActionUpdate((event?: GestureEvent) => { if (event) { // 判断滑动方向 const offsetY = event.offsetY ?? 0; const isSwipeUp = offsetY < 0; if (isSwipeUp) { // 向上滑动:不应用位移动画,保持 translateY = 0 this.translateY = 0; this.scaleValueImage = 1 this.scaleValueText = 1 this.playDragUiOpacity = 1 } else { if(this.isGeKongPlayNext){ return } // 向下滑动:应用位移动画 this.translateY = offsetY; // 向下滑动时的缩放效果 this.getUIContext().animateTo({ duration: 500, curve: Curve.Sharp }, () => { this.scaleValueImage = Math.min(1, Math.max(0.5, 1 - this.translateY / 880)); console.info('onecold scaleValueImage:', this.scaleValueImage) this.scaleValueText = Math.min(1, Math.max(0.88, 1 - this.translateY / 2000)); this.playDragUiOpacity = Math.min(1, Math.max(0, 1 - this.translateY / 300)); }) } } }) .onActionEnd((event?: GestureEvent) => { // 判断滑动方向 const offsetY = event?.offsetY ?? 0; const isSwipeUp = offsetY < 0; const minDistance = 300; // 最小滑动距离 const minVelocity = 500; // 最小滑动速度 const velocity = event?.velocityY ?? 0; if (isSwipeUp) { // 向上滑动:切换下一首 const shouldSwitch = this.isPlayPageSwipeNext && (Math.abs(offsetY) > minDistance || Math.abs(velocity) > minVelocity); if (shouldSwitch) { // 直接切换下一首,无需动画(因为界面没有移动) this.playbackController.playNext().catch(() => { Logger.error('heanup MusicPlayBuilder', '向上滑动切换下一首失败'); }); } } else { //这里加个变量,是否开启隔空手势播放下一首 if(this.isGeKongPlayNext){ void this.playbackController.playNext() return } // 向下滑动:关闭播放页 const shouldClose = this.translateY > minDistance || velocity > minVelocity; if (shouldClose) { // 添加关闭动画 this.getUIContext().animateTo({ duration: 600, curve: Curve.Friction }, () => { this.isShowPlay = false; this.scaleValueImage = 1 this.scaleValueText = 1 this.playDragUiOpacity = 1 this.translateY = 0; }) } else { // 回弹动画 this.getUIContext().animateTo({ duration: 600, curve: Curve.Friction }, () => { this.scaleValueImage = 1 this.scaleValueText = 1 this.playDragUiOpacity = 1 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 = 68; private isHiding: boolean = false; //是否是Pura X外屏,或者小屏幕 isPuraWP() { return this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM && this.currentHeightBreakpoint === HeightBreakpoint.HEIGHT_MD } //是否是Pura X外屏,或者小屏幕,手机横屏 /** * 是否是Pura X外屏,或者小屏幕,手机横屏 * @returns */ 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) logHiCarWindow(`isPhoneLan width=${this.windowWidth}`); logHiCarWindow(`isPhoneLan height=${this.windowHeight}`); logHiCarWindow(`window size: ${this.windowWidth}x${this.windowHeight}, isHiCarStatus=${this.isHiCarStatus}`); logHiCarWindow(`window size: ${this.windowWidth}x${this.windowHeight}, curDisplayIsHiCar=${this.curDisplayIsHiCar}`); logHiCarWindow(`window size: ${this.windowWidth}x${this.windowHeight}, isHiCarSmall=${this.isHiCarSmall()}`); if (this.isHiCarKuanBianPing()) { return true } 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(){ // if(!this.isHiCar()){ // return false // } const hiCarAspectRatios: HiCarAspectRatio[] = [ { ratio: 1920 / 720, name: "1920x720" }, ]; const currentRatio: number = this.windowWidth / this.windowHeight; const RATIO_TOLERANCE: number = 0.22; // 宽高比容差 logHiCarWindow(`HiCarKuanBianPing currentRatio=${currentRatio}`) for (const hiCarRatio of hiCarAspectRatios) { logHiCarWindow(`HiCarKuanBianPing hiCarRatio=${hiCarRatio.ratio}`) logHiCarWindow(`HiCarKuanBianPing diff=${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}`); //宽扁平的尺寸设置列表展示 this.twoFingerType ==3 this.isGridMusic = false return true; } } return false; } //是不是hicar的屏幕分辨率 isHiCarSmall() { if(!this.isHiCar()){ return false } if(this.windowWidth>1800&&this.windowHeight>=1200)//大屏幕可以Grid { this.columns = 6 return false } const hiCarAspectRatios: HiCarAspectRatio[] = [ { ratio: 800 / 480, name: "800x480" }, { ratio: 1280 / 720, name: "1280x720" }, { ratio: 1920 / 1080, name: "1920x1080" }, ]; const currentRatio: number = this.windowWidth / this.windowHeight; const RATIO_TOLERANCE: number = 0.22; // 宽高比容差 logHiCarWindow(`HiCarSmall currentRatio=${currentRatio}`) for (const hiCarRatio of hiCarAspectRatios) { logHiCarWindow(`HiCarSmall hiCarRatio=${hiCarRatio.ratio}`) logHiCarWindow(`HiCarSmall diff=${Math.abs(currentRatio - hiCarRatio.ratio)}`) logHiCarWindow(`HiCarSmall isHiCarStatus=${this.isHiCarStatus}`); if (Math.abs(currentRatio - hiCarRatio.ratio) <= RATIO_TOLERANCE) { // LogUtil.info(`HiCar detected: ${hiCarRatio.name}, actual: ${this.windowWidth}x${this.windowHeight}`); //宽扁平的尺寸设置列表展示 return true; } } return false; } isHiCar(): boolean { return 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 IjkMusicPlayerView() { Stack() { if(canIUse("SystemCapability.UIDesign.HDSComponent.Core") &&deviceInfo.sdkApiVersion>=20&&this.sceneBgController){ HdsVisualComponent() .scene(HdsSceneType.DUAL_EDGE_FLOW_LIGHT_WITH_BACKGROUND_MASK, this.sceneBgController, () => { console.info('Succeeded in finishing'); }) .width('100%') .height('100%') .visibility(this.isShowBgLiuGUANG?Visibility.Visible:Visibility.None) } Column() { Column() { //播放标题 Column() { this.PlayTitle() } .opacity(this.playDragUiOpacity) .position({ x: 0, y: this.isCoverOpacity() ? 10 : 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.mIjkMediaPlayer.setSpeed(this.longPressSpeed + 'f'); } }) .onActionEnd(() => { this.showSpeedDialog = false; this.mIjkMediaPlayer.setSpeed(this.playSpeed + 'f'); }), ) ) } .height('65%') Swiper() { this.CoverInfo() this.PlayerLyrics() } .onChange((index: number) => { this.currentSwiperIndex = index if (index === 1) { this.syncLyricPositionNow() } }) .indicator(false) .displayCount(this.getPlayerSwiperDisplayCount()) .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(() => { console.info('onecold startPlayOrResumePlay 6744') this.startPlayOrResumePlay(); }) } .width(CommonConstants.FULL_PERCENT) .height(CommonConstants.FULL_PERCENT) .justifyContent(FlexAlign.End) } .justifyContent(FlexAlign.End) .margin({ bottom: 5 }) } .backgroundImage(!this.isMusicBGCover||StrUtil.isEmpty(this.cover)?null: this.cover) .backgroundImageSize(this.isPuraWP()||!this.isMusicBGCover ? { width: '100%' } : { height: '150%', width: '100%' }) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) .backgroundBlurStyle((this.isPuraWP() && this.currentSwiperIndex === 0) ||!this.isMusicBGCover ? BlurStyle.NONE : BlurStyle.BACKGROUND_ULTRA_THICK) .hitTestBehavior(HitTestMode.Transparent) .backgroundBrightness({ rate: this.isPuraWP() ? 0.1 : 0, lightUpDegree: -0.1 }) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .onClick(() => { if(this.isHiCar()) return if(this.is_auto_hide_progress){ this.is_auto_hide_progress = false } this.startAutoHide() }) } @StorageLink('progressValue') 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 videoWidth: string = '100%'; @State videoHeight: string = '100%'; @State initAspectRatio: number = 1; @State videoAspectRatio: number = this.initAspectRatio; @State videoUrl: string = ''; private last: number = 0; @State videoParentAspectRatio: number = this.initAspectRatio; private mIjkMediaPlayer = IjkMediaPlayer.getInstance(); @StorageLink('CONTROL_PlayStatus') CONTROL_PlayStatus: number = PlayStatus.INIT; @State PROGRESS_MAX_VALUE: number = 100; @State updateProgressTimer: number = 0; private readonly progressUpdateIntervalMs: number = 120; private readonly lyricUpdateMinDeltaMs: number = 24; private lastLyricRenderPosition: number = Number.NaN; @State isHide: boolean = false; //是否因此播放器的按钮,默认是进入横屏模式隐藏,点击视频的时候显示 @State name: string = ''; // @State loop: boolean = false; @State playType: number = 0; //0:连续循环播放 1:单片重复播放 2:正常播放 3:随机播放 4:连续播放不循环 @State multiple: string = '1.0X'; xcomponentController: XComponentController = new XComponentController() private panOptionBright: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Up | PanDirection.Down }); private panOptionVolume: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Vertical | PanDirection.Up | PanDirection.Down }); private panOptionSeekTo: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Horizontal }); //手势的音量控制和亮度控制 private positionX: number = PlayConstants.POSITION_X; private positionY: number = PlayConstants.POSITION_Y; private windowClass: window.Window = globalThis.windowClass @State volume: number = 0.8; @State volumeBoostPercent: number = 0 @State volumeSmall: boolean = false @State autoParseMusicName: boolean = true @State isShowFileName: boolean = false @State isLongNameRoLL: boolean = true//长歌名滚动 private readonly volumeBoostPreferenceKey: string = 'volumeBoostPercent'; private readonly volumeBoostMaxPercent: number = 500; @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 private preloadNextRandomIndex: number = -1; // 预缓存的随机下一首索引 private playbackUrlRequestSerial: number = 0; eventHub = getContext().eventHub; private isAppForeground: boolean = true; private lastBufferingLogTs: number = 0; private readonly bufferingLogIntervalMs: number = 5000; //投播组件 private avSessionController: AvSessionController = AvSessionController.getInstance(false); // 新的投播控制器(封装了官方示例的逻辑) private castControllerWrapper: CastController | undefined = undefined; // 保留原始的 avCastController 引用(用于兼容旧代码) private castController: avSession.AVCastController | undefined = undefined; @State isCastPlaying: boolean = false; @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; private castFile: fs.File | undefined = undefined; // 保持文件描述符引用,避免被垃圾回收 // @State isBgPlayOpen:boolean = true //是否启用后台播放 @State rotateAngle2: number = -9 @StorageLink('imageColor') imageColor: string = 'rgba(0, 0, 2, 1.00)'; @StorageLink('cover') 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 randomShakenX: number = 0 @State randomShakenY: number = 0 @State lyricTextSize: number = 24 @State lyricContent: string = '' @State isDebug: boolean = false @State isHightLightCenter: boolean = true private syncWordByWordLyricMode(): void { this.lyricController.setEnableWordByWordLyric(this.isEnableWordByWordLyric) this.lyricControllerXF.setEnableWordByWordLyric(this.isEnableWordByWordLyric) this.lyricControllerSingle.setEnableWordByWordLyric(this.isEnableWordByWordLyric) } private syncLyricLightTextMode(): void { this.lyricController.setLightText(this.isLightText) this.lyricControllerXF.setLightText(this.isLightText) this.lyricControllerSingle.setLightText(this.isLightText) } /** * 初始化歌词加载与展示逻辑 * @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, isLocal?: boolean) { if (StrUtil.isEmpty(lyricPath)) { return } this.syncWordByWordLyricMode() this.syncLyricLightTextMode() this.initPipLyricSetting() this.lyricControllerSingle .setTextSize(15) .setCacheSize(4) .setTextColor("#FFFFFF") .setHighlightColor(this.currentHighLightLyricColor) .setHighlightScale(1.2) .setEmptyHint("") .setLineSpace(1) .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.loadLyricTimeOffset() this.lyricController.setLyric(null) this.lyricControllerSingle.setLyric(null) console.log("onecold lyricPath =" + lyricPath) // 1. 检查内嵌歌词 let neiqianLrc = '' let lyContent = this.currentSong?.lyricContent if(lyContent && StrUtil.isNotEmpty(lyContent)){ neiqianLrc = lyContent } if (StrUtil.isNotEmpty(neiqianLrc) && !isOnLineAndToast && !isLocal) { console.log("onecold 找到内嵌歌词 neiqianLrc =" + neiqianLrc) // Navidrome JSON歌词转换 if (this.currentSong && isNavidromeType(this.currentSong.type)) { const convertedLyric: string | undefined = LyricUtil.convertNavidromeJsonLyricToLrc(neiqianLrc); if (convertedLyric) { neiqianLrc = convertedLyric; console.log("onecold Navidrome歌词转换成功"); } } this.setLyricToControllers(neiqianLrc); return } // 2. 远程播放实时探测歌词: 优先探测内嵌歌词,失败后查找同路径同名 .lrc if (this.currentSong && shouldResolveRemoteLyricOnPlay(this.currentSong) && !isOnLineAndToast && !isLocal) { const remoteLyric: string = await tryResolveRemotePlaybackLyric({ song: this.currentSong, playbackUrl: this.videoUrl, manager: RemoteDriveManager.getInstance(), detectEncoding: (data: ArrayBuffer): string => this.detect(data) }); if (StrUtil.isNotEmpty(remoteLyric)) { this.currentSong.lyricContent = remoteLyric; this.setLyricToControllers(remoteLyric); void this.persistRemoteMetadataToDb(this.currentSong); return; } } // 3. 尝试从服务器获取歌词 (Navidrome/Jellyfin/Emby/AudioStation/Plex) if (this.currentSong && (isJellyfinType(this.currentSong.type)||isNavidromeType(this.currentSong.type) || isEmbyType(this.currentSong.type) || isAudioStationType(this.currentSong.type) || isPlexType(this.currentSong.type))) { const manager = RemoteDriveManager.getInstance(); const getAccount = (accountId: string) => manager.getWebDavAccountById(accountId); // 将currentSong转换为SongData接口 const songData: SongData = { webdav_account_id: this.currentSong.webdav_account_id, artist: this.currentSong.artist, name: this.currentSong.name, webdav_id: this.currentSong.webdav_id, lyricIndex: this.currentSong.lyricIndex, type: this.currentSong.type }; let serverLyric = ''; if (!serverLyric && isNavidromeType(songData.type)) { serverLyric = await lyricService.fetchNavidromeLyric(songData, getAccount,this.currentSong.id); } if (!serverLyric && isJellyfinType(songData.type)) { serverLyric = await lyricService.fetchJellyfinLyric(songData, getAccount); } if (!serverLyric && isEmbyType(songData.type)) { serverLyric = await lyricService.fetchEmbyLyric(songData, getAccount); } if (!serverLyric && isAudioStationType(songData.type)) { serverLyric = await lyricService.fetchAudioStationLyric(songData, getAccount); } if (!serverLyric && isPlexType(songData.type)) { serverLyric = await lyricService.fetchPlexLyric(songData, getAccount); } if (serverLyric) { this.setLyricToControllers(serverLyric, lyricPath); return; } } // 4. 尝试在线歌词API if (isOnLineAndToast || ((!FileUtil.accessSync(lyricPath)) && !Utility.isVideoByExtension(this.videoUrl))) { this.fetchOnlineLyric(lyricPath, isOnLineAndToast || false, isApi2); return; } // 5. 从本地文件读取歌词 await this.loadLyricFromFile(lyricPath, isLocal || false); } /** * 从本地文件加载歌词 */ private async loadLyricFromFile(lyricPath: string, isLocal: boolean): Promise { try { let realLyricPath = lyricPath; // 读取文件内容 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 = new Uint8Array(arrayBuffer, 0, readLen); if (buf.length === 0) { throw new Error("Buffer is empty after reading the file"); } try { // 检测文件编码 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}`); console.info(`onecold The content of file2: ${fileContent}`); this.lyricContent = fileContent; if(isLocal){ // 编辑标签选择本地歌词需要赋值给this.lyricConStr this.lyricConStr = fileContent; } this.setLyricToControllers(fileContent); console.info(`The content of file: ${fileContent}`); } catch (chardetError) { console.error("chardet.detect failed with error: " + chardetError.message); this.clearLyricControllers(); } }) .catch((err: BusinessError) => { this.clearLyricControllers(); console.error("read file data failed with error message: " + err.message + ", error code: " + err.code); }) .catch((err: Error) => { this.clearLyricControllers(); console.error("read file 2 data failed with error message: " + err.message); }) .finally(() => { fs.closeSync(file); }); } catch (error) { this.clearLyricControllers(); Logger.error(TAG, 'init Lyric failed with err: ' + JSON.stringify(error)); } } detect(data: ArrayBuffer): string { //创建检测对象 let detector: UniversalDetector = new UniversalDetector(); // 限制检测数据大小,避免内存溢出 // 通常只需要检测文件前16KB就足够判断编码格式 const MAX_DETECT_SIZE = 16 * 1024; // 16KB const detectSize = Math.min(data.byteLength, MAX_DETECT_SIZE); // 传入检测数据并实时判断编码格式 detector.handleData(data, 0, detectSize); // 标记检测数据 读取结束 detector.dataEnd(); // 获取检测结果 let detected: string = detector.getDetectedCharset(); // 释放资源 detector.reset(); Logger.info(TAG, "heanup detect encoding: " + detected + ", data size: " + data.byteLength + ", detect size: " + detectSize); return detected; } /** * 设置歌词到所有控制器 * @param lyricText 歌词文本 * @param savePath 可选,如果提供则保存到本地文件 */ private setLyricToControllers(lyricText: string, savePath?: string): void { if (StrUtil.isEmpty(lyricText)) { this.clearLyricControllers(); return; } this.lyricContent = lyricText; const lines: string[] = lyricText.split('\n').map((line: string) => line.trim()); const lyric: Lyric = this.parser.parse(lines); this.lyricController.setLyric(lyric); this.lyricControllerXF.setLyric(lyric); this.lyricControllerSingle.setLyric(lyric); this.showSingleLyric = true; // 如果提供了保存路径,保存歌词到本地 if (savePath) { this.saveDataToFile(lyricText, savePath); } } /** * 清空所有歌词控制器 */ private clearLyricControllers(): void { this.lyricController.setLyric(null); this.lyricControllerXF.setLyric(null); this.lyricControllerSingle.setLyric(null); } private extractLyricCopyLines(lyricText: string): string[] { if (StrUtil.isEmpty(lyricText)) { return []; } const normalizedLyric = LyricUtil.convertLyricToSimpleLrc(lyricText); const source = StrUtil.isNotEmpty(normalizedLyric) ? normalizedLyric : lyricText; const lines = source.split('\n'); const result: string[] = []; const metaTagPattern = /^\[(ti|ar|al|by|offset|hash|sign|qq|total|tool|re|ve|length|au):?.*]$/i; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line || metaTagPattern.test(line)) { continue; } const pureLine = line .replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?]/g, '') .replace(/<\d{1,2}:\d{2}(?:\.\d{1,3})?>/g, '') .trim(); if (pureLine.length > 0) { result.push(pureLine); } } return result; } private openLyricCopySheet(): void { const lines = this.extractLyricCopyLines(this.lyricContent); if (ArrayUtil.isEmpty(lines)) { ToastUtil.showToast('暂无可复制歌词'); return; } this.lyricCopyLines = lines; this.lyricSelectedIndexes = []; this.isShowLyricCopySheet = true; } private toggleLyricLineSelection(index: number): void { const selectedIndex = this.lyricSelectedIndexes.indexOf(index); if (selectedIndex >= 0) { const nextSelectedIndexes = [...this.lyricSelectedIndexes]; nextSelectedIndexes.splice(selectedIndex, 1); this.lyricSelectedIndexes = nextSelectedIndexes; return; } this.lyricSelectedIndexes = [...this.lyricSelectedIndexes, index]; } private toggleLyricSelectAll(): void { if (this.lyricSelectedIndexes.length === this.lyricCopyLines.length) { this.lyricSelectedIndexes = []; return; } this.lyricSelectedIndexes = this.lyricCopyLines.map((_: string, index: number) => index); } private copySelectedLyricLines(): void { const selectedLines = this.getSelectedLyricLines(); if (ArrayUtil.isEmpty(selectedLines)) { ToastUtil.showToast('暂无可复制歌词'); return; } Utility.copyText(selectedLines.join('\n')); ToastUtil.showToast(`已复制${selectedLines.length}行歌词`); } private getSelectedLyricLines(): string[] { if (ArrayUtil.isEmpty(this.lyricSelectedIndexes)) { return []; } const selectedIndexes = [...this.lyricSelectedIndexes].sort((a: number, b: number) => a - b); const selectedLines: string[] = []; for (let i = 0; i < selectedIndexes.length; i++) { const index = selectedIndexes[i]; if (index >= 0 && index < this.lyricCopyLines.length) { selectedLines.push(this.lyricCopyLines[index]); } } return selectedLines; } private shareSelectedLyricLines(): void { const selectedLines = this.getSelectedLyricLines(); if (ArrayUtil.isEmpty(selectedLines)) { ToastUtil.showToast('暂无可分享歌词'); return; } const shareTitle = `${this.currentSong?.name ?? '歌词'}歌词`; Utility.doShareText(selectedLines.join('\n'), this.context, shareTitle, '分享歌词'); } /** * 从在线API获取歌词 */ private fetchOnlineLyric(lyricPath: string, isOnLineAndToast: boolean, isApi2?: boolean): void { if (!this.currentSong) { this.clearLyricControllers(); if (isOnLineAndToast) { ToastUtil.showShort('未获取到歌词!'); } return; } let artist = this.currentSong.artist || ''; if (isApi2 === undefined) { isApi2 = false; } NetAxiosUtil.getLyric(this.name, artist, isApi2).then((res) => { if (StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') { this.setLyricToControllers(res, lyricPath); } else { this.clearLyricControllers(); if (isOnLineAndToast) { ToastUtil.showShort('未获取到歌词!'); } } }); } //初始化悬浮歌词的自定义设置 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 { let neiqianLrc = '' let lyContent = item?.lyricContent if(lyContent&&StrUtil.isNotEmpty(lyContent)){ neiqianLrc = lyContent return neiqianLrc } try { //取数据库里面的歌词lyContent const lyricContent = await this.table.getLyricContentByFilePath(item.filePath); if (lyricContent&&StrUtil.isNotEmpty(lyricContent)) { console.log("onecold Lyrics:", lyricContent); neiqianLrc = lyricContent return neiqianLrc } } catch (error) { console.error("Error fetching lyrics:", error); } try { let filePath = item.filePath; let lyricPath = filePath.substring(0, filePath.lastIndexOf('.')) + '.lrc'; let realLyricPath = lyricPath; 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}`); fs.closeSync(file); return fileContent; } catch (error) { console.error('init Lyric failed with err: ' + JSON.stringify(error)); return ''; } } async saveDataToFile(content: string, filePath: string) { if (StrUtil.isEmpty(filePath)) { return } try { let lyricContent = '' 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); } } // 定义开始旋转的方法 // 使用动画系统优化旋转性能,避免 setInterval 频繁更新导致卡顿 @State timer: number = 0 @State rotateAngle: number = 0 @State isRotationRunning: boolean = false // 旋转动画是否正在运行 @StorageProp('isPageVisible') @Watch('onPageVisibilityChange') isPageVisible: boolean = true // 页面可见性标志 // 页面可见性变化回调 onPageVisibilityChange() { // 当页面可见性改变时,重新启动/停止旋转动画 this.animationRoFun() } //封面旋转动画 animationRoFun() { if (this.isPlaying && !this.isCoverRectangle && this.isPageVisible) { // 启动旋转动画 if (!this.isRotationRunning) { this.isRotationRunning = true this.startContinuousRotation() } this.startRotation() } else { // 停止旋转动画 this.isRotationRunning = false clearInterval(this.timer) this.stopRotation() } } // 使用持续的动画进行旋转,避免频繁状态更新 private startContinuousRotation() { clearInterval(this.timer) // 改为每 1000ms 更新 36 度(10秒一圈),减少更新频率 // 不使用 % 360 取模,让角度持续累加,避免反转 this.timer = setInterval(() => { if (this.isRotationRunning) { this.rotateAngle = this.rotateAngle + 36 } }, 1000) } // 定义开始旋转的方法 startRotation() { if (this.isPlaying) { animateTo({ duration: 777, iterations: 1, curve: Curve.Linear }, () => { this.rotateAngle2 = -9 }) } } // 回调,暂停和播放按钮回调,下一首和上一首 playChange() { // if(this.isCoverRectangle){ // return // } if (this.CONTROL_PlayStatus === PlayStatus.PLAY) { this.setIsPlaying(true) this.animationState = AnimationStatus.Running } else { this.setIsPlaying(false) this.animationState = AnimationStatus.Paused } AppStorage.setOrCreate('animationState', this.animationState); } // 定义停止旋转的方法 stopRotation() { if (this.isCoverRectangle) { this.rotateAngle = 0 } else { //唱针以左上角为点选择逆时针40度 animateTo({ duration: 1000, curve: Curve.Linear, iterations: 1, }, () => { this.rotateAngle2 = -45; }); } } @Builder private LyricsTopItem() { Row() { Stack({ alignContent: Alignment.Center }) { Image( StrUtil.isNotEmpty(this.cover)? this.cover:$r('app.media.alt')) .height(58) .width(58) .clip(true) .sourceSize({width:38, height:38}) .alt($r('app.media.alt')) .borderRadius(8) .clickEffect({ level: ClickEffectLevel.HEAVY }) .margin({ left: this.currentLyricAlignMode === 0 ? 8 : 0 }) .shadow({ radius: 22, type: ShadowType.BLUR, color: 'on_primary' }) } .width(62) .height('100%') Column() { Column() { Text(this.name) .fontSize(16) .maxLines(1) .fontWeight(FontWeight.Bolder) .fontColor(Color.White) .textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ left: 10 }) Row() { Text(this.currentSong?.artist !== undefined ? this.currentSong?.artist : '') .fontSize(13) .fontWeight(FontWeight.Bolder) .padding({ top: 8 }) .fontColor(Color.White) .margin({ left: 10 }) Blank() } } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Start) } .height('100%') .layoutWeight(1) .margin({ right: 8 }) Column() { PointLightDefaultButton({ isSysBol:false, pointColor:Color.White, imageResource:$r('app.media.lyric'), isPx: false, builderHeight: 23, builderWidth: 23, }) .onClick(() => { this.isLyricSetting = !this.isLyricSetting; }) .bindSheet($$this.isLyricSetting, this.lyricSettingSheet(false), { height: '99%', dragBar: true, preferType: SheetType.CENTER, showClose: true, blurStyle: BlurStyle.Thin, backgroundColor: Color.Transparent, title: { title: $r('app.string.lyric_setting') } }) } .width(44) .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.End) } .width(this.currentLyricAlignModePip === 1?'98%':'90%%') .padding({ left: 28, right: 28 }) .height(58) .alignItems(VerticalAlign.Center) } //歌词显示 @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) { console.info('onecold startPlayOrResumePlay 7527') this.startPlayOrResumePlay() } else { this.dispatchSeekToPosition(position, 'lyric-seek-action') } return true } }) .width("100%") .layoutWeight(1) .margin({ left: this.currentLyricAlignMode === 0 ? 0 : 50, top: 2, bottom: this.isPhoneLan()||this.isHiCarKuanBianPing() ? 40 : 10 }) .gesture(LongPressGesture() .onAction(() => { this.openLyricCopySheet(); })) .bindSheet($$this.isShowLyricCopySheet, this.LyricCopySheetBuilder(), { height: '99%', dragBar: true, showClose: true, preferType: SheetType.CENTER, blurStyle: BlurStyle.Thin, title: { title: '歌词' } }) if (this.isPhoneLan()) { Column() { if (this.isShowSpectrum) { this.InlineSpectrumView() } this.playProgressView() } .margin({ bottom: 32, 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() ? this.isPhoneLan()?'95%':'90%' : '82%') } } @Builder LyricCopySheetBuilder() { LyricCopySheet({ lyricCopyLines: this.lyricCopyLines, lyricSelectedIndexes: this.lyricSelectedIndexes, isDarkMode: this.isDarkMode, themeColor: this.themeColor, bottomSafeHeight: this.bottomSafeHeight, onToggleLyricLine: (index: number) => { this.toggleLyricLineSelection(index); }, onToggleSelectAll: () => { this.toggleLyricSelectAll(); }, onCopySelectedLines: () => { this.copySelectedLyricLines(); }, onShareSelectedLines: () => { this.shareSelectedLyricLines(); } }) } @Builder SingleLyricView() { Column() { Column() { LyricView2({ controller: this.lyricControllerSingle, enableSeek: false, seekUIColor: "#ff0000", // 滑动定位的按钮和文本颜色 seekLineColor: "#80ffffff", // 滑动定位线颜色 seekUIStyle: "listItem", // 滑动定位样式(seekLine传统样式,listItem类似抖音汽水音乐样式) }) .gesture(LongPressGesture() .onAction(() => { this.openLyricCopySheet(); })) } .height(82) .margin({ bottom: 20 }) } .opacity(this.playDragUiOpacity) } //播放器的底部控制中心,上一首,暂停,下一首,播放进度条等 @Builder BottomControl() { Column() { if (this.isShowSpectrum) { this.InlineSpectrumView() } this.TopPlayProgressView() //播放 暂停,下一首,上一首 this.playCenterView() if(!this.curDisplayIsHiCar){ //播放进度条 this.BottomView() } } .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .opacity(this.playDragUiOpacity) .position({ bottom: this.isHiCarSmall()?50:(this.isCoverOpacity() ? 55 : 70) }) // 将 固定在底部 } @Builder playMenuBuilder(){ Image($r('app.media.menu')) .width(24) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .bindSheet($$this.isShowMoreView, this.PlayMoreSheet(), { height: '99%', preferType:SheetType.CENTER, dragBar: true, showClose: true, blurStyle: BlurStyle.Thin, title: { title: $r('app.string.more') }, backgroundColor: Color.Transparent, }) } //播放控制上一首 下一首 暂停和播放 @Builder playCenterView(){ Row() { PointLightButton({ builder: () => { this.playMenuBuilder() }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(async () =>{ this.isShowMoreView = !this.isShowMoreView; }) PointLightButton({ builder: () => { this.playPreviousBuilder() }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(async () =>{ await this.playbackController.playPrevious() }) PointLightButton({ builder: () => { this.PlayOrPauseButton() }, isPx: false, builderHeight: this.isPhoneLan()?38:41, builderWidth: this.isPhoneLan()?38:41, }) .onClick(() =>{ this.playbackController.playOrPause() }) PointLightButton({ builder: () => { this.playNextBuilder() }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(async () =>{ await this.playbackController.playNext() }) this.playModeButton() } .width('95%') .margin({ bottom:this.isCoverOpacity()?0:15 }) .justifyContent(FlexAlign.SpaceEvenly) } @Builder playPreviousBuilder(){ Image($r('app.media.ic_previous')) .width(32) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .aspectRatio(CommonConstants.ASPECT_RATIO) } @Builder playNextBuilder(){ Image($r('app.media.ic_next')) .width(32) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .aspectRatio(CommonConstants.ASPECT_RATIO) } @Builder PlayOrPauseButton(){ Stack() { 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()?38:40) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .fillColor(Color.White) .aspectRatio(CommonConstants.ASPECT_RATIO) } // 加载进度圈 if (this.isPlayerLoading) { Progress({value:0,total:100,type:ProgressType.Ring }) .width(this.isPhoneLan()?40:43) .height(this.isPhoneLan()?40:43) .color(Color.White) .style({ strokeWidth: 5, status: ProgressStatus.LOADING }) } } .width(this.isPhoneLan()?38:41) .height(this.isPhoneLan()?38:41) } @Builder TopPlayProgressView(){ Column() { Row(){ Stack(){ SymbolGlyph($r('sys.symbol.arrow_counterclockwise')) .fontColor([Color.White]) .fontSize(21) .effectStrategy(1) Text(this.fastForwardSeconds) .fontColor(Color.White) .fontSize(10) .fontWeight(FontWeight.Bold) } .padding({left:12,bottom:6}) .visibility(this.isShowBackFast?Visibility.Visible:Visibility.None) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .onClick(()=>{ this.backForward() }) Slider({ value: this.progressValue, min: 0, max: this.PROGRESS_MAX_VALUE, step: 1, style: SliderStyle.InSet }) .height(20) .blockColor('rgba(255,255,255,1)') .trackColor('rgba(255,255,255,0.3)') .selectedColor(Color.White) .trackThickness(4) .showSteps(false) .showTips(true) .layoutWeight(1) .enabled(this.slideEnable) .onChange((value: number, mode: SliderChangeMode) => { if (mode !== 2) { return } this.dispatchSeekFromPercent(value, 'top-progress-slider') }) Stack(){ SymbolGlyph($r('sys.symbol.arrow_clockwise')) .fontColor([Color.White]) .fontSize(21) .effectStrategy(1) Text(this.fastForwardSeconds) .fontColor(Color.White) .fontSize(10) .fontWeight(FontWeight.Bold) } .padding({right:10,bottom:6}) .visibility(this.isShowBackFast?Visibility.Visible:Visibility.None) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .onClick(()=>{ this.fastForward() }) } .justifyContent(FlexAlign.Center) .padding({left:20,right:20}) .width(this.isLandscape?'88%':'95%') Row() { Text(this.currentTime) .fontSize(10) .fontColor(Color.White) .margin({left:25}) Blank() Text(this.totalTime) .fontSize(10) .fontColor(Color.White) .margin({right:25}) } .width(this.isLandscape?'85%':'90%') .height(10) } .width('100%') .alignItems(HorizontalAlign.Center) } @Builder playModeButton(){ PointLightButton({ builder: () => { this.playModeBuilder() }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(async () =>{ await this.playbackController.setLoopMode() }) } @Builder playModeBuilder(){ Column() { if (this.playType === 0) { Image($r('app.media.loop')) .width(24) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .aspectRatio(CommonConstants.ASPECT_RATIO) } else if (this.playType === 1) { Image($r('app.media.single')) .width(24) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .aspectRatio(CommonConstants.ASPECT_RATIO) } else if (this.playType === 2) { Image($r('app.media.normal_play')) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .width(24) .aspectRatio(CommonConstants.ASPECT_RATIO) } else if (this.playType === 3) { Image($r('app.media.random')) .width(24) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .aspectRatio(CommonConstants.ASPECT_RATIO) } else if (this.playType === 4) { Image($r('app.media.noloop')) .width(24) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .aspectRatio(CommonConstants.ASPECT_RATIO) } } } //播放进度条 @Builder BottomView(){ Row() { PointLightDefaultButton({ isSysBol:true, pointColor:Color.White, imageResource:$r('sys.symbol.rectangle_portrait_rotate'), isPx: false, builderHeight: 22, builderWidth: 22, }) .onClick(async () =>{ if (this.isLandscape) { this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT); } else { this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE); } }) if(this.currentSong){ PointLightDefaultButton({ isSysBol:true, pointColor:Color.White, imageResource:$r('sys.symbol.rename'), isPx: false, builderHeight: 22, builderWidth: 22, }) .bindSheet($$this.isShowEdit, this.editSheet(this.currentSong), { height: '99%', dragBar: true, showClose: true, preferType: SheetType.BOTTOM, title: { title: '编辑标签' } }) .onClick(async () => { this.initEditFields(this.currentSong) this.tempLyricContent = this.lyricContent if (StrUtil.isNotEmpty(this.tempLyricContent)) { this.lyricConStr = this.tempLyricContent } this.isShowEdit = !this.isShowEdit; }) } PointLightDefaultButton({ isSysBol:true, pointColor:Color.White, imageResource:$r('sys.symbol.slider_vertical_3'), isPx: false, builderHeight: 22, builderWidth: 22, }) .onClick(async () => { this.isEqualizerSheet = !this.isEqualizerSheet; }) PointLightDefaultButton({ isSysBol:true, pointColor:Color.White, imageResource:Utility.getIsFav(this.favList,this.currentSong)? $r('sys.symbol.heart_fill'):$r('sys.symbol.heart'), isPx: false, builderHeight: 22, builderWidth: 22, }) .onClick(async () => { if(this.currentSong){ this.doFav(this.currentSong) } }) this.playListBuilder() } .width(this.isLandscape?'88%':'95%') .justifyContent(FlexAlign.SpaceAround) .visibility(this.showSimple?Visibility.None:Visibility.Visible) .height(30) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) } @Builder fullButton(){ Column() { Image($r('app.media.full_4')) .width(25) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .aspectRatio(CommonConstants.ASPECT_RATIO) } } @Builder playListBuilder(){ PointLightDefaultButton({ isSysBol:true, pointColor:Color.White, imageResource:$r('sys.symbol.music_note_list'), isPx: false, builderHeight: 26, builderWidth: 26, }) .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:this.getPlayListDisplayCount()==0?Utility.resourceToString(this.context, $r('app.string.current_play_list')) :Utility.resourceToString(this.context, $r('app.string.current_play_list')) + `(${this.getPlayListDisplayCount()}首)` } }) .onClick(() => { this.isShowSheetView = !this.isShowSheetView; if(this.isPlayListBgGrass){ this.isFrontWhite = true }else{ this.isFrontWhite = false } this.scrollIndex() if (this.shouldHydrateFullMediaPlayListOnOpen()) { void this.hydrateFullMediaPlayListIfNeeded(); } }) } //旧版本播放进度条(现在是手机横屏的播放条) @Builder playProgressView() { Row() { Column(){ PointLightDefaultButton({ isSysBol:true, pointColor:Color.White, imageResource:$r('sys.symbol.rectangle_portrait_rotate'), isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(async () =>{ if (this.isLandscape) { this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT); } else { this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE); } }) } .margin({ right: 10,bottom:6}) .visibility( this.is_auto_hide_progress ? Visibility.None : Visibility.Visible) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) 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.InSet }) .width('600px') .blockColor('rgba(255,255,255,1)') .trackColor('rgba(255,255,255,0.3)') .selectedColor(Color.White) .trackThickness(5) .layoutWeight(1) .margin({top:4, left: PlayConstants.PROGRESS_MARGIN_LEFT }) .showSteps(false) .showTips(true) .enabled(this.slideEnable) .onChange((value: number, mode: SliderChangeMode) => { if (mode !== 2) { return } console.info("onecold this.currentSong.type:"+this.currentSong?.type) this.dispatchSeekFromPercent(value, 'landscape-progress-slider') }) Text(this.totalTime) .fontSize($r('app.float.slider_font_size')) .fontColor(Color.White) .margin(3) //播放列表 Column(){ this.playListBuilder() } .margin({ left: 10,bottom:6}) .visibility( this.is_auto_hide_progress ? Visibility.None : Visibility.Visible) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) } //x5的竖屏模式需要99% .width(this.isPhonePortrait() ? '93.4%' : this.currentHeightBreakpoint == 1 && this.currentWidthBreakpoint == 2 ? '99%' : '82%') .height(this.isPhoneLan() ? 25 : 33) .alignItems(VerticalAlign.Bottom) .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 scaleValueImage: number = 1 @State scaleValueText: number = 1 @State playDragUiOpacity: number = 1 @Builder private CoverScaleGuideOverlay() { Stack() { Column({ space: 10 }) { SymbolGlyph($r('sys.symbol.arrow_up_left_and_arrow_down_right')) .fontSize(34) .fontColor([Color.White]) Text('双指缩放封面') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor(Color.White) Text('在封面区域双指捏合,可调整封面大小') .fontSize(12) .fontColor(Color.White) .opacity(0.88) .textAlign(TextAlign.Center) } .padding({ left: 18, right: 18, top: 16, bottom: 16 }) .backgroundColor('#A6000000') .borderRadius(18) .width(this.isPhoneLan() ? 230 : 240) } .width('100%') .height(this.isPhoneLan() ? 260 : 360) .align(Alignment.Center) .zIndex(10) .visibility(this.isShowCoverScaleGuide ? Visibility.Visible : Visibility.None) .onClick(() => { this.dismissCoverScaleGuide() }) } @Builder CoverInfo() { Column() { Stack() { Column() { //方形封面布局 if (this.isCoverRectangle) { this.RectangleCoverView() } else { //圆形封面布局 this.CicleCoverView() } this.musicNameInfo(false) if (this.isPhoneLan()) { Text(this.name+"--"+this.artist) .fontSize(18) .fontWeight(FontWeight.Bold) .margin({ top: 13 }) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(1) .width('99%') .textAlign(TextAlign.Center) .fontColor(Color.White) } } .width('100%') 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('85%') .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) this.CoverScaleGuideOverlay() } .margin({ bottom: this.isCoverOpacity() || this.isCoverRectangle ? (this.isPhonePortrait() && this.isShowSingleLineLyric ? 20 : 55) : 0, top: this.isCoverOpacity() ? 0 : 20 }) if (this.isShowSingleLineLyric&&this.isPhonePortrait()) { this.SingleLyricView() } if (this.isPhoneLan()) { Column() { this.playCenterView() } .position({ left: 28, bottom: 35 }) } else { this.BottomControl() } } .width(CommonConstants.FULL_PERCENT) .clip(true) } //方形封面布局 @Builder RectangleCoverView(){ Stack({alignContent:Alignment.Center}){ Image( StrUtil.isNotEmpty(this.cover)? this.cover:$r('app.media.alt')) .height(this.getRectangleCoverHeight()) .width(this.getRectangleCoverWidth()) .objectFit(this.isCoverRectangle ? ImageFit.Contain : ImageFit.Auto) .alt($r('app.media.alt')) .margin({ right: 8, left: 8, top: 5 }) .aspectRatio(1) .opacity(this.opacityValueImage) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.8 }) .bindContextMenu(this.MenuImageBuilder(this.cover), ResponseType.LongPress, { preview: MenuPreviewMode.IMAGE, previewAnimationOptions: { scale: [0.8, 1.0] }, }) .bindContextMenu(this.MenuImageBuilder(this.cover), ResponseType.RightClick, { preview: MenuPreviewMode.IMAGE, previewAnimationOptions: { scale: [0.8, 1.0] }, }) .visibility(this.isPuraWP() ? Visibility.None : Visibility.Visible) .borderRadius(this.isCoverRectangle ? 20 : '100%') .clip(true) .animation({ duration: 1000, // 与 setInterval 同步 curve: Curve.Linear, iterations: 1 }) .shadow({ radius: 22, type: ShadowType.BLUR, color: 'on_primary' }) .onClick(() => { this.isMusicBGCover = !this.isMusicBGCover }) .gesture(PinchGesture({ fingers: 2 }) .onActionUpdate((event: GestureEvent) => { this.updateRectangleCoverScaleByPinch(event.scale) }) .onActionEnd(() => { this.rectangleCoverPinchValue = this.rectangleCoverScale PreferencesUtil.put(SettingPage.RECTANGLE_COVER_SCALE, this.rectangleCoverScale) })) } .scale({ x: this.scaleValueImage, y: this.scaleValueImage}) } private getRectangleCoverWidth(): string | number { if (this.isHiCar() || this.isCoverOpacity()) { return 'auto' } const baseWidth = this.isBigScreen() ? 72 : this.isCoverTopBig ? 100 : 88 return `${Math.min(100, Math.max(48, baseWidth * this.rectangleCoverScale))}%` } private getRectangleCoverHeight(): string | number { if (this.isHiCar()) { return px2vp(this.windowHeight) * 0.4 * this.rectangleCoverScale } if (this.isCoverOpacity()) { return px2vp(this.windowHeight) * 0.54 * this.rectangleCoverScale } return 'auto' } private updateCoverScaleByPinch(scale: number): void { this.rectangleCoverScale = Math.min(1.4, Math.max(0.7, this.rectangleCoverPinchValue * scale)) } private updateRectangleCoverScaleByPinch(scale: number): void { const dampedScale = 1 + (scale - 1) * 0.45 this.rectangleCoverScale = Math.min(1.4, Math.max(0.7, this.rectangleCoverPinchValue * dampedScale)) } private persistCoverScaleAfterPinch(): void { this.rectangleCoverPinchValue = this.rectangleCoverScale PreferencesUtil.put(SettingPage.RECTANGLE_COVER_SCALE, this.rectangleCoverScale) } @Builder CicleCoverView() { //圆形封面布局 Stack({alignContent:Alignment.Center}) { Image($r('app.media.ic_music_disc')) .width(245) .height(245) .margin({ right: 20, left: 20 ,top:this.isLandscape?0:50 }) .aspectRatio(1) .opacity(this.isCoverOpacity() ? 0 : 1) .visibility(this.isCoverOpacity() ? Visibility.None : Visibility.Visible) .borderRadius('100%') .align(Alignment.Center) .clip(true) .rotate({ angle: this.rotateAngle, centerX: "50%", centerY: "50%", }) .animation({ duration: 1000, // 改为 1000ms 与 setInterval 同步 curve: Curve.Linear, iterations: 1 // 只执行一次,下一次更新会触发新的动画 }) .shadow({ radius: 22, type: ShadowType.BLUR, color: 'on_primary' }) Image( StrUtil.isNotEmpty(this.cover)? this.cover:$r('app.media.alt')) .height(this.isHiCarSmall()?(this.isHiCarKuanBianPing()?140:px2vp(this.windowHeight)*0.38): (this.isCoverOpacity() ? (px2vp(this.windowHeight)*0.62) : this.isCoverRectangle ? 320 : 168)) .objectFit(this.isCoverRectangle ? ImageFit.Contain : ImageFit.Auto) .alt($r('app.media.alt')) .margin({ right: 20, left: 20, top:this.isCoverRectangle||this.isLandscape?0:50}) .aspectRatio(1) .opacity(this.opacityValueImage) .visibility(this.isPuraWP() ? Visibility.None : Visibility.Visible) .borderRadius(this.isCoverRectangle ? 20 : '100%') .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.8 }) .align(Alignment.Center)// .clip(true) .shadow({ radius: 22, type: ShadowType.BLUR, color: 'on_primary' }) .rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle }) .animation({ duration: 1000, // 与 setInterval 同步 curve: Curve.Linear, iterations: 1 }) .onClick(() => { this.isMusicBGCover = !this.isMusicBGCover }) // 唱针以左上角为点选择逆时针40度 Image($r('app.media.ic_music_cover_hand')) .height(130) .margin({ bottom: 200, top: 0 }) // .position({ x: this.windowWidth/2, y: 0 }) .align(Alignment.Top) .rotate({ angle: this.rotateAngle2, centerX: 0, centerY: 0 }) .opacity(this.isCoverOpacity() || this.isCoverRectangle ? 0 : 1) .visibility(this.isHiCar()||this.isCoverOpacity() || this.isCoverRectangle ? Visibility.None : Visibility.Visible) } .scale({ x: this.scaleValueImage * this.rectangleCoverScale, y: this.scaleValueImage * this.rectangleCoverScale, centerX: '50%', centerY: '50%' }) .priorityGesture(PinchGesture({ fingers: 2 }) .onActionStart(() => { this.rectangleCoverPinchValue = this.rectangleCoverScale }) .onActionUpdate((event: GestureEvent) => { this.updateCoverScaleByPinch(event.scale) }) .onActionEnd(() => { this.persistCoverScaleAfterPinch() })) } @Builder MenuImageBuilder(cover: string|undefined) { Menu(){ MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.picture_2')), content: '查看封面' }) .onClick(async() => { if (StrUtil.isEmpty(cover)) { ToastUtil.showToast('当前没有可用封面') return; } this.getUIContext().getRouter().pushUrl({ url: 'pages/WallpaperPreviewPage', params: { imageUri: cover } }) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.picture')), content: '更换封面' }) .onClick(async() => { await this.replaceCurrentSongCoverByAlbum(); }) } } @Builder private musicNameInfo(isHidden: boolean) { Row(){ Column({ space: 8 }) { Text(this.name) .fontSize(this.name.length >= 28 || this.isCoverOpacity() ? 18 : 20) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .maxLines(1) .width('99%') .textAlign(TextAlign.Start) .fontColor(Color.White) Text(this.artist + ' ' + this.currentSong?.album) .fontSize(this.isCoverOpacity() ? 12 : 15) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .fontColor(Color.White) .textAlign(TextAlign.Start) .visibility(StrUtil.isEmpty(this.artist)?Visibility.None:Visibility.Visible) .width('99%') .maxLines(1) } .zIndex(1) .layoutWeight(1) .margin({ left: 15 }) Blank() Row(){ PointLightDefaultButton({ isSysBol:true, pointColor:Color.White, imageResource:this.showSimple? $r('sys.symbol.sun_max'):$r('sys.symbol.moon'), isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(async () => { this.showSimple = !this.showSimple PreferencesUtil.put('showSimple', this.showSimple) }) } .margin({ left: 8,right:8 }) } .width(this.isLandscape?'83%':'90%') .scale({ x: this.scaleValueText, y: this.scaleValueText }) // 添加缩放效果 .opacity(this.playDragUiOpacity) .margin({ top:this.isCoverRectangle?15: 0 }) .visibility(this.isCoverOpacity() || isHidden ? Visibility.None : Visibility.Visible) } @Builder private PlayTitle() { Row() { Row() { PointLightDefaultButton({ isSysBol: true, pointColor: Color.White, imageResource: $r('sys.symbol.chevron_down'), isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(() => { this.setShowPlayFalse() }) } .width('100%') .visibility(this.isHide ? Visibility.Hidden : Visibility.Visible) } .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 }) }else{ Row() { Row() { Text(`迷你歌词:`) .fontSize(14) .fontColor(Color.White) .width(88) Toggle({ type: ToggleType.Switch, isOn: this.isShowSingleLineLyric }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onChange((checked: boolean) => { this.isShowSingleLineLyric = checked; PreferencesUtil.put(SettingPage.IS_SHOW_SLLYRIC, this.isShowSingleLineLyric) }) .width(50) .height(30) .alignSelf(ItemAlign.Start) .margin({ left: 12 }) Blank() } .layoutWeight(1) Row() { Text(`逐字歌词:`) .fontSize(14) .fontColor(Color.White) .width(88) Toggle({ type: ToggleType.Switch, isOn: this.isEnableWordByWordLyric }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onChange((checked: boolean) => { this.isEnableWordByWordLyric = checked; PreferencesUtil.putSync(SettingPage.IS_ENABLE_WORD_BY_WORD_LYRIC, this.isEnableWordByWordLyric) this.syncWordByWordLyricMode() this.nodeController.updateBgColor(this.pipBg, this.lyricControllerXF) }) .width(50) .height(30) .alignSelf(ItemAlign.Start) .margin({ left: 12 }) Blank() } .layoutWeight(1) } .width('100%') .margin({ left: 20, right: 15, top: 10, bottom: 10 }) } if (!isPip) { Row() { Row() { Text(`歌词发光:`) .fontSize(14) .fontColor(Color.White) .width(88) Toggle({ type: ToggleType.Switch, isOn: this.isLightText }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .onChange((checked: boolean) => { this.isLightText = checked; PreferencesUtil.putSync(SettingPage.IS_LIGHT_TEXT, this.isLightText) this.syncLyricLightTextMode() }) .width(50) .height(30) .alignSelf(ItemAlign.Start) .margin({ left: 12 }) Blank() } .layoutWeight(1) Row() { Text(`高亮居中:`) .fontSize(14) .fontColor(Color.White) .width(88) 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() } .layoutWeight(1) } .width('100%') .margin({ left: 20, right: 15, top: 10, bottom: 10 }) } Row() { Text(`歌词居中:`) .fontSize(14) .fontColor(Color.White) 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 }) }) } .layoutWeight(1) Blank() } .width('100%') .margin({ top: 10, bottom: 10, right: 15, 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('100%') .margin({ bottom: 5, right: 15, left: 20 }) } Row() { Text(`歌词颜色:`) .fontSize(14) .fontColor(Color.White) Text(isPip ? this.currentLyricColorPip : this.currentLyricColor) .fontSize(15) .fontColor(Color.White) .margin({ left: 6, right: 6 }) 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('100%') .margin({ top: 10, bottom: 10, right: 15, left: 20 }) Row() { Text(`高亮颜色:`) .fontSize(14) .fontColor(Color.White) Text(isPip ? this.currentHighLightLyricColorPip : this.currentHighLightLyricColor) .fontSize(15) .fontColor(Color.White) .margin({ left: 6, right: 6 }) 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('100%') .margin({ top: 10, bottom: 10, right: 15, 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) } }) .layoutWeight(1) } .justifyContent(FlexAlign.Start) .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) } }) .layoutWeight(1) } .justifyContent(FlexAlign.Start) .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) }) .layoutWeight(1) .margin({ right: 3 }) Text(this.getLyricSizeDisplayValue(isPip)) .fontSize(13) .fontColor(Color.White) .textAlign(TextAlign.Start) } .width('100%') .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) }) .layoutWeight(1) .margin({ right: 3 }) Text(this.getHighLyricSizeDisplayValue(isPip)) .fontSize(13) .fontColor(Color.White) .textAlign(TextAlign.Start) } .width('100%') .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) }) .layoutWeight(1) .margin({ right: 3 }) Text(this.getLyricLineSpaceDisplayValue(isPip)) .fontSize(13) .fontColor(Color.White) .textAlign(TextAlign.Start) } .width('100%') .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) } }) .layoutWeight(1) .margin({ right: 3 }) Text(this.getLyricTextWeightDisplayValue(isPip)) .fontSize(13) .fontColor(Color.White) .textAlign(TextAlign.Start) } .width('100%') .margin({ left: 20, right: 15, top: 5, bottom: 5 }) Row() { this.pushLyricButton($r('app.media.cut_current'), 0, '本地歌词') this.pushLyricButton($r('app.media.share2'), 2, '分享歌词') this.pushLyricButton($r('app.media.white_search'), 1, '获取歌词') // this.pushLyricButton($r('app.media.white_search'), 1, '繁体转简体') } .justifyContent(FlexAlign.Center) .margin({ top: 3, bottom: 10 }) } .width('88%') } private getLyricSizeDisplayValue(isPip: boolean): string { const lyricSize = isPip ? this.currentLyricSizePip : this.currentLyricSize; return lyricSize.toFixed(1); } private getHighLyricSizeDisplayValue(isPip: boolean): string { const highLyricSize = isPip ? this.currentHightLyricSizePip : this.currentHightLyricSize; return highLyricSize.toFixed(1); } private getLyricLineSpaceDisplayValue(isPip: boolean): string { const lyricLineSpace = isPip ? this.currentLyricLineSpacePip : this.currentLyricLineSpace; return Math.round(lyricLineSpace).toString(); } private getLyricTextWeightDisplayValue(isPip: boolean): string { const lyricTextWeightValue = isPip ? this.lyricTextWeightPip : this.lyricTextWeight; return Math.round(lyricTextWeightValue).toString(); } 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 this.saveLyricTimeOffset() 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 // 保存歌词时间偏移 this.saveLyricTimeOffset() } // 按钮禁用逻辑 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)} 秒` } /** * 获取当前歌曲的唯一标识key * 使用歌曲的filePath作为唯一标识 */ private getLyricOffsetKey(): string { if (this.currentSong && StrUtil.isNotEmpty(this.currentSong.filePath)) { // 使用MD5对filePath进行加密,作为存储key return 'lyric_offset_' + MD5.digestSync(this.currentSong.filePath) } return '' } /** * 保存当前歌曲的歌词时间偏移 */ private saveLyricTimeOffset(): void { try { const key = this.getLyricOffsetKey() if (StrUtil.isEmpty(key)) { console.warn('heanup saveLyricTimeOffset: key is empty') return } PreferencesUtil.putSync(key, this.timeOffset) console.info('heanup saveLyricTimeOffset: key=' + key + ', offset=' + this.timeOffset) } catch (e) { console.error('heanup saveLyricTimeOffset error: ' + JSON.stringify(e)) } } /** * 加载当前歌曲的歌词时间偏移 */ private loadLyricTimeOffset(): void { try { const key = this.getLyricOffsetKey() if (StrUtil.isEmpty(key)) { console.warn('heanup loadLyricTimeOffset: key is empty') return } const savedOffset = PreferencesUtil.getNumberSync(key, 0) if (savedOffset !== 0) { this.timeOffset = savedOffset console.info('heanup loadLyricTimeOffset: key=' + key + ', offset=' + savedOffset) }else{ this.timeOffset = 0 } } catch (e) { console.error('heanup loadLyricTimeOffset error: ' + JSON.stringify(e)) } } // 导入 本地歌词 搜索歌词 @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: //判断有没有设置api 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; case 2: this.isLyricSetting = false this.openLyricCopySheet() break; } }) .padding(5) .layoutWeight(1) .backgroundColor(Color.Transparent) .height(40) .margin({ left: 4, right: 4 }) .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.isShowEdit = false this.longItemFilePath = '' // if(this.isGridMusic&&!this.isShowPlay){ // this.longItemFilePath== '' // }else{ // this.isShowEdit = false // } this.isShowMoreView = false; this.isShowPlay = false this.mType = 4 // 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: '99%', 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) } 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: '99%', dragBar: true, preferType: SheetType.CENTER, 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 { if(more.id >= 22){ SymbolGlyph(more.image) .fontSize(24) .fontColor([Color.White]) .alignSelf(ItemAlign.Center) .margin({ left: 10, right: 10,bottom:5}) // .padding({bottom:4}) .onClick(() => { this.doMore(more.id) }) }else{ Image(more.image) .width(24) .height(24) .fillColor(Color.White) .margin({ left: 10, right: 10 }) .onClick(() => { this.doMore(more.id) }) } Text(more.id == 17 ? (this.isLandscape ? '竖屏模式' : '横屏模式') : more.title) .fontSize(16) .margin({top:more.id >= 22?3:0}) .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; } this.playSpeed = speed this.mIjkMediaPlayer.setSpeed(this.playSpeed + 'f'); }) 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) => { this.volume = value; this.applyCurrentVolume(); PreferencesUtil.putSync('DefalutVolume',this.volume) }) .layoutWeight(1) Slider({ value: this.volumeBoostPercent, min: 0, max: this.volumeBoostMaxPercent, step: 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 === 28 ? Visibility.Visible : Visibility.None) .onChange((value: number) => { this.volumeBoostPercent = Math.round(value); this.applyCurrentVolume(); PreferencesUtil.putSync(this.volumeBoostPreferenceKey, this.volumeBoostPercent); }) .layoutWeight(1) Text(`${Math.round(this.volumeBoostPercent)}%`) .fontSize(13) .fontColor(Color.White) .margin({ left: 8, right: 8 }) .visibility(more.id === 28 ? Visibility.Visible : Visibility.None) Text(`${Math.round(this.volume*100)}%`) .fontSize(13) .fontColor(Color.White) .margin({ left: 8, right: 8 }) .visibility(more.id === 9 ? Visibility.Visible : Visibility.None) 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); Toggle({ type: ToggleType.Switch, isOn: this.isShowSpectrum }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 25 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 25) { this.isShowSpectrum = checked; PreferencesUtil.put("isShowSpectrum", this.isShowSpectrum) } }) .width(48) .height(24); // 频谱特效类型选择器 Select([ { value: '柱状' }, { value: '波形' }, { value: '水波纹' }, { value: '圆形' }, { value: '粒子' }, { value: '脉冲' }, { value: '镜像柱' }, { value: '影子柱' }, { value: '轨道线' }, { value: '点阵' }, { value: '星爆' }, { value: '心电图' }, { value: '特效' } ]) .font({ size: 14, weight: FontWeight.Medium }) .fontColor(Color.White) .margin({ left: 25 }) .margin({top:more.id >= 22?3:0}) .visibility(more.id === 26 ? Visibility.Visible : Visibility.None) .selected(this.spectrumModeIndex) .value(['柱状', '波形', '水波纹', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆', '心电图', '特效'][this.spectrumModeIndex]) .onSelect((index: number, _text?: string) => { this.spectrumModeIndex = index; // 使用同步方法保存,确保立即生效 PreferencesUtil.putSync('spectrumModeIndex', this.spectrumModeIndex); AppStorage.setOrCreate('spectrumMode', this.spectrumModeIndex); const modeNames = ['柱状', '波形', '水波纹', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆', '心电图', '特效']; Logger.info('heanup-LocalMusic', '频谱特效已切换为: ' + modeNames[index]); }); Toggle({ type: ToggleType.Switch, isOn: this.showSimple }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 22 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 22) { this.showSimple = checked; PreferencesUtil.put('showSimple', this.showSimple) } }) .width(48) .height(24); Toggle({ type: ToggleType.Switch, isOn: this.enablePointLight }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 23 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 23) { this.enablePointLight = checked; PreferencesUtil.put('enablePointLight', this.enablePointLight) AppStorage.setOrCreate('EnablePointLight', this.enablePointLight); } }) .width(48) .height(24); Toggle({ type: ToggleType.Switch, isOn: this.isGeKongPlayNext }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 27 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 选择开关状态变化时触发事件 if (more.id == 27) { this.isGeKongPlayNext = checked; PreferencesUtil.put('isGeKongPlayNext', this.isGeKongPlayNext) } }) .width(48) .height(24); Toggle({ type: ToggleType.Switch, isOn: this.isShowBgLiuGUANG }) .selectedColor(this.themeColor) .switchPointColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 }) .margin({ right: 15 }) .visibility(more.id == 21 ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => { // 是否开启背景流光效果 if (more.id == 21) { this.isShowBgLiuGUANG = checked; PreferencesUtil.putSync('isShowBgLiuGUANG',this.isShowBgLiuGUANG) } }) .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: 30, image: $r('sys.symbol.lyrics_square'), title: '歌词' }, { id: 24, image: $r('sys.symbol.slider_vertical_3'), 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:20,image: $r("app.media.a_b"), title: 'AB循环' }, { 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: 28, image: $r('sys.symbol.speaker_wave_3'), title: '音量增强' }, { id: 25, image: $r('sys.symbol.ranking'), title: '音乐频谱' }, { id: 26, image: $r('sys.symbol.eight_diagram'), title: '频谱类型' }, { id: 6, image: $r('app.media.memory'), title: '记忆播放' }, { id: 13, image: $r('app.media.rectangle'), title: '封面方形' }, { id : 22, image: $r('sys.symbol.satellite_map'), title: '沉浸式' }, { id : 23, image: $r('sys.symbol.lightbulb'), title: '点光源效果' }, { id : 21, image: $r('app.media.kp_music'), title: '背景双边流光' }, { id : 27, image: $r('sys.symbol.hand'), title: '隔空手势切歌' }, { id: 8, image: $r('app.media.cut_current'), title: '保存播放模式' }, { id: 7, image: $r('app.media.playlist2'), title: '播放背景随音乐封面' }, ]; @State isShowBgLiuGUANG: boolean = false; @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 @State isShowSpectrum: boolean = false//是否显示频谱 @State spectrumModeIndex: number = 0 //选中的频谱特效索引 private getEqualizerVolumeBoostRisk(): number { const equalizerManager = EqualizerManager.getInstance(); if (!equalizerManager.enabled) { return 0; } const gainStats = equalizerManager.getCurrentGainStats(); if (gainStats.maxPositiveGain <= 0 || gainStats.positiveGainSum <= 0) { return 0; } const peakRisk = (gainStats.maxPositiveGain / 12) * 0.65; const spreadRisk = (gainStats.positiveGainSum / 24) * 0.35; return Math.max(0, Math.min(peakRisk + spreadRisk, 1)); } private getSafeBoostPercent(): number { const rawBoostPercent = Math.max(0, Math.min(this.volumeBoostPercent, this.volumeBoostMaxPercent)); let compressedBoostPercent = rawBoostPercent; if (rawBoostPercent <= 150) { compressedBoostPercent = rawBoostPercent; } else if (rawBoostPercent <= 300) { compressedBoostPercent = 150 + (rawBoostPercent - 150) * 0.45; } else { compressedBoostPercent = 217.5 + (rawBoostPercent - 300) * 0.15; } const equalizerRisk = this.getEqualizerVolumeBoostRisk(); if (equalizerRisk <= 0) { return compressedBoostPercent; } const protectedBoostPercent = compressedBoostPercent * (1 - equalizerRisk * 0.45); const cappedBoostPercent = 220 - equalizerRisk * 140; return Math.max(0, Math.min(protectedBoostPercent, cappedBoostPercent)); } private getVolumeBoostFactor(): number { return 1 + this.getSafeBoostPercent() / 100; } private getCurrentOutputVolume(): number { const baseVolume = Math.max(0, Math.min(this.volume, 1)); const boostedVolume = baseVolume * this.getVolumeBoostFactor(); const maxOutputVolume = this.getVolumeBoostFactor(); return Math.max(0, Math.min(boostedVolume, maxOutputVolume)); } private applyCurrentVolume(forceDsfSafeVolume: boolean = false): void { if (!this.mIjkMediaPlayer) { return; } if (forceDsfSafeVolume && this.volumeSmall && this.videoUrl.toLowerCase().endsWith('.dsf')) { this.mIjkMediaPlayer.setVolume('1', '1'); return; } const outputVolume = this.getCurrentOutputVolume(); this.mIjkMediaPlayer.setVolume(outputVolume.toString(), outputVolume.toString()); } 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 30: //歌词 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.initEditFields(this.currentSong) this.tempLyricContent = this.lyricContent if (StrUtil.isNotEmpty(this.tempLyricContent)) { this.lyricConStr = this.tempLyricContent } 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; case 20://AB循环 this.isABSheet = !this.isABSheet break; case 24://均衡器 this.isEqualizerSheet = !this.isEqualizerSheet break; } } //AB循环 @State isOpenAB:boolean = false @State jumpATime:number = 0 @State jumpBTime:number = 0 @State isABSheet:boolean = false @State isEqualizerSheet: boolean = false @State eqApplying: boolean = false @State eqAppliedAt: number = 0 @State eqCurrentPresetName: string = '正常' @State eqCurrentEnabled: boolean = false @Builder ABLoopSheet() { ABLoopComptent({ jumpATime:this.jumpATime, jumpBTime:this.jumpBTime, onClickAItem: async ()=>{ if(this.mIjkMediaPlayer){ this.jumpATime = await this.mIjkMediaPlayer.getCurrentPosition() } }, onClickBItem: async ()=>{ if(this.mIjkMediaPlayer){ this.jumpBTime = await this.mIjkMediaPlayer.getCurrentPosition() if(this.jumpBTime<=this.jumpATime){ ToastUtil.showToast('设置B点时间不能小于A点时间') this.jumpBTime = 0 } } }, onCancleButtonClick: async ()=>{ this.isOpenAB = false this.isABSheet = false }, onStartButtonClick: async ()=>{ if(this.jumpBTime<=this.jumpATime||this.stringForTime(this.jumpATime)==this.stringForTime(this.jumpBTime)){ ToastUtil.showToast('设置B点时间不能小于或等于A点时间') return } this.isABSheet = false this.startABLoop() }, }) } private syncEqualizerUi(): void { const equalizerManager = EqualizerManager.getInstance(); this.eqCurrentEnabled = equalizerManager.enabled; this.eqCurrentPresetName = equalizerManager.getPresetDisplayName(equalizerManager.currentPresetId); if (this.CONTROL_PlayStatus === PlayStatus.INIT) { this.eqAppliedAt = 0; } } private markEqualizerApplied(): void { const equalizerManager = EqualizerManager.getInstance(); this.eqCurrentEnabled = equalizerManager.enabled; this.eqCurrentPresetName = equalizerManager.getPresetDisplayName(equalizerManager.currentPresetId); this.eqAppliedAt = Date.now(); this.eqApplying = false; } private applyEqualizerToCurrentPlayback(): void { if (this.eqApplying) { return; } if (this.castControllerWrapper && this.isCastPlaying) { ToastUtil.showToast('投播模式暂不支持实时均衡器'); return; } if (!this.currentSong || !this.mIjkMediaPlayer) { ToastUtil.showToast('暂无可应用的播放'); return; } this.eqApplying = true; try { const equalizerManager = EqualizerManager.getInstance(); equalizerManager.applyToPlayer(this.mIjkMediaPlayer); this.applyCurrentVolume(); this.markEqualizerApplied(); ToastUtil.showToast('均衡器已实时生效'); } catch (error) { this.eqApplying = false; Logger.warn(TAG, `heanup 应用均衡器失败: ${(error as Error).message}`); } } @Builder private EqualizerApplyStatusBar(): void { Row() { Column() { Text(`均衡器:${this.eqCurrentEnabled ? '开' : '关'} · ${this.eqCurrentPresetName}`) .fontSize(12) .fontColor($r('app.color.text_color')) Text(this.eqAppliedAt > 0 ? '当前播放:已生效' : '当前播放:未应用') .fontSize(10) .fontColor(this.eqAppliedAt > 0 ? this.themeColor : Color.Gray) } .alignItems(HorizontalAlign.Start) Blank() Button('重新应用') .fontSize(12) .fontColor(Color.White) .backgroundColor('#666666') .height(28) .padding({ left: 14, right: 14 }) .borderRadius(14) .enabled(!this.eqApplying && this.CONTROL_PlayStatus !== PlayStatus.INIT) .onClick(() => { this.applyEqualizerToCurrentPlayback(); }) } .width('100%') .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor($r('app.color.settings_background_main')) .borderRadius(12) } @Builder EqualizerSheet() { Column() { Column() { EqualizerViewWithCallback({ onEqualizerChanged: () => { this.syncEqualizerUi(); if (this.mIjkMediaPlayer) { const equalizerManager = EqualizerManager.getInstance(); equalizerManager.applyToPlayer(this.mIjkMediaPlayer); this.applyCurrentVolume(); this.markEqualizerApplied(); } } }) } .margin({ top: 8 }) Column() { this.EqualizerApplyStatusBar() } .margin({ top: 8 }) } .padding({ left: 12, right: 12, bottom: 20 }) .width('100%') } //开始AB循环 startABLoop(){ if(this.mIjkMediaPlayer){ this.seekTo(this.jumpATime+'') this.isOpenAB = true } } /** * 切换横竖屏 * */ setOrientation(orientation: number) { 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 { console.info('onecold startPlayOrResumePlay 9869') this.startPlayOrResumePlay(); } break; case 'nextVideo': // 切换到下一个视频 this.playbackController.playNext().catch(() => { Logger.error('heanup onActionEvent playNext', `播放下一首失败`); }); break; case 'previousVideo': // 切换到上一个视频 this.playbackController.playPrevious().catch(() => { Logger.error('heanup onActionEvent playPrevious', `播放上一首失败`); }); break; case 'previousVideo': // 切换到上一个视频 this.playbackController.playPrevious().catch(() => { Logger.error('heanup onActionEvent playPrevious', `播放上一首失败:`); }); break; default: } this.buttonAction = event + `-status:${status}`; Logger.info(`[${TAG}] onActionEvent: ${this.buttonAction} status:${status}}`); } /** * 画中画功能(悬浮歌词功能)结束 * */ async doSearchCover(item: VideoItem,title?:string,artist?:string): Promise { if (PreferencesUtil.getStringSync('COVER_API', '') === '') { this.isShowMoreView = false; this.showTipsDialog(); return ''; // Return empty string when no API is configured } if (!item) { return ''; // Return empty string if item is undefined } const titleS = title||item?.name || ''; const artistS = artist||item?.artist || ''; // Use empty string if artist is missing return this.searchCover(item, titleS, artistS, true); // Await is not needed here as we return the promise directly } /** *api搜索封面 *item,根据title和artist */ searchCover(item: VideoItem, title: string, artist: string, markAsCustomCover: boolean = false): Promise { return 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) { if (this.currentSong) { mergeCoverWithPriority(this.currentSong, res, markAsCustomCover ? 1 : 0); this.cover = this.currentSong.pixelMapPath || res; } } if (isRemoteCloudType(item.type)) { mergeCoverWithPriority(item, res, markAsCustomCover ? 1 : 0); this.syncRemoteManagerSong(item); try { await this.persistRemoteMetadataToDb(item); this.emitRemoteMetadataUpdated(item); } catch (error) { Logger.error(TAG, `searchCover 持久化网盘封面失败: ${(error as Error).message}`); } } else { // Update pixel map path but always return res regardless of success if (markAsCustomCover) { item.isCustomCover = 1; this.table.updateCustomCoverPath(item.filePath, res, (success: boolean, error?: string) => { if (success) { this.doUpdateData(); console.log("onecold 更新音乐封面成功,数据库已同步"); } else { console.error("onecold 更新音乐封面数据库失败原因: " + error); } }); } else { 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); } return res; // Always return res regardless of updatePixelMapPath result }); } @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) { console.log('The picker starts showing.'); } else if (state == AVCastPickerState.STATE_DISAPPEARING) { console.log('The picker finishes presenting.'); } } 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,false,false,true) } } 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() } } //startOffset 从cue分轨时间开始播 private startPlayOrResumePlay(startOffset?:number) { this.isHostSnapshotDisplayOnly = false let finalStartOffset = startOffset if ((finalStartOffset === undefined || finalStartOffset === null) && this.currentSong && isCueSplitItem(this.currentSong)) { finalStartOffset = 0 } //新版本ijkplayer可以直接播放dsf歌,不需要转格式了。所以注释掉代码 //针对dsf文件高采样率,统一转wav播放 // const sampleRate = this.currentSong?.sampleRate||0 // if(this.videoUrl.toLowerCase().endsWith('.dsf')&&sampleRate>=441000){ // console.info('onecold 采样率 ='+sampleRate) // let newDsfPath = this.context.filesDir + '/'+FileUtil.getFileName(this.videoUrl)+'.wav' // console.info('onecold newDsfPath ='+newDsfPath) // if(!FileUtil.accessSync(newDsfPath)){ // const task = new taskpool.Task(convertDsfToWav,this.videoUrl,newDsfPath); // taskpool.execute(task, taskpool.Priority.HIGH).then((data)=>{ // this.videoUrl = newDsfPath // this.startPlay(startOffset) // }).catch((e:object)=>{ // console.info("task1 catch e: " + e); // }) // }else{ // this.videoUrl = newDsfPath // this.startPlay(startOffset) // } // // }else{ // this.startPlay(startOffset) // } this.startPlay(finalStartOffset) // 仅当前播放的是网盘歌曲时,才提前缓存下一首 // 排除用户拖动进度条的情况 if (this.currentSong && isRemoteCloudType(this.currentSong.type) && !this.isSeekTo) { void this.preloadNextSongInternal(); } } startPlay(startOffset?:number) { this.startPipLyric() this.mDestroyPage = false; this.animationState = AnimationStatus.Running LogUtils.getInstance().LOGI("onecold startPlayOrResumePlay start this.CONTROL_PlayStatus:" + this.CONTROL_PlayStatus) if (this.CONTROL_PlayStatus == PlayStatus.INIT) { this.stopProgressTask(); this.startProgressTask(); this.clearInitialSeekGuardTimer(); this.activePlaybackMemoryKey = this.getPlaybackMemoryKey(); this.remoteRestoreSeekToken++; const shouldMemoryPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY, false) || (this.isMemoryLastPlay && this.isFirstStartPlay); this.pendingMemorySeekPosition = shouldMemoryPlay ? PreferencesUtil.getNumberSync(this.getPlaybackMemoryKey(), 0) : 0; const isRemoteStreamSong = this.currentSong && isRemoteCloudType(this.currentSong.type) && this.videoUrl.includes('://'); this.waitInitialSeekBeforeStart = !!isRemoteStreamSong && this.pendingMemorySeekPosition > 0; // 网盘歌曲起播阶段统一显示加载圈,prepared/首帧后再收起 this.isPlayerLoading = !!isRemoteStreamSong; if (this.currentSong && isRemoteCloudType(this.currentSong.type)) { Logger.info( TAG, `[remote-play] startPlay type=${this.currentSong.type}, memory=${this.pendingMemorySeekPosition}, ` + `waitInitial=${this.waitInitialSeekBeforeStart}, url=${this.sanitizePlaybackUrl(this.videoUrl)}` ); } this.play(this.videoUrl.toString(),startOffset); } if (this.CONTROL_PlayStatus == PlayStatus.PAUSE) { this.mIjkMediaPlayer.start(); this.setProgress() this.CONTROL_PlayStatus = PlayStatus.PLAY } this.setIsPlaying(true) this.updateSessionPlayState(true) this.watchStatus(); this.updateLastPlayTimeStr(this.videoUrl) } updateLastPlayTimeStr(filePath: string) { const activeSong = this.currentSong; let targetFilePath = this.currentSong?.filePath || filePath; if (activeSong && isRemoteCloudType(activeSong.type)) { const remoteStoragePath = this.resolveRemoteStoragePath(activeSong); if (remoteStoragePath) { targetFilePath = remoteStoragePath; } Logger.info(`heanup 检测到网络音频播放,改为更新远程歌曲最近播放: ${targetFilePath}`) } if (!targetFilePath) { Logger.warn(TAG, `更新最近播放时间失败,目标路径为空: ${filePath}`); return; } let lastPlayTime = DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss') const applyHistoryUpdate = (): void => { this.table.updateLastPlayedStrByFilePath(targetFilePath, lastPlayTime, (success: boolean, error?: string) => { if (success) { if (activeSong && this.currentSong?.filePath === activeSong.filePath) { this.currentSong.lastPlayedStr = lastPlayTime this.currentSong.playCount = (this.currentSong.playCount || 0) + 1 } this.getHistoryList(false) console.log(" onecold 更新最近播放时间成功,数据库已同步"); return; } if (activeSong && isRemoteCloudType(activeSong.type)) { Logger.warn(TAG, `远程歌曲最近播放更新失败,准备补入库后重试: ${targetFilePath}, error=${error}`); this.persistRemoteMetadataToDb(activeSong) .then(() => { this.table.updateLastPlayedStrByFilePath(targetFilePath, lastPlayTime, (retrySuccess: boolean, retryError?: string) => { if (retrySuccess) { if (activeSong && this.currentSong?.filePath === activeSong.filePath) { this.currentSong.lastPlayedStr = lastPlayTime this.currentSong.playCount = (this.currentSong.playCount || 0) + 1 } this.getHistoryList(false) Logger.info(TAG, `远程歌曲最近播放补入库后更新成功: ${targetFilePath}`); } else { Logger.error(TAG, `远程歌曲最近播放补入库后仍更新失败: ${targetFilePath}, error=${retryError}`); } }); }) .catch((persistError: Error) => { Logger.error(TAG, `远程歌曲最近播放补入库失败: ${targetFilePath}, error=${persistError.message}`); }); return; } console.error(" onecold 更新最近播放时间数据库失败原因: " + error); }); }; applyHistoryUpdate(); } private completionNum(num: number): string | number { if (num < 10) { return '0' + num; } else { return num; } } private parseDurationToMs(duration?: string): number { if (!duration) { return 0; } const parts = duration.split(':').map((item) => Number(item)); if (parts.some((item) => Number.isNaN(item))) { return 0; } if (parts.length === 2) { return Math.floor((parts[0] * 60 + parts[1]) * 1000); } if (parts.length === 3) { return Math.floor((parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000); } return 0; } private sanitizePlaybackUrl(url?: string): string { if (!url) { return ''; } let sanitized = url.replace(/(access_token=)[^&]+/ig, '$1***'); sanitized = sanitized.replace(/(refresh_token=)[^&]+/ig, '$1***'); sanitized = sanitized.replace(/(Authorization=)[^&]+/ig, '$1***'); if (sanitized.length > 220) { return `${sanitized.substring(0, 220)}...`; } return sanitized; } private sanitizePlaylistDebugPath(path?: string): string { if (!path) { return ''; } return path.length > 48 ? `...${path.substring(path.length - 48)}` : path; } private buildSongDebugItem(item?: VideoItem): string { if (!item) { return 'unknown'; } const title = item.name || item.fileName || '未知歌曲'; return `${title}|type=${item.type}|acc=${item.webdav_account_id || ''}|path=${this.sanitizePlaylistDebugPath(item.filePath)}`; } private buildSongListDebugLog(items: VideoItem[], limit: number = 5): string { if (!items || items.length === 0) { return '[]'; } const parts: string[] = []; const maxCount = Math.min(limit, items.length); for (let i = 0; i < maxCount; i++) { parts.push(this.buildSongDebugItem(items[i])); } return `[${parts.join('; ')}](${items.length})`; } private buildPathListDebugLog(paths: string[], limit: number = 5): string { if (!paths || paths.length === 0) { return '[]'; } const parts: string[] = []; const maxCount = Math.min(limit, paths.length); for (let i = 0; i < maxCount; i++) { parts.push(this.sanitizePlaylistDebugPath(paths[i])); } return `[${parts.join('; ')}](${paths.length})`; } private async dispatchPlaylistPlayRequest(playlistData: PlaylistPlayRequest): Promise { clearPendingPlaylistPlay(); Logger.info( TAG, `[remote-debug] dispatchPlaylistPlayRequest id=${playlistData.playlistId}, name=${playlistData.playlistName}, ` + `songCount=${playlistData.songCount}, startIndex=${playlistData.startIndex}, playType=${playlistData.playType ?? -1}, ` + `paths=${this.buildPathListDebugLog(playlistData.songFilePaths)}` ); if (playlistData.playType !== undefined) { this.applyRequestedPlayType(playlistData.playType); } await this.handlePlaylistPlayRequest( playlistData.playlistId, playlistData.playlistName, playlistData.songFilePaths, playlistData.startIndex, playlistData.isJump ?? false, playlistData.songCount ); } private async resolvePlaybackUrlForCurrentSong(source: string): Promise { const song = this.currentSong; if (!song) { Logger.warn(TAG, `[play-url] skip source=${source}, currentSong is null`); return null; } const expectedFilePath = song.filePath || ''; const requestSerial = ++this.playbackUrlRequestSerial; Logger.info( TAG, `[play-url] begin serial=${requestSerial}, source=${source}, song=${song.name}, path=${expectedFilePath}` ); const resolvedUrl = await setVideoUrlForSong(song, { context: this.context, autoParseMusicName: this.autoParseMusicName }); const currentFilePath = this.currentSong?.filePath || ''; if (requestSerial !== this.playbackUrlRequestSerial || currentFilePath !== expectedFilePath) { Logger.warn( TAG, `[play-url] stale serial=${requestSerial}, source=${source}, expected=${expectedFilePath}, ` + `current=${currentFilePath}, drop=${this.sanitizePlaybackUrl(resolvedUrl)}` ); return null; } Logger.info( TAG, `[play-url] apply serial=${requestSerial}, source=${source}, song=${song.name}, ` + `url=${this.sanitizePlaybackUrl(resolvedUrl)}` ); return resolvedUrl; } private beginSeekTrace(source: string, requestMs: number): void { this.lastSeekSource = source; this.lastSeekRequestMs = requestMs; this.seekTraceUntilTs = Date.now() + this.seekTraceLogWindowMs; this.lastSeekTraceLogTs = 0; } private isSeekTraceActive(): boolean { return this.seekTraceUntilTs > 0 && Date.now() <= this.seekTraceUntilTs; } private logSeekTrace(stage: string, position?: number, playerDuration?: number, force: boolean = false): void { if (!force && !this.isSeekTraceActive()) { return; } const now = Date.now(); if (!force && now - this.lastSeekTraceLogTs < this.seekTraceLogIntervalMs) { return; } this.lastSeekTraceLogTs = now; const currentPosition = typeof position === 'number' ? position : this.mIjkMediaPlayer.getCurrentPosition(); const resolvedPlayerDuration = typeof playerDuration === 'number' ? playerDuration : this.mIjkMediaPlayer.getDuration(); Logger.info( TAG, `[seek-trace] stage=${stage}, source=${this.lastSeekSource || 'unknown'}, req=${this.lastSeekRequestMs}, ` + `current=${currentPosition}, playerDuration=${resolvedPlayerDuration}, uiDuration=${this.duration}, ` + `activeDuration=${this.getActiveDuration()}, isPlaying=${this.mIjkMediaPlayer.isPlaying()}, ` + `isSeekTo=${this.isSeekTo}, waitInitial=${this.waitInitialSeekBeforeStart}, ` + `songType=${this.currentSong?.type ?? -1}, url=${this.sanitizePlaybackUrl(this.videoUrl)}` ); } private shouldShowSeekBlockedToast(source: string): boolean { return source === 'top-progress-slider' || source === 'landscape-progress-slider' || source === 'fast-forward-button' || source === 'rewind-button' || source === 'session-fast-forward' || source === 'session-rewind' || source === 'session-seek'; } private handleBlockedSeek(seekPos: number, source: string): boolean { const decision = resolveSeekGuardDecision(this.videoUrl, seekPos); if (decision.allow) { return false; } Logger.warn( TAG, `[seek] blocked source=${source}, target=${seekPos}, reason=${decision.reason}, ` + `url=${this.sanitizePlaybackUrl(this.videoUrl)}` ); if (this.shouldShowSeekBlockedToast(source) && StrUtil.isNotEmpty(decision.toastMessage)) { ToastUtil.showToast(decision.toastMessage); } if (this.waitInitialSeekBeforeStart) { this.finishInitialSeekWait(`blocked-${decision.reason}`, true); } else { this.hideLoadIng(); } this.isSeekTo = false; this.logSeekTrace(`seek-blocked-${decision.reason}`, undefined, undefined, true); return true; } private stringForTime(timeMs: number): string { let totalSeconds: number | string = (timeMs / 1000); let newSeconds: number | string = totalSeconds % 60; let minutes: number | string = (totalSeconds / 60) % 60; let hours: number | string = totalSeconds / 3600; //LogUtils.getInstance().LOGI("stringForTime hours:" + hours + ",minutes:" + minutes + ",seconds:" + newSeconds); hours = this.completionNum(Math.floor(Math.floor(hours * 100) / 100)); minutes = this.completionNum(Math.floor(Math.floor(minutes * 100) / 100)); newSeconds = Math.floor(Math.floor(newSeconds * 100) / 100) if (this.isCurrentTime) { if (this.oldSeconds < newSeconds || newSeconds === 0 || this.isSeekTo) { this.oldSeconds = newSeconds } else { newSeconds = this.oldSeconds } } newSeconds = this.completionNum(newSeconds); if (hours > 0) { return hours + ":" + minutes + ":" + newSeconds; } else { return minutes + ":" + newSeconds; } } private getPlayerSwiperDisplayCount(): number { return new BreakpointType({ sm: 1, md: 2, lg: 2, xl: 2, xxl: 2 }).getValue(this.currentBreakpoint) ?? 1 } private shouldUpdateMainLyricPanel(): boolean { return this.getPlayerSwiperDisplayCount() > 1 || this.currentSwiperIndex === 1 } private getActivePlaybackPositionMs(): number { if (this.castControllerWrapper && this.isCastPlaying) { return this.currentTime2 * 1000 } return this.mIjkMediaPlayer.getCurrentPosition() } private notifyPlaybackCardStateChanged(): void { try { const context = this.getUIContext().getHostContext() as common.UIAbilityContext const options = new MusicCardPlaybackStateOptions() options.currentSong = this.currentSong options.isPlaying = this.CONTROL_PlayStatus === PlayStatus.PLAY options.positionMs = this.getActivePlaybackPositionMs() options.durationMs = this.getActiveDuration() options.lyricText = this.lyricContent options.coverPath = this.currentSong?.pixelMapPath ?? this.cover ?? '' options.coverImageName = '' const snapshot = MusicCardManager.getInstance().buildSnapshotFromPlaybackState(options) MusicCardManager.getInstance().notifyPlaybackStateChanged(context, snapshot) } catch (error) { Logger.warn(TAG, `notifyPlaybackCardStateChanged failed: ${error}`) } } private syncLyricPositionNow(): void { const position = this.getActivePlaybackPositionMs() if (position < 0) { return } this.updateLyricPosition(position + this.timeOffset * 1000) } private updateLyricPosition(lyricPosition: number): void { if (!this.isAppForeground || !this.isPageVisible) { return; } const shouldUpdateMainLyric = this.shouldUpdateMainLyricPanel() const shouldUpdatePipLyric = this.showPipLyric const shouldUpdateSingleLineLyric = this.isShowSingleLineLyric if (!shouldUpdateMainLyric && !shouldUpdatePipLyric && !shouldUpdateSingleLineLyric) { return } if (!Number.isNaN(this.lastLyricRenderPosition) && Math.abs(lyricPosition - this.lastLyricRenderPosition) < this.lyricUpdateMinDeltaMs) { return; } this.lastLyricRenderPosition = lyricPosition; if (shouldUpdateMainLyric) { this.lyricController.updatePosition(lyricPosition); } if (shouldUpdatePipLyric) { this.lyricControllerXF.updatePosition(lyricPosition); } if (shouldUpdateSingleLineLyric) { this.lyricControllerSingle.updatePosition(lyricPosition); } } private setProgress() { if (this.castControllerWrapper && this.isCastPlaying) { return; } const shouldRefreshUi = this.isAppForeground && this.isPageVisible; const rawPosition = this.mIjkMediaPlayer.getCurrentPosition(); const rawDuration = this.mIjkMediaPlayer.getDuration(); const cueTrackItem = this.currentSong && isCueSplitItem(this.currentSong) ? this.currentSong : undefined; const cueTrackStartOffset = cueTrackItem ? getCueTrackStartOffset(cueTrackItem) : 0; const cueTrackEndOffset = cueTrackItem ? getCueTrackEndOffset(cueTrackItem) : 0; const cueTrackDuration = cueTrackItem ? getCueTrackDuration(cueTrackItem) : 0; let position = rawPosition; let duration = rawDuration; if (cueTrackItem) { position = Math.max(0, rawPosition - cueTrackStartOffset); if (cueTrackDuration > 0) { duration = cueTrackDuration; } else if (cueTrackEndOffset > cueTrackStartOffset) { duration = cueTrackEndOffset - cueTrackStartOffset; } else if (rawDuration > cueTrackStartOffset) { duration = rawDuration - cueTrackStartOffset; } if (duration > 0 && position > duration) { position = duration; } } if (duration <= 0 && this.duration > 0) { duration = this.duration; } let pos = 0; if (duration > 0) { this.slideEnable = true; let curPercent = position / duration; pos = curPercent * 100; if (shouldRefreshUi) { if (pos > this.PROGRESS_MAX_VALUE) { this.progressValue = this.PROGRESS_MAX_VALUE } else { this.progressValue = pos; } } } this.logSeekTrace('progress-tick', position, duration); // LogUtils.getInstance() // .LOGI("setProgress position:" + position + ",duration:" + duration + ",progressValue:" + pos); if (shouldRefreshUi) { this.totalTime = this.stringForTime(duration); } if (position > duration) { position = duration; } if (shouldRefreshUi) { const lyricPosition = (cueTrackItem ? rawPosition : position) + this.timeOffset * 1000; this.isCurrentTime = true; this.updateLyricPosition(lyricPosition); // 蓝牙歌词更新逻辑 if (this.bluetoothLyricEnabled && this.isBluetoothConnected && this.lyricContent && this.currentSong) { const currentLyricLine = LyricUtil.getCurrentLyricLine(this.lyricContent, lyricPosition); // 仅在歌词行变化时更新,避免频繁调用 if (currentLyricLine !== this.lastBluetoothLyricLine) { this.lastBluetoothLyricLine = currentLyricLine; this.avSessionController.updateBluetoothLyric( currentLyricLine, this.currentSong.name || '', this.currentSong.artist || '', this.bluetoothLyricMode ); } } this.currentTime = this.stringForTime(position); this.isCurrentTime = false } if (this.mIjkMediaPlayer.isPlaying()) { const isCueTrackCompleted = cueTrackItem && cueTrackEndOffset > cueTrackStartOffset && rawPosition >= cueTrackEndOffset; //如果是开启AB循环 if(this.isOpenAB){ if(position >=this.jumpBTime){ this.seekTo(this.jumpATime+'') return } } // 判断是否播放到结束时间 if ((this.isOpenJump && duration > this.jumpEndTime * 1000 && position >= duration - this.jumpEndTime * 1000) || isCueTrackCompleted) { const completionAction = MusicPlaybackController.resolveCompletionAction(this.playType, this.curIndex, this.songList.length) if(completionAction.action === 'replay_current'){//跳过尾部如果是单曲循环就选择单曲循环 this.CONTROL_PlayStatus = PlayStatus.INIT this.startPlayOrResumePlay(); }else if (completionAction.action === 'stop_current') { if (completionAction.showBoundaryToast) { Logger.info('heanup setProgress', '连续播放不循环模式:已到达最后一首,停止播放'); } this.stopPlaybackAtTrackEnd(duration, completionAction.showBoundaryToast) }else{ this.playbackController.playNext().catch(() => { Logger.error('heanup setProgress playNext', `播放下一首失败`); }); } } } try { this.playbackStateBridge.updatePlaybackState( this.CONTROL_PlayStatus === PlayStatus.PLAY, position, duration ) } catch (error) { // setProgress 频率较高,这里仅做兜底避免影响主流程 } try { MusicCardManager.getInstance().notifyProgressTick( this.getUIContext().getHostContext() as common.UIAbilityContext, this.currentSong, this.CONTROL_PlayStatus === PlayStatus.PLAY, position, duration, this.lyricContent, this.currentSong?.pixelMapPath ?? this.cover ?? '', '' ) } catch (_error) { // 卡片刷新不能影响主播放流程 } } private startProgressTask() { let that = this; if (this.updateProgressTimer) { clearInterval(this.updateProgressTimer); this.updateProgressTimer = 0; } this.updateProgressTimer = setInterval(() => { //LogUtils.getInstance().LOGI("startProgressTask"); if (!that.mDestroyPage) { that.setProgress(); } }, this.progressUpdateIntervalMs); } private stopPlaybackAtTrackEnd(duration: number, showBoundaryToast: boolean = false): void { if (showBoundaryToast) { ToastUtil.showToast('已经是最后一首了') } this.showRePlay() this.currentTime = this.stringForTime(duration) this.progressValue = this.PROGRESS_MAX_VALUE this.slideEnable = false this.stop() } private dispatchSeekFromPercent(percent: number, source: string): void { if (this.isSeekTo) { return } this.isSeekTo = true this.mDestroyPage = false if (this.currentSong && isRemoteCloudType(this.currentSong.type)) { this.showLoadIng() } const seekValue = MusicPlaybackController.resolveSeekValueFromPercent(percent, this.getActiveDuration()) void this.playbackController.seekTo(seekValue + '', source) } private dispatchSeekFromEditorSlider(percent: number, expectedFilePath: string): void { if (this.videoUrl != expectedFilePath) { this.videoUrl == expectedFilePath return } if (this.isSeekTo) { return } this.isSeekTo = true this.mDestroyPage = false this.showLoadIng() const seekValue = MusicPlaybackController.resolveSeekValueFromPercent(percent, this.getActiveDuration()) void this.playbackController.seekTo(seekValue + '', 'editor-slider') } private dispatchSeekToPosition(position: number, source: string): void { if (this.isSeekTo) { return } this.isSeekTo = true this.mDestroyPage = false if (this.currentSong && isRemoteCloudType(this.currentSong.type)) { this.showLoadIng() } void this.playbackController.seekTo(position + '', source) } private dispatchTogglePlayForItem(item: VideoItem, allowAnyCurrentPlaying: boolean = false): void { const toggleAction = MusicPlaybackController.resolveItemPlaybackToggleAction( this.videoUrl, item.filePath, this.isPlaying, allowAnyCurrentPlaying ) if (toggleAction === 'toggle_current') { this.playbackController.playOrPause() return } this.videoUrl == item.filePath this.doPlay(item, 0, false, true) } private applyCurrentSongMetadata(song?: VideoItem): void { if (!song) { return } this.name = song.name this.artist = song.artist this.notifyPlaybackCardStateChanged() } private applySelectedQueueSong(index: number): void { this.curIndex = index this.currentSong = this.songList[this.curIndex] this.applyCurrentSongMetadata(this.currentSong) this.playbackStateBridge.replaceQueue(this.songList, this.curIndex) } private ensureSongQueued(song: VideoItem): number { const queueFilePaths = this.songList.map((item: VideoItem): string => item.filePath) const queueResolution = MusicPlaybackController.resolveQueueSongIndex(queueFilePaths, song.filePath) if (queueResolution.shouldAppend) { this.songList = [...this.songList, song] this.sonDataSource.pushArrayData(this.songList) } return queueResolution.index } private markPreparedSongSwitch(song: VideoItem, addToPlayedIndices: boolean = false): void { const queueIndex = this.ensureSongQueued(song) this.curIndex = queueIndex if (addToPlayedIndices) { this.playedIndices.add(queueIndex) } this.CONTROL_PlayStatus = PlayStatus.INIT this.stop() this.songList[this.curIndex] = song this.currentSong = this.songList[this.curIndex] } private applyCastTrackDurationState(durationMs: number): void { if (durationMs <= 0 || !this.currentSong) { return } this.duration = durationMs this.durationTime = Math.floor(durationMs / 1000) this.durationStringTime = secondToTime(this.durationTime) this.totalTime = this.stringForTime(durationMs) this.avSessionController.setAVMetadataMusic(this.currentSong, durationMs, this.lyricContent) } private finalizeLocalTrackSwitch(loadingShown: boolean): void { this.CONTROL_PlayStatus = PlayStatus.INIT this.stop() if (loadingShown) { this.showLoadIng() } this.changeImageAnimation() } private async prepareCurrentSongPlayback(source: string): Promise { const loadingShown = this.beginRemoteSongSwitchLoading(this.currentSong) const playbackUrl = await this.resolvePlaybackUrlForCurrentSong(source) if (!playbackUrl) { if (loadingShown) { this.hideLoadIng() } return { success: false, loadingShown } } this.videoUrl = playbackUrl return { success: true, loadingShown } } private async startPreparedCurrentSongPlayback(source: string, useSwitchAnimation: boolean = true): Promise { const preparation = await this.prepareCurrentSongPlayback(source) if (!preparation.success) { return false } this.applyCurrentSongMetadata(this.currentSong) if (useSwitchAnimation) { this.changeImageAnimation() } else { this.cover = this.currentSong?.pixelMapPath this.startPlayOrResumePlay() } return true } private async dispatchPreparedQueueSwitch(source: string, castActionName: string, castRunner: (durationMs: number) => Promise, updateCoverAfterCast: boolean = false): Promise { const preparation = await this.prepareCurrentSongPlayback(source) if (!preparation.success) { return } if (this.castControllerWrapper) { Logger.info('heanup queueSwitch', `投播模式切歌: ${castActionName}`) try { const durationMs = this.getSongDurationMs(this.currentSong) this.applyCastTrackDurationState(durationMs) this.updateCastProgress(0) await castRunner(durationMs) this.isCastPlaying = this.castControllerWrapper.getIsCastPlaying() if (preparation.loadingShown) { this.hideLoadIng() } if (updateCoverAfterCast && this.currentSong) { this.cover = this.currentSong.pixelMapPath } return } catch (error) { Logger.error('heanup queueSwitch', `${castActionName}失败: ${error}`) this.isCastPlaying = false } } this.finalizeLocalTrackSwitch(preparation.loadingShown) } private async ensureRemoteSongAccountAvailable(song: VideoItem, source: string): Promise { if (!isRemoteCloudType(song.type) || !song.webdav_account_id) { return true } const manager = RemoteDriveManager.getInstance() const account = await manager.getWebDavAccountById(song.webdav_account_id) if (account) { return true } Logger.warn(TAG, `${source}: 歌曲账号ID无效: ${song.webdav_account_id}, 尝试查找同类型账号`) const allAccounts = manager.getAllWebDavAccounts() let foundAccount: WebDavAccount | null = null for (let i = 0; i < allAccounts.length; i++) { if (allAccounts[i].webType === song.type) { foundAccount = allAccounts[i] break } } if (foundAccount && foundAccount.id) { Logger.info(TAG, `${source}: 找到同类型账号,更新webdav_account_id: ${song.webdav_account_id} -> ${foundAccount.id}`) song.webdav_account_id = foundAccount.id.toString() await this.table.updateWebDavAccountId(song.filePath, foundAccount.id.toString()) return true } Logger.error(TAG, `${source}: 无法找到类型为${song.type}的账号`) return false } private stopProgressTask() { LogUtils.getInstance().LOGI("stopProgressTask"); clearInterval(this.updateProgressTimer); this.updateProgressTimer = 0; } private showLoadIng() { this.isPlayerLoading = true; this.loadingVisible = Visibility.Visible; this.replayVisible = Visibility.None; } private hideLoadIng() { this.isPlayerLoading = false; this.loadingVisible = Visibility.None; this.replayVisible = Visibility.None; } private showRePlay() { this.isPlayerLoading = false; this.loadingVisible = Visibility.None; this.replayVisible = Visibility.Visible; } private beginRemoteSongSwitchLoading(song?: VideoItem): boolean { const shouldShow = !!song && isRemoteCloudType(song.type); if (shouldShow) { this.showLoadIng(); } return shouldShow; } private clearInitialSeekGuardTimer(): void { if (this.initialSeekGuardTimer) { clearTimeout(this.initialSeekGuardTimer); this.initialSeekGuardTimer = 0; } } private ensurePlayerStartedAfterInitialSeek(reason: string): void { try { if (!this.mIjkMediaPlayer.isPlaying()) { this.mIjkMediaPlayer.start(); } this.CONTROL_PlayStatus = PlayStatus.PLAY; this.setIsPlaying(true); this.updateSessionPlayState(true); Logger.info(TAG, `记忆播放恢复开播: ${reason}`); } catch (error) { Logger.error(TAG, `记忆播放恢复开播失败(${reason}): ${(error as Error).message}`); } } private finishInitialSeekWait(reason: string, shouldStart: boolean): void { this.clearInitialSeekGuardTimer(); const wasWaiting = this.waitInitialSeekBeforeStart; this.waitInitialSeekBeforeStart = false; this.pendingMemorySeekPosition = 0; this.isPlayerLoading = false; this.hideLoadIng(); if (wasWaiting) { Logger.info(TAG, `结束记忆seek等待: ${reason}`); } if (shouldStart) { this.ensurePlayerStartedAfterInitialSeek(reason); } } private startInitialSeekGuardTimer(scene: string): void { this.clearInitialSeekGuardTimer(); if (!this.waitInitialSeekBeforeStart || this.pendingMemorySeekPosition <= 0) { return; } const token = this.remoteRestoreSeekToken; this.initialSeekGuardTimer = setTimeout(() => { if (token !== this.remoteRestoreSeekToken || !this.waitInitialSeekBeforeStart) { return; } Logger.warn(TAG, `记忆seek超时兜底开播, scene=${scene}, pos=${this.pendingMemorySeekPosition}`); this.finishInitialSeekWait('seek-timeout', true); }, this.initialSeekGuardTimeoutMs); } private async play(url: string,startOffset?:number) { this.lyricController.setLyric(null) this.lyricControllerSingle.setLyric(null) this.lastLyricRenderPosition = Number.NaN; this.isShowBgLiuGUANG = PreferencesUtil.getBooleanSync('isShowBgLiuGUANG', false) console.info('onecold this.url 1= '+url) if(!url.toLowerCase().startsWith('http')){ let hasPermission = await PermissionUtil.activatePermission(url) console.info('onecold this.hasPermission 1= '+hasPermission) if(!url.includes(this.packName)){ const file = fs.openSync(url) this.videoUrl = file.path url= this.videoUrl } console.info('onecold this.hasPermission 1= '+hasPermission) console.info('onecold this.videoUrl 1= '+this.videoUrl) } let that = this; const isBaiduStreamingPlayback = isBaiduStreamingPlaybackUrl(url); if (this.waitInitialSeekBeforeStart) { that.showLoadIng(); } else { that.hideLoadIng(); } const startOnPreparedOption = this.waitInitialSeekBeforeStart ? "0" : "1"; this.isOpenAB = false //设置XComponent回调的context if (!!this.mContext) { this.mIjkMediaPlayer.setContext(this.mContext, 'xcomponentId'); } // 每次播放前都先 reset,防止快速切换歌曲时崩溃 // 修复 SIGSEGV 崩溃:确保播放器在设置新数据源前完全停止并释放资源 try { this.mIjkMediaPlayer.reset(); Logger.info(TAG, 'IjkPlayer reset 成功'); } catch (error) { Logger.error(TAG, `IjkPlayer reset 失败: ${(error as Error).message}`); } this.CONTROL_PlayStatus = PlayStatus.PLAY; //设置debug模式(用于EQ日志采集) this.mIjkMediaPlayer.setDebug(true); //初始化配置 this.mIjkMediaPlayer.native_setup(); // 初始化配置后需要重新设置音频流音量,否则音量为默认值1.0 针对dsf格式设置最大值1.0 this.applyCurrentVolume(true); // 构建规范的HTTP请求头(统一一次性设置,避免未带认证提前发起连接) const headers = new Map(); headers.set("User-Agent", "TTMusic-WebDAV/1.0"); headers.set("Accept", "*/*"); if (this.currentSong&&StrUtil.isEmpty(this.currentSong.pixelMapPath) &&PreferencesUtil.getStringSync('COVER_API', '') != '') { //如果封面为空,则搜索封面 this.searchCover(this.currentSong, this.currentSong.name, this.currentSong.artist||'') } // 仅对确实需要额外认证头的远程类型补充请求头。 // Navidrome/Plex/AudioStation/DaoLiYu 等类型的播放链接已自带认证参数,这里不要再误走 WebDAV 认证分支。 if (this.currentSong && (isWebDavType(this.currentSong.type) || isJellyfinType(this.currentSong.type) || isEmbyType(this.currentSong.type))) { Logger.info(`heanup 远程歌曲认证头构建 - 歌曲名: ${this.currentSong.name}, webdav_account_id: ${this.currentSong.webdav_account_id || '未设置'}`); if (this.currentSong.webdav_account_id) { try { const webdavManager = RemoteDriveManager.getInstance(); const webDavHeaders = await webdavManager.buildHttpHeadersWithAccountId(this.currentSong, this.videoUrl); if (webDavHeaders && webDavHeaders.size > 0) { webDavHeaders.forEach((value, key) => headers.set(key, value)); Logger.info(`heanup 成功通过webdav_account_id获取并合并远程认证头`); } else { Logger.warn(`heanup 通过webdav_account_id未获取到额外认证头`); } } catch (error) { const err = error as Error; Logger.error(`heanup 获取远程认证头时出错: ${err.message}`); } } else { Logger.error(`heanup 远程歌曲 "${this.currentSong.name}" 缺少webdav_account_id,无法构建认证头`); } } else if (this.currentSong && isSmbType(this.currentSong.type)) { Logger.info(`heanup SMB歌曲通过HTTP代理播放,无需认证头`); } // 统一设置带认证的请求头 // 百度类型也需要特殊请求头 if (this.currentSong && isBaiduType(this.currentSong.type)) { try { const webdavManager = RemoteDriveManager.getInstance(); const baiduHeaders = await webdavManager.buildHttpHeadersWithAccountId(this.currentSong, this.videoUrl); baiduHeaders.forEach((value, key) => headers.set(key, value)); const headerSnapshotList: string[] = []; headers.forEach((value, key) => headerSnapshotList.push(`${key}=${value}`)); Logger.info(TAG, `Baidu播放请求头: ${headerSnapshotList.join('; ')}`); } catch (error) { Logger.error(`heanup 构建百度请求头失败: ${(error as Error).message}`); } } //设置视频源(先准备好认证头,再设置数据源) Logger.info(TAG, `IjkPlayer setDataSource url=${url}`); this.mIjkMediaPlayer.setDataSource(url); this.mIjkMediaPlayer.setDataSourceHeader(headers); if (this.currentSong && isRemoteCloudType(this.currentSong.type) && !isBaiduType(this.currentSong.type)) { console.log(`heanup 为WebDAV播放设置IjkPlayer选项`); // 网络超时设置(单位:微秒,文档显示应该是字符串格式) this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "30000000"); // 30秒 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "30000000"); // 连接超时30秒 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "listen_timeout", "30000000"); // 监听超时30秒 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", "30000000"); // DNS缓存超时30秒 // 缓冲和播放优化设置 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "1024000"); // 增大缓冲区 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "50"); // 减少最小帧数 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", startOnPreparedOption); // 预加载启动 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0"); // 无缓冲播放 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "30000"); // 最大缓存30秒 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1"); // 无限制收流 // 网络相关设置 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http_redirect", "1"); // 启用HTTP重定向 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "user_agent", "TTMusic-WebDAV/1.0"); // 用户代理 // 重连设置 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", "3"); // 重连3次 console.log(`heanup WebDAV IjkPlayer选项设置完成`); } else if ((this.currentSong && isBaiduType(this.currentSong.type)) || isBaiduStreamingPlayback) { console.log(`heanup 为百度网盘播放设置IjkPlayer选项`); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "15000000"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "15000000"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http_redirect", "1"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "1"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "2097152"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "25"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "user_agent", BaiduConstants.USER_AGENT); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", "3"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", "1"); console.log(`heanup 百度网盘 IjkPlayer 选项设置完成`); } // if(PreferencesUtil.getBooleanSync(SettingPage.IS_MIDIACODEC_OPEN,false)){ // this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", "1"); // 启用硬件解码,会导致无法顺序播放 // } this.isMusicBGCover = PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_BG_COVER, true) //使用精确寻帧 例如,拖动播放后,会寻找最近的关键帧进行播放,很有可能关键帧的位置不是拖动后的位置,而是较前的位置.可以设置这个参数来解决问题 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", "1"); let bufferSize = 50 * 1024 ; // 默认缓冲区大小为1MB //预读数据的缓冲区大小.修复大文件播放前几秒挂掉的bug,太大了会导致倍数播放失效 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", bufferSize + ''); //停止预读的最小帧数 // this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "100"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", '1'); ////重连模式,如果中途服务器断开了连接,让它重新连接 // 设置其他缓冲相关选项 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "100"); // 设置最小缓冲帧数 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-frames", "1000"); // 设置最大缓冲 //启动预加载 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", startOnPreparedOption); // 设置无缓冲,这是播放器的缓冲区,有数据就播放 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0"); //跳帧处理,放CPU处理较慢时,进行跳帧处理,保证播放流程,画面和声音同步 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", "5"); // 最大缓冲cache是3s, 有时候网络波动,会突然在短时间内收到好几秒的数据 // 因此需要播放器丢包,才不会累积延时 // 这个和第三个参数packet-buffering无关。 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "3000"); // 无限制收流 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1"); this.mIjkMediaPlayer.setOptionLong(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1") // 屏幕常亮 this.mIjkMediaPlayer.setScreenOnWhilePlaying(true); // 开启hls起播优化 默认是关闭的 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fetch_first", "on"); // 设置超时 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "10000000"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "10000000"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "addrinfo_timeout", "10000000"); this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", "10000000"); // 变速播放 this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", "1"); this.mIjkMediaPlayer.setSpeed(this.playSpeed + 'f'); let Speed = this.mIjkMediaPlayer.getSpeed() LogUtils.getInstance().LOGI('getSpeed--' + Speed) //是否开启循环播放 // this.mIjkMediaPlayer.setLoopCount(this.loop); let mOnVideoSizeChangedListener: OnVideoSizeChangedListener = new ImplOnVideoSizeChangedListener( (width: number, height: number, sar_num: number, sar_den: number) => { if (height === 0) { return; } const va = width / height; const vpa = that.videoParentAspectRatio; if (vpa > va) { that.videoWidth = (width / (height * vpa)) * 100 + '%'; } else { that.videoWidth = '100%'; } if (width && height) { that.videoAspectRatio = width / height; } LogUtils.getInstance() .LOGI("setOnVideoSizeChangedListener-->go:" + width + "," + height + "," + sar_num + "," + sar_den); if (!that.waitInitialSeekBeforeStart) { that.hideLoadIng(); } } ); this.mIjkMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener); let mOnPreparedListener: OnPreparedListener = new ImplOnPreparedListener( async () => { LogUtils.getInstance().LOGI("setOnPreparedListener-->go"); Logger.info( TAG, `[seek] prepared enter, waitInitial=${this.waitInitialSeekBeforeStart}, pendingMemory=${this.pendingMemorySeekPosition}, ` + `songType=${this.currentSong?.type ?? -1}, url=${this.sanitizePlaybackUrl(this.videoUrl)}` ); if (!this.waitInitialSeekBeforeStart) { // 无记忆seek时,prepared后可直接隐藏加载 this.isPlayerLoading = false; } try { //保存碰一碰分享的当前的歌曲 AppStorage.setOrCreate('currentSong',this.currentSong) ; let lyricPath = this.videoUrl.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc' await this.initLyric(lyricPath); const applyDuration = (durationMs: number): void => { const cueTrackDuration = this.currentSong && isCueSplitItem(this.currentSong) ? getCueTrackDuration(this.currentSong) : 0; const effectiveDuration = cueTrackDuration > 0 ? cueTrackDuration : durationMs; if (effectiveDuration <= 0) { return; } this.duration = effectiveDuration; this.durationTime = Math.floor(effectiveDuration / 1000); this.durationStringTime = secondToTime(this.durationTime); this.totalTime = this.stringForTime(effectiveDuration); if (this.currentSong) { this.currentSong.duration = this.totalTime; try { this.avSessionController.setAVMetadataMusic(this.currentSong, effectiveDuration, this.lyricContent); } catch (metaError) { Logger.warn(TAG, `heanup 首次播放写入AVSession失败: ${(metaError as Error).message}`); } } }; const fetchBaiduM3u8Duration = async (url: string): Promise => { if (!url || !isBaiduType(this.currentSong?.type ?? -1)) { return 0; } const httpRequest = http.createHttp(); try { const response = await httpRequest.request(url, { method: http.RequestMethod.GET, connectTimeout: 8000, readTimeout: 8000, expectDataType: http.HttpDataType.STRING, header: { 'User-Agent': BaiduConstants.STREAMING_USER_AGENT, 'Host': 'pan.baidu.com', 'Referer': 'https://pan.baidu.com/disk/home', 'Accept-Encoding': 'identity' } }); if (response.responseCode !== 200 || typeof response.result !== 'string') { return 0; } const text = response.result; if (text.indexOf('#EXTM3U') < 0) { return 0; } const lines = text.split('\n'); let totalSeconds = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#EXTINF:')) { const colonIndex = line.indexOf(':'); if (colonIndex < 0) { continue; } const content = line.substring(colonIndex + 1); const commaIndex = content.indexOf(','); const numberPart = commaIndex >= 0 ? content.substring(0, commaIndex) : content; const seconds = Number(numberPart); if (!Number.isNaN(seconds) && seconds > 0) { totalSeconds += seconds; } } } if (totalSeconds > 0) { return Math.floor(totalSeconds * 1000); } return 0; } catch (fetchError) { Logger.warn(TAG, `heanup 解析百度M3U8时长失败: ${(fetchError as Error).message}`); return 0; } finally { httpRequest.destroy(); } }; const fetchDurationFallback = async (): Promise => { const durationMs = await fetchBaiduM3u8Duration(this.videoUrl); if (durationMs > 0) { Logger.info(TAG, `heanup 通过M3U8推导出时长: ${durationMs}ms`); applyDuration(durationMs); return; } Logger.warn(TAG, 'heanup 首次播放未拿到时长,保持未知'); }; const preparedDuration = this.mIjkMediaPlayer.getDuration(); Logger.info( TAG, `[seek] prepared duration snapshot, playerDuration=${preparedDuration}, metaDuration=${this.currentSong?.duration ?? ''}, ` + `url=${this.sanitizePlaybackUrl(this.videoUrl)}` ); if (preparedDuration > 0) { applyDuration(preparedDuration); if (this.videoUrl.includes('method=streaming') && isBaiduType(this.currentSong?.type ?? -1)) { const durationMs = await fetchBaiduM3u8Duration(this.videoUrl); if (durationMs > 0 && Math.abs(durationMs - preparedDuration) > 1000) { Logger.info(TAG, `heanup 使用M3U8时长覆盖播放器时长: ${preparedDuration}ms -> ${durationMs}ms`); applyDuration(durationMs); } } } else { const metaDurationMs = this.parseDurationToMs(this.currentSong?.duration); if (metaDurationMs > 0) { Logger.info(TAG, `heanup 使用元数据时长: ${metaDurationMs}ms`); applyDuration(metaDurationMs); } else { fetchDurationFallback(); } } setTimeout(() => { this.avSessionController.initAvSession(false); if (this.duration > 0) { this.avSessionController.setAVMetadataMusic(this.songList[this.curIndex], this.duration, this.lyricContent); } this.setAvSessionListener(); this.updateSessionPlayState(true) this.setCurrentPlayMode() const cueTrackStartOffset = this.currentSong && isCueSplitItem(this.currentSong) ? getCueTrackStartOffset(this.currentSong) : 0; if ((startOffset && startOffset > 0) || cueTrackStartOffset > 0) {//cue分轨播放 const cueSeekValue = cueTrackStartOffset > 0 && (!startOffset || startOffset <= 0) ? 0 : startOffset; this.seekTo((cueSeekValue || 0) + ""); this.saveLastPlayList() return } }, 500) //rmvb和flac格式马上记忆播放会报错,会延迟1s在读取记忆播放。 let timeoutPlay = 0 if (StrUtil.isNotEmpty(this.videoUrl) && (this.videoUrl.toLowerCase().endsWith('.rmvb') || this.videoUrl.toLowerCase().endsWith('.flac'))) { timeoutPlay = 400 } // 网盘流地址在prepare后立刻seek容易触发底层不稳定,统一延后记忆恢复 if (this.currentSong && isRemoteCloudType(this.currentSong.type) && this.videoUrl.includes('://')) { timeoutPlay = Math.max(timeoutPlay, this.waitInitialSeekBeforeStart ? this.remoteMemorySeekInitialDelayMs : 700); } //跳过片头片尾关键代码,以下是跳过片头的关键代码 if (this.modeType === 0) { //文件夹 let parentPath = FileUtil.getParentPath(this.videoUrl) this.jumpTopTime = PreferencesUtil.getNumberSync(parentPath + 'jumpTopTime') this.jumpEndTime = PreferencesUtil.getNumberSync(parentPath + 'jumpEndTime') this.isOpenJump = PreferencesUtil.getBooleanSync(parentPath + 'isOpenJump', false) } else if (this.modeType === 1) { //媒体库 this.jumpTopTime = PreferencesUtil.getNumberSync(1 + 'jumpTopTime') this.jumpEndTime = PreferencesUtil.getNumberSync(1 + 'jumpEndTime') this.isOpenJump = PreferencesUtil.getBooleanSync(1 + 'isOpenJump', false) } else if (this.modeType === 2) { //艺术家 if (this.currentSong?.artist) { this.jumpTopTime = PreferencesUtil.getNumberSync(this.currentSong?.artist + 2 + 'jumpTopTime') this.jumpEndTime = PreferencesUtil.getNumberSync(this.currentSong?.artist + 2 + 'jumpEndTime') this.isOpenJump = PreferencesUtil.getBooleanSync(this.currentSong?.artist + 2 + 'isOpenJump', false) } } else if (this.modeType === 3) { //专辑 if (this.currentSong?.album) { this.jumpTopTime = PreferencesUtil.getNumberSync(this.currentSong?.album + 3 + 'jumpTopTime') this.jumpEndTime = PreferencesUtil.getNumberSync(this.currentSong?.album + 3 + 'jumpEndTime') this.isOpenJump = PreferencesUtil.getBooleanSync(this.currentSong?.album + 3 + 'isOpenJump', false) } } const playerDuration = this.mIjkMediaPlayer.getDuration() if (playerDuration > 0 && (!this.currentSong || !isCueSplitItem(this.currentSong))) { this.duration = playerDuration } const memoryPosition = this.pendingMemorySeekPosition > 0 ? this.pendingMemorySeekPosition : PreferencesUtil.getNumberSync(this.getPlaybackMemoryKey(), 0); if (this.waitInitialSeekBeforeStart && memoryPosition > 0) { let targetSeekPosition = memoryPosition; if (this.isOpenJump) { targetSeekPosition = Math.max(targetSeekPosition, this.jumpTopTime * 1000); } this.pendingMemorySeekPosition = targetSeekPosition; this.startInitialSeekGuardTimer('prepared-memory-seek'); Logger.info(TAG, `准备记忆播放seek: ${targetSeekPosition}ms, timeout=${timeoutPlay}ms`); setTimeout(() => { if (!this.waitInitialSeekBeforeStart) { return; } this.restorePlaybackPosition(); }, timeoutPlay); this.isFirstStartPlay = false this.saveLastPlayList() return } if (this.isOpenJump) { //用户是否设置跳过片头片尾 if (this.mIjkMediaPlayer != null) { let jumpTopTimeMs = this.jumpTopTime*1000; if (jumpTopTimeMs <= this.duration) { // 调用 seekTo 方法,跳到指定时间 setTimeout(() => { //rmvb和flac格式马上记忆播放会报错,会延迟1s在读取记忆播放。 if (StrUtil.isNotEmpty(this.videoUrl) && !this.videoUrl.toLowerCase().endsWith('.ts')) { this.seekTo(jumpTopTimeMs + ""); // 跳到开头或记忆进度中更靠后的位置 } }, timeoutPlay) } else { // 提示用户无法跳过 ToastUtil.showToast('跳过时间超过视频时长'); } } } else { this.jumpTopTime = 0 this.jumpEndTime = 0 // console.error('onecold 最后一首: isFirstStartPlay = ' + this.isFirstStartPlay); // console.error('onecold 最后一首: isMemoryLastPlay = ' + this.isMemoryLastPlay); // console.error('onecold 最后一首: PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY, false) = ' + // PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY, false)); //记忆播放(每一首都会记忆播放) if (StrUtil.isNotEmpty(this.videoUrl) && !this.videoUrl.toLowerCase().endsWith('.ts')) { setTimeout(() => { //rmvb和flac格式马上记忆播放会报错,会延迟1s在读取记忆播放。 if (this.pendingMemorySeekPosition > 0 || PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY, false)) { this.restorePlaybackPosition() //读取播放记忆功能 } else if (this.isMemoryLastPlay && this.isFirstStartPlay) { console.error('onecold 最后一首:读取播放记忆功能 = ' + this.isFirstStartPlay); this.restorePlaybackPosition() //读取播放记忆功能 } this.isFirstStartPlay = false }, timeoutPlay) } else { this.isFirstStartPlay = false } } this.saveLastPlayList() } catch (error) { let err: BusinessError = error as BusinessError; console.error('onecold failed with err: ' + JSON.stringify(err)); if (this.waitInitialSeekBeforeStart) { this.finishInitialSeekWait('prepared-exception', true); } } } ); this.mIjkMediaPlayer.setOnPreparedListener(mOnPreparedListener); let mOnTimedTextListener: OnTimedTextListener = new ImplOnTimedTextListener( (text: string) => { } ); this.mIjkMediaPlayer.setOnTimedTextListener(mOnTimedTextListener) let mOnCompletionListener: OnCompletionListener = new ImplOnCompletionListener( () => { LogUtils.getInstance().LOGI("OnCompletionListener-->go") const completionAction = MusicPlaybackController.resolveCompletionAction(this.playType, this.curIndex, this.songList.length) if (completionAction.action === 'play_next') { this.playbackController.playNext().catch(() => { Logger.error('heanup OnCompletionListener playNext', `播放下一首失败`); }); } else if (completionAction.action === 'replay_current') { this.CONTROL_PlayStatus = PlayStatus.INIT that.startPlayOrResumePlay(); } else if (completionAction.action === 'stop_current') { if (completionAction.showBoundaryToast) { Logger.info('heanup OnCompletionListener', '连续播放不循环模式:已到达最后一首,停止播放'); } that.stopPlaybackAtTrackEnd(that.getActiveDuration(), completionAction.showBoundaryToast) } else if (completionAction.action === 'random_next') { this.randomPlay().catch(() => { Logger.error('heanup OnCompletionListener randomPlay', `随机播放失败`); }); } } ); this.mIjkMediaPlayer.setOnCompletionListener(mOnCompletionListener); let mOnBufferingUpdateListener: OnBufferingUpdateListener = new ImplOnBufferingUpdateListener( (percent: number) => { if (this.isDebug) { const now = Date.now(); if (now - this.lastBufferingLogTs > this.bufferingLogIntervalMs) { this.lastBufferingLogTs = now; Logger.info(TAG, `OnBufferingUpdateListener percent=${percent}`); } } } ); this.mIjkMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener); let mOnSeekCompleteListener: OnSeekCompleteListener = new ImplOnSeekCompleteListener( () => { LogUtils.getInstance().LOGI("OnSeekCompleteListener-->go"); that.logSeekTrace('seek-complete', undefined, undefined, true); // seek 完成,延迟恢复播放,确保 ijkplayer 内部状态完全稳定 setTimeout(() => { const waitingInitialSeek = that.waitInitialSeekBeforeStart; that.isSeekTo = false; if (waitingInitialSeek) { that.finishInitialSeekWait('seek-complete', true); } else { that.hideLoadIng(); } }, 200); // 延迟200ms,确保ijkplayer内部状态稳定 } ); this.mIjkMediaPlayer.setOnSeekCompleteListener(mOnSeekCompleteListener); let mOnInfoListener: OnInfoListener = new ImplOnInfoListener( (what: number, extra: number) => { LogUtils.getInstance().LOGI("OnInfoListener-->go:" + what + "===" + extra); if (that.isSeekTraceActive()) { Logger.info( TAG, `[seek] onInfo what=${what}, extra=${extra}, source=${that.lastSeekSource || 'unknown'}, ` + `req=${that.lastSeekRequestMs}, url=${that.sanitizePlaybackUrl(that.videoUrl)}` ); that.logSeekTrace(`info-${what}`, undefined, undefined, true); } if (!that.waitInitialSeekBeforeStart) { that.hideLoadIng(); } } ); this.mIjkMediaPlayer.setOnInfoListener(mOnInfoListener); let mOnErrorListener: OnErrorListener = new ImplOnErrorListener( (what: number, extra: number) => { this.finishInitialSeekWait('player-error', false); this.stopProgressTask(); this.logSeekTrace(`error-${what}-${extra}`, undefined, undefined, true); Logger.error( TAG, `[seek] onError what=${what}, extra=${extra}, url=${this.sanitizePlaybackUrl(this.videoUrl)}, ` + `song=${JSON.stringify(this.currentSong)}` ); // 检查是否为WebDAV播放错误 let isWebDavError = false; if (this.currentSong && isRemoteCloudType(this.currentSong.type) && this.currentSong.filePath) { try { const globalContext = GlobalContext.getContext(); const webDavAuthInfo = globalContext.getObject('webDavAuthInfo') as WebDavAuthItem; if (webDavAuthInfo) { isWebDavError = true; } } catch (error) { console.error('OnErrorListener-->go: 获取WebDAV认证信息失败'); } } if (isWebDavError) { console.info(`heanup WebDAV播放错误 - what: ${what}, extra: ${extra}, URL: ${this.videoUrl}`); // 播放失败时清空实例变量中的WebDAV认证信息,防止错误账户持续使用 // this.currentWebDavAuthInfo = null; console.info('heanup WebDAV播放失败,保留认证信息用于后续歌曲切换'); // 根据错误代码提供更具体的错误信息 let errorMessage = "WebDAV播放失败"; if (what === -1) { // 网络错误 errorMessage = "网络连接失败,请检查WebDAV服务器连接"; } else if (what === -1004) { // HTTP 404 errorMessage = "文件未找到,请检查WebDAV服务器上的文件"; } else if (what === -1001) { // 超时 errorMessage = "连接超时,请检查网络或WebDAV服务器状态"; } else if (what === -1003) { // 无法解析主机 errorMessage = "无法连接到WebDAV服务器"; } ToastUtil.showToast(errorMessage); } else { if (StrUtil.isNotEmpty(this.videoUrl) && !this.videoUrl.startsWith('http') && this.currentSong &&this.currentSong.type==CommonConstants.TYPE_LOCAL&& !FileUtil.accessSync(this.videoUrl)) { console.info('heanup 播放.文件不存在'); ToastUtil.showToast(Utility.resourceToString(this.context, $r('app.string.file_not_exist'))) } } that.hideLoadIng(); } ); this.mIjkMediaPlayer.setOnErrorListener(mOnErrorListener); this.mIjkMediaPlayer.setMessageListener(); // 应用均衡器设置 try { const equalizerManager = EqualizerManager.getInstance(); equalizerManager.applyToPlayer(this.mIjkMediaPlayer); Logger.info(TAG, 'heanup 均衡器设置已应用'); this.markEqualizerApplied(); } catch (eqError) { Logger.warn(TAG, `heanup 应用均衡器失败: ${(eqError as Error).message}`); this.eqApplying = false; } this.mIjkMediaPlayer.prepareAsync(); this.mIjkMediaPlayer.start(); } //保存最后播放的那首歌和已经对应的播放列表 saveLastPlayList() { PreferencesUtil.putSync('LastMusicInfo', this.currentSong) // 新队列刚刚完成落盘时,首帧 prepared 会立刻再进一次这里,跳过这次重复写入即可。 if (this.skipNextPlaylistPersist) { this.skipNextPlaylistPersist = false } else { PreferencesUtil.putSync('LastMusicList', this.songList) } PreferencesUtil.putSync('LastPlayQueueScope', this.playQueueScope) PreferencesUtil.putSync('LastPlayModeType', this.modeType) PreferencesUtil.putSync('LastPlayTotalCount', this.totalCount) } // 清空播放记录 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); } }); } public async setAvSessionListener() { if (!this.avSessionController) { return; } this.avSessionController.getAvSession()?.on('play', this.sessionPlayCallback); this.avSessionController.getAvSession()?.on('pause', this.sessionPauseCallback); this.avSessionController.getAvSession()?.on('stop', this.sessionStopCallback); this.avSessionController.getAvSession()?.on('playNext', this.sessionPlayNextCallback); this.avSessionController.getAvSession()?.on('playPrevious', this.sessionPlayPreviousCallback); this.avSessionController.getAvSession()?.on('fastForward', this.sessionFastForwardCallback); this.avSessionController.getAvSession()?.on('rewind', this.sessionRewindCallback); this.avSessionController.getAvSession()?.on('seek', this.sessionSeekCallback); this.avSessionController.getAvSession()?.on('setLoopMode', this.sessionSetLoopModeCallback); this.avSessionController.getAvSession()?.on('toggleFavorite', this.sessionToggleFavoriteCallback); this.avSessionController.getAvSession()?.on('outputDeviceChange', this.sessionOutputDeviceChange) } setLoopMode() { const nextMode = MusicPlaybackController.resolveNextLoopMode(this.playType) this.playType = nextMode.playType ToastUtil.showToast(nextMode.toastText) PreferencesUtil.putSync('musicPlayType', this.playType) this.setCurrentPlayMode() } private sessionSetLoopModeCallback = (mode: number) => { if (this.mIjkMediaPlayer != null) { if (MusicPlaybackController.shouldIgnoreInternalLoopModeEvent(this.pendingInternalLoopModeReport, mode)) { this.pendingInternalLoopModeReport = -1 return } this.pendingInternalLoopModeReport = -1 ToastUtil.showToast('当前播放模式:' + mode) Logger.info('onecold 当前播放模式= ' + mode) const resolvedMode = MusicPlaybackController.resolveSessionLoopMode(mode) this.playType = resolvedMode.playType ToastUtil.showToast(resolvedMode.toastText) PreferencesUtil.putSync('musicPlayType', this.playType) // 应用收到设置循环模式的指令后,应用自定下一个模式,切换完毕后通过AVPlaybackState上报切换后的LoopMode。 this.pendingInternalLoopModeReport = resolvedMode.reportedLoopMode let playBackState: avSession.AVPlaybackState = { loopMode: resolvedMode.reportedLoopMode, }; this.avSessionController.getAvSession()?.setAVPlaybackState(playBackState).then(() => { console.info(`set setLoopMode AVPlaybackState successfully`); }).catch((err: BusinessError) => { this.pendingInternalLoopModeReport = -1 console.error(`Failed to setLoopMode set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); }); } } setCurrentPlayMode() { if (!this.avSessionController) { return; } const mLoopMode = MusicPlaybackController.resolveAvSessionLoopMode(this.playType) this.pendingInternalLoopModeReport = mLoopMode // 应用启动时/内部切换循环模式,需要把应用内的当前的循环模式设置给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) => { this.pendingInternalLoopModeReport = -1 console.error(`Failed to setLoopMode set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); }); } private applyRequestedPlayType(playType: number): void { if (playType < 0 || playType > 4 || this.playType === playType) { return } this.playType = playType PreferencesUtil.putSync('musicPlayType', this.playType) this.setCurrentPlayMode() } private sessionToggleFavoriteCallback = (assetId: string) => { if (this.mIjkMediaPlayer != null && this.currentSong) { console.info(`on toggleFavorite `); // 应用收到收藏命令,进行收藏处理。 this.isFac = Utility.getIsFav(this.favList, this.currentSong) this.doFav(this.currentSong) // 应用内完成或者取消收藏,把新的收藏状态设置给AVSession。 let playbackState: avSession.AVPlaybackState = { isFavorite: !this.isFac, }; this.avSessionController.getAvSession()?.setAVPlaybackState(playbackState).then(() => { console.info(`SetAVPlaybackState successfully`); }).catch((err: BusinessError) => { console.info(`SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`); }); } } private sessionOutputDeviceChange = async (connectState: avSession.ConnectionState, device: avSession.OutputDeviceInfo) => { let currentDevice: avSession.DeviceInfo = device?.devices?.[0]; // 将 connectState 转换为可读的字符串 let stateName = 'UNKNOWN'; switch (connectState) { case avSession.ConnectionState.STATE_CONNECTED: stateName = 'CONNECTED(1)'; break; case avSession.ConnectionState.STATE_DISCONNECTED: stateName = 'DISCONNECTED(2)'; break; case avSession.ConnectionState.STATE_CONNECTING: stateName = 'CONNECTING(3)'; break; default: stateName = `OTHER(${connectState})`; } Logger.info('heanup sessionOutputDeviceChange', `设备状态变化: connectState=${stateName}, castCategory=${currentDevice?.castCategory}, isCastPlaying=${this.isCastPlaying}`); if (currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_CONNECTED) { await this.startCasting(); } 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) { Logger.info('heanup sessionOutputDeviceChange', '设备断开连接'); Logger.info('heanup sessionOutputDeviceChange', '用户主动断开连接或已停止播放,调用endCasting'); this.endCasting(); } } }; private async startCasting() { try { Logger.info('heanup startCasting', '========== 开始投播初始化 =========='); // 获取原始的 AVCastController let avCastCtrl = await this.avSessionController.getAvSession()?.getAVCastController(); if (!avCastCtrl) { Logger.error('heanup startCasting', '获取 AVCastController 失败'); return; } this.castController = avCastCtrl; Logger.info('heanup startCasting', '✅ 获取 AVCastController 成功'); // 创建新的 CastController 封装器 this.castControllerWrapper = new CastController(avCastCtrl); Logger.info('heanup startCasting', '✅ 创建 CastController 成功'); // 设置回调函数 let callbacks: CastControllerCallbacks = { onPlayNext: () => { Logger.info('heanup CastController', '收到 playNext 回调'); void this.playbackController.playNext(); }, onPlayPrevious: () => { Logger.info('heanup CastController', '收到 playPrevious 回调'); void this.playbackController.playPrevious(); }, onPositionChanged: (position: number) => { this.positionChange(position); }, onDurationChanged: (duration: number) => { this.playDurationChange(duration); } }; this.castControllerWrapper.setCallbacks(callbacks); Logger.info('heanup startCasting', '✅ 设置回调成功'); // 暂停本地播放 this.pause(); Logger.info('heanup startCasting', '✅ 暂停本地播放成功'); // 初始化投播会话 Logger.info('heanup startCasting', `准备调用 initAVCast: songList.length=${this.songList.length}, curIndex=${this.curIndex}, videoUrl=${this.videoUrl}`); await this.castControllerWrapper.initAVCast( this.songList, this.curIndex, this.mIjkMediaPlayer.getCurrentPosition(), this.videoUrl, this.getActiveDuration() ); Logger.info('heanup startCasting', '✅ initAVCast 调用完成'); let refToIsCasting: AbstractProperty | undefined = AppStorage.ref('isCasting'); refToIsCasting?.set(true); this.castSeek = true; // 从 CastController 获取投播状态 this.isCastPlaying = this.castControllerWrapper.getIsCastPlaying(); if (this.isCastPlaying) { this.CONTROL_PlayStatus = PlayStatus.PLAY; this.setIsPlaying(true); this.updateSessionPlayState(true); } Logger.info('heanup startCasting', `✅ 投播初始化完成, isCastPlaying=${this.isCastPlaying}`); } catch (error) { Logger.error('heanup startCasting', `❌ 投播初始化失败: ${error}`); } } async changeCasting() { Logger.info('heanup changeCasting', '切换投播'); this.castController = await this.avSessionController.getAvSession()?.getAVCastController(); this.initQueueItem(); await this.prepare(); this.setPlaybackStateChangeListener(); // isCastPlaying 会在 playbackStateChangeListener 中根据实际状态更新 this.castSeek = true; } public endCasting() { Logger.info('heanup endCasting', 'endCasting被调用'); this.avSessionController.getAvSession()?.stopCasting(); this.stopCast(); // 关闭文件描述符 if (this.castFile) { try { fs.closeSync(this.castFile); this.castFile = undefined; Logger.info('heanup endCasting', '关闭投播文件描述符'); } catch (error) { Logger.warn('heanup endCasting', `关闭文件描述符失败: ${error}`); } } this.isCastPlaying = false; let refToIsCasting: AbstractProperty | undefined = AppStorage.ref('isCasting'); refToIsCasting?.set(false); this.pause(); this.setIsPlaying(false); // if (this.curIndex === this.curSource.index) { // this.play // this.seek(this.currentTime * 1000); // this.setIsPlaying(true); // } else { // this.pauseVideo(); // this.setIsPlaying(false); // } } public setIsPlaying(isPlayer: boolean) { this.isPlaying = isPlayer; // 发送播放状态变化事件给歌单详情页面 try { const eventPlaybackStatus: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYBACK_STATUS } const currentFilePath = this.currentSong?.filePath || '' const eventData: emitter.EventData = { data: { isPlaying: this.isPlaying, curIndex: this.curIndex, currentFilePath: currentFilePath } }; emitter.emit(eventPlaybackStatus, eventData) Logger.info(`heanup 发送播放状态变化事件: isPlaying=${this.isPlaying}, curIndex=${this.curIndex}, currentFilePath=${currentFilePath}`) } catch (error) { Logger.error('heanup 发送播放状态变化事件失败: ' + error) } try { this.playbackStateBridge.updatePlaybackState( this.CONTROL_PlayStatus === PlayStatus.PLAY, this.getActivePlaybackPositionMs(), this.getActiveDuration() ) } catch (error) { Logger.warn(TAG, `回写 PlaybackStateBridge 失败: ${error}`) } this.notifyPlaybackCardStateChanged() } private setPlaybackStateChangeListener(): void { // 注意: HarmonyOS 的投播控制器可能不返回 state 字段 // 我们需要通过其他方式来判断播放状态 // 1. 监听位置变化 this.castController?.on('playbackStateChange', ['position'], (playbackState: avSession.AVPlaybackState) => { if (playbackState.position && typeof playbackState.position.elapsedTime !== 'undefined') { Logger.info('heanup playbackStateChange[position]', `elapsedTime=${playbackState.position.elapsedTime}`); this.positionChange(playbackState.position.elapsedTime); } }); // 2. 监听时长变化 this.castController?.on('playbackStateChange', ['extras'], (playbackState: avSession.AVPlaybackState) => { const duration = playbackState?.extras?.duration; if (typeof duration === 'number') { this.playDurationChange(duration); } }); // 3. 监听所有状态变化(用于调试和状态判断) this.castController?.on('playbackStateChange', 'all', (playbackState: avSession.AVPlaybackState) => { Logger.info('heanup playbackStateChange[all]', `完整状态=${JSON.stringify(playbackState)}`); // 检查是否有 state 字段(某些设备可能返回) if (playbackState.state !== undefined) { Logger.info('heanup playbackStateChange', `state字段存在: ${playbackState.state}`); if (playbackState.state === CommonConstants2.PLAYBACK_STATE_PLAY) { this.isCastPlaying = true; } } }); // 4. 监听播放完成事件 this.castController?.on('endOfStream', this.playNextCallback); this.castController?.on('playNext', this.playNextCallback); this.castController?.on('playPrevious', this.playPreviousCallback); // 5. 监听错误事件 this.castController?.on('error', (error: BusinessError) => { Logger.error('heanup playbackStateChange', `投播错误: code=${error.code}, message=${error.message}`); this.isCastPlaying = false; }); } private updateSessionPlayState(isPlay: boolean): void { // if(!this.isBgPlayOpen) // return const elapsedTime = (this.castControllerWrapper && this.isCastPlaying) ? this.currentTime2 * 1000 : this.mIjkMediaPlayer.getCurrentPosition(); Logger.info(TAG, `updateIsPlay isPlay: ${isPlay}`); this.setAVSessionPlayState({ state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, position: { elapsedTime: elapsedTime, 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)); if (this.castControllerWrapper && this.isCastPlaying) { this.updateCastProgress(position); if (this.isSeekTo) { this.isSeekTo = false; } } } private playDurationChange(duration: number) { this.duration = duration; this.durationTime = Math.floor(this.duration / 1000); this.durationStringTime = secondToTime((this.durationTime)); if (this.castControllerWrapper && this.isCastPlaying) { this.totalTime = this.stringForTime(this.duration); } } // playbackStateChangeListener 方法已被删除,改用 setPlaybackStateChangeListener 中的内联函数 private reloadCasting = async () => { Logger.info('heanup reloadCasting', '重新加载投播'); // 使用 initQueueItem() 方法来准备投播项目,保持逻辑一致 this.initQueueItem(); await this.prepare(); }; public playNextCallback = (): void => { this.playbackController.playNext().catch(() => { Logger.error('heanup playNextCallback', `播放下一首失败`); }); }; public playPreviousCallback = (): void => { this.playbackController.playPrevious().catch(() => { Logger.error('heanup playNextCallback', `播放上一首失败`); }); }; async prepare() { try { Logger.info('heanup prepare', `开始准备投播: ${this.castItem?.description?.title}`); await this.castController?.prepare(this.castItem); Logger.info('heanup prepare', '投播prepare成功'); await this.castController?.start(this.castItem); Logger.info('heanup prepare', '投播start成功'); // 投播启动成功,设置为投播状态 this.isCastPlaying = true; Logger.info('heanup prepare', '投播启动成功,设置isCastPlaying=true'); } catch (error) { Logger.error('heanup prepare', `投播准备失败: ${error}`); this.isCastPlaying = false; } } public initQueueItem() { let item = this.songList[this.curIndex]; // 判断是否是流媒体地址 - 使用 videoUrl 判断而不是 filePath // 因为 filePath 可能是 navidrome:// 等自定义协议,而 videoUrl 才是真实的流地址 let isStreaming = this.isUrl(this.videoUrl); Logger.info('heanup initQueueItem', `准备投播歌曲: ${item.name}, 是否流媒体: ${isStreaming}, videoUrl: ${this.videoUrl}`); // 关闭之前的文件描述符 if (this.castFile) { try { fs.closeSync(this.castFile); Logger.info('heanup initQueueItem', '关闭之前的文件描述符'); } catch (error) { Logger.warn('heanup initQueueItem', `关闭文件描述符失败: ${error}`); } } let description: avSession.AVMediaDescription = { assetId: 'AUDIO-' + item.filePath, title: item.name, artist: item.artist || '', // 该字段大写,音频'AUDIO',视频'VIDEO' mediaType: 'AUDIO', mediaSize: item.videoSize, //startPosition为投播当前进度,设置该字段可将本机播放进度同步到远端 startPosition: this.mIjkMediaPlayer.getCurrentPosition(), // 投播资源播放时长,设置该字段可将本机播放时长同步到远端显示 duration: this.duration, albumCoverUri: item.pixelMapPath, // 本地路径电视可能无法访问,暂时注释 appName: this.appName, lyricContent: item.lyricContent, // DRM资源,需要配置支持的DRM类型, 以chinaDRM为例。 // drmScheme: '3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c', }; console.log('heanup initQueueItem this.lyricContent', item.lyricContent); // 网络资源投播,设置mediaUri; 本地资源投播,将本地文件打开后,相关的文件描述符设置到fdSrc if (isStreaming) { // 流媒体地址使用 mediaUri description.mediaUri = this.videoUrl; Logger.info('heanup initQueueItem', `使用流媒体地址投播: ${this.videoUrl}`); } else { // 本地文件使用 fdSrc const localPath = this.videoUrl || item.filePath; let uri = fileUri.getUriFromPath(localPath); this.castFile = fs.openSync(uri, fs.OpenMode.READ_ONLY); let fdSrc: media.AVFileDescriptor = { fd: this.castFile.fd }; description.fdSrc = fdSrc; Logger.info('heanup initQueueItem', `使用本地文件投播: ${localPath}, fd: ${this.castFile.fd}`); } this.castItem = { itemId: this.curIndex, description: description }; Logger.info('heanup initQueueItem', `投播项目创建完成: ${JSON.stringify({ title: description.title, mediaType: description.mediaType, hasMediaUri: !!description.mediaUri, hasFdSrc: !!description.fdSrc, duration: description.duration })}`); } // 判断是否是 URL 或流媒体地址 private isUrl(str: string): boolean { if (!str) { return false; } // 检查常见的流媒体协议 if (str.startsWith('http://') || str.startsWith('https://') || str.startsWith('rtmp://') || str.startsWith('rtsp://') || str.startsWith('rtp://') || str.startsWith('mms://')) { return true; } // 扩展的URL正则表达式,支持多种协议 const urlPattern = new RegExp( '^((https?|ftp|rtmp|rtsp|mms|ws|rtp|wss):\\/\\/)?' + // 支持多种协议 '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // 域名 '((\\d{1,3}\\.){3}\\d{1,3})|' + // 或IPv4地址 '\\[([a-f\\d]{1,4}:){7}[a-f\\d]{1,4}\\]|' + // 或IPv6地址 'localhost)' + // 或localhost '(\\:\\d+)?' + // 端口号(可选) '(\\/[-a-z\\d%_.~+]*)*' + // 路径 '(\\?[;&a-z\\d%_.~+=-]*)?' + // 查询字符串(可选) '(\\#[-a-z\\d_]*)?$', // 片段标识符(可选) 'i' // 不区分大小写 ); return urlPattern.test(str); } async stopCast() { try { // 使用 CastController 释放资源 if (this.castControllerWrapper) { await this.castControllerWrapper.releaseAVCast(); Logger.info('heanup stopCast', 'CastController 释放投播控制器成功'); } // 清空引用 this.castControllerWrapper = undefined; this.castController = undefined; this.castItem = undefined; this.castFile = undefined; } catch (error) { Logger.error('heanup stopCast', `停止投播失败: ${error}`); } } private sessionPlayCallback = (): void => { this.playbackController.playOrPause() }; private sessionPauseCallback = (): void => { this.pause() }; private async playOrPause() { if (this.isHostSnapshotDisplayOnly) { await BackgroundAudioPlaybackHost.getInstance().getRuntime().playOrPause() return } if (!this.debounce()) { return; } const toggleAction = MusicPlaybackController.resolveTogglePlaybackAction( this.CONTROL_PlayStatus === PlayStatus.PLAY, !!this.castControllerWrapper && this.isCastPlaying ) if (toggleAction === 'pause') { await this.pause(); } else { // 如果是投播模式,使用 CastController 的 play 方法 if (toggleAction === 'resume_cast' && this.castControllerWrapper) { try { Logger.info('heanup playOrPause', '投播模式播放'); await this.castControllerWrapper.setPlaying(); this.CONTROL_PlayStatus = PlayStatus.PLAY; this.setIsPlaying(true); this.updateSessionPlayState(true); Logger.info('heanup playOrPause', '投播播放成功'); this.playChange(); } catch (error) { Logger.error('heanup playOrPause', `投播播放失败: ${error}`); } } else { console.info('onecold startPlayOrResumePlay 11064') this.startPlayOrResumePlay(); this.playChange() } } } private sessionPlayNextCallback = (): void => { this.playbackController.playNext().catch(() => { Logger.error('heanup sessionPlayPreviousCallback', `播放下 一首失败`); }); }; private sessionPlayPreviousCallback = (): void => { this.playbackController.playPrevious().catch(() => { Logger.error('heanup sessionPlayPreviousCallback', `播放上一首失败`); }); }; private sessionStopCallback = (): void => { this.stop() }; private sessionFastForwardCallback = async (time?: number) => { if (!time) { return; } let curPosition: number; if (this.castController && this.isCastPlaying) { // 投播模式,使用UI显示的时间 curPosition = this.currentTime2 * 1000; } else if (this.mIjkMediaPlayer != null) { // 本地播放器模式 curPosition = this.mIjkMediaPlayer.getCurrentPosition(); if (this.currentSong && isCueSplitItem(this.currentSong)) { curPosition = Math.max(0, curPosition - getCueTrackStartOffset(this.currentSong)); } } else { return; } let seeTime = curPosition + time * 1000; // 限制 seekValue 在合法范围内 if (seeTime < 0) { seeTime = 0; } else if (seeTime > this.duration) { seeTime = this.duration; } Logger.info('heanup sessionFastForwardCallback', `快进: ${time}秒, 目标位置: ${seeTime}ms`); this.setSeekToActionProgress(seeTime); await this.seekTo(seeTime + "", 'session-fast-forward'); }; private getActiveDuration(): number { if (this.currentSong && isCueSplitItem(this.currentSong)) { const cueDuration = getCueTrackDuration(this.currentSong); if (cueDuration > 0) { return cueDuration; } } let duration = this.mIjkMediaPlayer.getDuration(); if (this.castControllerWrapper && this.isCastPlaying) { return this.duration > 0 ? this.duration : duration; } if (duration <= 0 && this.duration > 0) { duration = this.duration; } return duration; } @Builder InlineSpectrumView() { SpectrumView({ isVisible: this.currentSwiperIndex === 0, isPlaying: this.CONTROL_PlayStatus === PlayStatus.PLAY }) .width( '83%') .height(this.isCoverOpacity() ? 80 : 100) .margin({bottom: 3 }) .clip(true) .zIndex(-2) .onClick(()=>{ this.cycleSpectrumMode(); }) } private cycleSpectrumMode(): void { const modeNames: string[] = ['柱状', '波形', '水波纹', '圆形', '粒子', '脉冲', '镜像柱', '影子柱', '轨道线', '点阵', '星爆', '心电图', '特效']; if (modeNames.length <= 0) { return; } const nextIndex: number = (this.spectrumModeIndex + 1) % modeNames.length; this.spectrumModeIndex = nextIndex; PreferencesUtil.putSync('spectrumModeIndex', nextIndex); AppStorage.setOrCreate('spectrumMode', nextIndex); Logger.info('heanup-LocalMusic', `频谱特效已顺序切换为: ${modeNames[nextIndex]}`); } private getSongDurationMs(song?: VideoItem): number { if (!song || !song.duration) { return 0; } const raw = song.duration.trim(); if (!raw) { return 0; } const parts = raw.split(':').map((part) => Number(part)); if (parts.some((part) => Number.isNaN(part))) { return 0; } let seconds = 0; if (parts.length === 3) { seconds = parts[0] * 3600 + parts[1] * 60 + parts[2]; } else if (parts.length === 2) { seconds = parts[0] * 60 + parts[1]; } else if (parts.length === 1) { seconds = parts[0]; } return seconds > 0 ? seconds * 1000 : 0; } private updateCastProgress(position: number): void { const duration = this.getActiveDuration(); if (duration <= 0) { return; } this.slideEnable = true; let pos = (position / duration) * 100; if (pos > this.PROGRESS_MAX_VALUE) { this.progressValue = this.PROGRESS_MAX_VALUE; } else if (pos < 0) { this.progressValue = 0; } else { this.progressValue = pos; } this.totalTime = this.stringForTime(duration); if (position > duration) { position = duration; } this.isCurrentTime = true; this.currentTime = this.stringForTime(position); this.isCurrentTime = false; const lyricPosition = position + this.timeOffset * 1000; this.updateLyricPosition(lyricPosition); } /** * Gesture method onActionUpdate. * * @param event Gesture event.手势拖动设置快进和后退 */ private setSeekToActionProgress(position: number) { // 投播模式使用this.duration,本地模式使用播放器获取 let duration = this.getActiveDuration(); 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 = async (time?: number) => { if (!time) { return; } let curPosition: number; if (this.castController && this.isCastPlaying) { // 投播模式,使用UI显示的时间 curPosition = this.currentTime2 * 1000; } else if (this.mIjkMediaPlayer != null) { // 本地播放器模式 curPosition = this.mIjkMediaPlayer.getCurrentPosition(); if (this.currentSong && isCueSplitItem(this.currentSong)) { curPosition = Math.max(0, curPosition - getCueTrackStartOffset(this.currentSong)); } } else { return; } let seeTime = curPosition - time * 1000; // 限制 seekValue 在合法范围内 if (seeTime < 0) { seeTime = 0; } else if (seeTime > this.duration) { seeTime = this.duration; } Logger.info('heanup sessionRewindCallback', `快退: ${time}秒, 目标位置: ${seeTime}ms`); this.setSeekToActionProgress(seeTime); await this.seekTo(seeTime + "", 'session-rewind'); }; private sessionSeekCallback = async (seekTime: number) => { Logger.info('heanup sessionSeekCallback', `收到seek请求: ${seekTime}ms, 投播模式: ${this.isCastPlaying}`); // 投播模式直接调用seekTo,它会自动处理投播控制器的seek命令 await this.seekTo(seekTime + "", 'session-seek'); }; 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 async pause(clearInterruptResumeFlag: boolean = true) { if (this.isHostSnapshotDisplayOnly) { if (this.CONTROL_PlayStatus === PlayStatus.PLAY) { await BackgroundAudioPlaybackHost.getInstance().getRuntime().playOrPause() } return } if (clearInterruptResumeFlag) { this.audioInterruptResumePending = false; } // 如果是投播模式,使用 CastController 的 pause 方法 if (this.castControllerWrapper && this.isCastPlaying) { try { Logger.info('heanup pause', '投播模式暂停'); await this.castControllerWrapper.setPause(); this.CONTROL_PlayStatus = PlayStatus.PAUSE; this.setIsPlaying(false); this.updateSessionPlayState(false); Logger.info('heanup pause', '投播暂停成功'); } catch (error) { Logger.error('heanup pause', `投播暂停失败: ${error}`); } } else if (this.mIjkMediaPlayer.isPlaying() || this.CONTROL_PlayStatus === PlayStatus.PLAY) { // 本地播放器模式 this.savePlaybackPosition(); if (this.mIjkMediaPlayer.isPlaying()) { this.mIjkMediaPlayer.pause(); this.setProgress(); } this.mDestroyPage = true; this.CONTROL_PlayStatus = PlayStatus.PAUSE; this.updateSessionPlayState(false) this.playChange() if (this.pipController) { this.pipController.updatePiPControlStatus(PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE, PiPWindow.PiPControlStatus.PAUSE); } } } private stop() { this.audioInterruptResumePending = false; this.savePlaybackPosition(); this.clearInitialSeekGuardTimer(); this.waitInitialSeekBeforeStart = false; this.pendingMemorySeekPosition = 0; this.isPlayerLoading = false; this.CONTROL_PlayStatus = PlayStatus.INIT; this.mIjkMediaPlayer.stop(); this.mIjkMediaPlayer.release(); console.info('kkMusic stop ..this.CONTROL_PlayStatus '+this.CONTROL_PlayStatus ) this.stopProgressTask(); this.updateSessionPlayState(false) this.playChange() this.watchStatus(); } //保持记忆播放功能 private savePlaybackPosition() { if (this.mIjkMediaPlayer != null) { let position = this.mIjkMediaPlayer.getCurrentPosition(); // ToastUtil.showToast('保存记忆播放 = ' + position) let duration = this.mIjkMediaPlayer.getDuration(); if (this.currentSong && isCueSplitItem(this.currentSong)) { position = Math.max(0, position - getCueTrackStartOffset(this.currentSong)); const cueDuration = getCueTrackDuration(this.currentSong); if (cueDuration > 0) { duration = cueDuration; } } const threshold = 5000; // 阈值,单位为毫秒(这里设为5秒) // 如果播放位置接近视频末尾,则保存 position 为 0 const playbackPosition = (duration - position < threshold) ? 0 : position; const memoryKey = this.activePlaybackMemoryKey || this.getPlaybackMemoryKey(); PreferencesUtil.putSync(memoryKey, playbackPosition); } } private buildPlaybackMemoryKey(song?: VideoItem, playbackUrl?: string): string { if (song && isRemoteCloudType(song.type)) { const accountId = song.webdav_account_id || 'unknown'; const stablePath = song.remote_rel_path || song.id || song.baiduFsId || song.filePath || playbackUrl || ''; return `memory_play_remote:${song.type}:${accountId}:${stablePath}`; } if (song && isCueSplitItem(song)) { return `memory_play_local_cue:${song.filePath}`; } return playbackUrl || ''; } private getPlaybackMemoryKey(): string { return this.buildPlaybackMemoryKey(this.currentSong, this.videoUrl); } private restoreRemotePlaybackPositionSafely(position: number, token: number, retryCount: number = 0): void { if (token !== this.remoteRestoreSeekToken) { return; } if (!this.mIjkMediaPlayer || position <= 0) { if (this.waitInitialSeekBeforeStart) { this.finishInitialSeekWait('invalid-memory-position', true); } return; } const duration = this.getActiveDuration(); const currentPos = this.mIjkMediaPlayer.getCurrentPosition(); let safePosition = position; if (duration > 0) { safePosition = Math.max(0, Math.min(position, duration - 500)); } else { safePosition = Math.max(0, position); } if (safePosition <= 0) { if (this.waitInitialSeekBeforeStart) { this.finishInitialSeekWait('memory-position-too-small', true); } return; } if (retryCount === 0 && duration <= 0 && currentPos <= 0) { Logger.info(TAG, `远程记忆seek提前尝试: pos=${safePosition}, duration=${duration}, current=${currentPos}`); } this.seekTo(safePosition + "", retryCount === 0 ? 'remote-memory-restore' : 'remote-memory-retry'); if (retryCount >= this.remoteMemorySeekMaxRetryCount) { return; } setTimeout(() => { if (token !== this.remoteRestoreSeekToken || !this.waitInitialSeekBeforeStart || !this.mIjkMediaPlayer) { return; } const latestPos = this.mIjkMediaPlayer.getCurrentPosition(); if (Math.abs(latestPos - safePosition) <= 1500) { return; } this.restoreRemotePlaybackPositionSafely(position, token, retryCount + 1); }, this.remoteMemorySeekRetryIntervalMs); } //获取记忆播放功能 private restorePlaybackPosition() { const position: number = this.pendingMemorySeekPosition > 0 ? this.pendingMemorySeekPosition : PreferencesUtil.getNumberSync(this.getPlaybackMemoryKey(), 0); if (!this.mIjkMediaPlayer || position <= 0) { if (this.waitInitialSeekBeforeStart) { this.finishInitialSeekWait('no-memory-position', true); } return; } const isRemoteSong = this.currentSong ? isRemoteCloudType(this.currentSong.type) : false; if (isRemoteSong && this.videoUrl.includes('://')) { const token = this.remoteRestoreSeekToken; this.restoreRemotePlaybackPositionSafely(position, token); return; } const duration = this.getActiveDuration(); let safePosition = position; if (duration > 0) { safePosition = Math.max(0, Math.min(position, duration - 500)); } else { safePosition = Math.max(0, position); } if (safePosition <= 0) { if (this.waitInitialSeekBeforeStart) { this.finishInitialSeekWait('memory-position-too-small', true); } return; } this.seekTo(safePosition + "", isRemoteSong ? 'remote-memory-restore' : 'memory-restore'); } private async seekTo(value: string, source: string = 'internal') { if (this.isHostSnapshotDisplayOnly) { await BackgroundAudioPlaybackHost.getInstance().getRuntime().seekTo(value, source) return } // if (StrUtil.isNotEmpty(this.videoUrl) && this.videoUrl.toLowerCase().endsWith('.wma')) { // ToastUtil.showToast('wma格式不支持拖动快进。') // return // } // 如果是投播模式,使用 CastController 的 seek 方法 if (this.castControllerWrapper && this.isCastPlaying) { try { const cueTrackItem = this.currentSong && isCueSplitItem(this.currentSong) ? this.currentSong : undefined; const cueTrackStartOffset = cueTrackItem ? getCueTrackStartOffset(cueTrackItem) : 0; const cueTrackEndOffset = cueTrackItem ? getCueTrackEndOffset(cueTrackItem) : 0; const duration = this.getActiveDuration(); const resolvedSeek = MusicPlaybackController.resolveSeekPosition({ requestedValue: value, activeDuration: duration, cueTrackStartOffset, cueTrackEndOffset }) if (!resolvedSeek.canSeek) { return } const seekPos = resolvedSeek.seekPos this.beginSeekTrace(source, seekPos); this.isSeekTo = true; Logger.info( TAG, `[seek] cast dispatch, source=${source}, target=${seekPos}, duration=${duration}, ` + `uiDuration=${this.duration}, url=${this.sanitizePlaybackUrl(this.videoUrl)}` ); // 检查是否接近末尾 if (this.duration > 0 && seekPos >= this.duration - 500) { Logger.warn('heanup seekTo', `快进位置接近末尾(${seekPos}ms >= ${this.duration}ms),可能触发播放完成`); } await this.castControllerWrapper.seek(seekPos); Logger.info('heanup seekTo', `投播seek命令发送成功,位置: ${seekPos}ms`); } catch (error) { Logger.error('heanup seekTo', `投播seek失败: ${error}`); } } else { // 本地播放器模式 const cueTrackItem = this.currentSong && isCueSplitItem(this.currentSong) ? this.currentSong : undefined; const cueTrackStartOffset = cueTrackItem ? getCueTrackStartOffset(cueTrackItem) : 0; const cueTrackEndOffset = cueTrackItem ? getCueTrackEndOffset(cueTrackItem) : 0; const duration = this.getActiveDuration(); const isRemoteSong = this.currentSong ? isRemoteCloudType(this.currentSong.type) : false; const resolvedSeek = MusicPlaybackController.resolveSeekPosition({ requestedValue: value, activeDuration: duration, cueTrackStartOffset, cueTrackEndOffset, isRemoteSong }) if (!resolvedSeek.canSeek) { Logger.warn(TAG, `seekTo 跳过未知时长远程seek: ${resolvedSeek.seekPos}ms`); return; } const seekPos = resolvedSeek.seekPos if (isRemoteSong && duration <= 0) { Logger.info(TAG, `seekTo 远程歌曲时长未知,仅允许起点seek: ${seekPos}ms`); } if (source !== 'internal') { this.beginSeekTrace(source, seekPos); } if (this.handleBlockedSeek(seekPos, source)) { return; } this.isSeekTo = true try { if (isRemoteSong || source !== 'internal' || this.isSeekTraceActive()) { Logger.info( TAG, `[seek] player dispatch, source=${source}, target=${seekPos}, activeDuration=${duration}, ` + `playerDuration=${this.mIjkMediaPlayer.getDuration()}, current=${this.mIjkMediaPlayer.getCurrentPosition()}, ` + `isPlaying=${this.mIjkMediaPlayer.isPlaying()}, url=${this.sanitizePlaybackUrl(this.videoUrl)}` ); } this.mIjkMediaPlayer.seekTo(seekPos + ""); this.logSeekTrace('seek-dispatched', this.mIjkMediaPlayer.getCurrentPosition(), this.mIjkMediaPlayer.getDuration(), true); this.setProgress() } catch (error) { Logger.error(TAG, `本地播放器seek失败: ${error}`); this.logSeekTrace('seek-dispatch-error', undefined, undefined, true); } finally { this.isSeekTo = false } } } // 添加到下一首播放 private addToNextPlay(song: VideoItem) { if (!this.debounce()) { return; } if (this.currentSong) { if (this.currentSong === song) { ToastUtil.showToast('当前已经在播放这首歌了!') return } } // 如果未开始播放或队列为空 if (this.curIndex === -1 || this.songList.length === 0) { this.songList = [song]; this.curIndex = 0; this.doPlay(song) return; } // 插入到当前索引+1的位置 const insertPos = this.curIndex + 1; this.songList.splice(insertPos, 0, song); this.songList = [...this.songList]; // 触发状态更新 this.sonDataSource.pushArrayData(this.songList) ToastUtil.showToast('已添加至下一首播放') } //后退10s,可再设置设置默认几秒 async backForward() { // 如果是投播模式,从currentTime2获取当前位置 let pos: number; if (this.castController && this.isCastPlaying) { pos = this.currentTime2 * 1000; // 使用秒转毫秒 Logger.info('heanup backForward', `投播模式当前位置: ${pos}ms`); } else if (this.mIjkMediaPlayer) { pos = this.mIjkMediaPlayer.getCurrentPosition(); if (this.currentSong && isCueSplitItem(this.currentSong)) { pos = Math.max(0, pos - getCueTrackStartOffset(this.currentSong)); } } else { return; } let value = pos - Number(this.fastForwardSeconds) * 1000; if (value < 0) { value = 0; } await this.seekTo(value + "", 'rewind-button'); } //快进10s,可再设置设置默认几秒 async fastForward() { // 如果是投播模式,从currentTime2获取当前位置 let pos: number; if (this.castController && this.isCastPlaying) { pos = this.currentTime2 * 1000; // 使用秒转毫秒 Logger.info('heanup fastForward', `投播模式当前位置: ${pos}ms`); } else if (this.mIjkMediaPlayer) { pos = this.mIjkMediaPlayer.getCurrentPosition(); if (this.currentSong && isCueSplitItem(this.currentSong)) { pos = Math.max(0, pos - getCueTrackStartOffset(this.currentSong)); } } else { return; } let value = pos + Number(this.fastForwardSeconds) * 1000; // 限制不超过总时长 if (this.duration > 0 && value > this.duration) { value = this.duration; } await this.seekTo(value + "", 'fast-forward-button'); } //下一个 private async playNext() { if (this.isHostSnapshotDisplayOnly) { await BackgroundAudioPlaybackHost.getInstance().getRuntime().playNext() return } this.lyricController.setLyric(null) this.lyricControllerXF.setLyric(null) this.lyricControllerSingle.setLyric(null) Logger.info('heanup playNext', '========== playNext 开始 =========='); Logger.info('heanup playNext', `isCastPlaying=${this.isCastPlaying}, castControllerWrapper存在=${!!this.castControllerWrapper}`); if (!this.debounce()) { Logger.warn('heanup playNext', 'debounce() 返回 false,退出'); return; } Logger.info('heanup playNext', `playType=${this.playType}`); const nextPlaybackAction = MusicPlaybackController.resolveNextPlaybackAction( this.playType, this.curIndex, this.songList.length ) if (nextPlaybackAction === 'random_play') { Logger.info('heanup playNext', '随机播放模式'); await this.randomPlay() return; } if (ArrayUtil.isNotEmpty(this.songList)) { if (this.curIndex >= this.songList.length - 1) { if (nextPlaybackAction === 'stop_at_end') { Logger.info('heanup playNext', '连续播放不循环模式:已到达最后一首,停止播放'); this.stopPlaybackAtTrackEnd(this.getActiveDuration(), true) return; } await this.tryLoadNextPageForPlayback(); } const nextQueueIndex = MusicPlaybackController.resolveNextQueueIndex(this.curIndex, this.songList.length) this.curIndex = nextQueueIndex.nextIndex // 直接使用歌曲列表中的歌曲信息,songList中的数据已经是完整的 this.applySelectedQueueSong(this.curIndex) const nextSong = this.currentSong if (!nextSong) { return; } console.info('heanup playNext', `直接使用歌曲列表中的信息: ${nextSong.name}, type: ${nextSong.type}`); await this.dispatchPreparedQueueSwitch( 'playNext', '播放下一首', async (durationMs: number): Promise => { await this.castControllerWrapper!.playNext(this.songList, this.curIndex, this.videoUrl, durationMs); } ) } } 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 // 图标恢复到原始大小 this.startPlayOrResumePlay() }); }, 500); }else{ this.cover = this.songList[this.curIndex].pixelMapPath this.startPlayOrResumePlay() } } // 内部方法:提前缓存下一首 private async preloadNextSongInternal(): Promise { // 随机模式下传递已播放索引集合 if (this.playType === 3) { this.preloadNextRandomIndex = await preloadNextSongIfNeeded( this.songList, this.curIndex, this.playType, this.playedIndices ); } else { this.preloadNextRandomIndex = await preloadNextSongIfNeeded( this.songList, this.curIndex, this.playType ); } } // 存储已播放的歌曲索引 private playedIndices: Set = new Set(); // 存储已播放的本地歌曲路径(随机播放去重用) private playedLocalFilePaths: Set = new Set(); // 随机播放模式下的播放历史栈(记录实际播放顺序,用于"上一首"功能) private randomPlayHistory: VideoItem[] = []; //随机播放 private async randomPlay() { // if (!this.debounce()) { // return; // } // 网盘模式: 渐进式扩展播放库的随机播放 // 通过检查当前歌曲类型判断是否为网盘音乐(而非modeType,因为不同网盘类型modeType不同) const isCloudSong = this.currentSong && isRemoteCloudType(this.currentSong.type); if (isCloudSong) { const findRemoteQueue = getFindVideoItems(); const isSameFindQueue = this.songList.length === findRemoteQueue.length && this.songList.every((item: VideoItem, index: number) => item.filePath === findRemoteQueue[index]?.filePath); const useFindRemoteQueue = this.playQueueScope === 'playlist' && findRemoteQueue.length > 0 && isSameFindQueue && findRemoteQueue.every((item: VideoItem) => isRemoteCloudType(item.type)) && findRemoteQueue.some((item: VideoItem) => item.filePath === this.currentSong?.filePath); // 阈值常量 const UNPLAYED_THRESHOLD = 10; // 剩余未播放歌曲阈值 const LOAD_MORE_PAGES = 3; // 每次加载页数 // 获取网盘播放列表 const navidromeList = useFindRemoteQueue ? [] : getNavidromeVideoItems(); const webdavList = useFindRemoteQueue ? [] : getWebdavVideoItems(); const isNavidrome = !useFindRemoteQueue && navidromeList.length > 0; const remoteList = useFindRemoteQueue ? findRemoteQueue : (isNavidrome ? navidromeList : webdavList); if (remoteList.length > 0) { // 将当前歌加入历史栈 if (this.currentSong) { this.randomPlayHistory.unshift(this.currentSong); if (this.randomPlayHistory.length > 50) { this.randomPlayHistory = this.randomPlayHistory.slice(0, 50); } // 标记当前歌曲为已播放 markAsPlayed(this.currentSong.filePath); Logger.info(TAG, `randomPlay(网盘模式): 将当前歌加入历史栈, 栈大小: ${this.randomPlayHistory.length}`); } // 检查未播放歌曲数量(仅Navidrome支持渐进式加载) let unplayedCount = isNavidrome ? getUnplayedCount() : remoteList.length; const totalCount = remoteList.length; const playedCount = isNavidrome ? getPlayedCount() : 0; Logger.info(TAG, `randomPlay(网盘模式): source=${useFindRemoteQueue ? 'find-playlist' : (isNavidrome ? 'navidrome' : 'webdav')}, ` + `总数${totalCount}, 已播放${playedCount}, 未播放${unplayedCount}`); // 如果未播放数量不足阈值,且还有更多数据,触发加载更多页 if (isNavidrome && unplayedCount < UNPLAYED_THRESHOLD && hasMoreData()) { Logger.info(TAG, `randomPlay(网盘模式): 未播放(${unplayedCount})不足阈值(${UNPLAYED_THRESHOLD}),触发加载更多`); const loadSuccess = await triggerLoadMoreSongs(LOAD_MORE_PAGES); if (loadSuccess) { // 重新获取未播放数量 unplayedCount = getUnplayedCount(); Logger.info(TAG, `randomPlay(网盘模式): 加载完成,未播放数量更新为${unplayedCount}`); } } // 从未播放歌曲中随机选择 let candidates: VideoItem[]; if (isNavidrome) { candidates = getUnplayedSongs(); // 如果全部播放完,重置已播放记录 if (candidates.length === 0) { Logger.info(TAG, 'randomPlay(网盘模式): 全部播放完毕,重置已播放记录'); clearPlayedHistory(); // 排除当前歌曲 const currentFilePath = this.currentSong?.filePath; candidates = currentFilePath ? getNavidromeVideoItems().filter(v => v.filePath !== currentFilePath) : getNavidromeVideoItems(); } } else { // WebDAV模式:简单排除当前歌曲 const currentFilePath = this.currentSong?.filePath; candidates = currentFilePath ? remoteList.filter(v => v.filePath !== currentFilePath) : remoteList; } if (candidates.length > 0) { const randomIndex = Math.floor(Math.random() * candidates.length); const randomSong = candidates[randomIndex]; // 标记为已播放 if (isNavidrome) { markAsPlayed(randomSong.filePath); } Logger.info(TAG, `randomPlay(网盘模式): 从${candidates.length}首候选中随机选择: ${randomSong.name}`); this.markPreparedSongSwitch(randomSong, true) const didStartRemoteListSong = await this.startPreparedCurrentSongPlayback('randomPlay-remote-list') if (!didStartRemoteListSong) { return; } return; } Logger.warn(TAG, 'randomPlay(网盘模式): 无可用候选歌曲,回退到默认随机逻辑'); } } if (this.currentSong) { // 将当前歌加入到随机播放历史栈(在切换到新歌之前) this.randomPlayHistory.unshift(this.currentSong); // 限制历史栈大小,避免无限增长 if (this.randomPlayHistory.length > 50) { this.randomPlayHistory = this.randomPlayHistory.slice(0, 50); } Logger.info(TAG, `randomPlay: 将当前歌加入历史栈, 栈大小: ${this.randomPlayHistory.length}`); const isCurrentRemote = isRemoteCloudType(this.currentSong.type); const currentFilePath = this.currentSong.filePath; if (isCurrentRemote) { const remoteOptions: RandomSongQueryOptions = { type: this.currentSong.type, excludeFilePaths: currentFilePath ? [currentFilePath] : [] }; const randomRemoteSong = await this.table.queryRandomSong(remoteOptions); if (randomRemoteSong) { const isAccountAvailable = await this.ensureRemoteSongAccountAvailable(randomRemoteSong, 'randomPlay-remote-db') if (!isAccountAvailable) { return; } this.markPreparedSongSwitch(randomRemoteSong, true) const didStartRemoteDbSong = await this.startPreparedCurrentSongPlayback('randomPlay-remote-db') if (!didStartRemoteDbSong) { return; } return; } Logger.warn(TAG, '随机播放未获取到网盘歌曲,回退到列表随机'); } else { if (currentFilePath) { this.playedLocalFilePaths.add(currentFilePath); } // 根据modeType构建不同的查询选项 const localOptions: RandomSongQueryOptions = { type: CommonConstants.TYPE_LOCAL, excludeFilePaths: Array.from(this.playedLocalFilePaths) }; Logger.info(TAG, `randomPlay: 本地播放模式, modeType=${this.modeType}, currentPlaylistSongFilePaths.length=${this.currentPlaylistSongFilePaths.length}, currentPath=${this.currentPath}`); // modeType==0: 文件夹模式,添加parentPath过滤 if (this.modeType === 0 && this.currentPath) { localOptions.parentPath = this.currentPath; Logger.info(TAG, `randomPlay: 文件夹模式, parentPath=${this.currentPath}`); } // modeType==4: 歌单模式,添加playlistSongFilePaths else if (this.modeType === 4 && this.currentPlaylistSongFilePaths.length > 0) { localOptions.playlistSongFilePaths = this.currentPlaylistSongFilePaths; Logger.info(TAG, `randomPlay: 歌单模式,歌曲数量=${this.currentPlaylistSongFilePaths.length}, 前3首=${this.currentPlaylistSongFilePaths.slice(0, 3).join(', ')}`); } else if (this.modeType === 4) { Logger.warn(TAG, `randomPlay: 歌单模式但没有歌曲路径列表! currentPlaylistSongFilePaths.length=${this.currentPlaylistSongFilePaths.length}`); } let randomLocalSong = await this.table.queryRandomSong(localOptions); if (!randomLocalSong) { this.playedLocalFilePaths.clear(); if (currentFilePath) { this.playedLocalFilePaths.add(currentFilePath); } const fallbackLocalOptions: RandomSongQueryOptions = { type: CommonConstants.TYPE_LOCAL, excludeFilePaths: currentFilePath ? [currentFilePath] : [] }; // fallback时也要保持modeType的过滤条件 if (this.modeType === 0 && this.currentPath) { fallbackLocalOptions.parentPath = this.currentPath; } else if (this.modeType === 4 && this.currentPlaylistSongFilePaths.length > 0) { fallbackLocalOptions.playlistSongFilePaths = this.currentPlaylistSongFilePaths; } randomLocalSong = await this.table.queryRandomSong(fallbackLocalOptions); } if (randomLocalSong !== null) { const selectedLocalSong = randomLocalSong; this.markPreparedSongSwitch(selectedLocalSong, true) const didStartLocalRandomSong = await this.startPreparedCurrentSongPlayback('randomPlay-local-db') if (!didStartLocalRandomSong) { return; } return; } Logger.warn(TAG, '随机播放未获取到本地歌曲,回退到列表随机'); } } if (ArrayUtil.isNotEmpty(this.songList)) { if (this.curIndex >= 0 && this.curIndex < this.songList.length) { this.playedIndices.add(this.curIndex); } if (this.songList.length > 3) { // 优先使用预缓存的随机索引(仅网盘播放时) const nextSong = this.songList[this.preloadNextRandomIndex >= 0 ? this.preloadNextRandomIndex : 0]; const isNextRemote = nextSong && isRemoteCloudType(nextSong.type); const isCurrentRemote = this.currentSong && isRemoteCloudType(this.currentSong.type); if (this.preloadNextRandomIndex >= 0 && this.preloadNextRandomIndex < this.songList.length && isNextRemote && isCurrentRemote) { Logger.info('heanup randomPlay', `使用预缓存的随机索引: ${this.preloadNextRandomIndex} (网盘播放)`); this.curIndex = this.preloadNextRandomIndex; this.playedIndices.add(this.curIndex); this.preloadNextRandomIndex = -1; // 重置预缓存索引 // 立即预缓存下一首,避免下次点击时没有预缓存 void this.preloadNextSongInternal(); } else { // 如果没有预缓存索引,则正常随机选择 // 如果所有歌曲都播放过一次,清空已播放索引集合 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.markPreparedSongSwitch(this.songList[this.curIndex]) const didStartFallbackSong = await this.startPreparedCurrentSongPlayback('randomPlay-fallback') if (!didStartFallbackSong) { return; } // 预缓存下一首(包括网盘和本地) void this.preloadNextSongInternal(); } } private async playIndex(index: number) { if (!this.debounce()) { return; } if (ArrayUtil.isNotEmpty(this.songList)) { this.applySelectedQueueSong(index) const playIndexPreparation = await this.prepareCurrentSongPlayback('playIndex') if (!playIndexPreparation.success) { return; } // 如果是投播模式,需要为投播设备准备当前歌曲 if (this.castController && this.isCastPlaying) { Logger.info('heanup playIndex', '投播模式播放指定歌曲,不调用本地stop()'); this.initQueueItem(); await this.prepare(); if (playIndexPreparation.loadingShown) { this.hideLoadIng(); } const playIndexSong = this.currentSong if (!playIndexSong) { return; } this.cover = playIndexSong.pixelMapPath; return; } // 本地播放器模式,需要先stop this.finalizeLocalTrackSwitch(playIndexPreparation.loadingShown) } } //上一个 private async playPrevious() { if (this.isHostSnapshotDisplayOnly) { await BackgroundAudioPlaybackHost.getInstance().getRuntime().playPrevious() return } if (!this.debounce()) { return; } const previousPlaybackAction = MusicPlaybackController.resolvePreviousPlaybackAction(this.playType) if (previousPlaybackAction === 'history_previous') { this.randomModePlayFromHistory() return; } if (ArrayUtil.isEmpty(this.songList)) { return; } this.curIndex = MusicPlaybackController.resolvePreviousQueueIndex(this.curIndex, this.songList.length).nextIndex; // 直接使用歌曲列表中的歌曲信息,songList中的数据已经是完整的 this.applySelectedQueueSong(this.curIndex) const previousSong = this.currentSong if (!previousSong) { return; } Logger.info('heanup playPrevious', `直接使用歌曲列表中的信息: ${previousSong.name}, type: ${previousSong.type}`); const isCurrentSongAccountAvailable = await this.ensureRemoteSongAccountAvailable(previousSong, 'playPrevious') if (!isCurrentSongAccountAvailable) { this.curIndex = MusicPlaybackController.resolvePreviousQueueIndex(this.curIndex, this.songList.length).nextIndex; this.applySelectedQueueSong(this.curIndex) } await this.dispatchPreparedQueueSwitch( 'playPrevious', '播放上一首', async (durationMs: number): Promise => { await this.castControllerWrapper!.playPrevious(this.songList, this.curIndex, this.videoUrl, durationMs); }, true ) } //随机播放模式下点击上一首: 播放刚刚播放过的那一首 private async randomModePlayFromHistory() { if (this.randomPlayHistory.length >= 1) { // 播放历史栈的第一首(刚刚播放过的) const prevSong = this.randomPlayHistory[0]; Logger.info('heanup randomModePlayFromHistory', `从随机播放历史栈获取歌曲: ${prevSong.name}, type: ${prevSong.type}, 栈大小: ${this.randomPlayHistory.length}`); // 从栈中移除这首歌(因为即将播放它,它应该成为当前歌,而不是历史) this.randomPlayHistory.shift(); this.curIndex = this.ensureSongQueued(prevSong); this.CONTROL_PlayStatus = PlayStatus.INIT; this.stop(); const isHistorySongAccountAvailable = await this.ensureRemoteSongAccountAvailable(prevSong, 'randomModePlayFromHistory') if (!isHistorySongAccountAvailable) { if (this.randomPlayHistory.length >= 1) { this.randomModePlayFromHistory(); return; } else { ToastUtil.showToast('没有更多历史记录'); return; } } this.songList[this.curIndex] = prevSong; this.currentSong = this.songList[this.curIndex]; const didStartHistorySong = await this.startPreparedCurrentSongPlayback('randomPlay-history', false) if (!didStartHistorySong) { return; } } else { ToastUtil.showToast('随机播放模式暂无历史记录'); } } 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.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(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) // .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(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None) .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({ left:5,top:5,bottom:5,right:20} ) .onClick(() => { this.selectSingleItemForDelete(item) this.showWarnIsDelete() // 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 = "广告加载中..." } /** * 展示Banner广告 */ // showBannerAd() { // if (!this.bannerAd) { // return // } // this.isShowAd = true // this.status = "已展示" // } /** * 穿山甲广告代码结束 */ /** * 处理歌单播放请求 */ private syncWebdavQueueWithoutRestart(): void { try { if (!this.currentSong || !isWebDavType(this.currentSong.type)) { return; } const webdavList = getWebdavVideoItems(); if (!ArrayUtil.isNotEmpty(webdavList)) { return; } const currentPath = this.currentSong.filePath; let targetIndex = webdavList.findIndex((item: VideoItem) => item.filePath === currentPath); if (targetIndex < 0) { const memoryIndex = getWebdavCurrentPlayIndex(); if (memoryIndex >= 0 && memoryIndex < webdavList.length) { targetIndex = memoryIndex; } } if (targetIndex < 0) { targetIndex = 0; } this.songList = [...webdavList]; this.currentSongList = [...webdavList]; this.sonDataSource.pushArrayData(this.songList); this.currentPlaylistSongFilePaths = this.songList.map(song => song.filePath); this.curIndex = targetIndex; PreferencesUtil.putSync('LastMusicList', this.songList); this.skipNextPlaylistPersist = true Logger.info(TAG, `WebDAV播放队列已刷新,当前索引=${this.curIndex}, 队列长度=${this.songList.length}`); } catch (error) { Logger.error(TAG, `刷新WebDAV播放队列失败: ${(error as Error).message}`); } } private async handlePlaylistPlayRequest(playlistId: string, playlistName: string, songFilePaths: string[], startIndex: number, isJump: boolean, songCount?: number) { try { const totalCount = songCount ?? songFilePaths.length; Logger.info(`heanup 处理歌单播放请求: ${playlistName}, 已加载: ${songFilePaths.length}, 服务端总数: ${totalCount}, 开始索引: ${startIndex}`) // 检查是否为WebDAV播放请求 if (playlistId === 'webdav-playlist') { Logger.info(`heanup 检测到WebDAV播放请求,从内存读取videoItems`) // 从全局内存读取videoItems数据 const videoItems = getWebdavVideoItems(); const currentPlayIndex = getWebdavCurrentPlayIndex(); Logger.info(`heanup 从内存读取到WebDAV歌曲列表,长度:${videoItems.length},索引:${currentPlayIndex}`) if (videoItems && videoItems.length > 0) { // 验证webdav_account_id设置情况 const accountIds = videoItems.map(item => item.webdav_account_id).filter(id => id); Logger.info(`heanup 有 ${accountIds.length}/${videoItems.length} 首歌曲设置了webdav_account_id`) if(isJump){ this.setShowPlayTrue()//显示播放页 } this.finishLoadingPlaylist(videoItems, currentPlayIndex, playlistName, totalCount) return } else { return } } if (playlistId === 'navidrome-playlist') { Logger.info(`heanup 检测到Navidrome播放请求,从内存读取videoItems`); const videoItems = getNavidromeVideoItems(); const currentPlayIndex = getNavidromeCurrentPlayIndex(); Logger.info(`heanup Navidrome歌曲列表长度: ${videoItems.length}, 服务端总数: ${totalCount}`); if (videoItems && videoItems.length > 0) { if(isJump){ this.setShowPlayTrue()//显示播放页 } this.finishLoadingPlaylist(videoItems, currentPlayIndex, playlistName, totalCount); return; } else { Logger.warn('heanup Navidrome播放请求缺少歌曲数据'); return; } } if (playlistId === 'find-album-playlist' || playlistId.indexOf('find-') === 0) { Logger.info(`heanup 检测到发现页播放请求,从内存读取videoItems`) const videoItems = getFindVideoItems(); const currentPlayIndex = getFindCurrentPlayIndex(); const findPlaylistMeta = getFindPlaylistMeta(); Logger.info(`heanup 发现页歌曲列表长度: ${videoItems.length}, 索引: ${currentPlayIndex}`) Logger.info( TAG, `[remote-debug] handlePlaylistPlayRequest find-memory playlist=${playlistId}, loaded=${videoItems.length}, ` + `declaredTotal=${totalCount}, sample=${this.buildSongListDebugLog(videoItems)}` ) if (videoItems && videoItems.length > 0) { if (isJump) { this.setShowPlayTrue() } // 只有“随心所欲分页随机”才读取分页元数据,其他发现页队列仍按原有歌单模式处理。 const findMeta = isFindLocalRandomPagedPlaylist(playlistId) && findPlaylistMeta.isPagedLocalQueue ? findPlaylistMeta : undefined; this.finishLoadingPlaylist(videoItems, currentPlayIndex, playlistName, totalCount, findMeta) return } else { Logger.warn('heanup 发现页播放请求缺少歌曲数据') return } } // 从数据库重新加载这些歌曲,使用索引记录位置以保持顺序 const songMap: Map = new Map() let completedQueries = 0 Logger.info(`heanup 开始从数据库查询 ${songFilePaths.length} 首歌曲`) // 遍历所有文件路径,使用索引记录位置 for (let i = 0; i < songFilePaths.length; i++) { const filePath = songFilePaths[i] const index = i // Logger.info(`heanup 正在查询歌曲[${index}]: ${filePath}`) // 使用已初始化的MediaTable实例从数据库查询歌曲信息 const queryPromise = this.table.queryVideoByFilePath(filePath) // Logger.info(`heanup 创建了查询Promise,开始等待结果[${index}]: ${filePath}`) queryPromise.then((videoItem) => { completedQueries++ // Logger.info(`heanup 查询Promise返回结果[${index}]: ${filePath}, 找到歌曲: ${videoItem ? videoItem.name : 'null'}`) if (videoItem) { // 使用索引作为key保存到Map中,保持原始顺序 songMap.set(index, videoItem) // Logger.info(`heanup 从数据库找到歌曲[${index}]: ${videoItem.name} (${completedQueries}/${songFilePaths.length})`) } else { Logger.warn(`heanup 数据库中未找到歌曲[${index}]: ${filePath} (${completedQueries}/${songFilePaths.length})`) } // 检查是否所有歌曲都已加载完成 if (completedQueries === songFilePaths.length) { // Logger.info(`heanup 所有数据库查询完成,共找到 ${songMap.size} 首歌曲`) // 按照索引顺序重建歌曲数组 const songs: VideoItem[] = [] for (let j = 0; j < songFilePaths.length; j++) { const song = songMap.get(j) if (song) { songs.push(song) // Logger.info(`heanup 按顺序添加歌曲[${j}]: ${song.name}`) } } Logger.info(`heanup 最终歌曲列表顺序: ${songs.map((s, idx) => `[${idx}]${s.name}`).join(', ')}`) this.finishLoadingPlaylist(songs, startIndex, playlistName) } }).catch((error: Error) => { completedQueries++ Logger.error(`heanup 查询歌曲失败[${index}]: ${filePath}, 错误: ${error.message} (${completedQueries}/${songFilePaths.length})`) // 即使出错也要检查是否完成所有查询 if (completedQueries === songFilePaths.length) { Logger.info(`heanup 所有数据库查询完成(包含错误),共找到 ${songMap.size} 首歌曲`) // 按照索引顺序重建歌曲数组 const songs: VideoItem[] = [] for (let j = 0; j < songFilePaths.length; j++) { const song = songMap.get(j) if (song) { songs.push(song) } } this.finishLoadingPlaylist(songs, startIndex, playlistName) } }) } } catch (error) { Logger.error('heanup 处理简化歌单播放请求失败: ' + error) ToastUtil.showToast('播放失败') } } /** * 完成歌单加载并开始播放 */ private finishLoadingPlaylist(songs: VideoItem[], startIndex: number, playlistName: string, totalCount?: number, findMeta?: FindPlaylistMeta) { if (songs.length === 0) { Logger.error('heanup 没有找到任何可播放的歌曲') ToastUtil.showToast('没有找到可播放的歌曲') return } const isFindPagedLocalQueue = !!findMeta?.isPagedLocalQueue; this.songList = songs this.currentSongList = songs // 同步更新 currentSongList this.sonDataSource.pushArrayData(songs) this.curIndex = startIndex const actualTotal = totalCount ?? songs.length; if (isFindPagedLocalQueue) { // 这类队列只保存当前页歌曲,总数和分页游标放到独立状态里继续滚动加载。 this.applyFindPagedLocalQueueState(findMeta, actualTotal); this.currentPlaylistSongFilePaths = []; } else { this.resetFindPagedLocalQueueState(); this.currentPlaylistSongFilePaths = songs.map(song => song.filePath); } // 如果是网盘播放,保存服务端总数到全局存储 if (!isFindPagedLocalQueue && totalCount && totalCount > 0 && songs.length > 0) { const firstSong = songs[0]; if (firstSong && (isNavidromeType(firstSong.type) || isJellyfinType(firstSong.type) || isEmbyType(firstSong.type) || isAudioStationType(firstSong.type) || isPlexType(firstSong.type) || isDaoLiYuType(firstSong.type))) { setNavidromeTotalCount(totalCount); Logger.info('heanup finishLoadingPlaylist', `已保存服务端总数到全局存储: ${totalCount}`); } } Logger.info('heanup finishLoadingPlaylist', `保存歌单歌曲路径列表,已加载=${this.currentPlaylistSongFilePaths.length}, 服务端总数=${actualTotal}`); Logger.info( TAG, `[remote-debug] finishLoadingPlaylist playlist=${playlistName}, loaded=${songs.length}, actualTotal=${actualTotal}, ` + `startIndex=${startIndex}, sample=${this.buildSongListDebugLog(songs)}` ); if (songs[startIndex]) { // 确保当前播放的歌曲也更新到存储 AppStorage.setOrCreate('currentSong', songs[startIndex]) if (isFindPagedLocalQueue) { // 不要把发现页分页随机队列降级成普通歌单,否则后面又会触发全量 hydrate。 this.preserveExistingQueueOnNextPlay = true this.nextPlayQueueScope = 'find_paged_local' this.doPlay(songs[startIndex], startIndex, false) } else { // 设置isFromSonPlayList=true,避免doPlay方法重新从全局列表构建播放列表 this.doPlay(songs[startIndex], startIndex, true) } } PreferencesUtil.putSync('LastMusicList', this.songList) this.skipNextPlaylistPersist = true // ToastUtil.showToast(`开始播放歌单: ${playlistName}`) } } //视频气泡窗口的布局 @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) }) }) } .scrollBar(BarState.Off) .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 }) } .onClick(() => { dataBu.onItemClick?.(index) }) }) } .scrollBar(BarState.Off) .width(150) .divider({ strokeWidth: 1, color: "#22FFFFFF" }) } .height(300) .padding({ bottom: 30 }) } @Concurrent function buildAlphabetIndexTask(items: Array, sortType: number, isShowFileName: boolean): Array { const alphabetSet = new Set() for (const item of items) { let text = isShowFileName ? (item.fileName || '') : (item.type != CommonConstants.TYPE_IS_DIR && item.pyStr) ? item.pyStr : (item.name || '') if (sortType === 0 || sortType == 1) { text = item.artist || '' } else if (sortType === 2 || sortType == 3) { text = item.album || '' } if (text && text.length > 0) { const first = getFirstLetter(text) if (!alphabetSet.has(first)) { alphabetSet.add(first) } } } const sorted = Array.from(alphabetSet).sort((a, b) => { if (sortType === 4 || sortType == 0 || sortType == 2 || sortType == 6) { return a.localeCompare(b) } return b.localeCompare(a) }) return sorted } 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 } //只扫描当前目录下文件(不扫描子文件夹)的方法 @Concurrent async function scanCurrentDirectoryTask(context: Context, dirPath: string, lockPath: string, cover_api: string) { const table: MediaTable = new MediaTable(context); try { // 直接获取当前目录下的文件列表 const files = FileUtil.listFileSync(dirPath); await Promise.all(files.map(async (file) => { const fPath = `${dirPath}/${file}`; // 跳过目录,只处理文件 if (FileUtil.isDirectory(fPath)) { return; } // 跳过特定格式文件 if (fPath.endsWith('.lrc') || fPath.endsWith('.srt')) { return; } // 检查是否为媒体文件 if (Utility.isMeidaByExtension(fPath)) { let mType = CommonConstants.TYPE_LOCAL; if (fPath.includes(lockPath)) { mType = CommonConstants.TYPE_LOCK; } const mediaItem = await Utility.uriGetMusicAssetsFromFile( context, fPath, mType, true ); table.insert(mediaItem, (id: number) => { // 插入回调函数 }, cover_api); } })); } catch (error) { console.error(' 扫描当前目录失败:', error); } } //内嵌标签后更新库 @Concurrent async function updateInfoDb( context: Context, filePath: string, titleStr: string, artistStr: string, albumStr: string, lyricConStr: string, yearStr: string, genreStr: string, trackStr: string, albumArtistStr: string, composerStr: string, lyricistStr: string, commentStr: string, discStr: string, ): Promise { const table: MediaTable = new MediaTable(context); await new Promise((resolve, reject) => { table.getRdbStore(context, (err:Error) => { err ? reject(err) : resolve(); }); }); return await new Promise((resolve) => { table.updateMediaInfo(filePath, titleStr, artistStr, albumStr, lyricConStr, yearStr, genreStr, trackStr, albumArtistStr, composerStr, lyricistStr, commentStr, discStr, (success: boolean, error?: string) => { if (success) { console.log("onecold 编辑信息成功,数据库已同步"); } else { console.error("onecold 编辑信息数据库失败原因: " + error); } resolve(success); }); }); } @Concurrent async function replaceLocalMusicCoverTask( context: Context, filePath: string, sandboxCoverPath: string ): Promise { const coverResult: boolean = await changeMusicCover(context, filePath, sandboxCoverPath, true, ''); if (!coverResult) { return false; } const table: MediaTable = new MediaTable(context); await new Promise((resolve, reject) => { table.getRdbStore(context, (err: Error) => { err ? reject(err) : resolve(); }); }); return await new Promise((resolve) => { table.updateCustomCoverPath(filePath, fileUri.getUriFromPath(sandboxCoverPath), (success: boolean, _error?: string) => { resolve(success); }); }); }