|
@@ -1,4 +1,4 @@
|
|
|
-import { BreadcrumbItem, RemoteDirectorySnapshot, RemoteDriveGlobalSearchResult, RemoteDriveManager } from '../common/util/RemoteDriveManager';
|
|
|
|
|
|
|
+import { BreadcrumbItem, RemoteDriveGlobalSearchResult, RemoteDriveManager } from '../common/util/RemoteDriveManager';
|
|
|
import { WebDavAccount } from '../viewmodel/WebDavAccount';
|
|
import { WebDavAccount } from '../viewmodel/WebDavAccount';
|
|
|
import { Song } from '../viewmodel/Song';
|
|
import { Song } from '../viewmodel/Song';
|
|
|
import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
|
|
import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
|
|
@@ -131,6 +131,7 @@ export struct WebDavMainPage {
|
|
|
@Link isShowDrawer: boolean;
|
|
@Link isShowDrawer: boolean;
|
|
|
@State isLoading: boolean = false;
|
|
@State isLoading: boolean = false;
|
|
|
@State isRefreshingCache: boolean = false;
|
|
@State isRefreshingCache: boolean = false;
|
|
|
|
|
+ @State isDirectoryNavigationLoading: boolean = false;
|
|
|
private webDavFiles: FileInfo[] = []; // 原始目录数据保持为普通字段,避免大数组响应式代理卡顿
|
|
private webDavFiles: FileInfo[] = []; // 原始目录数据保持为普通字段,避免大数组响应式代理卡顿
|
|
|
@StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
|
|
@StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
|
|
|
@StorageProp('isDarkMode') isDarkMode: boolean = false;
|
|
@StorageProp('isDarkMode') isDarkMode: boolean = false;
|
|
@@ -173,7 +174,6 @@ export struct WebDavMainPage {
|
|
|
};
|
|
};
|
|
|
private downloadCenterRefreshTimer: number = -1;
|
|
private downloadCenterRefreshTimer: number = -1;
|
|
|
private displayReloadToken: number = 0;
|
|
private displayReloadToken: number = 0;
|
|
|
- private pendingDirectoryRefreshTimer: number = -1;
|
|
|
|
|
private pendingDirectoryRefreshToken: number = 0;
|
|
private pendingDirectoryRefreshToken: number = 0;
|
|
|
|
|
|
|
|
private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
|
|
private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
|
|
@@ -271,19 +271,23 @@ export struct WebDavMainPage {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private cancelPendingDirectoryRefresh(): void {
|
|
private cancelPendingDirectoryRefresh(): void {
|
|
|
- if (this.pendingDirectoryRefreshTimer >= 0) {
|
|
|
|
|
- clearTimeout(this.pendingDirectoryRefreshTimer)
|
|
|
|
|
- this.pendingDirectoryRefreshTimer = -1
|
|
|
|
|
- }
|
|
|
|
|
this.pendingDirectoryRefreshToken += 1
|
|
this.pendingDirectoryRefreshToken += 1
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private scheduleDirectoryRefresh(previewPath: string | undefined, loadAction: () => Promise<void>,
|
|
|
|
|
|
|
+ private startDirectoryNavigationLoading(): void {
|
|
|
|
|
+ this.isDirectoryNavigationLoading = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private stopDirectoryNavigationLoading(): void {
|
|
|
|
|
+ this.isDirectoryNavigationLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private scheduleDirectoryRefresh(_targetPath: string | undefined, loadAction: () => Promise<void>,
|
|
|
errorPrefix: string): void {
|
|
errorPrefix: string): void {
|
|
|
this.cancelPendingDirectoryRefresh()
|
|
this.cancelPendingDirectoryRefresh()
|
|
|
|
|
+ this.startDirectoryNavigationLoading()
|
|
|
const refreshToken: number = this.pendingDirectoryRefreshToken
|
|
const refreshToken: number = this.pendingDirectoryRefreshToken
|
|
|
- const hasPreview: boolean = previewPath !== undefined ? this.showDirectoryPreviewCache(previewPath) : false
|
|
|
|
|
- this.isLoading = !hasPreview
|
|
|
|
|
|
|
+ this.isLoading = true
|
|
|
this.isRefreshingCache = false
|
|
this.isRefreshingCache = false
|
|
|
|
|
|
|
|
const executeLoad = (): void => {
|
|
const executeLoad = (): void => {
|
|
@@ -297,18 +301,11 @@ export struct WebDavMainPage {
|
|
|
Logger.error(TAG, `${errorPrefix}: ${error.message}`)
|
|
Logger.error(TAG, `${errorPrefix}: ${error.message}`)
|
|
|
this.isLoading = false
|
|
this.isLoading = false
|
|
|
this.isRefreshingCache = false
|
|
this.isRefreshingCache = false
|
|
|
|
|
+ this.stopDirectoryNavigationLoading()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (!hasPreview) {
|
|
|
|
|
- executeLoad()
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- this.pendingDirectoryRefreshTimer = setTimeout(() => {
|
|
|
|
|
- this.pendingDirectoryRefreshTimer = -1
|
|
|
|
|
- executeLoad()
|
|
|
|
|
- }, 220) as number
|
|
|
|
|
|
|
+ executeLoad()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private buildThumbnailIdentity(item: VideoItem): string {
|
|
private buildThumbnailIdentity(item: VideoItem): string {
|
|
@@ -2218,6 +2215,7 @@ export struct WebDavMainPage {
|
|
|
// 取消订阅
|
|
// 取消订阅
|
|
|
this.cancelPendingDirectoryRefresh();
|
|
this.cancelPendingDirectoryRefresh();
|
|
|
this.resetRemoteThumbPrefetchState();
|
|
this.resetRemoteThumbPrefetchState();
|
|
|
|
|
+ this.stopDirectoryNavigationLoading();
|
|
|
this.webdavManager.unsubscribe(this.eventHandler);
|
|
this.webdavManager.unsubscribe(this.eventHandler);
|
|
|
this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
|
|
this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
|
|
|
this.stopDownloadCenterRefresh();
|
|
this.stopDownloadCenterRefresh();
|
|
@@ -2229,6 +2227,7 @@ export struct WebDavMainPage {
|
|
|
switch (event) {
|
|
switch (event) {
|
|
|
case RemoteDriveManagerStates.LoadFilesInfoStart:
|
|
case RemoteDriveManagerStates.LoadFilesInfoStart:
|
|
|
this.resetRemoteThumbPrefetchState();
|
|
this.resetRemoteThumbPrefetchState();
|
|
|
|
|
+ this.startDirectoryNavigationLoading();
|
|
|
if (this.dataSource.totalCount() > 0 || this.visibleFoldersState.length > 0) {
|
|
if (this.dataSource.totalCount() > 0 || this.visibleFoldersState.length > 0) {
|
|
|
this.isLoading = false;
|
|
this.isLoading = false;
|
|
|
this.isRefreshingCache = true;
|
|
this.isRefreshingCache = true;
|
|
@@ -2246,6 +2245,7 @@ export struct WebDavMainPage {
|
|
|
this.currentDirectoryPath = this.webdavManager.currentPath || '/';
|
|
this.currentDirectoryPath = this.webdavManager.currentPath || '/';
|
|
|
this.isLoading = false;
|
|
this.isLoading = false;
|
|
|
this.isRefreshingCache = false;
|
|
this.isRefreshingCache = false;
|
|
|
|
|
+ this.stopDirectoryNavigationLoading();
|
|
|
|
|
|
|
|
// 更新可见文件夹列表
|
|
// 更新可见文件夹列表
|
|
|
this.updateVisibleFolders();
|
|
this.updateVisibleFolders();
|
|
@@ -2264,6 +2264,7 @@ export struct WebDavMainPage {
|
|
|
case RemoteDriveManagerStates.LoadFilesInfoFailed:
|
|
case RemoteDriveManagerStates.LoadFilesInfoFailed:
|
|
|
this.isLoading = false;
|
|
this.isLoading = false;
|
|
|
this.isRefreshingCache = false;
|
|
this.isRefreshingCache = false;
|
|
|
|
|
+ this.stopDirectoryNavigationLoading();
|
|
|
// this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
|
|
// this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
|
|
|
break;
|
|
break;
|
|
|
case RemoteDriveManagerStates.InsertAccountSucceed:
|
|
case RemoteDriveManagerStates.InsertAccountSucceed:
|
|
@@ -2367,103 +2368,6 @@ export struct WebDavMainPage {
|
|
|
return -1;
|
|
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.isCustomCover = item.isCustomCover;
|
|
|
|
|
- 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 cloneFolderInfo(item: FileInfo): FileInfo {
|
|
|
|
|
- const clone = new FileInfo(item.rootpath, item.name, item.totalSize, item.time);
|
|
|
|
|
- clone.readOnly = item.readOnly;
|
|
|
|
|
- clone.fileName = item.fileName;
|
|
|
|
|
- clone.href = item.href;
|
|
|
|
|
- clone.contentLength = item.contentLength;
|
|
|
|
|
- clone.isDirectory = item.isDirectory;
|
|
|
|
|
- return clone;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private applyDirectoryPreviewSnapshot(snapshot: RemoteDirectorySnapshot, targetPath: string): void {
|
|
|
|
|
- const previewSongs: VideoItem[] = snapshot.songs.map((item: VideoItem) => this.cloneSong(item));
|
|
|
|
|
- const previewFiles: FileInfo[] = snapshot.files.map((item: FileInfo) => this.cloneFolderInfo(item));
|
|
|
|
|
- this.songs = previewSongs;
|
|
|
|
|
- this.webDavFiles = previewFiles;
|
|
|
|
|
- this.currentDirectoryPath = targetPath;
|
|
|
|
|
- this.webdavManager.webDavSongs = previewSongs.slice();
|
|
|
|
|
- this.webdavManager.webDavFiles = previewFiles.slice();
|
|
|
|
|
- this.webdavManager.currentPath = targetPath;
|
|
|
|
|
- this.updateVisibleFolders();
|
|
|
|
|
- this.breadcrumbs = this.webdavManager.getBreadcrumbsForPreview(this.selectedAccount, targetPath);
|
|
|
|
|
- if (this.isSearchMode && this.searchText.length > 0) {
|
|
|
|
|
- void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- this.restoreCurrentDirectorySearchView();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private showDirectoryPreviewCache(targetPath?: string): boolean {
|
|
|
|
|
- if (!this.selectedAccount) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- const resolvedPath = targetPath && targetPath.length > 0 ? targetPath : (this.selectedAccount.filepath || '/');
|
|
|
|
|
- const preview = this.webdavManager.getDirectoryPreview(this.selectedAccount, resolvedPath);
|
|
|
|
|
- if (!preview) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- this.applyDirectoryPreviewSnapshot(preview, resolvedPath);
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// 加载账户列表
|
|
// 加载账户列表
|
|
|
private loadAccounts(): void {
|
|
private loadAccounts(): void {
|
|
|
this.accounts = this.webdavManager.getAllWebDavAccounts();
|
|
this.accounts = this.webdavManager.getAllWebDavAccounts();
|
|
@@ -3533,54 +3437,100 @@ export struct WebDavMainPage {
|
|
|
return this.topSafeHeight + 85
|
|
return this.topSafeHeight + 85
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 目录切换时统一在当前可见的账户图标外层显示转圈,避免页面中间闪大 loading。
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildDirectoryNavigationIndicator(iconSize: number, containerSize: number) {
|
|
|
|
|
+ Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
+ if (this.isDirectoryNavigationLoading) {
|
|
|
|
|
+ // 使用环形进度外圈承载目录切换态,避免在标题栏或页面中央再出现第二套 loading。
|
|
|
|
|
+ Progress({ value: 0, total: 100, type: ProgressType.Ring })
|
|
|
|
|
+ .width(containerSize + 8)
|
|
|
|
|
+ .height(containerSize + 8)
|
|
|
|
|
+ .color(this.themeColor)
|
|
|
|
|
+ .style({ strokeWidth: 2.5, status: ProgressStatus.LOADING, shadow: false })
|
|
|
|
|
+ .scale({ x: -1, y: 1 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Image(this.selectedAccount.coverPath ?
|
|
|
|
|
+ this.selectedAccount.coverPath : getCloudDiskIcon(this.selectedAccount.webType))
|
|
|
|
|
+ .width(iconSize)
|
|
|
|
|
+ .height(iconSize)
|
|
|
|
|
+ .alt($r('app.media.cloudDisk'))
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(containerSize)
|
|
|
|
|
+ .height(containerSize)
|
|
|
|
|
+ .borderRadius(containerSize / 2)
|
|
|
|
|
+ .backgroundColor('#EFEFEF')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Builder
|
|
@Builder
|
|
|
breaker() {
|
|
breaker() {
|
|
|
// 面包屑导航
|
|
// 面包屑导航
|
|
|
Column({ space: 8 }) {
|
|
Column({ space: 8 }) {
|
|
|
- // 面包屑导航
|
|
|
|
|
- if (this.currentDirectoryPath !== '') {
|
|
|
|
|
|
|
+ if (!this.isSearchMode || this.webdavManager.canGoBack()) {
|
|
|
Row({ space: 8 }) {
|
|
Row({ space: 8 }) {
|
|
|
- Button({ type: ButtonType.Circle }) {
|
|
|
|
|
- Image(this.selectedAccount.coverPath?
|
|
|
|
|
- this.selectedAccount.coverPath:getCloudDiskIcon(this.selectedAccount.webType))
|
|
|
|
|
- .width(18)
|
|
|
|
|
- .height(18)
|
|
|
|
|
- .alt($r('app.media.cloudDisk'))
|
|
|
|
|
|
|
+ // 根目录只展示账号图标和加载态,不再承载返回动作;子目录才点击返回上级。
|
|
|
|
|
+ if (this.webdavManager.canGoBack()) {
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ this.buildDirectoryNavigationIndicator(18, 24)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(24)
|
|
|
|
|
+ .height(24)
|
|
|
|
|
+ .margin({left:5})
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.92 })
|
|
|
|
|
+ .onClick(() => this.goBack())
|
|
|
|
|
+ .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(),
|
|
|
|
|
+ {
|
|
|
|
|
+ modalTransition:ModalTransition.DEFAULT,
|
|
|
|
|
+ onWillDisappear: () => {
|
|
|
|
|
+ this.isShowDownloadCenter = false
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ this.buildDirectoryNavigationIndicator(18, 24)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(24)
|
|
|
|
|
+ .height(24)
|
|
|
|
|
+ .margin({left:5})
|
|
|
|
|
+ .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(),
|
|
|
|
|
+ {
|
|
|
|
|
+ modalTransition:ModalTransition.DEFAULT,
|
|
|
|
|
+ onWillDisappear: () => {
|
|
|
|
|
+ this.isShowDownloadCenter = false
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
- .width(24)
|
|
|
|
|
- .height(24)
|
|
|
|
|
- .backgroundColor('#EFEFEF')
|
|
|
|
|
- .margin({left:5})
|
|
|
|
|
- .onClick(() => this.goBack())
|
|
|
|
|
- .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(),
|
|
|
|
|
- {
|
|
|
|
|
- modalTransition:ModalTransition.DEFAULT,
|
|
|
|
|
- onWillDisappear: () => {
|
|
|
|
|
- this.isShowDownloadCenter = false
|
|
|
|
|
- },
|
|
|
|
|
- })
|
|
|
|
|
|
|
|
|
|
Row({ space: 4 }) {
|
|
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 (this.breadcrumbs.length === 0) {
|
|
|
|
|
+ Text('根目录')
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'))
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ 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)
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
- // 添加分隔符(除了最后一个元素)
|
|
|
|
|
- if (index < this.breadcrumbs.length - 1) {
|
|
|
|
|
- Text('/')
|
|
|
|
|
- .fontSize(15)
|
|
|
|
|
- .fontColor($r('app.color.index_tab_font_color'))
|
|
|
|
|
- .opacity(0.6)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
.layoutWeight(1)
|
|
.layoutWeight(1)
|
|
|
|
|
|