import { BreadcrumbItem, 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 { promptAction, SymbolGlyphModifier, router, window, curves } from '@kit.ArkUI'; import { CommonConstants } from '../common/constants/CommonConstants'; import { VideoItem } from '../viewmodel/VideoItem'; import { GlobalContext } from '../common/util/GlobalContext'; import { display } from '@kit.ArkUI'; import { FileInfo } from '../viewmodel/FileInfo'; import { emitter } from '@kit.BasicServicesKit'; import { EventConstants } from '../common/constants/EventConstants'; import { LazyDataSource } from '../common/util/LazyDataSource'; import { ArrayUtil, 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'; /** * 歌单播放事件数据 */ 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; } const TAG = 'heanup WebDavMainPage'; // WebDAV歌曲数据全局内存存储 let globalWebdavVideoItems: VideoItem[] = []; let globalWebdavCurrentPlayIndex: number = 0; // 导出函数供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() @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 private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED }; async onSwitchAccount(){ console.log('onecold 切换账户:', this.selectedAccount.name); 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) } this.dataSource.pushArrayData(mList) if(mList.length > 0){ setTimeout(() => { this.listScroller.scrollToIndex(0) },200) } } // 更新可见文件夹列表 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.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.isMultiSelect = false; this.selectedSongs = []; this.isAllSelected = false; 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 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.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(); // 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; } } 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; } const clonedSongs: VideoItem[] = []; for (let i = 0; i < this.songs.length; i++) { clonedSongs.push(this.cloneSong(this.songs[i])); } this.songs = clonedSongs; this.dataSource.pushArrayData(clonedSongs); 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; this.webdavManager.loadFilesInfoFromWebdav() .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) } // 播放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); // 检查歌曲是否有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: index, isJump: isJump,//设置true会弹出播放页 songFilePaths: songFilePaths }; // 保存videoItems到全局内存 globalWebdavVideoItems = videoItems; globalWebdavCurrentPlayIndex = index; Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${index}`); // 发送播放请求事件,只传递索引信息 emitter.emit(eventPlaylistPlay, { data: playlistData }); Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${index}`); 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.queryAllPlaylists()).find(p => p.name === 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 playlists = await playlistTable.queryAllPlaylists(); const target = playlists.reverse().find(p => p.name === 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.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') }) .onClick(async () => { this.doSortType(0) PreferencesUtil.put("webDavSortType", 0) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按名称降序' }) .onClick(async () => { this.doSortType(1) PreferencesUtil.put("webDavSortType", 1) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')), content:'按时间升序' }) .onClick(async () => { this.doSortType(2) PreferencesUtil.put("webDavSortType", 2) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')), content: '按时间降序' }) .onClick(async () => { this.doSortType(3) PreferencesUtil.put("webDavSortType", 3) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')), content:'按大小升序' }) .onClick(async () => { this.doSortType(4) PreferencesUtil.put("webDavSortType", 4) }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按大小降序' }) .onClick(async () => { this.doSortType(5) PreferencesUtil.put("webDavSortType", 5) }) }.attributeModifier(new MenuModifier()) } doSortType(index: number) { // 对歌曲列表进行排序 switch (index) { case 0: Utility.doSortListAscending(this.songs,this.isShowFileName) this.visibleFoldersState.sort((a, b) => { return a.fileName.localeCompare(b.fileName); }); break; case 1: Utility.doSortListDescending(this.songs,this.isShowFileName) this.visibleFoldersState.sort((a, b) => { return b.fileName.localeCompare(a.fileName); }); break; case 2: this.songs.sort((a, b) => { return a.cTime.localeCompare(b.cTime); }); this.visibleFoldersState.sort((a, b) => { return a.time-b.time; }); break; case 3: this.songs.sort((a, b) => { return b.cTime.localeCompare(a.cTime); }); this.visibleFoldersState.sort((a, b) => { return b.time-a.time; }); break; case 4: this.songs.sort((a, b) => { return a.videoSize - b.videoSize; }); break; case 5: this.songs.sort((a, b) => { return b.videoSize - a.videoSize; }); break; } this.updateListData(this.songs,true) } @Builder topTitleBar(){ Column() { Row({ space: 15 }) { if (!this.isSearchMode ) { //左侧滑动按钮 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) 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 }) }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(() => { this.isSearchMode = false this.onSearchInput('') }) .attributeModifier(new ShadowModifier()) .zIndex(0) } //搜索框 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(this.searchText); }) .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.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹 this.isSearchMode = true }) //排序按钮 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%') } @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%') } //搜索功能的实现 @State searchText: string = ''; // 用户输入内容 @State filteredList: Array = []; // 过滤后的歌曲结果 @State filteredFolderList: Array = []; // 过滤后的文件夹结果 // 实时搜索逻辑(带防抖) // 实时搜索逻辑(带防抖) private onSearchInput(value: string) { this.searchText = value.trim(); let mSearchList: Array = [] mSearchList = this.songs // 新增条件判断:空输入时显示所有数据 if (this.searchText === '') { this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新 this.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹 } else { this.filteredList = mSearchList.filter((item: VideoItem) => { //支持模糊匹配和艺术家 专辑匹配 const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i'); return regex.test(item.name.toLowerCase())|| regex.test(item.fileName?.toLowerCase() ?? "") || regex.test(item.artist?.toLowerCase() ?? "") || regex.test(item.album?.toLowerCase() ?? "") }); // 对文件夹进行过滤 this.filteredFolderList = this.visibleFoldersState.filter((folder: FileInfo) => { const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i'); return regex.test(folder.fileName?.toLowerCase() ?? "") || regex.test(decodeUrlEncodedString(folder.fileName?.replace('/', '') ?? "").toLowerCase()); }); } this.updateListData(this.filteredList); } 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}) .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)) .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%') .padding({top: this.topSafeHeight+160}) .layoutWeight(1) } // 内容视图 @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.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) } .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }), TransitionEffect.scale({ x: 0, y: 0 }))) .clickEffect({ level: ClickEffectLevel.MIDDLE }) }, (folder: FileInfo) => folder.name+folder.fileName) // 显示歌曲 LazyForEach(this.dataSource, (song: VideoItem, index: number) => { ListItem() { this.buildSongItem(song, index) } .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }), TransitionEffect.scale({ x: 0, y: 0 }))) .clickEffect({ level: ClickEffectLevel.MIDDLE }) }, (item: VideoItem) => item.filePath + '_' + 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('暂无内容') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) Text('点击"左侧菜单"网盘加载') .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 }) .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('文件夹') .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(() => { 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%') } .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.isMultiSelect) { this.toggleSongSelection(song); } else { this.playSong(song, index); } }) .gesture(LongPressGesture().onAction(() => { this.enterMultiSelect(song); })) } // 对话框控制器 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; } } }