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?: WebDavAuthInfo; // 新增WebDAV认证信息 } /** * WebDAV认证信息 */ interface WebDavAuthInfo { accountId: number; host: string; port: number; account: string; password: string; enableHttps: boolean; } 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: Song[] = []; @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[] = []; // 改为普通状态变量 onSwitchAccount(){ console.log('onecold 切换账户:', this.selectedAccount.name); this.songs = []; this.updateListData(this.songs) this.isLoading = true; 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; } const shouldShow = this.isDirectChildOfCurrentPath(folder); if (shouldShow) { visible.push(folder); } } 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 === '/') { // 根目录情况下,显示所有第一级文件夹(href格式为/foldername) return folder.href.startsWith('/') && folder.href !== '/' && !folder.href.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) } // 将Song转换为VideoItem private convertSongToVideoItem(song: Song, index: number): VideoItem { const videoItem = new VideoItem( song.title, index.toString(), song.src, // WebDAV URL作为文件路径 CommonConstants.TYPE_INTERNET, // 使用网络类型 song.fileSize, song.time.toString(), undefined, // pixelMap undefined, // size typeof song.img === 'string' ? song.img : undefined, // pixelMapPath song.artist, undefined, // album song.name // fileName ); return videoItem; } // 播放WebDAV歌曲 private playSong(song: Song, index: number): void { try { Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`); Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.title + ', 索引: ' + index); Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length); // 保存WebDAV认证信息到全局上下文(用于播放器认证) const globalContext = GlobalContext.getContext(); if (this.selectedAccount && this.selectedAccount.account && this.selectedAccount.password) { const webDavAuthInfo: WebDavAuthInfo = { accountId: this.selectedAccount.id, host: this.selectedAccount.host, port: this.selectedAccount.port, account: this.selectedAccount.account, password: this.selectedAccount.password, enableHttps: this.selectedAccount.enableHttps }; globalContext.setObject('webDavAuthInfo', webDavAuthInfo); Logger.info(TAG, 'heanup 已保存WebDAV认证信息到全局上下文'+JSON.stringify(webDavAuthInfo)); } // 将当前歌曲列表转换为VideoItem数组 const videoItems: VideoItem[] = []; const songFilePaths: string[] = []; for (let i = 0; i < this.songs.length; i++) { const item = this.convertSongToVideoItem(this.songs[i], i); videoItems.push(item); songFilePaths.push(item.filePath); // 使用filePath作为文件路径 } Logger.info(TAG, 'heanup 所有WebDAV歌曲文件路径: ' + JSON.stringify(songFilePaths)); // 保存WebDAV歌曲数据到全局上下文 globalContext.setObject('videoItems', videoItems); globalContext.setObject('currentPlayIndex', index); Logger.info(TAG, 'heanup 已将WebDAV歌曲列表保存到全局上下文,长度:' + videoItems.length); // 验证保存是否成功 const savedVideoItems = globalContext.getObject('videoItems') as VideoItem[]; const savedIndex = globalContext.getObject('currentPlayIndex') as number; Logger.info(TAG, 'heanup 验证保存结果 - videoItems长度: ' + (savedVideoItems?.length || 0) + ', currentPlayIndex: ' + savedIndex); // 检查认证信息是否还在 const savedAuthInfo = globalContext.getObject('webDavAuthInfo') as WebDavAuthInfo; if (savedAuthInfo) { Logger.info(TAG, 'heanup 认证信息验证成功,账户ID: ' + savedAuthInfo.accountId); } else { Logger.error(TAG, 'heanup 认证信息验证失败,webDavAuthInfo为空'); } // 发送播放事件,类似歌单播放的方式 const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }; // 准备WebDAV认证信息用于传递 let authInfoForEvent: WebDavAuthInfo | undefined = undefined; if (this.selectedAccount && this.selectedAccount.account && this.selectedAccount.password) { authInfoForEvent = { accountId: this.selectedAccount.id, host: this.selectedAccount.host, port: this.selectedAccount.port, account: this.selectedAccount.account, password: this.selectedAccount.password, enableHttps: this.selectedAccount.enableHttps }; Logger.info(TAG, 'heanup 将WebDAV认证信息包含在播放事件中'); } const playlistData: PlaylistEventData = { playlistId: 'webdav-playlist', // 使用特殊的ID标识WebDAV播放列表 playlistName: 'WebDAV - ' + (this.selectedAccount?.name || '未知账户'), songCount: this.songs.length, startIndex: index, songFilePaths: songFilePaths, webDavAuthInfo: authInfoForEvent // 直接传递认证信息 }; 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); // 跳转到首页播放器 router.pushUrl({ url: 'pages/NewIndex', params: { fromWebDAV: true } }).catch((error: Error) => { Logger.error(TAG, '跳转首页失败: ' + error.message); this.getUIContext().getPromptAction().showToast({ message: '跳转失败' }); }); } 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.mType =0 this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) }); Text('WebDAV网盘') .fontSize(18) .fontColor(Color.White) .fontWeight(FontWeight.Medium) .layoutWeight(1) .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.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) } }) // 显示歌曲 LazyForEach(this.dataSource, (song: Song, index: number) => { ListItem() { this.buildSongItem(song, index) } }) } .layoutWeight(1) .divider({ strokeWidth: 1, color: '#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) { 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) } .width('100%') .padding(12) .backgroundColor($r('app.color.start_window_background')) .onClick(() => { this.enterFolder(folder); this.breadcrumbs = this.webdavManager.getBreadcrumbs(); }) } // 歌曲列表项 @Builder buildSongItem(song: Song, index: number) { Row({ space: 12 }) { // 序号 // 歌曲封面 Image(song.img) .width(48) .height(48) .borderRadius(4) .objectFit(ImageFit.Cover) // 歌曲信息 Column({ space: 4 }) { Text(decodeUrlEncodedString(song.title)) .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(decodeUrlEncodedString(song.artist)) .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) .backgroundColor($r('app.color.start_window_background')) .onClick(() => { this.playSong(song, index); }) } }