import { WebdavManager } from '../common/util/WebdavManager'; import { WebDavAccount } from '../viewmodel/WebDavAccount'; import { Song } from '../viewmodel/Song'; import { WebdavManagerStates } from '../common/enums/WebdavManagerStates'; import Logger from '../common/util/Logger'; import { promptAction, router, window } 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'; /** * 歌单播放事件数据 */ interface PlaylistEventData { playlistId: string; playlistName: string; songCount: number; startIndex: number; songFilePaths: string[]; // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id } const TAG = 'heanup WebDavMainPage'; // URL解码函数 function decodeUrlEncodedString(encodedStr: string): string { try { return decodeURIComponent(encodedStr); } catch (error) { // 如果解码失败,返回原始字符串 return encodedStr; } } @Preview @Entry @Component export struct WebDavMainPage { @State webdavManager: WebdavManager = WebdavManager.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 topRectHeight: number = 0; // 顶部安全区高度 @State breadcrumbs:string[] = []//面包屑导航 @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量 async onSwitchAccount(){ console.log('onecold 切换账户:', this.selectedAccount.name); this.songs = []; this.visibleFoldersState = []; this.updateListData(this.songs) // 注意:不再需要清空全局上下文,因为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){ this.dataSource.pushArrayData(mList) } // 更新可见文件夹列表 private updateVisibleFolders(): void { try { // 安全检查webDavFiles if (!this.webDavFiles || !Array.isArray(this.webDavFiles)) { this.visibleFoldersState = []; return; } const allFolders = this.webDavFiles.filter(f => f.isDirectory); 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 = []; } } // 对话框控制器 private accountDialogController: CustomDialogController | null = null; // 保存事件处理器引用,用于取消订阅 private eventHandler: (event: string) => void = (event: string) => { this.handleWebdavEvent(event); }; aboutToAppear(): void { // 获取顶部安全区高度 this.getTopRectHeight(); // 加载账户列表 this.loadAccounts(); this.loadFiles() this.breadcrumbs = this.webdavManager.getBreadcrumbs(); // 订阅WebDAV状态变化 this.webdavManager.subscribe(this.eventHandler); } // 获取顶部安全区高度 private getTopRectHeight(): void { window.getLastWindow(getContext(this), (err, data) => { if (err.code) { Logger.error(TAG, '获取窗口失败: ' + JSON.stringify(err)); return; } const area = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); this.topRectHeight = px2vp(area.topRect.height); Logger.info(TAG, '顶部安全区高度: ' + this.topRectHeight); }); } aboutToDisappear(): void { // 取消订阅 this.webdavManager.unsubscribe(this.eventHandler); } // 处理WebDAV事件 private handleWebdavEvent(event: string): void { switch (event) { case WebdavManagerStates.LoadFilesInfoSucceed: this.songs = this.webdavManager.webDavSongs; this.updateListData(this.songs) // 直接引用webdavManager的数组,避免@Observed序列化问题 this.webDavFiles = this.webdavManager.webDavFiles; this.isLoading = false; // 更新可见文件夹列表 this.updateVisibleFolders(); promptAction.showToast({ message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲' }); break; case WebdavManagerStates.LoadFilesInfoFailed: this.isLoading = false; this.getUIContext().getPromptAction().showToast({ message: '加载失败' }); break; case WebdavManagerStates.InsertAccountSucceed: case WebdavManagerStates.EditAccountSucceed: case WebdavManagerStates.RemoveAccountSucceed: this.loadAccounts(); break; } } // 加载账户列表 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(); } } // 切换账户 private switchAccount(account: WebDavAccount): void { this.selectedAccount = account; this.songs = []; this.updateListData(this.songs) } // 播放WebDAV歌曲 private playSong(song: VideoItem, index: number): 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作为文件路径 } // 保存WebDAV歌曲数据到全局上下文 const globalContext = GlobalContext.getContext(); globalContext.setObject('videoItems', videoItems); globalContext.setObject('currentPlayIndex', index); Logger.info(TAG, 'heanup 已将WebDAV歌曲列表保存到全局上下文,长度:' + videoItems.length); // 发送播放事件,类似歌单播放的方式 const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }; const playlistData: PlaylistEventData = { playlistId: 'webdav-playlist', // 使用特殊的ID标识WebDAV播放列表 playlistName: 'WebDAV - ' + (this.selectedAccount?.name || '未知账户'), songCount: this.songs.length, startIndex: index, songFilePaths: songFilePaths // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id }; Logger.info(TAG, 'heanup 准备发送WebDAV播放事件,eventId: ' + eventPlaylistPlay.eventId); Logger.info(TAG, 'heanup 发送WebDAV播放事件数据: ' + JSON.stringify(playlistData)); const eventData: emitter.EventData = { data: playlistData }; emitter.emit(eventPlaylistPlay, eventData); // 跳转到首页播放器 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: '播放失败' }); } } // 导航到指定层级的面包屑路径 private navigateToBreadcrumb(breadcrumbIndex: number): void { try { this.isLoading = true; this.webdavManager.navigateToBreadcrumb(breadcrumbIndex).then((path: string) => { this.webdavManager.enterFolderFromPath(path) this.breadcrumbs = this.webdavManager.getBreadcrumbs(); }) .catch((error: Error) => { Logger.error(TAG, '导航到面包屑路径失败: ' + error.message); this.isLoading = false; }); } catch (error) { Logger.error(TAG, '导航到面包屑路径异常: ' + (error as Error).message); this.isLoading = false; } } build() { Column() { // 顶部安全区和标题栏 Column() { Blank() .height(this.topRectHeight + 5) .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) // 标题栏 Row() { Image($r('app.media.menu')) .width(24) .height(24) .margin({ left: 12, right: 8 }) .onClick(() => { this.getUIContext()?.animateTo({ duration: 555 }, () => { this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) }); Text(this.selectedAccount.name || 'WebDav') .fontSize(18) .fontColor(Color.White) .fontWeight(FontWeight.Medium) .textAlign(TextAlign.Center) } .height(48) .width('100%') .alignItems(VerticalAlign.Center) .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor) } // 内容区域 if (this.accounts.length === 0) { this.buildEmptyView(); } else { this.buildContentView(); } } .width('100%') .height('100%') .backgroundColor($r('app.color.start_window_background')) } // 空状态视图 @Builder buildEmptyView() { Column({ space: 20 }) { Image($r('app.media.cloudDisk')) .width(120) .height(120) .opacity(0.3) Text('暂无WebDAV账户') .fontSize(16) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) } .justifyContent(FlexAlign.Center) .width('100%') .layoutWeight(1) } // 内容视图 @Builder buildContentView() { Column() { // 加载按钮和面包屑导航 Column({ space: 8 }) { // 账户信息显示 // if (this.selectedAccount) { // Row({ space: 12 }) { // // 账户封面 // Stack() { // if (this.selectedAccount.coverPath) { // Image(this.selectedAccount.coverPath) // .width(20) // .height(20) // .borderRadius(10) // .objectFit(ImageFit.Cover) // .border({ width: 2, color: this.themeColor }) // } else { // // Image($r('app.media.cloudDisk')) // .width(20) // .height(20) // .fillColor(this.themeColor) // } // } // // // 账户信息 // Column({ space: 4 }) { // Text(this.selectedAccount.name || '未知账户') // .fontSize(16) // .fontWeight(FontWeight.Medium) // .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) // .maxLines(1) // .textOverflow({ overflow: TextOverflow.Ellipsis }) // // Text(`${this.selectedAccount.host}:${this.selectedAccount.port}`) // .fontSize(12) // .fontColor(this.isDarkMode ? '#8E8E93' : $r('app.color.index_tab_font_color')) // .opacity(0.7) // .maxLines(1) // .textOverflow({ overflow: TextOverflow.Ellipsis }) // } // .alignItems(HorizontalAlign.Start) // .layoutWeight(1) // // } // .width('100%') // .padding({ left: 4, right: 4, top: 8, bottom: 8 }) // .backgroundColor(this.isDarkMode ? 'rgba(44,44,46,0.8)' : 'rgba(248,248,248,0.8)') // .borderRadius(8) // .margin({ bottom: 8 }) // } // // 面包屑导航 if (this.webdavManager.currentPath !== '') { Row({ space: 8 }) { Button({ type: ButtonType.Circle }) { Image(this.webdavManager.canGoBack()?$r('app.media.back'):$r('app.media.cloudDisk')) .width(15) .height(15) .fillColor(Color.White) } .width(20) .height(20) .backgroundColor(this.themeColor) .onClick(() => this.goBack()) Row({ space: 4 }) { ForEach(this.breadcrumbs, (crumb: string, index: number) => { Row() { Text(crumb) .fontSize(15) .fontColor(this.themeColor) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .onClick(() => { this.navigateToBreadcrumb(index); }) // 添加分隔符(除了最后一个元素) 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 }) } // 统计信息 if (this.webDavFiles.length > 0) { Row() { Text(this.visibleFoldersState.length + ' 个文件夹, ' + this.songs.length + ' 首歌曲') .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .layoutWeight(1) .textAlign(TextAlign.Start) Blank() } .padding({ left: 4, right: 4 }) } } .width('100%') .padding(12) .margin({ top: 8 }) // 加载状态 Row() { LoadingProgress() .width(30) .height(30) .color(this.themeColor) Text('加载中...') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')) .margin({ left: 12 }) } .padding(20) .visibility(this.isLoading?Visibility.Visible:Visibility.None) .opacity(this.isLoading ? 1 : 0) .animation({ duration: 500, curve: 'ease-in-out' // 可选动画曲线 }) // 文件列表(文件夹 + 歌曲) if (this.webDavFiles.length > 0) { List({ space: 0 }) { // 显示文件夹 - 只显示当前目录下的直接子文件夹 ForEach(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 }) }) // 显示歌曲 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 }) }) } .layoutWeight(1) .divider({ strokeWidth: 1, color: this.isDarkMode ? '#333333' :'#EEEEEE' }) .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 buildFolderItem(folder: FileInfo) { Button({ type: ButtonType.Normal, stateEffect: false }) { 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.Normal, stateEffect: false }) { Row({ space: 12 }) { // 序号 // 歌曲封面 Image(song.pixelMap) .width(48) .height(48) .borderRadius(4) .alt($r('app.media.music_red')) .fillColor(this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 8 }) // 歌曲信息 Column({ space: 4 }) { Text(decodeUrlEncodedString(song.name)) .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row(){ Text(song.artist) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .visibility(song.artist?Visibility.Visible:Visibility.None) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(decodeUrlEncodedString(song.size||"")+' '+song.cTime) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } } .alignItems(HorizontalAlign.Start) .layoutWeight(1) // 播放图标 Image($r('app.media.ic_play')) .width(20) .height(20) .fillColor($r('app.color.index_tab_font_color')) .opacity(0.4) } } .width('100%') .padding(12) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor(Color.Transparent) .onClick(() => { this.playSong(song, index); }) } }