import { BreadcrumbItem, RemoteDriveGlobalSearchResult, RemoteDriveManager } from '../common/util/RemoteDriveManager'; import { WebDavAccount } from '../viewmodel/WebDavAccount'; import { Song } from '../viewmodel/Song'; import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates'; import Logger from '../common/util/Logger'; import { SymbolGlyphModifier, router, curves } from '@kit.ArkUI'; import { CommonConstants } from '../common/constants/CommonConstants'; import { VideoItem } from '../viewmodel/VideoItem'; import { GlobalContext } from '../common/util/GlobalContext'; import { FileInfo } from '../viewmodel/FileInfo'; import { emitter } from '@kit.BasicServicesKit'; import { EventConstants } from '../common/constants/EventConstants'; import { LazyDataSource } from '../common/util/LazyDataSource'; import { ArrayUtil, FileUtil, MD5, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'; import { DialogHelper } from '@pura/harmony-dialog'; import { ButtonFancyModifier, MenuModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'; import { getRemoteDriveAccountLabel, getRemoteDriveDisplayLabel, getRemoteDrivePlaylistPrefix } from '../common/util/RemoteDriveLabel'; import { RemoteDriveType } from '../common/enums/RemoteDriveType'; import { Utility } from '../common/util/Utility'; import { SettingPage } from './SettingPage'; import { getCloudDiskIcon } from '../dialog/RemoteDriveAccountDialog'; import { CreateFolderDialog } from '../dialog/CreateFolderDialog'; import { UploadMusicPage } from './UploadMusicPage'; import { FFmpeg } from '@sj/ffmpeg'; import FileManager from '../common/util/FileManager'; import { fileIo, fileUri, picker } from '@kit.CoreFileKit'; import { setVideoUrlForSong } from '../common/util/RemotePlayerUtil'; import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog'; import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog'; import { Playlist } from '../viewmodel/Playlist'; import PlaylistTable from '../common/util/PlaylistTable'; import MediaTable from '../common/util/MediaTable'; import { DownloadCenterManager } from '../common/util/DownloadCenterManager'; import { DownloadCenter } from '../view/DownloadCenter'; /** * 歌单播放事件数据 */ interface PlaylistEventData { playlistId: string; playlistName: string; songCount: number; startIndex: number; isJump: boolean; songFilePaths: string[]; // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id } interface WebDavMetadataUpdatePayload { filePath: string; pixelMapPath?: string; name?: string; artist?: string; } interface RemoteThumbSource { playUrl: string; headers: Map; } const TAG = 'heanup WebDavMainPage'; const REMOTE_THUMB_CACHE_DIR: string = 'remote_thumbs'; const REMOTE_THUMB_MAX_TASK_COUNT: number = 36; const REMOTE_THUMB_CAPTURE_SECONDS: string = '1.2'; // WebDAV歌曲数据全局内存存储 let globalWebdavVideoItems: VideoItem[] = []; let globalWebdavCurrentPlayIndex: number = 0; let globalRemoteThumbFfmpegQueue: Promise = Promise.resolve(); // 导出函数供LocalMusic访问 export function getWebdavVideoItems(): VideoItem[] { return globalWebdavVideoItems; } export function getWebdavCurrentPlayIndex(): number { return globalWebdavCurrentPlayIndex; } export function clearWebdavVideoItems(): void { globalWebdavVideoItems = []; globalWebdavCurrentPlayIndex = 0; } // URL解码函数 function decodeUrlEncodedString(encodedStr: string): string { try { return decodeURIComponent(encodedStr); } catch (error) { // 如果解码失败,返回原始字符串 return encodedStr; } } @Preview @Entry @Component export struct WebDavMainPage { @State appName: string = '' @State isShowUploadFile: boolean = false @State isShowTitleBar: boolean = true //是否显示分类导航条 private prevOffsetY: number = 0; // 记录上一次的Y轴偏移量 @State autoHideTitle: boolean = true //滚动自动隐藏标题栏 @State isCustomizeBg: boolean = false //自定义背景界面 @StorageProp('isLandscape') isLandscape: boolean = false; @State blurValue: number = 0 //背景模糊 @State bgBrightness: number = 0 //背景亮度 @State customizeBgPath: string | undefined = ''; private listScroller: ListScroller = new ListScroller() @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0; searchController: SearchController = new SearchController() private searchTicket: number = 0; @StorageProp('animationState') animationState: AnimationStatus= AnimationStatus.Running @State isNoJumpToHome: boolean = false //网盘播放不跳转首页 @State isSearchMode: boolean = false @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined; @StorageProp('topSafeHeight') topSafeHeight: number = 0; @State webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance(); @State accounts: WebDavAccount[] = []; @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount; @State songs: VideoItem[] = []; @State dataSource:LazyDataSource = new LazyDataSource(this.songs) @Link mType: number; @Link offsetX: number; @Link isShowDrawer: boolean; @State isLoading: boolean = false; @State webDavFiles: FileInfo[] = []; // 直接在页面中保存文件列表副本 @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; @StorageProp('isDarkMode') isDarkMode: boolean = false; @State breadcrumbs:BreadcrumbItem[] = []//面包屑导航 @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量 @State isShowFileName: boolean = false//是否显示文件名 @State isLongNameRoLL: boolean = true//长歌名滚动 @State sortType: number = 0 //默认排序方式 @State listRefreshKey: number = 0 // 列表刷新标识 @Consume isMultiSelect: boolean @State selectedSongs: VideoItem[] = [] @State isAllSelected: boolean = false @State isDeletingSelection: boolean = false @State ignoreTapAfterExitMultiSelect: boolean = false @State isShowSongPropertySheet: boolean = false @State songForPropertySheet: VideoItem | undefined = undefined @State rootPath: string = '' @State isShowDownloadCenter: boolean = false @State downloadCenterTabIndex: number[] = [0] @State isSearchLoading: boolean = false private readonly downloadFolderName: string = '下载' private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED }; private thumbnailTaskToken: number = 0; private thumbnailRunningKeys: Set = new Set(); private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance(); private async runSerializedThumbnailFfmpeg(task: () => Promise): Promise { const nextTask: Promise = globalRemoteThumbFfmpegQueue.then(async (): Promise => { await task(); }, async (): Promise => { await task(); }); globalRemoteThumbFfmpegQueue = nextTask.catch((): void => {}); await nextTask; } async onSwitchAccount(){ console.log('heanup 切换账户:', this.selectedAccount.name); this.thumbnailTaskToken += 1; this.thumbnailRunningKeys.clear(); this.searchTicket++; this.searchText = ''; this.filteredList = []; this.filteredFolderList = []; this.isSearchMode = false; this.isSearchLoading = false; this.songs = []; this.visibleFoldersState = []; this.updateListData(this.songs) this.exitMultiSelect(); // 注意:不再需要清空全局上下文,因为LocalMusic已改用实例变量缓存 // 当新账户加载时,新的认证信息会自动覆盖旧的 Logger.info(TAG, 'heanup 切换账户,准备加载新账户文件'); this.isLoading = true; await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount) .catch((error: Error) => { Logger.error(TAG, '加载文件失败: ' + error.message); this.isLoading = false; }); this.breadcrumbs = this.webdavManager.getBreadcrumbs(); } updateListData(mList:Array, noSort?: boolean){ if (!noSort) { this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0) this.doSortType(this.sortType) return } this.refreshDisplaySongs(mList) } private refreshDisplaySongs(mList: Array): void { this.dataSource.pushArrayData(mList) if(mList.length > 0){ setTimeout(() => { this.listScroller.scrollToIndex(0) },200) } } private buildThumbnailIdentity(item: VideoItem): string { const accountId: string = item.webdav_account_id ? item.webdav_account_id : ''; const relPath: string = item.remote_rel_path ? item.remote_rel_path : ''; return `${item.type}:${accountId}:${relPath}:${item.filePath}`; } private async ensureThumbnailCacheDir(): Promise { const context = getContext(this); const cacheDir: string = `${context.filesDir}/${REMOTE_THUMB_CACHE_DIR}`; await FileManager.createDir(cacheDir); return cacheDir; } private async resolveRemoteThumbPath(item: VideoItem): Promise { const cacheDir: string = await this.ensureThumbnailCacheDir(); const identity: string = this.buildThumbnailIdentity(item); const hashName: string = MD5.digestSync(identity); return `${cacheDir}/${hashName}.jpg`; } private buildFfmpegHeadersArg(headers: Map): string { if (!headers || headers.size <= 0) { return ''; } const lines: string[] = []; headers.forEach((value: string, key: string): void => { if (StrUtil.isEmpty(key) || StrUtil.isEmpty(value)) { return; } lines.push(`${key}: ${value}`); }); if (lines.length <= 0) { return ''; } return `${lines.join('\r\n')}\r\n`; } private buildRemoteThumbFfmpegCmd(playUrl: string, headersArg: string, outputPath: string): string[] { const commands: string[] = ['ffmpeg', '-y', '-ss', REMOTE_THUMB_CAPTURE_SECONDS]; const networkArgs: string[] = this.buildRemoteFfmpegNetworkArgs(playUrl); for (let index: number = 0; index < networkArgs.length; index += 1) { commands.push(networkArgs[index]); } if (StrUtil.isNotEmpty(headersArg)) { commands.push('-headers', headersArg); } commands.push( '-i', playUrl, '-frames:v', '1', '-vf', 'scale=320:-2', '-q:v', '4', outputPath ); return commands; } private buildRemoteAudioCoverFfmpegCmd(playUrl: string, headersArg: string, outputPath: string): string[] { const commands: string[] = ['ffmpeg', '-y']; const networkArgs: string[] = this.buildRemoteFfmpegNetworkArgs(playUrl); for (let index: number = 0; index < networkArgs.length; index += 1) { commands.push(networkArgs[index]); } if (StrUtil.isNotEmpty(headersArg)) { commands.push('-headers', headersArg); } commands.push( '-i', playUrl, '-map', '0:v:0', '-frames:v', '1', '-q:v', '4', outputPath ); return commands; } private buildRemoteFfmpegNetworkArgs(playUrl: string): string[] { if (StrUtil.isEmpty(playUrl)) { return []; } const lowerPlayUrl: string = playUrl.toLowerCase(); if (!lowerPlayUrl.startsWith('http://') && !lowerPlayUrl.startsWith('https://')) { return []; } return ['-rw_timeout', '15000000', '-probesize', '1048576', '-analyzeduration', '2000000']; } private sanitizeFfmpegCommands(commands: string[]): string[] { const safeCommands: string[] = []; for (let index: number = 0; index < commands.length; index++) { const value: string = commands[index]; if (value === undefined || value === null) { Logger.warn(TAG, `缩略图命令存在空参数 index=${index}`); continue; } const text: string = `${value}`; if (text.length <= 0) { Logger.warn(TAG, `缩略图命令存在空字符串参数 index=${index}`); continue; } safeCommands.push(text); } return safeCommands; } private async executeSafeFfmpeg(commands: string[]): Promise { // 兼容部分版本 @sj/ffmpeg 对第二个参数空值处理不安全的问题 await FFmpeg.execute(commands, {}); } private normalizeThumbImageSource(path: string): string { if (StrUtil.isEmpty(path)) { return ''; } if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('file://')) { return path; } if (path.startsWith('/')) { return fileUri.getUriFromPath(path); } return path; } private normalizeThumbLocalPath(path: string): string { if (StrUtil.isEmpty(path)) { return ''; } if (path.startsWith('file://')) { try { return new fileUri.FileUri(path).path; } catch (error) { const err: Error = error as Error; Logger.warn(TAG, `缩略图URI转路径失败 uri=${path}, error=${err.message}`); return ''; } } return path; } private isThumbCacheValid(localPath: string): boolean { if (StrUtil.isEmpty(localPath)) { return false; } if (!FileUtil.accessSync(localPath)) { return false; } try { const stat: fileIo.Stat = fileIo.statSync(localPath); if (stat.size <= 1024) { Logger.warn(TAG, `缩略图缓存文件过小,判定无效 path=${localPath}, size=${stat.size}`); return false; } if (this.isImageMagicValid(localPath)) { return true; } Logger.warn(TAG, `缩略图缓存文件头无效,判定无效 path=${localPath}, size=${stat.size}`); } catch (error) { const err: Error = error as Error; Logger.warn(TAG, `缩略图缓存校验失败 path=${localPath}, error=${err.message}`); } return false; } private isImageMagicValid(localPath: string): boolean { let file: fileIo.File | undefined = undefined; try { file = fileIo.openSync(localPath, fileIo.OpenMode.READ_ONLY); const headerBuffer: ArrayBuffer = new ArrayBuffer(12); const readLen: number = fileIo.readSync(file.fd, headerBuffer); if (readLen < 4) { return false; } const bytes: Uint8Array = new Uint8Array(headerBuffer); const isJpg: boolean = bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF; const isPng: boolean = readLen >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47 && bytes[4] === 0x0D && bytes[5] === 0x0A && bytes[6] === 0x1A && bytes[7] === 0x0A; const isGif: boolean = readLen >= 4 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38; const isBmp: boolean = bytes[0] === 0x42 && bytes[1] === 0x4D; const isWebp: boolean = readLen >= 12 && bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50; return isJpg || isPng || isGif || isBmp || isWebp; } catch (error) { const err: Error = error as Error; Logger.warn(TAG, `缩略图文件头校验失败 path=${localPath}, error=${err.message}`); return false; } finally { if (file !== undefined) { try { fileIo.closeSync(file); } catch (error) { const err: Error = error as Error; Logger.warn(TAG, `关闭缩略图文件失败 path=${localPath}, error=${err.message}`); } } } } private removeInvalidThumbCache(localPath: string): void { if (StrUtil.isEmpty(localPath) || !FileUtil.accessSync(localPath)) { return; } try { fileIo.unlinkSync(localPath); Logger.info(TAG, `已删除无效缩略图缓存 path=${localPath}`); } catch (error) { const err: Error = error as Error; Logger.warn(TAG, `删除无效缩略图缓存失败 path=${localPath}, error=${err.message}`); } } private resolveFileNameForTypeCheck(item: VideoItem): string { const rawName: string = item.fileName ?? item.remote_rel_path ?? item.filePath; if (StrUtil.isEmpty(rawName)) { return ''; } const hashIndex = rawName.indexOf('#'); const queryIndex = rawName.indexOf('?'); let endIndex = rawName.length; if (queryIndex >= 0) { endIndex = queryIndex; } if (hashIndex >= 0 && hashIndex < endIndex) { endIndex = hashIndex; } return rawName.substring(0, endIndex); } private isVideoMediaItem(item: VideoItem): boolean { if (item.mimeType && item.mimeType.toLowerCase().startsWith('video/')) { return true; } const fileNameForCheck: string = this.resolveFileNameForTypeCheck(item); return Utility.isVideoByExtension(fileNameForCheck); } private refreshThumbItem(item: VideoItem): void { const dataIndex: number = this.dataSource.dataArray.indexOf(item); if (dataIndex >= 0) { this.dataSource.notifyDataChange(dataIndex); } this.listRefreshKey += 1; } private async prepareRemoteThumbSource(item: VideoItem): Promise { let playUrl: string = item.filePath; if (StrUtil.isNotEmpty(playUrl)) { const lowerPath = playUrl.toLowerCase(); const isDirectPath = lowerPath.startsWith('http://') || lowerPath.startsWith('https://') || lowerPath.startsWith('/') || lowerPath.startsWith('file://'); if (!isDirectPath) { try { playUrl = await setVideoUrlForSong(item, { extractCover: false, extractLyric: false, extractAudioInfo: false }); } catch (error) { Logger.warn(TAG, `解析缩略图播放地址失败 name=${item.name}, error=${(error as Error).message}`); } } } const headers: Map = await this.webdavManager.buildHttpHeadersWithAccountId(item, playUrl); const source: RemoteThumbSource = { playUrl: playUrl, headers: headers }; return source; } private async tryResolveRemoteMusicCover(item: VideoItem, token: number): Promise { const coverPath: string = await this.resolveRemoteThumbPath(item); if (this.isThumbCacheValid(coverPath)) { item.pixelMapPath = this.normalizeThumbImageSource(coverPath); this.refreshThumbItem(item); return; } this.removeInvalidThumbCache(coverPath); const source: RemoteThumbSource = await this.prepareRemoteThumbSource(item); if (StrUtil.isEmpty(source.playUrl)) { return; } const headersArg: string = this.buildFfmpegHeadersArg(source.headers); const rawCommands: string[] = this.buildRemoteAudioCoverFfmpegCmd(source.playUrl, headersArg, coverPath); const commands: string[] = this.sanitizeFfmpegCommands(rawCommands); if (commands.length < 2) { return; } await this.runSerializedThumbnailFfmpeg(async (): Promise => { if (token !== this.thumbnailTaskToken) { return; } await this.executeSafeFfmpeg(commands); }); if (token !== this.thumbnailTaskToken) { return; } if (this.isThumbCacheValid(coverPath)) { item.pixelMapPath = this.normalizeThumbImageSource(coverPath); this.refreshThumbItem(item); } } private async generateRemoteVideoThumb(item: VideoItem, token: number): Promise { if (token !== this.thumbnailTaskToken) { return; } const identity: string = this.buildThumbnailIdentity(item); if (this.thumbnailRunningKeys.has(identity)) { return; } this.thumbnailRunningKeys.add(identity); try { const isVideoFile: boolean = this.isVideoMediaItem(item); if (!isVideoFile) { await this.tryResolveRemoteMusicCover(item, token); return; } const thumbPath: string = await this.resolveRemoteThumbPath(item); if (this.isThumbCacheValid(thumbPath)) { item.pixelMapPath = this.normalizeThumbImageSource(thumbPath); this.refreshThumbItem(item); return; } this.removeInvalidThumbCache(thumbPath); const source = await this.prepareRemoteThumbSource(item); if (StrUtil.isEmpty(source.playUrl)) { return; } const headersArg: string = this.buildFfmpegHeadersArg(source.headers); const rawCommands: string[] = this.buildRemoteThumbFfmpegCmd(source.playUrl, headersArg, thumbPath); const commands: string[] = this.sanitizeFfmpegCommands(rawCommands); if (commands.length < 2) { return; } await this.runSerializedThumbnailFfmpeg(async (): Promise => { if (token !== this.thumbnailTaskToken) { return; } await this.executeSafeFfmpeg(commands); }); if (token !== this.thumbnailTaskToken) { return; } if (this.isThumbCacheValid(thumbPath)) { item.pixelMapPath = this.normalizeThumbImageSource(thumbPath); this.refreshThumbItem(item); } } catch (error) { const err: Error = error as Error; Logger.info(TAG, `远程缩略图/封面生成失败 name=${item.name}, error=${err.message}`); } finally { this.thumbnailRunningKeys.delete(identity); } } private scheduleRemoteThumbPrefetch(): void { const token: number = this.thumbnailTaskToken + 1; this.thumbnailTaskToken = token; this.thumbnailRunningKeys.clear(); const mediaItems: VideoItem[] = this.songs.slice(); if (mediaItems.length <= 0) { return; } const limitCount: number = Math.min(mediaItems.length, REMOTE_THUMB_MAX_TASK_COUNT); const targetItems: VideoItem[] = mediaItems.slice(0, limitCount); setTimeout((): void => { void this.runRemoteThumbPrefetchQueue(targetItems, token); }, 120); } private async runRemoteThumbPrefetchQueue(items: VideoItem[], token: number): Promise { for (let index: number = 0; index < items.length; index += 1) { if (token !== this.thumbnailTaskToken) { return; } const item: VideoItem = items[index]; if (StrUtil.isNotEmpty(item.pixelMapPath)) { const currentThumbRef: string = item.pixelMapPath ? item.pixelMapPath : ''; if (currentThumbRef.startsWith('http')) { continue; } const currentThumbPath: string = this.normalizeThumbLocalPath(currentThumbRef); if (StrUtil.isNotEmpty(currentThumbPath) && this.isThumbCacheValid(currentThumbPath)) { continue; } } await this.generateRemoteVideoThumb(item, token); } } // 更新可见文件夹列表 private updateVisibleFolders(): void { try { // 安全检查webDavFiles if (!this.webDavFiles || !Array.isArray(this.webDavFiles)) { this.visibleFoldersState = []; return; } const allFolders = this.webDavFiles.filter(f => f.isDirectory); const isFlatFolderAccount = this.selectedAccount?.webType === RemoteDriveType.Navidrome || this.selectedAccount?.webType === RemoteDriveType.Jellyfin || this.selectedAccount?.webType === RemoteDriveType.Emby; if (isFlatFolderAccount) { const sortedNavFolders = allFolders.sort((a, b) => a.fileName.localeCompare(b.fileName)); this.visibleFoldersState = sortedNavFolders; return; } const visible: FileInfo[] = []; for (let i = 0; i < allFolders.length; i++) { const folder = allFolders[i]; // 安全检查folder对象 if (!folder || typeof folder.fileName !== 'string') { continue; } let shouldShow = this.isDirectChildOfCurrentPath(folder); if (shouldShow) { visible.push(folder); } } console.log('更新文件夹列表:', visible); this.visibleFoldersState = visible; } catch (error) { Logger.error(TAG, '更新文件夹列表失败:', error.toString()); this.visibleFoldersState = []; } this.isShowTitleBar = true } private isSongSelected(song: VideoItem): boolean { return this.selectedSongs.some(item => item.filePath === song.filePath); } private enterMultiSelect(song: VideoItem): void { if (!this.isMultiSelect) { this.isMultiSelect = true; this.selectedSongs = [song]; this.isAllSelected = this.selectedSongs.length === this.songs.length && this.songs.length > 0; return; } if (!this.isSongSelected(song)) { this.toggleSongSelection(song); } } private toggleSongSelection(song: VideoItem): void { if (!this.isMultiSelect) { this.isMultiSelect = true; } const exists = this.isSongSelected(song); if (exists) { const next = this.selectedSongs.filter(item => item.filePath !== song.filePath); this.selectedSongs = next; this.isAllSelected = next.length > 0 && next.length === this.songs.length; if (next.length === 0) { this.exitMultiSelect(); } } else { const next = [...this.selectedSongs, song]; this.selectedSongs = next; this.isAllSelected = next.length === this.songs.length && this.songs.length > 0; } } private handleCheckboxSelection(song: VideoItem, checked: boolean): void { if (this.ignoreTapAfterExitMultiSelect) { return; } if (!this.isMultiSelect) { this.isMultiSelect = true; } const exists = this.isSongSelected(song); if (checked && !exists) { this.toggleSongSelection(song); } else if (!checked && exists) { this.toggleSongSelection(song); } } private toggleSelectAll(): void { if (this.isAllSelected) { this.selectedSongs = []; this.isAllSelected = false; return; } if (!this.isMultiSelect) { this.isMultiSelect = true; } this.selectedSongs = this.songs.slice(); this.isAllSelected = this.selectedSongs.length > 0 && this.selectedSongs.length === this.songs.length; } private exitMultiSelect(): void { this.ignoreTapAfterExitMultiSelect = true; this.isMultiSelect = false; this.selectedSongs = []; this.isAllSelected = false; setTimeout(() => { this.isMultiSelect = false; this.ignoreTapAfterExitMultiSelect = false; }, 180); console.info('onecold this.isMultiSelect ' + this.isMultiSelect) } private syncSelectionAfterRefresh(): void { if (!this.isMultiSelect) { return; } const currentKeys: Set = new Set(this.songs.map(item => item.filePath)); const filtered = this.selectedSongs.filter(item => currentKeys.has(item.filePath)); if (filtered.length !== this.selectedSongs.length) { this.selectedSongs = filtered; } this.isAllSelected = filtered.length > 0 && filtered.length === this.songs.length; if (filtered.length === 0) { this.exitMultiSelect(); } } private confirmDeleteSelected(): void { if (!this.selectedAccount) { this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` }); return; } if (!this.isMultiSelect || this.selectedSongs.length === 0) { this.getUIContext().getPromptAction().showToast({ message: '请先选择要删除的文件' }); return; } this.getUIContext().showAlertDialog({ title: '删除文件', message: `确定删除选中的${this.selectedSongs.length}个文件吗?`, primaryButton: { value: '取消', action: () => {} }, secondaryButton: { value: '删除', fontColor: Color.Red, action: () => { void this.performDeleteSelected(); } } }); } private async performDeleteSelected(): Promise { if (!this.selectedAccount || this.selectedSongs.length === 0) { return; } this.isDeletingSelection = true; try { await this.webdavManager.deleteRemoteSongs(this.selectedAccount, this.selectedSongs.slice()); await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath); this.exitMultiSelect(); this.getUIContext().getPromptAction().showToast({ message: '删除成功' }); this.exitMultiSelect() } catch (error) { const err = error as Error; this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` }); } finally { this.isDeletingSelection = false; } } private confirmDeleteSingleSong(song: VideoItem): void { if (!this.selectedAccount) { this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` }); return; } this.getUIContext().showAlertDialog({ title: '删除文件', message: `确定删除 "${song.name}" 吗?`, primaryButton: { value: '取消', action: () => {} }, secondaryButton: { value: '删除', fontColor: Color.Red, action: () => { void this.performDeleteSingleSong(song); } } }); } private async performDeleteSingleSong(song: VideoItem): Promise { if (!this.selectedAccount) { return; } this.isDeletingSelection = true; try { await this.webdavManager.deleteRemoteSongs(this.selectedAccount, [song]); await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath); this.getUIContext().getPromptAction().showToast({ message: '删除成功' }); } catch (error) { const err = error as Error; this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` }); } finally { this.isDeletingSelection = false; } } private addSongToNextPlay(song: VideoItem): void { if (!song) { return; } const queue: VideoItem[] = globalWebdavVideoItems && globalWebdavVideoItems.length > 0 ? globalWebdavVideoItems : this.songs; if (!queue || queue.length === 0) { this.getUIContext().getPromptAction().showToast({ message: '当前列表为空' }); return; } const currentPath = this.currentSong?.filePath; if (StrUtil.isEmpty(currentPath)) { const targetIndex = this.songs.findIndex((item: VideoItem) => item.filePath === song.filePath); if (targetIndex >= 0) { this.playSong(song, targetIndex, false); this.getUIContext().getPromptAction().showToast({ message: '已开始播放' }); } return; } if (currentPath === song.filePath) { this.getUIContext().getPromptAction().showToast({ message: '当前正在播放这首歌' }); return; } let currentIndex = queue.findIndex((item: VideoItem) => item.filePath === currentPath); if (currentIndex < 0 && globalWebdavCurrentPlayIndex >= 0 && globalWebdavCurrentPlayIndex < queue.length) { currentIndex = globalWebdavCurrentPlayIndex; } if (currentIndex < 0) { currentIndex = 0; } const existIndex = queue.findIndex((item: VideoItem) => item.filePath === song.filePath); if (existIndex >= 0) { const movedSong = queue.splice(existIndex, 1)[0]; if (existIndex < currentIndex) { currentIndex -= 1; } queue.splice(currentIndex + 1, 0, movedSong); } else { queue.splice(currentIndex + 1, 0, song); } globalWebdavCurrentPlayIndex = currentIndex; globalWebdavVideoItems = queue; this.songs = [...queue]; this.dataSource.pushArrayData(this.songs); const eventQueueRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_QUEUE_REFRESH }; emitter.emit(eventQueueRefresh, { data: { source: 'webdav' } }); this.getUIContext().getPromptAction().showToast({ message: '已添加到下一首播放' }); } private cloneSongForDb(song: VideoItem): VideoItem { const dbSong = new VideoItem( song.name || '', song.id || song.filePath, song.filePath, song.type, song.videoSize || 0, song.cTime || Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm') ); dbSong.parentPath = song.parentPath; dbSong.isFav = song.isFav; dbSong.size = song.size; dbSong.pixelMapPath = song.pixelMapPath; dbSong.artist = song.artist; dbSong.album = song.album; dbSong.fileName = song.fileName; dbSong.duration = song.duration; dbSong.mimeType = song.mimeType; dbSong.sampleRate = song.sampleRate; dbSong.trackCount = song.trackCount; dbSong.lastPlayedStr = song.lastPlayedStr; dbSong.playCount = song.playCount; dbSong.lyricContent = song.lyricContent; dbSong.md5Str = song.md5Str; dbSong.extra_json = song.extra_json; dbSong.pyStr = song.pyStr; dbSong.bit_rate = song.bit_rate; dbSong.probe_score = song.probe_score; dbSong.year = song.year; dbSong.nb_streams = song.nb_streams; dbSong.nb_programs = song.nb_programs; dbSong.genre = song.genre; dbSong.track = song.track; dbSong.bits_per_raw_sample = song.bits_per_raw_sample; dbSong.channels = song.channels; dbSong.channel_layout = song.channel_layout; dbSong.start_time = song.start_time; dbSong.ALBUMARTIST = song.ALBUMARTIST; dbSong.COMPOSER = song.COMPOSER; dbSong.LYRICIST = song.LYRICIST; dbSong.COMMENT = song.COMMENT; dbSong.disc = song.disc; dbSong.webdav_account_id = song.webdav_account_id; dbSong.remote_rel_path = song.remote_rel_path; dbSong.navArtistId = song.navArtistId; dbSong.navAlbumId = song.navAlbumId; dbSong.baiduFsId = song.baiduFsId; dbSong.webdav_id = song.webdav_id; dbSong.lyricIndex = song.lyricIndex; return dbSong; } private emitPlaylistRefresh(): void { emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, {}); } private buildPlaylistSongCandidates(songs: VideoItem[]): VideoItem[] { if (!songs || songs.length <= 0) { return []; } const result: VideoItem[] = []; const existedPaths: Set = new Set(); for (let index: number = 0; index < songs.length; index += 1) { const song = songs[index]; if (!song || StrUtil.isEmpty(song.filePath)) { continue; } if (existedPaths.has(song.filePath)) { continue; } existedPaths.add(song.filePath); result.push(song); } return result; } private async resolvePlaylistSongPaths(mediaTable: MediaTable, songs: VideoItem[]): Promise { const paths: string[] = []; for (let index: number = 0; index < songs.length; index += 1) { const song: VideoItem = songs[index]; const songForDb: VideoItem = this.cloneSongForDb(song); if (!songForDb.webdav_account_id && this.selectedAccount?.id) { songForDb.webdav_account_id = this.selectedAccount.id.toString(); } if (StrUtil.isEmpty(songForDb.id)) { songForDb.id = songForDb.remote_rel_path || songForDb.filePath; } if (StrUtil.isEmpty(songForDb.remote_rel_path)) { songForDb.remote_rel_path = songForDb.id || songForDb.filePath; } if (StrUtil.isEmpty(songForDb.parentPath) && songForDb.filePath.includes('/')) { const pathIndex = songForDb.filePath.lastIndexOf('/'); if (pathIndex > 0) { songForDb.parentPath = songForDb.filePath.substring(0, pathIndex); } } const saved: boolean = await mediaTable.saveOrUpdateWebDavItem(songForDb); if (!saved) { Logger.warn(TAG, `歌曲入库失败,跳过加入歌单: ${songForDb.filePath}`); continue; } const storedSong: VideoItem | null = await mediaTable.queryVideoByFilePath(songForDb.filePath); if (storedSong && StrUtil.isNotEmpty(storedSong.filePath)) { paths.push(storedSong.filePath); } } const uniquePaths: string[] = []; const pathSet: Set = new Set(); for (let index: number = 0; index < paths.length; index += 1) { const path = paths[index]; if (StrUtil.isEmpty(path) || pathSet.has(path)) { continue; } pathSet.add(path); uniquePaths.push(path); } return uniquePaths; } private async openSongsAddToPlaylistDialog(songs: VideoItem[]): Promise { const candidateSongs: VideoItem[] = this.buildPlaylistSongCandidates(songs); if (candidateSongs.length <= 0) { this.getUIContext().getPromptAction().showToast({ message: '请选择要加入歌单的歌曲' }); return; } try { const uiContext = this.getUIContext(); const hostCtx = uiContext ? uiContext.getHostContext() : undefined; if (!hostCtx) { this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' }); return; } const playlistTable: PlaylistTable = new PlaylistTable(hostCtx); const mediaTable: MediaTable = new MediaTable(hostCtx); await new Promise((resolve) => { mediaTable.getRdbStore(hostCtx, () => resolve()); }); const playlistSongPaths: string[] = await this.resolvePlaylistSongPaths(mediaTable, candidateSongs); if (playlistSongPaths.length <= 0) { this.getUIContext().getPromptAction().showToast({ message: '歌曲入库失败,无法添加到歌单' }); return; } const resolveSuccessToast = (): string => { if (playlistSongPaths.length > 1) { return `已添加${playlistSongPaths.length}首到歌单`; } return '已添加到歌单'; }; const openDialog = async (): Promise => { const playlists: Playlist[] = await playlistTable.queryAllPlaylists(); showAddToPlaylistDialog( candidateSongs, playlists, async (playlistId: string) => { const success = await playlistTable.addSongsToPlaylist(playlistId, playlistSongPaths); if (success) { this.emitPlaylistRefresh(); } this.getUIContext().getPromptAction().showToast({ message: success ? resolveSuccessToast() : '歌曲已在该歌单中' }); }, () => {}, () => { showCreatePlaylistDialog( async (name: string, description: string, coverPath?: string) => { const created = await playlistTable.createPlaylist(name, description, coverPath); if (!created) { this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' }); return; } const target = await playlistTable.queryPlaylistByName(name); if (!target) { this.getUIContext().getPromptAction().showToast({ message: '未找到新建歌单' }); return; } const addSuccess = await playlistTable.addSongsToPlaylist(target.id, playlistSongPaths); if (addSuccess) { this.emitPlaylistRefresh(); } this.getUIContext().getPromptAction().showToast({ message: addSuccess ? resolveSuccessToast() : '歌曲已在该歌单中' }); }, () => {} ); } ); }; await openDialog(); } catch (error) { const err = error as Error; Logger.error(TAG, `打开添加到歌单失败: ${err.message}`); this.getUIContext().getPromptAction().showToast({ message: '添加到歌单失败' }); } } private async openSelectedSongsAddToPlaylistDialog(): Promise { if (!this.isMultiSelect || this.selectedSongs.length <= 0) { this.getUIContext().getPromptAction().showToast({ message: '请先选择歌曲' }); return; } await this.openSongsAddToPlaylistDialog(this.selectedSongs.slice()); } private async openSongAddToPlaylistDialog(song: VideoItem): Promise { if (!song) { return; } await this.openSongsAddToPlaylistDialog([song]); } private sanitizeDownloadFileName(fileName: string): string { const trimmed = (fileName || '').trim(); const sanitized = trimmed.replace(/[\\/:*?"<>|]/g, '_'); return sanitized.length > 0 ? sanitized : '未知歌曲.mp3'; } private resolveDownloadFileName(song: VideoItem, sourceUrlOrPath: string): string { if (StrUtil.isNotEmpty(song.fileName)) { return this.sanitizeDownloadFileName(song.fileName as string); } if (StrUtil.isNotEmpty(song.name) && (song.name as string).includes('.')) { return this.sanitizeDownloadFileName(song.name as string); } if (StrUtil.isNotEmpty(sourceUrlOrPath)) { const purePath = sourceUrlOrPath.split('?')[0]; const lastSlash = purePath.lastIndexOf('/'); if (lastSlash >= 0 && lastSlash < purePath.length - 1) { const urlName = purePath.substring(lastSlash + 1); if (StrUtil.isNotEmpty(urlName)) { return this.sanitizeDownloadFileName(urlName); } } } const baseName = StrUtil.isNotEmpty(song.name) ? song.name as string : '未知歌曲'; return this.sanitizeDownloadFileName(`${baseName}.mp3`); } private buildUniqueDownloadPath(downloadDir: string, fileName: string): string { const safeName = this.sanitizeDownloadFileName(fileName); const dotIndex = safeName.lastIndexOf('.'); const hasExt = dotIndex > 0; const namePart = hasExt ? safeName.substring(0, dotIndex) : safeName; const extPart = hasExt ? safeName.substring(dotIndex) : ''; let targetPath = `${downloadDir}/${safeName}`; let suffix = 1; while (FileUtil.accessSync(targetPath)) { targetPath = `${downloadDir}/${namePart}(${suffix})${extPart}`; suffix += 1; } return targetPath; } private async pickDownloadRootPath(): Promise { try { const documentViewPicker = new picker.DocumentViewPicker(); const documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD }); if (!documentSaveResult || documentSaveResult.length === 0) { return ''; } const resolvedPath = new fileUri.FileUri(documentSaveResult[0]).path; this.rootPath = resolvedPath; return resolvedPath; } catch (error) { const err = error as Error; Logger.error(TAG, `选择下载目录失败: ${err.message}`); return ''; } } private async ensureDownloadDirectory(): Promise { let basePath = this.rootPath; if (StrUtil.isEmpty(basePath) || !FileUtil.accessSync(basePath)) { basePath = await this.pickDownloadRootPath(); } if (StrUtil.isEmpty(basePath)) { return ''; } const downloadDir = `${basePath}/${this.downloadFolderName}`; if (!FileUtil.accessSync(downloadDir)) { await fileIo.mkdir(downloadDir); } return downloadDir; } private async upsertDownloadedSongToLibrary(targetPath: string, sourceSong: VideoItem): Promise { if (StrUtil.isEmpty(targetPath) || !FileUtil.accessSync(targetPath)) { return; } try { const pageContext = getContext(this); const mediaModule = await import('../common/util/MediaTable'); const mediaTable = new mediaModule.default(pageContext); await new Promise((resolve) => { mediaTable.getRdbStore(pageContext, () => resolve()); }); const exists = await mediaTable.isRecordExists(targetPath); if (exists) { return; } let mediaItem: VideoItem; try { mediaItem = await Utility.readMetaInfoFFmpeg(pageContext, targetPath, CommonConstants.TYPE_LOCAL, false); } catch (error) { Logger.warn(TAG, `下载歌曲读取元数据失败,使用兜底入库: ${(error as Error).message}`); mediaItem = new VideoItem( sourceSong.name || FileUtil.getFileName(targetPath), targetPath, targetPath, CommonConstants.TYPE_LOCAL, 0, Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm') ); mediaItem.fileName = FileUtil.getFileName(targetPath); mediaItem.artist = sourceSong.artist; mediaItem.album = sourceSong.album; } mediaItem.id = targetPath; mediaItem.filePath = targetPath; mediaItem.type = CommonConstants.TYPE_LOCAL; if (StrUtil.isEmpty(mediaItem.fileName)) { mediaItem.fileName = FileUtil.getFileName(targetPath); } if (StrUtil.isEmpty(mediaItem.name)) { mediaItem.name = mediaItem.fileName as string; } if (StrUtil.isEmpty(mediaItem.parentPath)) { mediaItem.parentPath = FileUtil.getParentPath(targetPath); } await new Promise((resolve) => { mediaTable.insert(mediaItem, () => resolve()); }); emitter.emit({ eventId: EventConstants.EVENT_SCAN_UPDATE }, {}); } catch (error) { Logger.error(TAG, `下载歌曲入库失败: ${(error as Error).message}`); } } private openDownloadCenter(): void { this.isShowDownloadCenter = true; } private resolveSongDownloadSizeText(song: VideoItem): string { if (StrUtil.isNotEmpty(song.size)) { return decodeUrlEncodedString(song.size as string); } if (song.videoSize > 0) { return this.formatDownloadBytes(song.videoSize); } return ''; } private parseSizeTextToBytes(sizeText: string): number { if (StrUtil.isEmpty(sizeText)) { return 0; } const normalized = sizeText.trim(); const match = normalized.match(/([0-9]+(?:\.[0-9]+)?)\s*(B|KB|MB|GB|TB)/i); if (!match) { return 0; } const value = parseFloat(match[1]); if (!Number.isFinite(value) || value <= 0) { return 0; } const unit = match[2].toUpperCase(); const factor = this.resolveUnitFactor(unit); return Math.floor(value * factor); } private resolveUnitFactor(unit: string): number { switch (unit) { case 'KB': return 1024; case 'MB': return 1024 * 1024; case 'GB': return 1024 * 1024 * 1024; case 'TB': return 1024 * 1024 * 1024 * 1024; case 'B': default: return 1; } } private formatDownloadBytes(bytes: number): string { if (!Number.isFinite(bytes) || bytes <= 0) { return ''; } const units: string[] = ['B', 'KB', 'MB', 'GB', 'TB']; let size: number = bytes; let index: number = 0; while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; } const precision: number = index === 0 ? 0 : 2; return `${size.toFixed(precision)} ${units[index]}`; } private pauseDownloadTask(taskId: string): void { if (StrUtil.isEmpty(taskId)) { return; } this.downloadCenterManager.pauseTask(taskId); } private resumeDownloadTask(taskId: string): void { if (StrUtil.isEmpty(taskId)) { return; } this.downloadCenterManager.resumeTask(taskId); } private async resolveSongDownloadSourceUrl(song: VideoItem): Promise { if (song.type === CommonConstants.TYPE_BAIDU && song.webdav_account_id) { try { const account = await this.webdavManager.getWebDavAccountById(song.webdav_account_id); if (account) { const dlink = await this.webdavManager.getBaiduDownloadUrl(account, song); if (StrUtil.isNotEmpty(dlink)) { Logger.info(TAG, `下载中心使用百度直链下载: ${song.name}`); return dlink; } } } catch (error) { Logger.warn(TAG, `百度下载直链获取失败,回退播放地址: ${(error as Error).message}`); } } return setVideoUrlForSong(song, { context: getContext(this), autoParseMusicName: false, extractCover: false, extractLyric: false, extractAudioInfo: false }); } private async buildDownloadRequestHeaders(song: VideoItem, sourceUrl: string): Promise> { if (song.type === CommonConstants.TYPE_BAIDU) { const headers = new Map(); // 参考 BaiduFileCache 下载链路,避免复用播放头导致 Range 行为异常 headers.set('User-Agent', 'pan.baidu.com'); headers.set('Accept', '*/*'); headers.set('Connection', 'Keep-Alive'); return headers; } return this.webdavManager.buildHttpHeadersWithAccountId(song, sourceUrl); } private async enqueueSongDownloadWithDirectory(song: VideoItem, downloadDir: string): Promise { if (!song) { throw new Error('歌曲不存在'); } if (StrUtil.isEmpty(downloadDir)) { throw new Error('未选择下载目录'); } const sourceUrl = await this.resolveSongDownloadSourceUrl(song); const normalizedSource = sourceUrl.startsWith('file://') ? new fileUri.FileUri(sourceUrl).path : sourceUrl; const fileName = this.resolveDownloadFileName(song, normalizedSource); const targetPath = this.buildUniqueDownloadPath(downloadDir, fileName); const sizeText = this.resolveSongDownloadSizeText(song); const expectedBytes = this.parseSizeTextToBytes(sizeText); const requestHeaders = await this.buildDownloadRequestHeaders(song, sourceUrl); const coverPath = song.pixelMapPath ?? ''; this.downloadCenterManager.enqueueDownload({ title: song.name || fileName, fileName: fileName, coverPath: coverPath, sizeText: sizeText, expectedBytes: expectedBytes, sourceUrl: sourceUrl, targetPath: targetPath, downloadDir: downloadDir, headers: requestHeaders, onCompleted: async () => { await this.upsertDownloadedSongToLibrary(targetPath, song); } }); return fileName; } private async enqueueSongDownload(song: VideoItem): Promise { if (!song) { return; } try { const downloadDir = await this.ensureDownloadDirectory(); if (StrUtil.isEmpty(downloadDir)) { this.getUIContext().getPromptAction().showToast({ message: '未选择下载目录' }); return; } const fileName: string = await this.enqueueSongDownloadWithDirectory(song, downloadDir); this.openDownloadCenter(); this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列: ${fileName}` }); } catch (error) { const err = error as Error; Logger.error(TAG, `加入下载队列失败: ${err.message}`); this.getUIContext().getPromptAction().showToast({ message: `下载任务创建失败: ${err.message}` }); } } private async enqueueSelectedSongsDownload(): Promise { if (!this.isMultiSelect || this.selectedSongs.length <= 0) { this.getUIContext().getPromptAction().showToast({ message: '请先选择要下载的歌曲' }); return; } const candidates: VideoItem[] = this.buildPlaylistSongCandidates(this.selectedSongs.slice()); if (candidates.length <= 0) { this.getUIContext().getPromptAction().showToast({ message: '没有可下载的歌曲' }); return; } try { const downloadDir = await this.ensureDownloadDirectory(); if (StrUtil.isEmpty(downloadDir)) { this.getUIContext().getPromptAction().showToast({ message: '未选择下载目录' }); return; } let successCount: number = 0; let failedCount: number = 0; for (let index: number = 0; index < candidates.length; index += 1) { const song: VideoItem = candidates[index]; try { await this.enqueueSongDownloadWithDirectory(song, downloadDir); successCount += 1; } catch (error) { failedCount += 1; const err = error as Error; Logger.error(TAG, `批量下载入队失败: ${(song.name || song.fileName || song.filePath)}, ${err.message}`); } } if (successCount > 0) { this.openDownloadCenter(); this.exitMultiSelect(); } if (successCount > 0 && failedCount <= 0) { this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列: ${successCount}首` }); return; } if (successCount > 0) { this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列${successCount}首,失败${failedCount}首` }); return; } this.getUIContext().getPromptAction().showToast({ message: '下载任务创建失败' }); } catch (error) { const err = error as Error; Logger.error(TAG, `批量下载失败: ${err.message}`); this.getUIContext().getPromptAction().showToast({ message: `批量下载失败: ${err.message}` }); } } private buildSongMetaLine(song: VideoItem): string { const parts: string[] = []; if (StrUtil.isNotEmpty(song.duration)) { parts.push(song.duration as string); } else if (StrUtil.isNotEmpty(song.size)) { parts.push(decodeUrlEncodedString(song.size as string)); } if (parts.length === 0 && StrUtil.isNotEmpty(song.cTime)) { parts.push(song.cTime as string); } return parts.join(' · '); } // 对话框控制器 private accountDialogController: CustomDialogController | null = null; // 保存事件处理器引用,用于取消订阅 private eventHandler: (event: string) => void = (event: string) => { this.handleWebdavEvent(event); }; initSetting(){ this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false) this.customizeBgPath = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '') this.blurValue = PreferencesUtil.getNumberSync(SettingPage.CUSTOMIZE_BG_BLUR, 0) this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0) this.isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false) this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true) this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0) this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true) this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true) this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true) } aboutToAppear(): void { Utility.getAppName(getContext(this)).then((appName: string) => { this.appName = appName }) this.initSetting() let eventSetting: emitter.InnerEvent = { eventId: 333 } // 监听广播事件(通用设置配置更新) emitter.on(eventSetting, (eventData: emitter.EventData) => { this.initSetting() }); // 加载账户列表 this.loadAccounts(); this.loadFiles() this.breadcrumbs = this.webdavManager.getBreadcrumbs(); // 订阅WebDAV状态变化 this.webdavManager.subscribe(this.eventHandler); this.subscribeWebDavMetadataUpdates(); // 监听手势返回事件 let eventBackSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_DISK } emitter.on(eventBackSwipeBack, (eventData: emitter.EventData) => { console.info('onecold', ' 收到 EVENT_SWIPE_BACK_DISK 事件'); // 如果在详情视图模式,退出详情视图 this.goBack() }); } aboutToDisappear(): void { // 取消订阅 this.thumbnailTaskToken += 1; this.thumbnailRunningKeys.clear(); this.webdavManager.unsubscribe(this.eventHandler); emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED); } // 处理WebDAV事件 private handleWebdavEvent(event: string): void { switch (event) { case RemoteDriveManagerStates.LoadFilesInfoSucceed: this.songs = this.webdavManager.webDavSongs; this.updateListData(this.songs) // 直接引用webdavManager的数组,避免@Observed序列化问题 this.webDavFiles = this.webdavManager.webDavFiles; this.isLoading = false; // 更新可见文件夹列表 this.updateVisibleFolders(); this.breadcrumbs = this.webdavManager.getBreadcrumbs(); this.syncSelectionAfterRefresh(); this.scheduleRemoteThumbPrefetch(); if (this.isSearchMode && this.searchText.length > 0) { void this.applyGlobalSearch(this.searchText, ++this.searchTicket); } else { this.restoreCurrentDirectorySearchView(); } // promptAction.showToast({ // message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲' // }); break; case RemoteDriveManagerStates.LoadFilesInfoFailed: this.isLoading = false; // this.getUIContext().getPromptAction().showToast({ message: '加载失败' }); break; case RemoteDriveManagerStates.InsertAccountSucceed: case RemoteDriveManagerStates.EditAccountSucceed: case RemoteDriveManagerStates.RemoveAccountSucceed: this.loadAccounts(); break; case RemoteDriveManagerStates.GlobalSearchIndexBuildStart: case RemoteDriveManagerStates.GlobalSearchIndexUpdated: case RemoteDriveManagerStates.GlobalSearchIndexReady: if (this.isSearchMode && this.searchText.length > 0) { void this.applyGlobalSearch(this.searchText, ++this.searchTicket); } break; case RemoteDriveManagerStates.GlobalSearchIndexFailed: this.isSearchLoading = false; break; } } private subscribeWebDavMetadataUpdates(): void { emitter.on(this.metadataUpdateEvent, (eventData: emitter.EventData) => { const payloads = eventData?.data as WebDavMetadataUpdatePayload[] | undefined; if (!payloads || payloads.length === 0) { Logger.info(TAG, 'WebDav metadata event received but payload empty'); return; } Logger.info(TAG, `WebDav metadata event payload count: ${payloads.length}`); this.handleWebDavMetadataUpdates(payloads); }); } private handleWebDavMetadataUpdates(payloads: WebDavMetadataUpdatePayload[]): void { if (!payloads || payloads.length === 0) { return; } Logger.info(TAG, 'handleWebDavMetadataUpdates start'); let hasChanges = false; for (let i = 0; i < payloads.length; i++) { const payload = payloads[i]; if (!payload || !payload.filePath) { continue; } Logger.info(TAG, `WebDav metadata detail path=${payload.pixelMapPath ?? 'null'}`); const songUpdated = this.updateSongMetadata(payload); if (songUpdated) { hasChanges = true; Logger.info(TAG, `metadata updated for ${payload.filePath}`); } } if (!hasChanges) { Logger.info(TAG, 'handleWebDavMetadataUpdates no changes detected'); return; } // 更新dataSource的数据,不滚动,只触发刷新 this.dataSource.pushArrayData(this.songs); this.listRefreshKey++; Logger.info(TAG, 'handleWebDavMetadataUpdates trigger refresh'); } private updateSongMetadata(payload: WebDavMetadataUpdatePayload): boolean { if (!payload.filePath) { return false; } const targetIndex = this.findSongIndex(payload.filePath); if (targetIndex < 0) { return false; } const targetSong = this.songs[targetIndex]; let mutated = false; if (payload.pixelMapPath && payload.pixelMapPath.length > 0) { targetSong.pixelMapPath = payload.pixelMapPath; mutated = true; } if (payload.name && payload.name.length > 0) { targetSong.name = payload.name; mutated = true; } if (payload.artist && payload.artist.length > 0) { targetSong.artist = payload.artist; mutated = true; } return mutated; } private findSongIndex(filePath: string): number { for (let i = 0; i < this.songs.length; i++) { if (this.songs[i].filePath === filePath) { return i; } } return -1; } private cloneSong(item: VideoItem): VideoItem { const clone = new VideoItem( item.name, item.id, item.filePath, item.type, item.videoSize, item.cTime, item.size, item.pixelMapPath, item.artist, item.album, item.fileName, item.lastPlayed ); clone.duration = item.duration; clone.mimeType = item.mimeType; clone.trackCount = item.trackCount; clone.sampleRate = item.sampleRate; clone.size = item.size; clone.webdav_account_id = item.webdav_account_id; clone.remote_rel_path = item.remote_rel_path; clone.artist = item.artist; clone.album = item.album; clone.lyricContent = item.lyricContent; clone.pixelMapPath = item.pixelMapPath; clone.cTime = item.cTime; clone.fileName = item.fileName; clone.md5Str = item.md5Str; clone.bit_rate = item.bit_rate; clone.probe_score = item.probe_score; clone.year = item.year; clone.nb_streams = item.nb_streams; clone.nb_programs = item.nb_programs; clone.genre = item.genre; clone.track = item.track; clone.disc = item.disc; clone.channels = item.channels; clone.channel_layout = item.channel_layout; clone.start_time = item.start_time; clone.ALBUMARTIST = item.ALBUMARTIST; clone.COMPOSER = item.COMPOSER; clone.COMMENT = item.COMMENT; clone.LYRICIST = item.LYRICIST; clone.pyStr = item.pyStr; clone.extra_json = item.extra_json; clone.parentPath = item.parentPath; clone.isFav = item.isFav; clone.playCount = item.playCount; clone.lastPlayed = item.lastPlayed; clone.videoSize = item.videoSize; clone.isFav = item.isFav; return clone; } // 加载账户列表 private loadAccounts(): void { this.accounts = this.webdavManager.getAllWebDavAccounts(); } // 加载文件列表 private loadFiles(): void { if (!this.selectedAccount) { this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' }); return; } this.isLoading = true; console.info('heanup '+this.selectedAccount.name+'type '+this.selectedAccount.webType) this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount) .catch((error: Error) => { Logger.error(TAG, '加载文件失败: ' + error.message); this.isLoading = false; }); } // 进入文件夹 private enterFolder(folder: FileInfo): void { this.isLoading = true; this.webdavManager.enterFolder(folder) .catch((error: Error) => { Logger.error(TAG, '进入文件夹失败: ' + error.message); this.isLoading = false; }); } // 检查是否为当前目录的直接子项 private isDirectChildOfCurrentPath(folder: FileInfo): boolean { const currentPath = this.webdavManager.currentPath || ''; // 如果在根目录(currentPath为空或'/'),显示所有第一级文件夹 if (currentPath === '' || currentPath === '/') { const folderPath = folder.href.replace(/\/$/, ''); // 去掉尾部斜杠 return folder.href.startsWith('/') && folder.href !== '/' && !folderPath.substring(1).includes('/'); } // 非根目录情况,计算相对路径 let relativePath = folder.href; if (currentPath !== '/') { relativePath = folder.href.replace(currentPath, ''); } relativePath = relativePath.replace(/^\//, '').replace(/\/$/, ''); // 只有相对路径不为空且不包含/时才认为是直接子项 return relativePath !== '' && !relativePath.includes('/'); } // 返回上级目录 private goBack(): void { if(this.webdavManager.canGoBack()){ this.isLoading = true; this.webdavManager.goBack() .catch((error: Error) => { Logger.error(TAG, '返回失败: ' + error.message); this.isLoading = false; }); this.breadcrumbs = this.webdavManager.getBreadcrumbs(); }else{ this.getUIContext().animateTo({ duration: 555 }, () => { // 动画闭包内控制Image组件的出现和消失 this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) } } // 切换账户 private switchAccount(account: WebDavAccount): void { this.selectedAccount = account; this.songs = []; this.updateListData(this.songs) } private resolveSongPlayIndex(song: VideoItem, fallbackIndex: number): number { if (this.songs.length <= 0) { return -1; } if (song && StrUtil.isNotEmpty(song.filePath)) { for (let i = 0; i < this.songs.length; i++) { if (this.songs[i].filePath === song.filePath) { return i; } } } if (fallbackIndex >= 0 && fallbackIndex < this.songs.length) { return fallbackIndex; } return 0; } // 播放WebDAV歌曲 private playSong(song: VideoItem, index: number,isJump:boolean=false): void { try { Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`); Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index); Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length); const playIndex = this.resolveSongPlayIndex(song, index); if (playIndex < 0 || playIndex >= this.songs.length) { this.getUIContext().getPromptAction().showToast({ message: '播放失败,未找到歌曲' }); Logger.warn(TAG, `heanup 无法定位播放索引: fallback=${index}, songPath=${song.filePath}`); return; } if (playIndex !== index) { Logger.info(TAG, `heanup 修正播放索引: fallback=${index}, real=${playIndex}, song=${song.name}`); } // 检查歌曲是否有webdav_account_id Logger.info(TAG, `heanup 播放歌曲的webdav_account_id: ${song.webdav_account_id || '未设置'}`); // 确保所有歌曲都设置了正确的webdav_account_id if (this.selectedAccount && this.selectedAccount.id) { const accountIds = this.songs.map(s => s.webdav_account_id).filter(id => id); Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${this.songs.length} 首歌曲设置了webdav_account_id`); // 如果发现歌曲缺少webdav_account_id,立即设置 this.songs.forEach((item, idx) => { if (!item.webdav_account_id) { item.webdav_account_id = this.selectedAccount!.id.toString(); Logger.info(TAG, `heanup 为歌曲 "${item.name}" 设置webdav_account_id: ${item.webdav_account_id}`); } }); } // 直接使用当前的VideoItem数组 const videoItems: VideoItem[] = this.songs; const songFilePaths: string[] = []; for (let i = 0; i < this.songs.length; i++) { const item = this.songs[i]; songFilePaths.push(item.filePath); // 使用filePath作为文件路径 } // 直接通过事件传递videoItems数据,不使用GlobalContext const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }; const playlistData: PlaylistEventData = { playlistId: 'webdav-playlist', // 使用特殊的ID标识网盘播放列表 playlistName: `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`, songCount: this.songs.length, startIndex: playIndex, isJump: isJump,//设置true会弹出播放页 songFilePaths: songFilePaths }; // 保存videoItems到全局内存 globalWebdavVideoItems = videoItems; globalWebdavCurrentPlayIndex = playIndex; Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${playIndex}`); // 发送播放请求事件,只传递索引信息 emitter.emit(eventPlaylistPlay, { data: playlistData }); Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${playIndex}`); if(!this.isNoJumpToHome){ // 跳转到首页播放器 this.getUIContext()?.animateTo({ duration: 555 }, () => { this.mType = 0 }) } } catch (error) { const err = error as Error; Logger.error(TAG, '播放歌曲失败: ' + err.message); this.getUIContext().getPromptAction().showToast({ message: '播放失败' }); } } /** * 一键创建歌单:将当前WebDAV歌曲全部加入新歌单 */ private async createPlaylistFromCurrentWebDav(): Promise { try { Logger.info(TAG, 'heanup 一键创建歌单开始'); if (!this.selectedAccount || !this.selectedAccount.id) { this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` }); return; } if (!this.songs || this.songs.length === 0) { // 回退到manager内的歌曲(可能还未复制到页面state) this.songs = this.webdavManager.webDavSongs; Logger.info(TAG, `heanup 页面songs为空,回退webdavManager.webDavSongs,长度=${this.songs.length}`); if (this.songs.length === 0) { this.getUIContext().getPromptAction().showToast({ message: '当前目录没有可添加的歌曲' }); return; } } Logger.info(TAG, `heanup 歌单创建前歌曲数量: ${this.songs.length}`); // 确保每首歌都有webdav_account_id for (let i = 0; i < this.songs.length; i++) { if (!this.songs[i].webdav_account_id) { this.songs[i].webdav_account_id = this.selectedAccount.id.toString(); } } // 构建歌单名称:账户名 + 当前路径(简化) const rawPath = this.webdavManager.currentPath || '/'; const shortPath = rawPath === '/' ? '根目录' : rawPath.replace(/\/$/, '').split('/').slice(-1)[0]; const playlistName = `${getRemoteDrivePlaylistPrefix(this.selectedAccount.webType)}-${this.selectedAccount.name}-${shortPath}`; // 创建歌单 // 安全获取HostContext const uiContext = this.getUIContext(); const hostCtx = uiContext ? uiContext.getHostContext() : undefined; if (!hostCtx) { this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' }); return; } const playlistModule = await import('../common/util/PlaylistTable'); const mediaModule = await import('../common/util/MediaTable'); const playlistTable = new playlistModule.default(hostCtx); const mediaTable = new mediaModule.default(hostCtx); // 等待MediaTable底层RDB初始化完成 await new Promise((resolve) => { mediaTable.getRdbStore(hostCtx, () => { Logger.info(TAG, 'heanup mediaTable RDB 初始化完成'); resolve(); }); }); // 先将WebDAV歌曲入库(若不存在) let upsertSuccess = 0; for (let i = 0; i < this.songs.length; i++) { const v = this.songs[i]; if (!v.id || v.id === '') { // 使用filePath作为唯一ID v.id = v.filePath; } if (!v.parentPath) { const idxp = v.filePath.lastIndexOf('/'); if (idxp > 0) { v.parentPath = v.filePath.substring(0, idxp); } } const ok = await mediaTable.upsertWebDavVideoItem(v); Logger.info(TAG, `heanup upsert 第${i+1}/${this.songs.length}首: ${v.filePath} => ${ok}`); if (ok) { upsertSuccess++; } } Logger.info(TAG, `heanup WebDAV歌曲入库完成: 成功 ${upsertSuccess}/${this.songs.length}`); // 先查询是否已有同名歌单,避免重复创建导致混淆 const existing = await playlistTable.queryPlaylistByName(playlistName); if (existing) { this.getUIContext().getPromptAction().showToast({ message: '歌单已存在,直接追加歌曲' }); const filePathsExist: string[] = this.songs.map(s => s.filePath); await playlistTable.addSongsToPlaylist(existing.id, filePathsExist); router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: existing } }); return; } Logger.info(TAG, `heanup 准备创建歌单: ${playlistName}`); const created = await playlistTable.createPlaylist(playlistName, `来自${getRemoteDriveDisplayLabel(this.selectedAccount.webType)}: ${this.selectedAccount.name} 路径: ${rawPath}`); if (!created) { this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' }); return; } // 查询刚创建的歌单ID const target = await playlistTable.queryPlaylistByName(playlistName); if (!target) { this.getUIContext().getPromptAction().showToast({ message: '无法找到新建歌单' }); return; } // 批量添加歌曲 const filePaths: string[] = this.songs.map(s => s.filePath); Logger.info(TAG, `heanup 开始批量添加歌曲到歌单: ${target.id}`); const addResult = await playlistTable.addSongsToPlaylist(target.id, filePaths); Logger.info(TAG, `heanup 批量添加结果: ${addResult}`); if (!addResult) { Logger.warn(TAG, '批量添加歌曲返回false,可能全部已存在或写入失败'); } this.getUIContext().getPromptAction().showToast({ message: `歌单创建成功: ${playlistName}` }); Logger.info(TAG, `heanup 一键创建歌单成功: ${playlistName}, 添加 ${filePaths.length} 首歌曲`); // 跳转到歌单详情页面 router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: target } }); } catch (error) { Logger.error(TAG, '一键创建歌单失败: ' + (error as Error).message); this.getUIContext().getPromptAction().showToast({ message: '一键创建歌单失败' }); } } // 导航到指定层级的面包屑路径 private async navigateToBreadcrumb(crumb: BreadcrumbItem): Promise { if (!crumb) { return; } try { this.isLoading = true; await this.webdavManager.enterFolderFromPath(crumb.path); } catch (error) { Logger.error(TAG, '导航到面包屑路径失败: ' + (error as Error).message); this.isLoading = false; } } @Builder MoreMenuBuilder() { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')), content: $r('app.string.onekey_add_playlist') }) .onClick(async () => { this.createPlaylistFromCurrentWebDav(); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_up_to_line')), content: $r('app.string.upload_music_file') }) .onClick(async () => { // this.navigateToUploadPage(); this.isShowUploadFile = !this.isShowUploadFile }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.download')), content: '下载中心' }) .onClick(() => { this.openDownloadCenter(); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.folder_badge_plus')), content: $r('app.string.create_foder') }) .onClick(async () => { this.showCreateFolderDialog(); }) }.attributeModifier(new MenuModifier()) } @Builder SortMenuBuilder() { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')), content: $r('app.string.sort_by_name'), symbolEndIcon: this.sortType === 0 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortItemBackground(0)) .onClick(async () => { this.doSortType(0) PreferencesUtil.put("webDavSortType", 0) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按名称降序', symbolEndIcon: this.sortType === 1 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortItemBackground(1)) .onClick(async () => { this.doSortType(1) PreferencesUtil.put("webDavSortType", 1) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')), content:'按时间升序', symbolEndIcon: this.sortType === 2 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortItemBackground(2)) .onClick(async () => { this.doSortType(2) PreferencesUtil.put("webDavSortType", 2) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')), content: '按时间降序', symbolEndIcon: this.sortType === 3 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortItemBackground(3)) .onClick(async () => { this.doSortType(3) PreferencesUtil.put("webDavSortType", 3) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')), content:'按大小升序', symbolEndIcon: this.sortType === 4 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortItemBackground(4)) .onClick(async () => { this.doSortType(4) PreferencesUtil.put("webDavSortType", 4) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按大小降序', symbolEndIcon: this.sortType === 5 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined }) .backgroundColor(this.getSortItemBackground(5)) .onClick(async () => { this.doSortType(5) PreferencesUtil.put("webDavSortType", 5) }) }.attributeModifier(new MenuModifier()) } private sortSongsForType(target: Array, index: number): Array { switch (index) { case 0: Utility.doSortListAscending(target, this.isShowFileName) break; case 1: Utility.doSortListDescending(target, this.isShowFileName) break; case 2: target.sort((a, b) => { return a.cTime.localeCompare(b.cTime); }); break; case 3: target.sort((a, b) => { return b.cTime.localeCompare(a.cTime); }); break; case 4: target.sort((a, b) => { return a.videoSize - b.videoSize; }); break; case 5: target.sort((a, b) => { return b.videoSize - a.videoSize; }); break; } return target; } private sortFoldersForType(target: Array, index: number): Array { switch (index) { case 0: target.sort((a, b) => { return a.fileName.localeCompare(b.fileName); }); break; case 1: target.sort((a, b) => { return b.fileName.localeCompare(a.fileName); }); break; case 2: target.sort((a, b) => { return a.time - b.time; }); break; case 3: target.sort((a, b) => { return b.time - a.time; }); break; } return target; } doSortType(index: number) { this.sortType = index; this.sortSongsForType(this.songs, index) this.sortFoldersForType(this.visibleFoldersState, index) if (this.filteredList.length > 0) { this.filteredList = this.sortSongsForType([...this.filteredList], index) } if (this.filteredFolderList.length > 0) { this.filteredFolderList = this.sortFoldersForType([...this.filteredFolderList], index) } if (this.isSearchMode && this.searchText.length > 0) { this.refreshDisplaySongs(this.filteredList) return } this.updateListData(this.songs,true) } private getSortItemBackground(sortType: number): ResourceColor { if (this.sortType !== sortType) { return Color.Transparent; } const isAsc: boolean = sortType % 2 === 0; if (isAsc) { return this.isDarkMode ? '#295B8A' : '#DCEEFF'; } return this.isDarkMode ? '#7A4D22' : '#FFE9D5'; } @Builder topTitleBar(){ Column() { Row({ space: 15 }) { if (!this.isSearchMode &&!(this.webdavManager.canGoBack())) { //左侧滑动按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.sort')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.getUIContext().animateTo({ duration: 555 }, () => { // 动画闭包内控制Image组件的出现和消失 this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) }) .attributeModifier(new ShadowModifier()) .zIndex(0) }else{ //左侧搜索返回按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.chevron_left')) .attributeModifier(new SymbolGlyphFancyModifier(24, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { if(this.isSearchMode){ this.isSearchMode = false this.searchController.stopEditing() this.onSearchInput('') }else{ if(this.webdavManager.canGoBack()){ this.goBack() } } }) .attributeModifier(new ShadowModifier()) .zIndex(0) } if(!this.isSearchMode){ Text(this.selectedAccount.name) .margin({left:3,right:10}) .fontColor($r('app.color.text_color')) .fontSize(19) .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(35) .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() this.onSearchInput(value); }) .onChange((value: string) => { console.log('onecold onChange ='+value) this.onSearchInput(value); }) .visibility(this.isSearchMode?Visibility.Visible:Visibility.None) .animation({ duration: 300, curve: Curve.Ease }) //搜索按钮 if (!this.isSearchMode) { Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.magnifyingglass')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .attributeModifier(new ShadowModifier()) .zIndex(0) .onClick(()=>{ this.isSearchMode = true this.restoreCurrentDirectorySearchView() void this.webdavManager.ensureGlobalSearchIndex(this.selectedAccount) }) //排序按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.list_number')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .bindMenu(this.SortMenuBuilder) .attributeModifier(new ShadowModifier()) .zIndex(0) //添加/上传综合按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.plus')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .bindMenu(this.MoreMenuBuilder) .attributeModifier(new ShadowModifier()) .zIndex(0) .bindContentCover($$this.isShowUploadFile, this.UploadFielBuilder(), { transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) }) }) } } } .padding({ top: this.topSafeHeight + 10, left: 10, right: 10, bottom: 2 }) .width('100%') .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(), { modalTransition:ModalTransition.DEFAULT, onWillDisappear: () => { this.isShowDownloadCenter = false }, }) } @Builder UploadFielBuilder() { Scroll() { Column() { UploadMusicPage({ accountId: this.selectedAccount.id, isShowUploadFile:this.isShowUploadFile, onResult:(result:boolean)=>{ console.info("heanup onResult,上传成功,刷新列表") if(result){ // 上传文件成功后刷新当前路径的列表 if(this.selectedAccount.webType==RemoteDriveType.Baidu){ ToastUtil.showToast(`由于百度网盘限制,百度网盘的上传路径是apps/${this.appName}`) } this.refreshFileListAfterUpload() } }, }) } } .width('100%') .height('100%') } @Builder DownloadCenterBuilder() { DownloadCenter({ themeColor: this.themeColor, isDarkMode: this.isDarkMode, appName: this.appName, topSafeHeight: this.topSafeHeight, bottomSafeHeight: this.bottomSafeHeight, selectedIndexes: $downloadCenterTabIndex, onPauseTask: (taskId: string) => { this.pauseDownloadTask(taskId); }, onResumeTask: (taskId: string) => { this.resumeDownloadTask(taskId); }, onClose: () => { this.isShowDownloadCenter = false; } }) .width('100%') .height('100%') } //搜索功能的实现 @State searchText: string = ''; // 用户输入内容 @State filteredList: Array = []; // 过滤后的歌曲结果 @State filteredFolderList: Array = []; // 过滤后的文件夹结果 private restoreCurrentDirectorySearchView(): void { this.filteredList = this.sortSongsForType([...this.songs], this.sortType) this.filteredFolderList = this.sortFoldersForType([...this.visibleFoldersState], this.sortType) this.isSearchLoading = false this.refreshDisplaySongs(this.filteredList) } private shouldShowSearchLocation(): boolean { return this.isSearchMode && this.searchText.length > 0 } private getSongSearchLocation(song: VideoItem): string { if (!this.shouldShowSearchLocation() || !this.selectedAccount) { return '' } return this.webdavManager.getSongSearchLocationLabel(this.selectedAccount, song) } private getFolderSearchLocation(folder: FileInfo): string { if (!this.shouldShowSearchLocation() || !this.selectedAccount) { return '' } return this.webdavManager.getFolderSearchLocationLabel(this.selectedAccount, folder) } private async applyGlobalSearch(keyword: string, ticket: number): Promise { if (!this.selectedAccount) { if (ticket === this.searchTicket) { this.isSearchLoading = false this.filteredList = [] this.filteredFolderList = [] this.refreshDisplaySongs([]) } return } try { const result: RemoteDriveGlobalSearchResult = await this.webdavManager.searchGlobalIndex(this.selectedAccount, keyword); if (ticket !== this.searchTicket) { return } this.filteredList = this.sortSongsForType([...result.songs], this.sortType) this.filteredFolderList = this.sortFoldersForType([...result.folders], this.sortType) this.isSearchLoading = result.isIndexing this.refreshDisplaySongs(this.filteredList) } catch (error) { if (ticket !== this.searchTicket) { return } this.isSearchLoading = false this.filteredList = [] this.filteredFolderList = [] this.refreshDisplaySongs([]) ToastUtil.showToast(`搜索失败: ${(error as Error).message}`) } } private onSearchInput(value: string) { const keyword = value.trim(); this.searchText = keyword; const ticket = ++this.searchTicket; if (keyword.length === 0) { this.restoreCurrentDirectorySearchView(); return; } if (!this.isSearchMode) { this.isSearchMode = true; } this.isSearchLoading = true; void this.applyGlobalSearch(keyword, ticket); } private openSongPropertySheet(song: VideoItem): void { this.songForPropertySheet = song; this.isShowSongPropertySheet = true; } private getSongPropertyValue(value: string | number | undefined): string { if (value === undefined) { return '--'; } if (typeof value === 'number') { return `${value}`; } if (StrUtil.isEmpty(value)) { return '--'; } return value; } private getSongTypeLabel(song: VideoItem | undefined): string { if (!song) { return '--'; } if (song.type === CommonConstants.TYPE_LOCAL) { return '本地'; } if (song.type === CommonConstants.TYPE_WEBDAV) { return 'WebDAV'; } if (song.type === CommonConstants.TYPE_JELLYFIN) { return 'Jellyfin'; } if (song.type === CommonConstants.TYPE_EMBY) { return 'Emby'; } if (song.type === CommonConstants.TYPE_AUDIOSTATION) { return 'AudioStation'; } if (song.type === CommonConstants.TYPE_BAIDU) { return '百度网盘'; } if (song.type === CommonConstants.TYPE_NAVIDROME) { return 'Navidrome'; } if (song.type === CommonConstants.TYPE_PLEX) { return 'Plex'; } return `${song.type}`; } @Builder private SongPropertyRow(label: string, value: string) { Column({ space: 4 }) { Text(label) .fontSize(12) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.7) Text(value) .fontSize(14) .fontColor($r('app.color.text_color')) .maxLines(6) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') .alignItems(HorizontalAlign.Start) .padding({ left: 2, right: 2, top: 4, bottom: 4 }) } @Builder private SongPropertyCoverRow(label: string, coverPath: string | undefined) { Column({ space: 8 }) { Text(label) .fontSize(12) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.7) if (StrUtil.isNotEmpty(coverPath)) { Image(coverPath as string) .width(136) .height(136) .borderRadius(10) .sourceSize({ width: 136, height: 136 }) .objectFit(ImageFit.Cover) .alt($r('app.media.alt')) .fillColor(this.themeColor) } else { Text('--') .fontSize(14) .fontColor($r('app.color.text_color')) } } .width('100%') .alignItems(HorizontalAlign.Start) .padding({ left: 2, right: 2, top: 4, bottom: 4 }) } @Builder private SongPropertyLyricRow(label: string, lyricContent: string | undefined) { Column({ space: 4 }) { Text(label) .fontSize(12) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.7) Text(this.getSongPropertyValue(lyricContent)) .fontSize(14) .fontColor($r('app.color.text_color')) .textAlign(TextAlign.Start) } .width('100%') .alignItems(HorizontalAlign.Start) .padding({ left: 2, right: 2, top: 4, bottom: 4 }) } @Builder private SongPropertySheetBuilder() { if (this.songForPropertySheet) { Scroll() { Column({ space: 10 }) { this.SongPropertyCoverRow('音乐封面', this.songForPropertySheet?.pixelMapPath) this.SongPropertyRow('歌曲名称', this.getSongPropertyValue(this.songForPropertySheet?.name)) this.SongPropertyRow('文件名称', this.getSongPropertyValue(this.songForPropertySheet?.fileName)) this.SongPropertyRow('艺术家', this.getSongPropertyValue(this.songForPropertySheet?.artist)) this.SongPropertyRow('专辑', this.getSongPropertyValue(this.songForPropertySheet?.album)) this.SongPropertyRow('时长', this.getSongPropertyValue(this.songForPropertySheet?.duration)) this.SongPropertyRow('文件大小', this.getSongPropertyValue(this.songForPropertySheet?.size)) this.SongPropertyRow('比特率', this.getSongPropertyValue(this.songForPropertySheet?.bit_rate)) this.SongPropertyRow('采样率', this.getSongPropertyValue(this.songForPropertySheet?.sampleRate)) this.SongPropertyLyricRow('内嵌歌词', this.songForPropertySheet?.lyricContent) this.SongPropertyRow('创建时间', this.getSongPropertyValue(this.songForPropertySheet?.cTime)) this.SongPropertyRow('来源类型', this.getSongTypeLabel(this.songForPropertySheet)) this.SongPropertyRow('文件路径', this.getSongPropertyValue(this.songForPropertySheet?.filePath)) } .width('100%') .padding({ left: 12, right: 12, top: 8, bottom: 12 }) } .width('100%') .height('100%') .scrollBar(BarState.Auto) } else { Column() { Text('暂无歌曲属性信息') .fontSize(14) .fontColor($r('app.color.text_color')) .opacity(0.65) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } } build() { Stack() { if (this.accounts.length === 0) { this.buildEmptyView(); } else { this.buildContentView(); } // 顶部区域:标题栏在上,面包屑在下 Column() { this.topTitleBar() this.breaker() } .width('100%') .visibility(this.isShowTitleBar?Visibility.Visible: this.autoHideTitle?Visibility.None:Visibility.Visible) .animation({ duration: 500, curve: Curve.Friction // 可选动画曲线 }) .position({ top: 0, left: 0 }) // .backgroundColor($r('app.color.start_window_background')) // 多选操作条,位于播放条之上(仅在多选模式下显示) if (this.isMultiSelect || this.isDeletingSelection) { this.buildSelectionOverlay() } } .width('100%') .height('100%') .alignContent(Alignment.Bottom) .backgroundImage(this.isCustomizeBg?this.customizeBgPath:$r('app.color.start_window_background')) .backgroundImageSize(this.isLandscape?{width:'100%'}:{ height: '100%'}) .backgroundImagePosition(Alignment.Center) .backdropBlur(this.blurValue) .backgroundBrightness({rate:this.isCustomizeBg?0.1:0,lightUpDegree:this.bgBrightness}) .bindSheet($$this.isShowSongPropertySheet, this.SongPropertySheetBuilder(), { height: '99%', dragBar: true, showClose: true, preferType: SheetType.CENTER, blurStyle: BlurStyle.Thin, title: { title: '歌曲属性' } }) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) } @Builder breaker() { // 面包屑导航 Column({ space: 8 }) { // 面包屑导航 if (this.webdavManager.currentPath !== '') { Row({ space: 8 }) { Button({ type: ButtonType.Circle }) { // Image(this.webdavManager.canGoBack()?$r('app.media.back'): // this.selectedAccount.coverPath?this.selectedAccount.coverPath:getCloudDiskIcon(this.selectedAccount.webType)) Image(this.selectedAccount.coverPath) .width(15) .height(15) .borderRadius(10) .alt($r('app.media.cloudDisk')) .fillColor(Color.White) } .width(20) .height(20) .margin({left:5}) .backgroundColor(this.themeColor) .onClick(() => this.goBack()) Row({ space: 4 }) { ForEach(this.breadcrumbs, (crumb: BreadcrumbItem, index: number) => { Row() { Text(crumb.label) .fontSize(15) .fontColor($r('app.color.text_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .onClick(() => { this.navigateToBreadcrumb(crumb); }) // 添加分隔符(除了最后一个元素) if (index < this.breadcrumbs.length - 1) { Text('/') .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) } }) } .layoutWeight(1) } .width('100%') .padding({ left: 4, right: 4 }) } } .width('100%') .padding({ left: 12, right: 12, top: 8, bottom: 5 }) } // 空状态视图 @Builder buildEmptyView() { Column({ space: 20 }) { Image($r('app.media.cloudDisk')) .width(120) .height(120) .opacity(0.3) Text(`暂无${getRemoteDriveAccountLabel()}`) .fontSize(16) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) } .justifyContent(FlexAlign.Center) .width('100%') .height('100%') } // 内容视图 @Builder buildContentView() { Column() { // 加载状态 Row() { LoadingProgress() .width(30) .height(30) .color(this.themeColor) Text('加载中...') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')) .margin({ left: 12 }) } .padding({top: this.topSafeHeight + 90, left: 20, right: 20}) .visibility(this.isLoading?Visibility.Visible:Visibility.None) .opacity(this.isLoading ? 1 : 0) .animation({ duration: 800, curve: Curve.Smooth // 可选动画曲线 }) if (this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading) { Row({ space: 8 }) { LoadingProgress() .width(18) .height(18) .color(this.themeColor) Text('正在构建全局索引,结果会持续补全') .fontSize(12) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.65) } .padding({ left: 20, right: 20, top: this.topSafeHeight + 74, bottom: 6 }) } // 文件列表(文件夹 + 歌曲) if (this.webDavFiles.length > 0 || this.songs.length > 0) { List({ scroller: this.listScroller ,space: 0 }) { // 显示文件夹 - 只显示当前目录下的直接子文件夹 ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => { ListItem() { this.buildFolderItem(folder) } .clickEffect({ level: ClickEffectLevel.MIDDLE }) }, (folder: FileInfo) => folder.name+folder.fileName) // 显示歌曲 LazyForEach(this.dataSource, (song: VideoItem, index: number) => { ListItem() { this.buildSongItem(song, index) } .clickEffect({ level: ClickEffectLevel.MIDDLE }) }, (item: VideoItem, index: number) => item.filePath + '_' + index+this.listRefreshKey) } .scrollBar(BarState.Off) .onScrollFrameBegin((offset: number) => { // 获取当前滚动偏移量 if(this.autoHideTitle){ const currentOffsetY = this.listScroller.currentOffset().yOffset; // 判断滚动方向 if (currentOffsetY > this.prevOffsetY) { this.isShowTitleBar = false } else if (currentOffsetY < this.prevOffsetY) { this.isShowTitleBar = true } // 更新前一次偏移量 this.prevOffsetY = currentOffsetY; } return { offsetRemain: offset }; }) .contentStartOffset(this.topSafeHeight + 85) .contentEndOffset(this.bottomSafeHeight+70) .layoutWeight(1) .margin({ top: 4 }) } else if (!this.isLoading) { Column() { Text(this.isSearchMode && this.searchText.length > 0 ? '没有找到匹配结果' : '暂无内容') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) Text(this.isSearchMode && this.searchText.length > 0 ? (this.isSearchLoading ? '正在继续扫描更多目录...' : '可以换个关键词再试') : '点击"左侧菜单"网盘加载') .fontSize(12) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.4) .margin({ top: 8 }) } .justifyContent(FlexAlign.Center) .layoutWeight(1) } } .layoutWeight(1) } @Builder private buildMultiSelectBar() { Row({ space: 12 }) { Button(this.isAllSelected ? '反选' : '全选', { type: ButtonType.Circle, stateEffect: true }) .width(55) .height(55) .fontSize(13) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => this.toggleSelectAll()) Button('删除', { type: ButtonType.Circle, stateEffect: true }) .width(55) .height(55) .fontSize(13) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection) .onClick(() => this.confirmDeleteSelected()) Button('+歌单', { type: ButtonType.Circle, stateEffect: true }) .width(55) .height(55) .fontSize(13) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection) .onClick(() => { void this.openSelectedSongsAddToPlaylistDialog(); }) Button('下载', { type: ButtonType.Circle, stateEffect: true }) .width(55) .height(55) .fontSize(13) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection) .onClick(() => { void this.enqueueSelectedSongsDownload(); }) Button('取消', { type: ButtonType.Circle, stateEffect: true }) .width(55) .height(55) .fontSize(13) .backgroundColor(this.themeColor) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 }) .onClick(() => this.exitMultiSelect()) } .width('100%') .padding({ bottom: 10, top: 12 }) .justifyContent(FlexAlign.Center) .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None) .opacity(this.isMultiSelect ? 1 : 0) .animation({ duration: 300, curve: 'ease-in-out' }) } @Builder private buildSelectionOverlay() { Column() { Row({ space: 10 }) { LoadingProgress() .width(22) .height(22) .color(this.themeColor) Text('正在删除...') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')) } .padding({ top: 12, bottom: 6 }) .justifyContent(FlexAlign.Center) .visibility(this.isDeletingSelection ? Visibility.Visible : Visibility.None) this.buildMultiSelectBar() } .width('100%') .padding({ left: 12, right: 12, bottom: this.bottomSafeHeight }) } // 文件夹列表项 @Builder buildFolderItem(folder: FileInfo) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row({ space: 2 }) { SymbolGlyph($r('sys.symbol.folder')) .fontSize(48) .fontColor([this.themeColor]) .alignSelf(ItemAlign.Center) .margin({ left: 8, right: 6 }) // 文件夹信息 Column({ space: 4 }) { Text(decodeUrlEncodedString(folder.fileName.replace('/', ''))) .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.getFolderSearchLocation(folder)) .fontSize(12) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.48) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .visibility(this.getFolderSearchLocation(folder).length > 0 ? Visibility.Visible : Visibility.None) Text('文件夹') .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) } } .reuseId('dir_item') .width('100%') .padding(12) .backgroundColor(Color.Transparent) // .backgroundColor($r('app.color.start_window_background')) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .onClick(() => { if (this.isSearchMode && this.searchText.length > 0) { this.isSearchMode = false this.searchController.stopEditing() this.onSearchInput('') } this.enterFolder(folder); this.breadcrumbs = this.webdavManager.getBreadcrumbs(); }) } // 歌曲列表项 @Builder buildSongItem(song: VideoItem, index: number) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row({ space: 12 }) { // 序号 // 歌曲封面 Image(song.pixelMapPath ? song.pixelMapPath : $r('app.media.alt')) .width(52) .height(52) .borderRadius(9) .sourceSize({width:38, height:38}) .alt($r('app.media.alt')) .fillColor(this.themeColor) .objectFit(ImageFit.Cover) .shadow({ radius: StrUtil.isEmpty(song.pixelMapPath) ?6:14, type: ShadowType.BLUR, color: 'on_primary' }) .margin({ left: 8 }) .onClick(() => { if (this.isMultiSelect) { this.toggleSongSelection(song); } else { this.playSong(song, index, true); } }) // 歌曲信息 Column({ space: 4 }) { Text(this.isShowFileName?song.fileName :song.name) .fontSize(15) .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动 .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row(){ Text((song.artist ?? '') + " ") .fontSize(13) .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .visibility(song.artist?Visibility.Visible:Visibility.None) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildSongMetaLine(song)) .fontSize(13) .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor: $r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('90%') Text(this.getSongSearchLocation(song)) .fontSize(12) .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color')) .opacity(0.42) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .visibility(this.getSongSearchLocation(song).length > 0 ? Visibility.Visible : Visibility.None) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) .padding({ right: 20 }) Column() { Checkbox({ name: 'checkbox_' + index }) .select(this.isSongSelected(song)) .selectedColor(this.themeColor) .opacity(this.isMultiSelect ? 1 : 0) .animation({ duration: 300, curve: 'Smooth' }) .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None) .onChange((checked: boolean) => this.handleCheckboxSelection(song, checked)) .margin({ left: 10, top: 8, bottom: 8, right: 12 }) .width(22) .height(22) ImageAnimator() .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组 .duration(1000)// 持续 //.state(this.animationState)// 动画状态 .state(AnimationStatus.Running)// 动画状态 .fillMode(FillMode.Forwards) .width(18) .margin({ right: 12, top: 8, bottom: 8 }) .visibility(this.currentSong?.filePath==song.filePath ? (this.isMultiSelect ? Visibility.None : Visibility.Visible) : Visibility.None) .height(18) .iterations(-1) // 播放次数 } } } .width('100%') .padding(12) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor(Color.Transparent) .onClick(() => { if (this.ignoreTapAfterExitMultiSelect) { return; } if (this.isMultiSelect) { this.toggleSongSelection(song); } else { this.playSong(song, index); } }) // .gesture(LongPressGesture().onAction(() => { // this.enterMultiSelect(song); // })) .bindContextMenu(this.LongPressMenuBuilder(song), ResponseType.LongPress, { preview: MenuPreviewMode.IMAGE }) .bindContextMenu(this.LongPressMenuBuilder(song), ResponseType.RightClick, { preview: MenuPreviewMode.IMAGE }) } @Builder LongPressMenuBuilder(song: VideoItem) { Scroll() { Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.forward_end_fill')), content: '下一首播放' }) .onClick(() => { this.addSongToNextPlay(song); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')), content: '添加到歌单' }) .onClick(async () => { await this.openSongAddToPlaylistDialog(song); }) //多选 MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square_on_square')), content: $r('app.string.select_all') }) .onClick(() => { this.enterMultiSelect(song); }) //下载 MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.download')), content: '下载' }) .onClick(async () => { await this.enqueueSongDownload(song); }) //属性 MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')), content: '属性' }) .onClick(() => { this.openSongPropertySheet(song); }) //删除 MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')), content: $r('app.string.delete') }) .onClick(() => { this.confirmDeleteSingleSong(song); }) } .font({ size: 14, weight: FontWeight.Normal }) } .width(180) } .height('auto') .enableScrollInteraction(true) .scrollBar(BarState.Off) } // 对话框控制器 private createFolderDialogController: CustomDialogController | null = null; /** * 显示创建文件夹对话框 */ private showCreateFolderDialog(): void { if (!this.selectedAccount) { this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` }); return; } // 获取当前主题色 const currentThemeColor = this.themeColor; // 创建自定义对话框 this.createFolderDialogController = new CustomDialogController({ builder: CreateFolderDialog({ onConfirm: (folderName: string) => { if (folderName.trim().length > 0) { void this.createFolder(folderName.trim()); } else { this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' }); } }, onCancel: () => { // 用户取消创建 }, initialName: '', themeColor: currentThemeColor }), autoCancel: true, alignment: DialogAlignment.Center, customStyle: false }); this.createFolderDialogController.open(); } /** * 上传成功后刷新文件列表 */ private async refreshFileListAfterUpload(): Promise { if (!this.selectedAccount) { Logger.warn(TAG, '刷新失败:未选择账户'); return; } try { Logger.info(TAG, 'heanup 开始刷新上传后的文件列表'); // 显示加载状态 this.isLoading = true; // 重新加载当前账户的文件信息 await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath); Logger.info(TAG, 'heanup 文件列表刷新完成'); } catch (error) { const err = error as Error; Logger.error(TAG, `heanup 刷新文件列表失败: ${err.message}`); // 显示错误提示 this.getUIContext().getPromptAction().showToast({ message: `刷新失败: ${err.message}` }); } finally { this.isLoading = false; } } /** * 创建文件夹的实际方法 * @param folderName 文件夹名称 */ private async createFolder(folderName: string): Promise { if (!this.selectedAccount) { this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` }); return; } if (folderName.length === 0) { this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' }); return; } try { Logger.info(TAG, `heanup 开始创建文件夹: ${folderName}`); // 显示加载状态 this.isLoading = true; if(this.selectedAccount.webType==RemoteDriveType.Baidu){ // 调用RemoteDriveManager的createBaiduFolder方法 await this.webdavManager.createBaiduFolder(this.selectedAccount, folderName); }else if(this.selectedAccount.webType==RemoteDriveType.WebDav){ // 调用RemoteDriveManager的createWebDavFolder方法 await this.webdavManager.createWebDavFolder(this.selectedAccount, folderName); }else if(this.selectedAccount.webType==RemoteDriveType.Smb){ // 调用RemoteDriveManager的createSMBFolder方法 await this.webdavManager.createSMBFolder(this.selectedAccount, folderName); }else if(this.selectedAccount.webType==RemoteDriveType.Ftp){ // 调用RemoteDriveManager的createFTPFolder方法 this.getUIContext().getPromptAction().showToast({ message: 'FTP目前不支持创建文件夹' }); }else{ this.getUIContext().getPromptAction().showToast({ message: '当前账户类型不支持创建文件夹' }); } // 创建成功后刷新当前目录 await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath); this.getUIContext().getPromptAction().showToast({ message: `文件夹 "${folderName}" 创建成功` }); Logger.info(TAG, `heanup 文件夹创建成功`); } catch (error) { const err = error as Error; Logger.error(TAG, `heanup 创建文件夹失败: ${err.message}`); // 显示具体的错误信息 this.getUIContext().getPromptAction().showToast({ message: `创建失败: ${err.message}` }); } finally { this.isLoading = false; } } }