|
|
@@ -37,6 +37,8 @@ import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
|
|
|
import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
|
|
|
import { PlayingIndicator } from '../view/PlayingIndicator';
|
|
|
import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
|
|
|
+import { buildWebDavVisibleThumbPrefetchWindow, WebDavThumbPrefetchRange } from '../common/util/WebDavThumbPrefetchHelper';
|
|
|
+import { buildWebDavSongItemRenderKey } from '../common/util/WebDavListRenderHelper';
|
|
|
|
|
|
interface WebDavMetadataUpdatePayload {
|
|
|
filePath: string;
|
|
|
@@ -54,15 +56,12 @@ interface RemoteThumbSource {
|
|
|
|
|
|
const TAG = 'heanup WebDavMainPage';
|
|
|
const REMOTE_THUMB_CACHE_DIR: string = 'remote_thumbs';
|
|
|
-const REMOTE_THUMB_MAX_TASK_COUNT: number = 12;
|
|
|
-const REMOTE_THUMB_LARGE_LIST_THRESHOLD: number = 120;
|
|
|
-const REMOTE_THUMB_LARGE_LIST_PREFETCH_COUNT: number = 6;
|
|
|
-const REMOTE_THUMB_LARGE_LIST_DELAY_MS: number = 900;
|
|
|
const REMOTE_THUMB_CAPTURE_SECONDS: string = '1.2';
|
|
|
const WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD: number = 180;
|
|
|
const WEBDAV_PROGRESSIVE_RELOAD_INITIAL_COUNT: number = 20;
|
|
|
const WEBDAV_PROGRESSIVE_RELOAD_BATCH_SIZE: number = 20;
|
|
|
const WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS: number = 32;
|
|
|
+const WEBDAV_VISIBLE_THUMB_PREFETCH_DELAY_MS: number = 120;
|
|
|
|
|
|
// WebDAV歌曲数据全局内存存储
|
|
|
let globalWebdavVideoItems: VideoItem[] = [];
|
|
|
@@ -144,7 +143,6 @@ export struct WebDavMainPage {
|
|
|
@State isShowFileName: boolean = false//是否显示文件名
|
|
|
@State isLongNameRoLL: boolean = true//长歌名滚动
|
|
|
@State sortType: number = 0 //默认排序方式
|
|
|
- @State listRefreshKey: number = 0 // 列表刷新标识
|
|
|
@Consume isMultiSelect: boolean
|
|
|
@State selectedSongs: VideoItem[] = []
|
|
|
@State selectedFolders: FileInfo[] = []
|
|
|
@@ -164,6 +162,11 @@ export struct WebDavMainPage {
|
|
|
private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
|
|
|
private thumbnailTaskToken: number = 0;
|
|
|
private thumbnailRunningKeys: Set<string> = new Set<string>();
|
|
|
+ private visibleListStartIndex: number = -1;
|
|
|
+ private visibleListEndIndex: number = -1;
|
|
|
+ private scheduledThumbPrefetchTimer: number = -1;
|
|
|
+ private lastVisibleThumbStart: number = -1;
|
|
|
+ private lastVisibleThumbEndExclusive: number = -1;
|
|
|
private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
|
|
|
private readonly downloadCenterListener: () => void = (): void => {
|
|
|
this.scheduleDownloadCenterRefresh();
|
|
|
@@ -186,8 +189,7 @@ export struct WebDavMainPage {
|
|
|
async onSwitchAccount(){
|
|
|
console.log('heanup 切换账户:', this.selectedAccount.name);
|
|
|
this.cancelPendingDirectoryRefresh();
|
|
|
- this.thumbnailTaskToken += 1;
|
|
|
- this.thumbnailRunningKeys.clear();
|
|
|
+ this.resetRemoteThumbPrefetchState();
|
|
|
this.searchTicket++;
|
|
|
this.searchText = '';
|
|
|
this.filteredList = [];
|
|
|
@@ -220,6 +222,7 @@ export struct WebDavMainPage {
|
|
|
|
|
|
private refreshDisplaySongs(mList: Array<VideoItem>, shouldScrollToTop: boolean = true): void {
|
|
|
const reloadToken: number = ++this.displayReloadToken;
|
|
|
+ this.invalidateRemoteThumbPrefetchRange()
|
|
|
const displayItems: Array<VideoItem> = mList ? [...mList] : [];
|
|
|
if (displayItems.length <= WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD) {
|
|
|
this.dataSource.pushArrayData(displayItems)
|
|
|
@@ -550,8 +553,39 @@ export struct WebDavMainPage {
|
|
|
if (dataIndex >= 0) {
|
|
|
this.dataSource.notifyDataChange(dataIndex);
|
|
|
}
|
|
|
- // 旧版本这里会额外刷新列表 key,ArkUI 长列表在异步封面回填时更稳定。
|
|
|
- this.listRefreshKey += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private getDisplayedFolderCount(): number {
|
|
|
+ return this.isSearchMode ? this.filteredFolderList.length : this.visibleFoldersState.length
|
|
|
+ }
|
|
|
+
|
|
|
+ private stopScheduledRemoteThumbPrefetch(): void {
|
|
|
+ if (this.scheduledThumbPrefetchTimer >= 0) {
|
|
|
+ clearTimeout(this.scheduledThumbPrefetchTimer)
|
|
|
+ this.scheduledThumbPrefetchTimer = -1
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保留当前视口索引,但让新的列表内容可以重新触发同一可见区间的封面补全。
|
|
|
+ private invalidateRemoteThumbPrefetchRange(): void {
|
|
|
+ this.stopScheduledRemoteThumbPrefetch()
|
|
|
+ this.thumbnailTaskToken += 1
|
|
|
+ this.thumbnailRunningKeys.clear()
|
|
|
+ this.lastVisibleThumbStart = -1
|
|
|
+ this.lastVisibleThumbEndExclusive = -1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 列表数据源切换时需要显式取消旧任务,避免后台继续为已经离开的可见区生成封面。
|
|
|
+ private resetRemoteThumbPrefetchState(): void {
|
|
|
+ this.invalidateRemoteThumbPrefetchRange()
|
|
|
+ this.visibleListStartIndex = -1
|
|
|
+ this.visibleListEndIndex = -1
|
|
|
+ }
|
|
|
+
|
|
|
+ private updateVisibleRemoteThumbRange(start: number, end: number): void {
|
|
|
+ this.visibleListStartIndex = start
|
|
|
+ this.visibleListEndIndex = end
|
|
|
+ this.scheduleRemoteThumbPrefetch()
|
|
|
}
|
|
|
|
|
|
private async prepareRemoteThumbSource(item: VideoItem): Promise<RemoteThumbSource> {
|
|
|
@@ -671,22 +705,44 @@ export struct WebDavMainPage {
|
|
|
}
|
|
|
|
|
|
private scheduleRemoteThumbPrefetch(): void {
|
|
|
- const token: number = this.thumbnailTaskToken + 1;
|
|
|
- this.thumbnailTaskToken = token;
|
|
|
- this.thumbnailRunningKeys.clear();
|
|
|
- const mediaItems: VideoItem[] = this.songs.slice();
|
|
|
- if (mediaItems.length <= 0) {
|
|
|
- return;
|
|
|
+ if (this.isLoading || this.isRefreshingCache) {
|
|
|
+ this.stopScheduledRemoteThumbPrefetch()
|
|
|
+ this.thumbnailTaskToken += 1
|
|
|
+ this.thumbnailRunningKeys.clear()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const totalSongs: number = this.dataSource.totalCount()
|
|
|
+ const visibleWindow = buildWebDavVisibleThumbPrefetchWindow(totalSongs, this.getDisplayedFolderCount(),
|
|
|
+ this.visibleListStartIndex, this.visibleListEndIndex)
|
|
|
+ if (!visibleWindow.hasVisibleSongs) {
|
|
|
+ this.stopScheduledRemoteThumbPrefetch()
|
|
|
+ this.thumbnailTaskToken += 1
|
|
|
+ this.thumbnailRunningKeys.clear()
|
|
|
+ return
|
|
|
}
|
|
|
- const isLargeList: boolean = mediaItems.length >= REMOTE_THUMB_LARGE_LIST_THRESHOLD;
|
|
|
- const limitCount: number = isLargeList ?
|
|
|
- Math.min(mediaItems.length, REMOTE_THUMB_LARGE_LIST_PREFETCH_COUNT) :
|
|
|
- Math.min(mediaItems.length, REMOTE_THUMB_MAX_TASK_COUNT);
|
|
|
- const targetItems: VideoItem[] = mediaItems.slice(0, limitCount);
|
|
|
- const delayMs: number = isLargeList ? REMOTE_THUMB_LARGE_LIST_DELAY_MS : 120;
|
|
|
- setTimeout((): void => {
|
|
|
- void this.runRemoteThumbPrefetchQueue(targetItems, token);
|
|
|
- }, delayMs);
|
|
|
+ const range: WebDavThumbPrefetchRange = visibleWindow.range
|
|
|
+ const isSameVisibleRange: boolean = range.start === this.lastVisibleThumbStart &&
|
|
|
+ range.endExclusive === this.lastVisibleThumbEndExclusive
|
|
|
+ if (isSameVisibleRange) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.lastVisibleThumbStart = range.start
|
|
|
+ this.lastVisibleThumbEndExclusive = range.endExclusive
|
|
|
+ this.stopScheduledRemoteThumbPrefetch()
|
|
|
+ const token: number = this.thumbnailTaskToken + 1
|
|
|
+ this.thumbnailTaskToken = token
|
|
|
+ this.thumbnailRunningKeys.clear()
|
|
|
+ this.scheduledThumbPrefetchTimer = setTimeout((): void => {
|
|
|
+ this.scheduledThumbPrefetchTimer = -1
|
|
|
+ if (token !== this.thumbnailTaskToken) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const mediaItems: VideoItem[] = this.dataSource.dataArray.slice(range.start, range.endExclusive)
|
|
|
+ if (mediaItems.length <= 0) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ void this.runRemoteThumbPrefetchQueue(mediaItems, token)
|
|
|
+ }, WEBDAV_VISIBLE_THUMB_PREFETCH_DELAY_MS)
|
|
|
}
|
|
|
|
|
|
private async runRemoteThumbPrefetchQueue(items: VideoItem[], token: number): Promise<void> {
|
|
|
@@ -1117,9 +1173,9 @@ export struct WebDavMainPage {
|
|
|
if (this.isSearchMode && this.searchText.length > 0) {
|
|
|
this.syncSelectionAfterRefresh()
|
|
|
} else {
|
|
|
- this.listRefreshKey += 1
|
|
|
this.syncSelectionAfterRefresh()
|
|
|
}
|
|
|
+ this.invalidateRemoteThumbPrefetchRange()
|
|
|
this.scheduleRemoteThumbPrefetch()
|
|
|
}
|
|
|
|
|
|
@@ -2161,8 +2217,7 @@ export struct WebDavMainPage {
|
|
|
aboutToDisappear(): void {
|
|
|
// 取消订阅
|
|
|
this.cancelPendingDirectoryRefresh();
|
|
|
- this.thumbnailTaskToken += 1;
|
|
|
- this.thumbnailRunningKeys.clear();
|
|
|
+ this.resetRemoteThumbPrefetchState();
|
|
|
this.webdavManager.unsubscribe(this.eventHandler);
|
|
|
this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
|
|
|
this.stopDownloadCenterRefresh();
|
|
|
@@ -2173,6 +2228,7 @@ export struct WebDavMainPage {
|
|
|
private handleWebdavEvent(event: string): void {
|
|
|
switch (event) {
|
|
|
case RemoteDriveManagerStates.LoadFilesInfoStart:
|
|
|
+ this.resetRemoteThumbPrefetchState();
|
|
|
if (this.dataSource.totalCount() > 0 || this.visibleFoldersState.length > 0) {
|
|
|
this.isLoading = false;
|
|
|
this.isRefreshingCache = true;
|
|
|
@@ -2183,6 +2239,7 @@ export struct WebDavMainPage {
|
|
|
break;
|
|
|
case RemoteDriveManagerStates.LoadFilesInfoSucceed:
|
|
|
const shouldScrollToTop: boolean = !this.isRefreshingCache;
|
|
|
+ this.resetRemoteThumbPrefetchState();
|
|
|
this.songs = this.webdavManager.webDavSongs;
|
|
|
// 直接引用webdavManager的数组,避免@Observed序列化问题
|
|
|
this.webDavFiles = this.webdavManager.webDavFiles;
|
|
|
@@ -2194,7 +2251,6 @@ export struct WebDavMainPage {
|
|
|
this.updateVisibleFolders();
|
|
|
this.breadcrumbs = this.webdavManager.getBreadcrumbs();
|
|
|
this.syncSelectionAfterRefresh();
|
|
|
- this.scheduleRemoteThumbPrefetch();
|
|
|
if (this.isSearchMode && this.searchText.length > 0) {
|
|
|
void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
|
|
|
} else {
|
|
|
@@ -2245,36 +2301,42 @@ export struct WebDavMainPage {
|
|
|
return;
|
|
|
}
|
|
|
Logger.info(TAG, 'handleWebDavMetadataUpdates start');
|
|
|
- let hasChanges = false;
|
|
|
+ const changedIndices: number[] = [];
|
|
|
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;
|
|
|
+ const changedIndex = this.updateSongMetadata(payload);
|
|
|
+ if (changedIndex >= 0) {
|
|
|
+ if (changedIndices.indexOf(changedIndex) < 0) {
|
|
|
+ changedIndices.push(changedIndex);
|
|
|
+ }
|
|
|
Logger.info(TAG, `metadata updated for ${payload.filePath}`);
|
|
|
}
|
|
|
}
|
|
|
- if (!hasChanges) {
|
|
|
+ if (changedIndices.length === 0) {
|
|
|
Logger.info(TAG, 'handleWebDavMetadataUpdates no changes detected');
|
|
|
return;
|
|
|
}
|
|
|
- // 更新dataSource的数据,不滚动,只触发刷新
|
|
|
- this.dataSource.pushArrayData(this.songs);
|
|
|
- this.listRefreshKey++;
|
|
|
+ // 元数据回填只刷新变更行,避免后台补封面时整列表反复重建导致卡顿。
|
|
|
+ for (let index = 0; index < changedIndices.length; index += 1) {
|
|
|
+ const changedIndex = changedIndices[index];
|
|
|
+ if (changedIndex >= 0 && changedIndex < this.dataSource.totalCount()) {
|
|
|
+ this.dataSource.notifyDataChange(changedIndex);
|
|
|
+ }
|
|
|
+ }
|
|
|
Logger.info(TAG, 'handleWebDavMetadataUpdates trigger refresh');
|
|
|
}
|
|
|
|
|
|
- private updateSongMetadata(payload: WebDavMetadataUpdatePayload): boolean {
|
|
|
+ private updateSongMetadata(payload: WebDavMetadataUpdatePayload): number {
|
|
|
if (!payload.filePath) {
|
|
|
- return false;
|
|
|
+ return -1;
|
|
|
}
|
|
|
const targetIndex = this.findSongIndex(payload.filePath);
|
|
|
if (targetIndex < 0) {
|
|
|
- return false;
|
|
|
+ return -1;
|
|
|
}
|
|
|
const targetSong = this.songs[targetIndex];
|
|
|
let mutated = false;
|
|
|
@@ -2293,7 +2355,7 @@ export struct WebDavMainPage {
|
|
|
targetSong.artist = payload.artist;
|
|
|
mutated = true;
|
|
|
}
|
|
|
- return mutated;
|
|
|
+ return mutated ? targetIndex : -1;
|
|
|
}
|
|
|
|
|
|
private findSongIndex(filePath: string): number {
|
|
|
@@ -3638,9 +3700,14 @@ export struct WebDavMainPage {
|
|
|
edgeEffect: SwipeEdgeEffect.None
|
|
|
})
|
|
|
}
|
|
|
- }, (item: VideoItem, index: number) => item.filePath + '_' + index+this.listRefreshKey)
|
|
|
+ }, (item: VideoItem, index: number) => {
|
|
|
+ return buildWebDavSongItemRenderKey(item.filePath, index, item.pixelMapPath)
|
|
|
+ })
|
|
|
}
|
|
|
.scrollBar(BarState.Off)
|
|
|
+ .onScrollIndex((start: number, end: number) => {
|
|
|
+ this.updateVisibleRemoteThumbRange(start, end)
|
|
|
+ })
|
|
|
.onScrollFrameBegin((offset: number) => {
|
|
|
// 获取当前滚动偏移量
|
|
|
if(this.autoHideTitle){
|