|
@@ -0,0 +1,2443 @@
|
|
|
|
|
+import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
|
|
|
|
|
+import { WebDavAccount } from '../viewmodel/WebDavAccount';
|
|
|
|
|
+import { VideoItem } from '../viewmodel/VideoItem';
|
|
|
|
|
+import { FileInfo } from '../viewmodel/FileInfo';
|
|
|
|
|
+import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
|
|
|
|
|
+import Logger from '../common/util/Logger';
|
|
|
|
|
+import { router } from '@kit.ArkUI';
|
|
|
|
|
+import { CommonConstants } from '../common/constants/CommonConstants';
|
|
|
|
|
+import { Constants } from '../Constants';
|
|
|
|
|
+import { picker, fileUri } from '@kit.CoreFileKit';
|
|
|
|
|
+import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
+import { PreferencesUtil } from '@pura/harmony-utils';
|
|
|
|
|
+import { UploadTaskDataSource } from '../viewmodel/UploadTask';
|
|
|
|
|
+import { promptAction } from '@kit.ArkUI';
|
|
|
|
|
+import fs from '@ohos.file.fs';
|
|
|
|
|
+import ReqPermissionUtil from '../common/util/ReqPermissionUtil';
|
|
|
|
|
+
|
|
|
|
|
+const TAG = 'heanup UploadMusicPage';
|
|
|
|
|
+
|
|
|
|
|
+interface LocalFileItem {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ path: string;
|
|
|
|
|
+ isDirectory: boolean;
|
|
|
|
|
+ isAudioFile: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface LocalAudioFile {
|
|
|
|
|
+ fullPath: string;
|
|
|
|
|
+ relativePath: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@Entry
|
|
|
|
|
+@Component
|
|
|
|
|
+export struct UploadMusicPage {
|
|
|
|
|
+ @StorageProp('topSafeHeight') topSafeHeight: number = 0;
|
|
|
|
|
+ @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
|
|
|
|
|
+ @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
|
|
|
|
|
+ @StorageProp('isDarkMode') isDarkMode: boolean = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 页面状态变量
|
|
|
|
|
+ @State selectedFiles: string[] = [];
|
|
|
|
|
+ @State selectedFileNames: string[] = [];
|
|
|
|
|
+ @State selectedFileTypes: string[] = []; // 'file' 或 'folder'
|
|
|
|
|
+ @State accounts: WebDavAccount[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 文件浏览器状态
|
|
|
|
|
+ @State isShowFileBrowser: boolean = false;
|
|
|
|
|
+ @State fileBrowserPath: string = '';
|
|
|
|
|
+ @State browserFiles: LocalFileItem[] = [];
|
|
|
|
|
+ @State selectedBrowserItems: Set<string> = new Set();
|
|
|
|
|
+ @State isLoadingFiles: boolean = false;
|
|
|
|
|
+ @State selectedAccount: WebDavAccount | null = null;
|
|
|
|
|
+ @State uploadPath: string = '/';
|
|
|
|
|
+ @State availablePaths: string[] = [];
|
|
|
|
|
+ @State duplicateAction: string = 'skip';
|
|
|
|
|
+ @State uploadProgress: number = 0;
|
|
|
|
|
+ @State uploadSpeed: string = '0 KB/s';
|
|
|
|
|
+ @State uploadedCount: number = 0;
|
|
|
|
|
+ @State totalUploadCount: number = 0;
|
|
|
|
|
+ @State isUploading: boolean = false;
|
|
|
|
|
+ @State uploadQueue: VideoItem[] = [];
|
|
|
|
|
+ @State finishQueue: VideoItem[] = [];
|
|
|
|
|
+ @State currentTask: VideoItem | null = null;
|
|
|
|
|
+ @State isShowPathBrowser: boolean = false;
|
|
|
|
|
+ @State currentBrowsePath: string = '/';
|
|
|
|
|
+ @State browseFolders: FileInfo[] = [];
|
|
|
|
|
+ @State isLoadingFolders: boolean = false;
|
|
|
|
|
+ @State pendingCount: number = 0;
|
|
|
|
|
+ @State finishedCount: number = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // RemoteDriveManager实例
|
|
|
|
|
+ private webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
|
|
|
|
|
+
|
|
|
|
|
+ // 性能优化:使用LazyDataSource优化队列列表渲染
|
|
|
|
|
+ // LazyForEach配合IDataSource可以实现按需加载,避免一次性渲染大量列表项
|
|
|
|
|
+ // 对于包含数百个上传任务的队列,可以显著提升UI响应性能
|
|
|
|
|
+ private uploadQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource();
|
|
|
|
|
+ private finishQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource();
|
|
|
|
|
+
|
|
|
|
|
+ // 事件处理器引用
|
|
|
|
|
+ private eventHandler: (event: string) => void = (event: string) => {
|
|
|
|
|
+ this.handleUploadEvent(event);
|
|
|
|
|
+ };
|
|
|
|
|
+ private cachedDownloadRoot: string = '';
|
|
|
|
|
+ private localBrowseRoot: string = '';
|
|
|
|
|
+
|
|
|
|
|
+ aboutToAppear(): void {
|
|
|
|
|
+ Logger.info(TAG, '页面即将显示');
|
|
|
|
|
+
|
|
|
|
|
+ // 加载用户设置
|
|
|
|
|
+ this.loadUserSettings();
|
|
|
|
|
+
|
|
|
|
|
+ // 加载WebDAV账户列表
|
|
|
|
|
+ this.loadAccounts();
|
|
|
|
|
+
|
|
|
|
|
+ // 从路由参数获取预选账户ID
|
|
|
|
|
+ const params = router.getParams() as Record<string, Object>;
|
|
|
|
|
+ if (params && params['accountId']) {
|
|
|
|
|
+ const accountId = params['accountId'] as number;
|
|
|
|
|
+ this.preselectAccount(accountId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 订阅RemoteDriveManager的上传事件
|
|
|
|
|
+ this.subscribeUploadEvents();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载用户设置
|
|
|
|
|
+ */
|
|
|
|
|
+ private loadUserSettings(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 加载默认重复文件处理方式
|
|
|
|
|
+ this.duplicateAction = PreferencesUtil.getStringSync('webdavUploadDuplicateAction', 'skip');
|
|
|
|
|
+ Logger.info(TAG, `加载用户设置 - 重复文件处理: ${this.duplicateAction}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `加载用户设置失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ aboutToDisappear(): void {
|
|
|
|
|
+ Logger.info(TAG, '页面即将销毁');
|
|
|
|
|
+
|
|
|
|
|
+ // 取消订阅事件
|
|
|
|
|
+ this.unsubscribeUploadEvents();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载WebDAV账户列表
|
|
|
|
|
+ */
|
|
|
|
|
+ private loadAccounts(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.accounts = this.webdavManager.getAllWebDavAccounts();
|
|
|
|
|
+ Logger.info(TAG, `加载了 ${this.accounts.length} 个WebDAV账户`);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有选中账户且有可用账户,默认选择第一个
|
|
|
|
|
+ if (!this.selectedAccount && this.accounts.length > 0) {
|
|
|
|
|
+ this.selectedAccount = this.accounts[0];
|
|
|
|
|
+ this.applyDefaultUploadPath(this.selectedAccount);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `加载账户列表失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getPreferredDownloadRoot(): string {
|
|
|
|
|
+ if (this.cachedDownloadRoot) {
|
|
|
|
|
+ return this.cachedDownloadRoot;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ let saved = PreferencesUtil.getStringSync('download_path', '');
|
|
|
|
|
+ if (saved && saved.length > 0) {
|
|
|
|
|
+ this.cachedDownloadRoot = this.normalizeLocalDirectory(saved);
|
|
|
|
|
+ if (this.cachedDownloadRoot) {
|
|
|
|
|
+ return this.cachedDownloadRoot;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.warn(TAG, `读取download_path失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private normalizeLocalDirectory(path: string): string {
|
|
|
|
|
+ if (!path || path.length === 0) {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+ let normalized = path.replace(/\\/g, '/');
|
|
|
|
|
+ normalized = normalized.replace(/\/+$/, '');
|
|
|
|
|
+ return normalized;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private ensureDirectoryAccessible(path: string): boolean {
|
|
|
|
|
+ if (!path || path.length === 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const stat = fs.statSync(path);
|
|
|
|
|
+ return stat.isDirectory();
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fs.mkdirSync(path);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (createErr) {
|
|
|
|
|
+ const err = createErr as Error;
|
|
|
|
|
+ Logger.warn(TAG, `创建目录失败(${path}): ${err.message}`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async requestDownloadRootFromSystem(force: boolean = false): Promise<string> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const documentViewPicker = new picker.DocumentViewPicker();
|
|
|
|
|
+ const documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD });
|
|
|
|
|
+ if (documentSaveResult && documentSaveResult.length > 0) {
|
|
|
|
|
+ const resolvedPath = new fileUri.FileUri(documentSaveResult[0]).path;
|
|
|
|
|
+ const normalized = this.normalizeLocalDirectory(resolvedPath);
|
|
|
|
|
+ if (normalized) {
|
|
|
|
|
+ this.cachedDownloadRoot = normalized;
|
|
|
|
|
+ const storedValue = normalized.endsWith('/') ? normalized : `${normalized}/`;
|
|
|
|
|
+ PreferencesUtil.putSync('download_path', storedValue);
|
|
|
|
|
+ await ReqPermissionUtil.persistPermission(documentSaveResult[0]);
|
|
|
|
|
+ return normalized;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `请求系统下载目录失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async resolveInitialBrowserPath(): Promise<string> {
|
|
|
|
|
+ let preferred = this.getPreferredDownloadRoot();
|
|
|
|
|
+ if (preferred && this.ensureDirectoryAccessible(preferred)) {
|
|
|
|
|
+ return preferred;
|
|
|
|
|
+ }
|
|
|
|
|
+ const requested = await this.requestDownloadRootFromSystem();
|
|
|
|
|
+ if (requested && this.ensureDirectoryAccessible(requested)) {
|
|
|
|
|
+ return requested;
|
|
|
|
|
+ }
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: '请先在本地音乐页授权下载目录后再使用此功能',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ throw new Error('未授权任何本地目录');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private normalizeBrowsePath(path: string): string {
|
|
|
|
|
+ if (!path || path.trim().length === 0) {
|
|
|
|
|
+ return '/';
|
|
|
|
|
+ }
|
|
|
|
|
+ let normalized = path.trim().replace(/\\/g, '/');
|
|
|
|
|
+ if (!normalized.startsWith('/')) {
|
|
|
|
|
+ normalized = `/${normalized}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ normalized = normalized.replace(/\/+/g, '/');
|
|
|
|
|
+ if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
|
|
|
+ normalized = normalized.slice(0, -1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return normalized || '/';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 预选指定账户
|
|
|
|
|
+ * @param accountId 账户ID
|
|
|
|
|
+ */
|
|
|
|
|
+ private preselectAccount(accountId: number): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (let i = 0; i < this.accounts.length; i++) {
|
|
|
|
|
+ const account = this.accounts[i];
|
|
|
|
|
+ if (account.id === accountId) {
|
|
|
|
|
+ this.selectedAccount = account;
|
|
|
|
|
+ this.applyDefaultUploadPath(account);
|
|
|
|
|
+ Logger.info(TAG, `预选账户: ${account.name}`);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `预选账户失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 订阅RemoteDriveManager的上传事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private subscribeUploadEvents(): void {
|
|
|
|
|
+ this.webdavManager.subscribe(this.eventHandler);
|
|
|
|
|
+ Logger.info(TAG, '已订阅上传事件');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取消订阅RemoteDriveManager的上传事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private unsubscribeUploadEvents(): void {
|
|
|
|
|
+ this.webdavManager.unsubscribe(this.eventHandler);
|
|
|
|
|
+ Logger.info(TAG, '已取消订阅上传事件');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传事件
|
|
|
|
|
+ * @param event 事件类型
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadEvent(event: string): void {
|
|
|
|
|
+ Logger.info(TAG, `收到上传事件: ${event}`);
|
|
|
|
|
+
|
|
|
|
|
+ switch (event) {
|
|
|
|
|
+ case RemoteDriveManagerStates.UploadStart:
|
|
|
|
|
+ this.handleUploadStart();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.UploadProgress:
|
|
|
|
|
+ this.handleUploadProgress();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.UploadSuccess:
|
|
|
|
|
+ this.handleUploadSuccess();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.UploadFailed:
|
|
|
|
|
+ this.handleUploadFailed();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.UploadPaused:
|
|
|
|
|
+ this.handleUploadPaused();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.UploadResumed:
|
|
|
|
|
+ this.handleUploadResumed();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.SetCurrentUploadTask:
|
|
|
|
|
+ this.handleCurrentTaskChanged();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.ChangeUploadQueue:
|
|
|
|
|
+ this.handleUploadQueueChanged();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RemoteDriveManagerStates.ChangeFinishUploadQueue:
|
|
|
|
|
+ this.handleFinishQueueChanged();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传开始事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadStart(): void {
|
|
|
|
|
+ this.isUploading = true;
|
|
|
|
|
+ Logger.info(TAG, '上传开始');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传进度更新事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadProgress(): void {
|
|
|
|
|
+ const uploaded = this.webdavManager.uploadReceivedSize;
|
|
|
|
|
+ const total = this.webdavManager.uploadTotalSize;
|
|
|
|
|
+
|
|
|
|
|
+ if (total > 0) {
|
|
|
|
|
+ this.uploadProgress = Math.floor((uploaded / total) * 100);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算上传速度(简化版,实际应该基于时间差计算)
|
|
|
|
|
+ const speedMBps = uploaded / (1024 * 1024);
|
|
|
|
|
+ this.uploadSpeed = `${speedMBps.toFixed(2)} MB/s`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `上传进度: ${this.uploadProgress}%`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传成功事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadSuccess(): void {
|
|
|
|
|
+ this.uploadedCount++;
|
|
|
|
|
+ Logger.info(TAG, `上传成功,已完成: ${this.uploadedCount}/${this.totalUploadCount}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传失败事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadFailed(): void {
|
|
|
|
|
+ Logger.error(TAG, '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传暂停事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadPaused(): void {
|
|
|
|
|
+ Logger.info(TAG, '上传已暂停');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传恢复事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadResumed(): void {
|
|
|
|
|
+ Logger.info(TAG, '上传已恢复');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理当前任务变更事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleCurrentTaskChanged(): void {
|
|
|
|
|
+ const task = this.webdavManager.currentUploadTask;
|
|
|
|
|
+ if (task) {
|
|
|
|
|
+ this.currentTask = task.song;
|
|
|
|
|
+ Logger.info(TAG, `当前上传任务: ${task.song.name}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.currentTask = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理上传队列变更事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleUploadQueueChanged(): void {
|
|
|
|
|
+ const tasks = this.webdavManager.uploadQueue;
|
|
|
|
|
+ this.uploadQueue = [];
|
|
|
|
|
+ for (let i = 0; i < tasks.length; i++) {
|
|
|
|
|
+ this.uploadQueue.push(tasks[i].song);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.pendingCount = this.uploadQueue.length;
|
|
|
|
|
+
|
|
|
|
|
+ if (this.pendingCount === 0) {
|
|
|
|
|
+ this.isUploading = false;
|
|
|
|
|
+ this.currentTask = null;
|
|
|
|
|
+ this.uploadProgress = 0;
|
|
|
|
|
+ this.uploadSpeed = '0 KB/s';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.uploadQueueDataSource.updateTasks(this.uploadQueue);
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `上传队列更新,当前数量: ${this.pendingCount}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理完成队列变更事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleFinishQueueChanged(): void {
|
|
|
|
|
+ const tasks = this.webdavManager.finishUploadQueue;
|
|
|
|
|
+ this.finishQueue = [];
|
|
|
|
|
+ for (let i = 0; i < tasks.length; i++) {
|
|
|
|
|
+ this.finishQueue.push(tasks[i].song);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.finishedCount = this.finishQueue.length;
|
|
|
|
|
+
|
|
|
|
|
+ this.finishQueueDataSource.updateTasks(this.finishQueue);
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `完成队列更新,当前数量: ${this.finishedCount}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开文件选择器
|
|
|
|
|
+ */
|
|
|
|
|
+ private async openFilePicker(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const documentSelectOptions = new picker.DocumentSelectOptions();
|
|
|
|
|
+
|
|
|
|
|
+ // 设置音频文件过滤器
|
|
|
|
|
+ documentSelectOptions.fileSuffixFilters = Constants.AUDIO_EXTENSIONS;
|
|
|
|
|
+
|
|
|
|
|
+ // 支持多选
|
|
|
|
|
+ documentSelectOptions.maxSelectNumber = 100;
|
|
|
|
|
+
|
|
|
|
|
+ const documentPicker = new picker.DocumentViewPicker();
|
|
|
|
|
+
|
|
|
|
|
+ const documentSelectResult = await documentPicker.select(documentSelectOptions);
|
|
|
|
|
+
|
|
|
|
|
+ if (documentSelectResult && documentSelectResult.length > 0) {
|
|
|
|
|
+ for (let i = 0; i < documentSelectResult.length; i++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ReqPermissionUtil.persistPermission(documentSelectResult[i]);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `持久化授权失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Logger.info(TAG, `选择了 ${documentSelectResult.length} 个文件`);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存选中的文件URI
|
|
|
|
|
+ this.selectedFiles = documentSelectResult;
|
|
|
|
|
+
|
|
|
|
|
+ // 提取文件名用于显示
|
|
|
|
|
+ this.selectedFileNames = [];
|
|
|
|
|
+ this.selectedFileTypes = [];
|
|
|
|
|
+ for (let i = 0; i < documentSelectResult.length; i++) {
|
|
|
|
|
+ const uri = documentSelectResult[i];
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fileUriObj = new fileUri.FileUri(uri);
|
|
|
|
|
+ const fileName = fileUriObj.name;
|
|
|
|
|
+ this.selectedFileNames.push(fileName);
|
|
|
|
|
+ this.selectedFileTypes.push('file');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `解析文件URI失败: ${err.message}`);
|
|
|
|
|
+ this.selectedFileNames.push('未知文件');
|
|
|
|
|
+ this.selectedFileTypes.push('file');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `文件列表: ${JSON.stringify(this.selectedFileNames)}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as BusinessError;
|
|
|
|
|
+ Logger.error(TAG, `文件选择失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开文件浏览器
|
|
|
|
|
+ */
|
|
|
|
|
+ private async openFileBrowser(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const initialPath = await this.resolveInitialBrowserPath();
|
|
|
|
|
+ if (!initialPath) {
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: '无法定位到下载目录,请授予文件访问权限',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.fileBrowserPath = initialPath;
|
|
|
|
|
+ this.localBrowseRoot = initialPath;
|
|
|
|
|
+ this.selectedBrowserItems.clear();
|
|
|
|
|
+ this.isShowFileBrowser = true;
|
|
|
|
|
+ await this.loadLocalFiles(initialPath);
|
|
|
|
|
+ Logger.info(TAG, `文件浏览器已定位到: ${initialPath}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `打开文件浏览器失败: ${err.message}`);
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: err.message || '打开文件浏览器失败',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载本地文件列表
|
|
|
|
|
+ */
|
|
|
|
|
+ private async loadLocalFiles(path: string): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.isLoadingFiles = true;
|
|
|
|
|
+ this.browserFiles = [];
|
|
|
|
|
+ let targetPath = path;
|
|
|
|
|
+ if (this.localBrowseRoot && !targetPath.startsWith(this.localBrowseRoot)) {
|
|
|
|
|
+ targetPath = this.localBrowseRoot;
|
|
|
|
|
+ }
|
|
|
|
|
+ Logger.info(TAG, `加载目录: ${targetPath}`);
|
|
|
|
|
+
|
|
|
|
|
+ const files = fs.listFileSync(targetPath);
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
|
|
+ const fileName = files[i];
|
|
|
|
|
+ const fullPath = `${targetPath}/${fileName}`;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const stat = fs.statSync(fullPath);
|
|
|
|
|
+ const isDirectory = stat.isDirectory();
|
|
|
|
|
+ if (fileName.startsWith('.')) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const lowerFileName = fileName.toLowerCase();
|
|
|
|
|
+ const isAudioFile = !isDirectory && Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext));
|
|
|
|
|
+
|
|
|
|
|
+ // 只显示文件夹和音频文件
|
|
|
|
|
+ if (isDirectory || isAudioFile) {
|
|
|
|
|
+ this.browserFiles.push({
|
|
|
|
|
+ name: fileName,
|
|
|
|
|
+ path: fullPath,
|
|
|
|
|
+ isDirectory: isDirectory,
|
|
|
|
|
+ isAudioFile: isAudioFile
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.warn(TAG, `无法访问: ${fullPath}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 排序:文件夹在前,文件在后
|
|
|
|
|
+ this.browserFiles.sort((a, b) => {
|
|
|
|
|
+ if (a.isDirectory && !b.isDirectory) return -1;
|
|
|
|
|
+ if (!a.isDirectory && b.isDirectory) return 1;
|
|
|
|
|
+ return a.name.localeCompare(b.name);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `加载了 ${this.browserFiles.length} 个项目`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `加载文件列表失败: ${err.message}`);
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: `加载失败: ${err.message}`,
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.isLoadingFiles = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 进入子目录
|
|
|
|
|
+ */
|
|
|
|
|
+ private async enterDirectory(item: LocalFileItem): Promise<void> {
|
|
|
|
|
+ if (!item.isDirectory) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.localBrowseRoot && !item.path.startsWith(this.localBrowseRoot)) {
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: '没有此目录的访问权限',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.fileBrowserPath = item.path;
|
|
|
|
|
+ await this.loadLocalFiles(item.path);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 返回上级目录
|
|
|
|
|
+ */
|
|
|
|
|
+ private async goBackDirectory(): Promise<void> {
|
|
|
|
|
+ if (!this.localBrowseRoot) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.fileBrowserPath === this.localBrowseRoot) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const lastSlash = this.fileBrowserPath.lastIndexOf('/');
|
|
|
|
|
+ if (lastSlash > 0) {
|
|
|
|
|
+ const parent = this.fileBrowserPath.substring(0, lastSlash);
|
|
|
|
|
+ if (!parent.startsWith(this.localBrowseRoot)) {
|
|
|
|
|
+ this.fileBrowserPath = this.localBrowseRoot;
|
|
|
|
|
+ await this.loadLocalFiles(this.localBrowseRoot);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.fileBrowserPath = parent;
|
|
|
|
|
+ await this.loadLocalFiles(this.fileBrowserPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 切换项目选中状态
|
|
|
|
|
+ */
|
|
|
|
|
+ private toggleItemSelection(item: LocalFileItem): void {
|
|
|
|
|
+ if (this.selectedBrowserItems.has(item.path)) {
|
|
|
|
|
+ this.selectedBrowserItems.delete(item.path);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.selectedBrowserItems.add(item.path);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 触发UI更新
|
|
|
|
|
+ this.selectedBrowserItems = new Set(this.selectedBrowserItems);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 确认选择文件
|
|
|
|
|
+ */
|
|
|
|
|
+ private async confirmFileSelection(): Promise<void> {
|
|
|
|
|
+ if (this.selectedBrowserItems.size === 0) {
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: '请至少选择一个项目',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 将选中的项目添加到已选列表
|
|
|
|
|
+ const selectedItems: LocalFileItem[] = [];
|
|
|
|
|
+ const selectedSet = new Set<string>();
|
|
|
|
|
+ this.selectedBrowserItems.forEach((path) => {
|
|
|
|
|
+ const found = this.browserFiles.find((f) => f.path === path);
|
|
|
|
|
+ if (found && !selectedSet.has(found.path)) {
|
|
|
|
|
+ selectedSet.add(found.path);
|
|
|
|
|
+ selectedItems.push(found);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < selectedItems.length; i++) {
|
|
|
|
|
+ const item = selectedItems[i];
|
|
|
|
|
+ const uri = `file://${item.path}`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ReqPermissionUtil.persistPermission(uri);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `持久化授权失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.selectedFiles.push(uri);
|
|
|
|
|
+ this.selectedFileNames.push(item.name);
|
|
|
|
|
+ this.selectedFileTypes.push(item.isDirectory ? 'folder' : 'file');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: `已添加 ${this.selectedBrowserItems.size} 个项目`,
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.closeFileBrowser();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 关闭文件浏览器
|
|
|
|
|
+ */
|
|
|
|
|
+ private closeFileBrowser(): void {
|
|
|
|
|
+ this.isShowFileBrowser = false;
|
|
|
|
|
+ this.selectedBrowserItems.clear();
|
|
|
|
|
+ this.browserFiles = [];
|
|
|
|
|
+ this.localBrowseRoot = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 递归扫描文件夹,获取所有音频文件
|
|
|
|
|
+ */
|
|
|
|
|
+ private async scanFolderForAudioFiles(folderPath: string, relativePrefix: string = ''): Promise<LocalAudioFile[]> {
|
|
|
|
|
+ const audioFiles: LocalAudioFile[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const files = fs.listFileSync(folderPath);
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
|
|
+ const fileName = files[i];
|
|
|
|
|
+ const fullPath = `${folderPath}/${fileName}`;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const stat = fs.statSync(fullPath);
|
|
|
|
|
+
|
|
|
|
|
+ if (stat.isDirectory()) {
|
|
|
|
|
+ const nextPrefix = relativePrefix ? `${relativePrefix}/${fileName}` : fileName;
|
|
|
|
|
+ const subFiles = await this.scanFolderForAudioFiles(fullPath, nextPrefix);
|
|
|
|
|
+ audioFiles.push(...subFiles);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const lowerFileName = fileName.toLowerCase();
|
|
|
|
|
+ const isAudio = Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext));
|
|
|
|
|
+ if (isAudio) {
|
|
|
|
|
+ const relativePath = relativePrefix ? `${relativePrefix}/${fileName}` : fileName;
|
|
|
|
|
+ audioFiles.push({
|
|
|
|
|
+ fullPath,
|
|
|
|
|
+ relativePath
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.warn(TAG, `无法访问: ${fullPath}, 错误: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `扫描文件夹失败: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return audioFiles;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getFileNameFromPath(fullPath: string): string {
|
|
|
|
|
+ if (!fullPath) {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+ const normalized = fullPath.replace(/\\/g, '/');
|
|
|
|
|
+ const lastSlash = normalized.lastIndexOf('/');
|
|
|
|
|
+ return lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 移除已选文件
|
|
|
|
|
+ * @param index 文件索引
|
|
|
|
|
+ */
|
|
|
|
|
+ private removeSelectedFile(index: number): void {
|
|
|
|
|
+ if (index >= 0 && index < this.selectedFiles.length) {
|
|
|
|
|
+ this.selectedFiles.splice(index, 1);
|
|
|
|
|
+ this.selectedFileNames.splice(index, 1);
|
|
|
|
|
+ this.selectedFileTypes.splice(index, 1);
|
|
|
|
|
+ Logger.info(TAG, `移除项目,剩余 ${this.selectedFiles.length} 个`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 选择上传账户
|
|
|
|
|
+ * @param account 选中的账户
|
|
|
|
|
+ */
|
|
|
|
|
+ private selectUploadAccount(account: WebDavAccount): void {
|
|
|
|
|
+ this.selectedAccount = account;
|
|
|
|
|
+ this.applyDefaultUploadPath(account);
|
|
|
|
|
+ Logger.info(TAG, `选择账户: ${account.name}, 上传路径: ${this.uploadPath}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private applyDefaultUploadPath(account: WebDavAccount | null): void {
|
|
|
|
|
+ if (!account) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const managerAccount = this.webdavManager.currentAccount;
|
|
|
|
|
+ const managerPath = this.webdavManager.currentPath;
|
|
|
|
|
+ if (
|
|
|
|
|
+ managerPath &&
|
|
|
|
|
+ managerPath.length > 0 &&
|
|
|
|
|
+ managerAccount &&
|
|
|
|
|
+ managerAccount.id === account.id
|
|
|
|
|
+ ) {
|
|
|
|
|
+ this.uploadPath = managerPath;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.uploadPath = account.uploadFilePath || '/';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开路径浏览器
|
|
|
|
|
+ */
|
|
|
|
|
+ private openPathBrowser(): void {
|
|
|
|
|
+ if (!this.selectedAccount) {
|
|
|
|
|
+ Logger.warn(TAG, '请先选择账户');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, '========== 打开路径浏览器 ==========');
|
|
|
|
|
+ Logger.info(TAG, `当前上传路径: ${this.uploadPath}`);
|
|
|
|
|
+ Logger.info(TAG, `选中账户: ${this.selectedAccount.name}`);
|
|
|
|
|
+
|
|
|
|
|
+ const managerAccount = this.webdavManager.currentAccount;
|
|
|
|
|
+ const defaultPath =
|
|
|
|
|
+ managerAccount &&
|
|
|
|
|
+ managerAccount.id === this.selectedAccount.id &&
|
|
|
|
|
+ this.webdavManager.currentPath &&
|
|
|
|
|
+ this.webdavManager.currentPath.length > 0
|
|
|
|
|
+ ? this.webdavManager.currentPath
|
|
|
|
|
+ : this.uploadPath;
|
|
|
|
|
+
|
|
|
|
|
+ this.currentBrowsePath = defaultPath;
|
|
|
|
|
+ this.isShowPathBrowser = true;
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `isShowPathBrowser 设置为: ${this.isShowPathBrowser}`);
|
|
|
|
|
+ Logger.info(TAG, '开始加载文件夹列表...');
|
|
|
|
|
+
|
|
|
|
|
+ this.loadWebDavFolders(this.currentBrowsePath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 关闭路径浏览器
|
|
|
|
|
+ */
|
|
|
|
|
+ private closePathBrowser(): void {
|
|
|
|
|
+ Logger.info(TAG, '========== 关闭路径浏览器 ==========');
|
|
|
|
|
+ this.isShowPathBrowser = false;
|
|
|
|
|
+ this.browseFolders = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载WebDAV文件夹列表
|
|
|
|
|
+ * @param path 路径
|
|
|
|
|
+ */
|
|
|
|
|
+ private async loadWebDavFolders(path: string): Promise<void> {
|
|
|
|
|
+ if (!this.selectedAccount) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.isLoadingFolders = true;
|
|
|
|
|
+ Logger.info(TAG, `加载文件夹列表: ${path}`);
|
|
|
|
|
+
|
|
|
|
|
+ await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, path);
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤出文件夹
|
|
|
|
|
+ const allFiles = this.webdavManager.webDavFiles;
|
|
|
|
|
+ this.browseFolders = [];
|
|
|
|
|
+ const normalizedCurrent = this.normalizeBrowsePath(this.currentBrowsePath);
|
|
|
|
|
+ for (let i = 0; i < allFiles.length; i++) {
|
|
|
|
|
+ const file = allFiles[i];
|
|
|
|
|
+ if (file.isDirectory) {
|
|
|
|
|
+ const folderPath = this.normalizeBrowsePath(file.href);
|
|
|
|
|
+ if (folderPath !== normalizedCurrent) {
|
|
|
|
|
+ this.browseFolders.push(file);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `加载了 ${this.browseFolders.length} 个文件夹`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `加载文件夹失败: ${err.message}`);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.isLoadingFolders = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 进入子文件夹
|
|
|
|
|
+ * @param folder 文件夹信息
|
|
|
|
|
+ */
|
|
|
|
|
+ private async enterBrowseFolder(folder: FileInfo): Promise<void> {
|
|
|
|
|
+ this.currentBrowsePath = folder.href;
|
|
|
|
|
+ await this.loadWebDavFolders(folder.href);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 返回上级文件夹
|
|
|
|
|
+ */
|
|
|
|
|
+ private async goBackBrowseFolder(): Promise<void> {
|
|
|
|
|
+ if (this.currentBrowsePath === '/') {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const lastSlashIndex = this.currentBrowsePath.lastIndexOf('/');
|
|
|
|
|
+ if (lastSlashIndex > 0) {
|
|
|
|
|
+ this.currentBrowsePath = this.currentBrowsePath.substring(0, lastSlashIndex);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.currentBrowsePath = '/';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await this.loadWebDavFolders(this.currentBrowsePath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 确认选择路径
|
|
|
|
|
+ */
|
|
|
|
|
+ private confirmPathSelection(): void {
|
|
|
|
|
+ this.uploadPath = this.currentBrowsePath;
|
|
|
|
|
+ Logger.info(TAG, `选择上传路径: ${this.uploadPath}`);
|
|
|
|
|
+ this.closePathBrowser();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 开始上传
|
|
|
|
|
+ */
|
|
|
|
|
+ private async startUpload(): Promise<void> {
|
|
|
|
|
+ // 验证
|
|
|
|
|
+ if (!this.selectedAccount) {
|
|
|
|
|
+ Logger.warn(TAG, '请先选择账户');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (this.selectedFiles.length === 0) {
|
|
|
|
|
+ Logger.warn(TAG, '请先选择文件');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.uploadPath) {
|
|
|
|
|
+ Logger.warn(TAG, '请先选择上传路径');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ Logger.info(TAG, '开始上传任务');
|
|
|
|
|
+
|
|
|
|
|
+ // 显示处理提示
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: '正在处理文件...',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 构建VideoItem列表
|
|
|
|
|
+ const songs: VideoItem[] = [];
|
|
|
|
|
+ let totalFiles = 0;
|
|
|
|
|
+ let totalFolders = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < this.selectedFiles.length; i++) {
|
|
|
|
|
+ const selectedUri = this.selectedFiles[i];
|
|
|
|
|
+ const displayName = this.selectedFileNames[i];
|
|
|
|
|
+ const itemType = this.selectedFileTypes[i];
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fileUriObj = new fileUri.FileUri(selectedUri);
|
|
|
|
|
+ const resolvedPath = fileUriObj.path || '';
|
|
|
|
|
+
|
|
|
|
|
+ if (!resolvedPath) {
|
|
|
|
|
+ Logger.error(TAG, `无法获取本地路径,跳过: ${selectedUri}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (itemType === 'folder') {
|
|
|
|
|
+ totalFolders++;
|
|
|
|
|
+ const folderName = displayName || this.getFileNameFromPath(resolvedPath);
|
|
|
|
|
+ Logger.info(TAG, `扫描文件夹: ${folderName}`);
|
|
|
|
|
+ const audioFiles = await this.scanFolderForAudioFiles(resolvedPath, folderName);
|
|
|
|
|
+ Logger.info(TAG, `文件夹 ${folderName} 包含 ${audioFiles.length} 个音频文件`);
|
|
|
|
|
+
|
|
|
|
|
+ for (let j = 0; j < audioFiles.length; j++) {
|
|
|
|
|
+ const audioFile = audioFiles[j];
|
|
|
|
|
+ const audioFileName = this.getFileNameFromPath(audioFile.fullPath);
|
|
|
|
|
+
|
|
|
|
|
+ const videoItem = new VideoItem(
|
|
|
|
|
+ audioFileName,
|
|
|
|
|
+ `file://${audioFile.fullPath}`,
|
|
|
|
|
+ audioFile.fullPath,
|
|
|
|
|
+ CommonConstants.TYPE_LOCAL,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ '',
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ audioFileName
|
|
|
|
|
+ );
|
|
|
|
|
+ videoItem.remote_rel_path = audioFile.relativePath;
|
|
|
|
|
+ songs.push(videoItem);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 处理单个文件
|
|
|
|
|
+ totalFiles++;
|
|
|
|
|
+ const videoItem = new VideoItem(
|
|
|
|
|
+ displayName,
|
|
|
|
|
+ selectedUri,
|
|
|
|
|
+ resolvedPath,
|
|
|
|
|
+ CommonConstants.TYPE_LOCAL,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ '',
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ displayName
|
|
|
|
|
+ );
|
|
|
|
|
+ videoItem.remote_rel_path = displayName;
|
|
|
|
|
+ songs.push(videoItem);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `处理项目失败: ${err.message}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (songs.length === 0) {
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: '未找到可上传的音频文件',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ Logger.warn(TAG, '未找到可上传的有效文件,取消任务');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, `处理完成: ${totalFiles} 个文件, ${totalFolders} 个文件夹, 共 ${songs.length} 个音频文件`);
|
|
|
|
|
+
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: `准备上传 ${songs.length} 个文件`,
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到上传队列,传递用户选择的上传路径
|
|
|
|
|
+ Logger.info(TAG, `使用上传路径: ${this.uploadPath}`);
|
|
|
|
|
+ const uniqueSongs = this.deduplicateSongs(songs);
|
|
|
|
|
+ this.webdavManager.addToUploadQueue(uniqueSongs, this.selectedAccount, this.uploadPath);
|
|
|
|
|
+ this.totalUploadCount = uniqueSongs.length;
|
|
|
|
|
+ this.uploadedCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 开始处理上传队列
|
|
|
|
|
+ await this.webdavManager.startUploadQueue();
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, '上传任务已启动');
|
|
|
|
|
+ this.selectedFiles = [];
|
|
|
|
|
+ this.selectedFileNames = [];
|
|
|
|
|
+ this.selectedFileTypes = [];
|
|
|
|
|
+ this.pendingCount = this.webdavManager.uploadQueue.length;
|
|
|
|
|
+ this.selectedFiles = [];
|
|
|
|
|
+ this.selectedFileNames = [];
|
|
|
|
|
+ this.selectedFileTypes = [];
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ Logger.error(TAG, `启动上传失败: ${err.message}`);
|
|
|
|
|
+ promptAction.showToast({
|
|
|
|
|
+ message: `上传失败: ${err.message}`,
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private deduplicateSongs(songs: VideoItem[]): VideoItem[] {
|
|
|
|
|
+ const unique: VideoItem[] = [];
|
|
|
|
|
+ const seen = new Set<string>();
|
|
|
|
|
+ for (let i = 0; i < songs.length; i++) {
|
|
|
|
|
+ const song = songs[i];
|
|
|
|
|
+ const key = song.filePath || song.name;
|
|
|
|
|
+ if (!key) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (seen.has(key)) {
|
|
|
|
|
+ Logger.warn(TAG, `跳过重复文件: ${song.name}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ seen.add(key);
|
|
|
|
|
+ unique.push(song);
|
|
|
|
|
+ }
|
|
|
|
|
+ return unique;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 暂停上传
|
|
|
|
|
+ */
|
|
|
|
|
+ private pauseUpload(): void {
|
|
|
|
|
+ this.webdavManager.pauseUploadQueue();
|
|
|
|
|
+ Logger.info(TAG, '暂停上传');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 恢复上传
|
|
|
|
|
+ */
|
|
|
|
|
+ private async resumeUpload(): Promise<void> {
|
|
|
|
|
+ await this.webdavManager.resumeUploadQueue();
|
|
|
|
|
+ Logger.info(TAG, '恢复上传');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取消上传
|
|
|
|
|
+ */
|
|
|
|
|
+ private cancelUpload(): void {
|
|
|
|
|
+ this.webdavManager.pauseUploadQueue();
|
|
|
|
|
+ this.webdavManager.clearUploadQueue();
|
|
|
|
|
+ this.isUploading = false;
|
|
|
|
|
+ this.currentTask = null;
|
|
|
|
|
+ Logger.info(TAG, '取消上传');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传进度显示组件
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ UploadProgressView() {
|
|
|
|
|
+ if (this.isUploading && this.currentTask) {
|
|
|
|
|
+ Column({ space: 16 }) {
|
|
|
|
|
+ // 标题行
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('⏫')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ Text('上传中')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignSelf(ItemAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ // 当前文件信息卡片
|
|
|
|
|
+ Column({ space: 10 }) {
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('🎵')
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ Text(this.currentTask.name)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ // 进度条
|
|
|
|
|
+ Progress({ value: this.uploadProgress, total: 100, type: ProgressType.Linear })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .color(this.themeColor)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
|
|
|
|
|
+ .style({ strokeWidth: 6 })
|
|
|
|
|
+
|
|
|
|
|
+ // 进度信息行
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Text(`${this.uploadProgress}%`)
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .fontColor(this.themeColor)
|
|
|
|
|
+
|
|
|
|
|
+ Blank()
|
|
|
|
|
+
|
|
|
|
|
+ Text(`${this.uploadSpeed}`)
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ // 已上传数量
|
|
|
|
|
+ Row({ space: 6 }) {
|
|
|
|
|
+ Text('✓')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.themeColor)
|
|
|
|
|
+ Text(`已完成 ${this.uploadedCount}/${this.totalUploadCount}`)
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(14)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+
|
|
|
|
|
+ // 控制按钮
|
|
|
|
|
+ Row({ space: 10 }) {
|
|
|
|
|
+ Button({ type: ButtonType.Normal }) {
|
|
|
|
|
+ Row({ space: 6 }) {
|
|
|
|
|
+ Text(this.webdavManager.isPauseUpload ? '▶️' : '⏸️')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ Text(this.webdavManager.isPauseUpload ? '恢复' : '暂停')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .height(44)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ if (this.webdavManager.isPauseUpload) {
|
|
|
|
|
+ this.resumeUpload();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.pauseUpload();
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Button({ type: ButtonType.Normal }) {
|
|
|
|
|
+ Row({ space: 6 }) {
|
|
|
|
|
+ Text('❌')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ Text('取消')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .height(44)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30')
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.cancelUpload();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: this.isDarkMode ? 8 : 12,
|
|
|
|
|
+ color: this.isDarkMode ? '#0C000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 2
|
|
|
|
|
+ })
|
|
|
|
|
+ .margin({ bottom: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传队列管理组件(使用LazyDataSource优化性能)
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ UploadQueueView() {
|
|
|
|
|
+ Column({ space: 16 }) {
|
|
|
|
|
+ // 标题行
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('📊')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ Text('任务队列')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignSelf(ItemAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ Tabs({ barPosition: BarPosition.Start }) {
|
|
|
|
|
+ TabContent() {
|
|
|
|
|
+ this.QueueList(this.uploadQueueDataSource, 'pending')
|
|
|
|
|
+ }
|
|
|
|
|
+ .tabBar(this.TabBarBuilder('⏳', '待上传', this.pendingCount))
|
|
|
|
|
+
|
|
|
|
|
+ TabContent() {
|
|
|
|
|
+ this.QueueList(this.finishQueueDataSource, 'finished')
|
|
|
|
|
+ }
|
|
|
|
|
+ .tabBar(this.TabBarBuilder('✅', '已完成', this.finishedCount))
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(300)
|
|
|
|
|
+ .barMode(BarMode.Fixed)
|
|
|
|
|
+ .barBackgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .animationDuration(300)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: this.isDarkMode ? 8 : 12,
|
|
|
|
|
+ color: this.isDarkMode ? '#0C000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 2
|
|
|
|
|
+ })
|
|
|
|
|
+ .margin({ bottom: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自定义Tab标签构建器
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ TabBarBuilder(icon: string, title: string, count: number) {
|
|
|
|
|
+ Row({ space: 6 }) {
|
|
|
|
|
+ Text(icon)
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ Text(`${count}`)
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding({ left: 4, right: 4 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 队列列表组件(使用LazyForEach优化性能)
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ QueueList(dataSource: UploadTaskDataSource, type: string) {
|
|
|
|
|
+ if (dataSource.totalCount() === 0) {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Text(type === 'pending' ? '暂无待上传任务' : '暂无已完成任务')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ List({ space: 8 }) {
|
|
|
|
|
+ LazyForEach(dataSource, (item: VideoItem, index: number) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ // 文件图标
|
|
|
|
|
+ Text('🎵')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+
|
|
|
|
|
+ Text(item.name)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+
|
|
|
|
|
+ if (type === 'pending') {
|
|
|
|
|
+ Button('移除')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .height(32)
|
|
|
|
|
+ .padding({ left: 12, right: 12 })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#D94838' : '#E84026')
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(6)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ // 从队列移除
|
|
|
|
|
+ const tasks = this.webdavManager.uploadQueue;
|
|
|
|
|
+ if (index < tasks.length) {
|
|
|
|
|
+ this.webdavManager.removeFromUploadQueue(tasks[index]);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Text('✓')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#5BA854' : '#64BB5C')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ }
|
|
|
|
|
+ }, (item: VideoItem, index: number) => `${type}-${index}-${item.name}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .cachedCount(5) // 缓存5个列表项以提升滚动性能
|
|
|
|
|
+ .divider({
|
|
|
|
|
+ strokeWidth: 1,
|
|
|
|
|
+ color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 开始上传按钮组件
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ StartUploadButton() {
|
|
|
|
|
+ Button({ type: ButtonType.Normal }) {
|
|
|
|
|
+ Row({ space: 10 }) {
|
|
|
|
|
+ Text('🚀')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ Text('开始上传')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(54)
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .enabled(!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null)
|
|
|
|
|
+ .opacity((!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null) ? 1.0 : 0.4)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: 12,
|
|
|
|
|
+ color: this.themeColor + '50',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 4
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.startUpload();
|
|
|
|
|
+ })
|
|
|
|
|
+ .margin({ bottom: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 远程配置区域组件
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ UploadConfigSection() {
|
|
|
|
|
+ Column({ space: 16 }) {
|
|
|
|
|
+ // 标题行
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('📁')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ Text('远程配置')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignSelf(ItemAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ // 上传路径选择卡片
|
|
|
|
|
+ Column({ space: 12 }) {
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('远程目录')
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000')
|
|
|
|
|
+
|
|
|
|
|
+ Blank()
|
|
|
|
|
+
|
|
|
|
|
+ Button('浏览')
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .height(32)
|
|
|
|
|
+ .padding({ left: 12, right: 12 })
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(6)
|
|
|
|
|
+ .enabled(this.selectedAccount !== null)
|
|
|
|
|
+ .opacity(this.selectedAccount !== null ? 1.0 : 0.4)
|
|
|
|
|
+ .bindContentCover(this.isShowPathBrowser, this.PathBrowserDialogBuilder(), {
|
|
|
|
|
+ modalTransition: ModalTransition.DEFAULT,
|
|
|
|
|
+ onDisappear: () => {
|
|
|
|
|
+ Logger.info(TAG, 'PathBrowser onDisappear 回调');
|
|
|
|
|
+ this.isShowPathBrowser = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.openPathBrowser();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('📂')
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.uploadPath || '请选择远程目录')
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .fontColor(this.uploadPath ?
|
|
|
|
|
+ (this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') :
|
|
|
|
|
+ (this.isDarkMode ? '#66FFFFFF' : '#99000000'))
|
|
|
|
|
+ .fontWeight(this.uploadPath ? FontWeight.Medium : FontWeight.Regular)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(14)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: 1,
|
|
|
|
|
+ color: this.uploadPath ?
|
|
|
|
|
+ (this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30') :
|
|
|
|
|
+ (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 重复文件处理卡片
|
|
|
|
|
+ Column({ space: 12 }) {
|
|
|
|
|
+ Text('重复文件处理')
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000')
|
|
|
|
|
+ .alignSelf(ItemAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 10 }) {
|
|
|
|
|
+ // 跳过选项
|
|
|
|
|
+ Row({ space: 10 }) {
|
|
|
|
|
+ Radio({ value: 'skip', group: 'duplicateAction' })
|
|
|
|
|
+ .checked(this.duplicateAction === 'skip')
|
|
|
|
|
+ .radioStyle({
|
|
|
|
|
+ checkedBackgroundColor: this.themeColor,
|
|
|
|
|
+ uncheckedBorderColor: this.isDarkMode ? '#4D4D4D' : '#C7C7CC'
|
|
|
|
|
+ })
|
|
|
|
|
+ .onChange((checked: boolean) => {
|
|
|
|
|
+ if (checked) {
|
|
|
|
|
+ this.duplicateAction = 'skip';
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 2 }) {
|
|
|
|
|
+ Text('跳过')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ Text('保留远程文件,不上传')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .backgroundColor(this.duplicateAction === 'skip' ?
|
|
|
|
|
+ (this.isDarkMode ? '#2E3238' : '#FFFFFF') :
|
|
|
|
|
+ 'transparent')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: this.duplicateAction === 'skip' ? 2 : 1,
|
|
|
|
|
+ color: this.duplicateAction === 'skip' ?
|
|
|
|
|
+ this.themeColor :
|
|
|
|
|
+ (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.duplicateAction = 'skip';
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 覆盖选项
|
|
|
|
|
+ Row({ space: 10 }) {
|
|
|
|
|
+ Radio({ value: 'overwrite', group: 'duplicateAction' })
|
|
|
|
|
+ .checked(this.duplicateAction === 'overwrite')
|
|
|
|
|
+ .radioStyle({
|
|
|
|
|
+ checkedBackgroundColor: this.themeColor,
|
|
|
|
|
+ uncheckedBorderColor: this.isDarkMode ? '#4D4D4D' : '#C7C7CC'
|
|
|
|
|
+ })
|
|
|
|
|
+ .onChange((checked: boolean) => {
|
|
|
|
|
+ if (checked) {
|
|
|
|
|
+ this.duplicateAction = 'overwrite';
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 2 }) {
|
|
|
|
|
+ Text('覆盖')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ Text('替换远程文件')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .backgroundColor(this.duplicateAction === 'overwrite' ?
|
|
|
|
|
+ (this.isDarkMode ? '#2E3238' : '#FFFFFF') :
|
|
|
|
|
+ 'transparent')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: this.duplicateAction === 'overwrite' ? 2 : 1,
|
|
|
|
|
+ color: this.duplicateAction === 'overwrite' ?
|
|
|
|
|
+ this.themeColor :
|
|
|
|
|
+ (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.duplicateAction = 'overwrite';
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 重命名选项
|
|
|
|
|
+ Row({ space: 10 }) {
|
|
|
|
|
+ Radio({ value: 'rename', group: 'duplicateAction' })
|
|
|
|
|
+ .checked(this.duplicateAction === 'rename')
|
|
|
|
|
+ .radioStyle({
|
|
|
|
|
+ checkedBackgroundColor: this.themeColor,
|
|
|
|
|
+ uncheckedBorderColor: this.isDarkMode ? '#4D4D4D' : '#C7C7CC'
|
|
|
|
|
+ })
|
|
|
|
|
+ .onChange((checked: boolean) => {
|
|
|
|
|
+ if (checked) {
|
|
|
|
|
+ this.duplicateAction = 'rename';
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 2 }) {
|
|
|
|
|
+ Text('重命名')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ Text('自动重命名后上传')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .backgroundColor(this.duplicateAction === 'rename' ?
|
|
|
|
|
+ (this.isDarkMode ? '#2E3238' : '#FFFFFF') :
|
|
|
|
|
+ 'transparent')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: this.duplicateAction === 'rename' ? 2 : 1,
|
|
|
|
|
+ color: this.duplicateAction === 'rename' ?
|
|
|
|
|
+ this.themeColor :
|
|
|
|
|
+ (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.duplicateAction = 'rename';
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(14)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: this.isDarkMode ? 8 : 12,
|
|
|
|
|
+ color: this.isDarkMode ? '#0C000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 2
|
|
|
|
|
+ })
|
|
|
|
|
+ .margin({ bottom: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 路径浏览器对话框
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ PathBrowserDialog() {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ // 标题栏
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Text('选择上传路径')
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ Button('关闭')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .height(36)
|
|
|
|
|
+ .padding({ left: 16, right: 16 })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3033' : '#E5E5EA')
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.closePathBrowser();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+
|
|
|
|
|
+ // 当前路径显示
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Column({ space: 4 }) {
|
|
|
|
|
+ Text('当前路径')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.currentBrowsePath)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.currentBrowsePath !== '/') {
|
|
|
|
|
+ Button('返回上级')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .height(36)
|
|
|
|
|
+ .padding({ left: 12, right: 12 })
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.goBackBrowseFolder();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
|
|
|
|
|
+
|
|
|
|
|
+ // 文件夹列表
|
|
|
|
|
+ if (this.isLoadingFolders) {
|
|
|
|
|
+ Column({ space: 12 }) {
|
|
|
|
|
+ LoadingProgress()
|
|
|
|
|
+ .width(48)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .color(this.themeColor)
|
|
|
|
|
+
|
|
|
|
|
+ Text('加载中...')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(300)
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ } else if (this.browseFolders.length === 0) {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Text('📂')
|
|
|
|
|
+ .fontSize(48)
|
|
|
|
|
+ .margin({ bottom: 12 })
|
|
|
|
|
+
|
|
|
|
|
+ Text('当前目录没有子文件夹')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(300)
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ List({ space: 8 }) {
|
|
|
|
|
+ ForEach(this.browseFolders, (folder: FileInfo, index: number) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Text('📁')
|
|
|
|
|
+ .fontSize(24)
|
|
|
|
|
+
|
|
|
|
|
+ Text(folder.fileName)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+
|
|
|
|
|
+ Text('→')
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.enterBrowseFolder(folder);
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }, (folder: FileInfo, index: number) => `folder-${index}-${folder.fileName}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(300)
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .divider({
|
|
|
|
|
+ strokeWidth: 1,
|
|
|
|
|
+ color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 底部按钮
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Button('取消')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3033' : '#E5E5EA')
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.closePathBrowser();
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Button('选择此路径')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.confirmPathSelection();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .constraintSize({ maxWidth: 600 })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#191A1C' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(16)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: 24,
|
|
|
|
|
+ color: this.isDarkMode ? '#33000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 8
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 账户选择区域组件
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ AccountSelectorSection() {
|
|
|
|
|
+ Column({ space: 16 }) {
|
|
|
|
|
+ // 标题行
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('☁️')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ Text('选择账户')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignSelf(ItemAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.accounts.length === 0) {
|
|
|
|
|
+ Column({ space: 12 }) {
|
|
|
|
|
+ Text('📦')
|
|
|
|
|
+ .fontSize(48)
|
|
|
|
|
+
|
|
|
|
|
+ Text('暂无WebDAV账户')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
|
|
|
|
|
+
|
|
|
|
|
+ Text('请先添加账户')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(32)
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ List({ space: 8 }) {
|
|
|
|
|
+ ForEach(this.accounts, (account: WebDavAccount, index: number) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ // 账户图标
|
|
|
|
|
+ Text('☁️')
|
|
|
|
|
+ .fontSize(24)
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 4 }) {
|
|
|
|
|
+ Text(account.name)
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+
|
|
|
|
|
+ Text(`${account.host}:${account.port}`)
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.selectedAccount && this.selectedAccount.id === account.id) {
|
|
|
|
|
+ Text('✓')
|
|
|
|
|
+ .fontSize(24)
|
|
|
|
|
+ .fontColor(this.themeColor)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(14)
|
|
|
|
|
+ .backgroundColor(this.selectedAccount && this.selectedAccount.id === account.id
|
|
|
|
|
+ ? (this.isDarkMode ? '#2E3238' : '#FFFFFF')
|
|
|
|
|
+ : (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: this.selectedAccount && this.selectedAccount.id === account.id ? 2 : 1,
|
|
|
|
|
+ color: this.selectedAccount && this.selectedAccount.id === account.id
|
|
|
|
|
+ ? this.themeColor
|
|
|
|
|
+ : (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.selectUploadAccount(account);
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }, (account: WebDavAccount) => `account-${account.id}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .divider({
|
|
|
|
|
+ strokeWidth: 1,
|
|
|
|
|
+ color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: this.isDarkMode ? 8 : 12,
|
|
|
|
|
+ color: this.isDarkMode ? '#0C000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 2
|
|
|
|
|
+ })
|
|
|
|
|
+ .margin({ bottom: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 文件选择区域组件
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ FilePickerSection() {
|
|
|
|
|
+ Column({ space: 16 }) {
|
|
|
|
|
+ // 标题行
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('🎵')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ Text('选择文件')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignSelf(ItemAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ // 按钮组
|
|
|
|
|
+ Row({ space: 10 }) {
|
|
|
|
|
+ Button({ type: ButtonType.Normal }) {
|
|
|
|
|
+ Row({ space: 6 }) {
|
|
|
|
|
+ Text('📄')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ Text('快速选择')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: 8,
|
|
|
|
|
+ color: this.themeColor + '40',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 3
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.openFilePicker();
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Button({ type: ButtonType.Normal }) {
|
|
|
|
|
+ Row({ space: 6 }) {
|
|
|
|
|
+ Text('📁')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ Text('浏览选择')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: 8,
|
|
|
|
|
+ color: this.themeColor + '40',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 3
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.openFileBrowser();
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ if (this.selectedFiles.length > 0) {
|
|
|
|
|
+ Row({ space: 10 }) {
|
|
|
|
|
+ Text('✓')
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontColor(this.themeColor)
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 2 }) {
|
|
|
|
|
+ Text(`已选择 ${this.selectedFiles.length} 项`)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ if (this.selectedFileTypes.filter(t => t === 'file').length > 0) {
|
|
|
|
|
+ Text(`${this.selectedFileTypes.filter(t => t === 'file').length} 个文件`)
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.selectedFileTypes.filter(t => t === 'folder').length > 0) {
|
|
|
|
|
+ Text(`${this.selectedFileTypes.filter(t => t === 'folder').length} 个文件夹`)
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: 1,
|
|
|
|
|
+ color: this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: this.isDarkMode ? 8 : 12,
|
|
|
|
|
+ color: this.isDarkMode ? '#0C000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 2
|
|
|
|
|
+ })
|
|
|
|
|
+ .margin({ bottom: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 已选文件列表组件
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ SelectedFilesList() {
|
|
|
|
|
+ Column({ space: 16 }) {
|
|
|
|
|
+ // 标题行
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Text('📋')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ Text('已选文件')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+
|
|
|
|
|
+ Blank()
|
|
|
|
|
+
|
|
|
|
|
+ Text(`${this.selectedFiles.length}`)
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .alignSelf(ItemAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ List({ space: 8 }) {
|
|
|
|
|
+ ForEach(this.selectedFileNames, (fileName: string, index: number) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ // 文件/文件夹图标
|
|
|
|
|
+ Text(this.selectedFileTypes[index] === 'folder' ? '📁' : '🎵')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 2 }) {
|
|
|
|
|
+ Text(fileName)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+
|
|
|
|
|
+ if (this.selectedFileTypes[index] === 'folder') {
|
|
|
|
|
+ Text('文件夹')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ Button({ type: ButtonType.Normal }) {
|
|
|
|
|
+ Text('移除')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ }
|
|
|
|
|
+ .height(32)
|
|
|
|
|
+ .padding({ left: 12, right: 12 })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30')
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(6)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.removeSelectedFile(index);
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: 1,
|
|
|
|
|
+ color: this.isDarkMode ? '#19FFFFFF' : '#E5E5EA'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }, (fileName: string, index: number) => `${index}-${fileName}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .constraintSize({ maxHeight: 300 })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: this.isDarkMode ? 8 : 12,
|
|
|
|
|
+ color: this.isDarkMode ? '#0C000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 2
|
|
|
|
|
+ })
|
|
|
|
|
+ .margin({ bottom: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ build() {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ // 标题栏
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Button({ type: ButtonType.Circle }) {
|
|
|
|
|
+ Text('←')
|
|
|
|
|
+ .fontSize(24)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(44)
|
|
|
|
|
+ .height(44)
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: 8,
|
|
|
|
|
+ color: this.themeColor + '40',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 2
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ router.back();
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Text('上传音乐到WebDAV')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(56)
|
|
|
|
|
+ .padding({ left: 16, right: 16 })
|
|
|
|
|
+ .margin({ top: this.topSafeHeight })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+
|
|
|
|
|
+ // 页面内容区域
|
|
|
|
|
+ Scroll() {
|
|
|
|
|
+ Column({ space: 0 }) {
|
|
|
|
|
+ // 账户选择区域
|
|
|
|
|
+ this.AccountSelectorSection()
|
|
|
|
|
+
|
|
|
|
|
+ // 上传配置区域
|
|
|
|
|
+ this.UploadConfigSection()
|
|
|
|
|
+
|
|
|
|
|
+ // 文件选择区域
|
|
|
|
|
+ this.FilePickerSection()
|
|
|
|
|
+
|
|
|
|
|
+ // 已选文件列表
|
|
|
|
|
+ if (this.selectedFiles.length > 0) {
|
|
|
|
|
+ this.SelectedFilesList()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 开始上传按钮
|
|
|
|
|
+ if (!this.isUploading) {
|
|
|
|
|
+ this.StartUploadButton()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 上传进度显示
|
|
|
|
|
+ this.UploadProgressView()
|
|
|
|
|
+
|
|
|
|
|
+ // 上传队列管理
|
|
|
|
|
+ if (this.uploadQueue.length > 0 || this.finishQueue.length > 0) {
|
|
|
|
|
+ this.UploadQueueView()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .constraintSize({ minHeight: '100%' })
|
|
|
|
|
+ }
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .scrollBar(BarState.Auto)
|
|
|
|
|
+
|
|
|
|
|
+ // 底部安全区
|
|
|
|
|
+ Row()
|
|
|
|
|
+ .height(this.bottomSafeHeight)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
|
|
|
|
|
+ .bindContentCover(this.isShowFileBrowser, this.FileBrowserDialogBuilder(), {
|
|
|
|
|
+ modalTransition: ModalTransition.DEFAULT,
|
|
|
|
|
+ onDisappear: () => {
|
|
|
|
|
+ Logger.info(TAG, 'FileBrowser onDisappear 回调');
|
|
|
|
|
+ this.isShowFileBrowser = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ PathBrowserDialogBuilder() {
|
|
|
|
|
+ Stack() {
|
|
|
|
|
+ // 背景层
|
|
|
|
|
+ Column()
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .backgroundColor('rgba(0, 0, 0, 0.5)')
|
|
|
|
|
+
|
|
|
|
|
+ // 对话框内容(居中显示)
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ this.PathBrowserDialog()
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .alignItems(HorizontalAlign.Center)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 文件浏览器对话框
|
|
|
|
|
+ */
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ FileBrowserDialog() {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ // 标题栏
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Text('浏览选择文件')
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ Button('关闭')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .height(36)
|
|
|
|
|
+ .padding({ left: 16, right: 16 })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.closeFileBrowser();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+
|
|
|
|
|
+ // 当前路径和返回按钮
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Column({ space: 4 }) {
|
|
|
|
|
+ Text('当前目录')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.fileBrowserPath)
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.fileBrowserPath.lastIndexOf('/') > 0) {
|
|
|
|
|
+ Button('返回上级')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .height(36)
|
|
|
|
|
+ .padding({ left: 12, right: 12 })
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.goBackDirectory();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
|
|
|
|
|
+
|
|
|
|
|
+ // 文件列表
|
|
|
|
|
+ if (this.isLoadingFiles) {
|
|
|
|
|
+ Column({ space: 12 }) {
|
|
|
|
|
+ LoadingProgress()
|
|
|
|
|
+ .width(48)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .color(this.themeColor)
|
|
|
|
|
+
|
|
|
|
|
+ Text('加载中...')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(400)
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ } else if (this.browserFiles.length === 0) {
|
|
|
|
|
+ Column({ space: 12 }) {
|
|
|
|
|
+ Text('📂')
|
|
|
|
|
+ .fontSize(48)
|
|
|
|
|
+
|
|
|
|
|
+ Text('当前目录为空')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(400)
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ List({ space: 8 }) {
|
|
|
|
|
+ ForEach(this.browserFiles, (item: LocalFileItem, index: number) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ // 复选框
|
|
|
|
|
+ Checkbox()
|
|
|
|
|
+ .select(this.selectedBrowserItems.has(item.path))
|
|
|
|
|
+ .selectedColor(this.themeColor)
|
|
|
|
|
+ .onChange((checked: boolean) => {
|
|
|
|
|
+ this.toggleItemSelection(item);
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 图标
|
|
|
|
|
+ Text(item.isDirectory ? '📁' : '🎵')
|
|
|
|
|
+ .fontSize(24)
|
|
|
|
|
+
|
|
|
|
|
+ // 文件名
|
|
|
|
|
+ Column({ space: 2 }) {
|
|
|
|
|
+ Text(item.name)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+
|
|
|
|
|
+ if (item.isDirectory) {
|
|
|
|
|
+ Text('文件夹')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ // 进入按钮(仅文件夹)
|
|
|
|
|
+ if (item.isDirectory) {
|
|
|
|
|
+ Text('→')
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.enterDirectory(item);
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .backgroundColor(this.selectedBrowserItems.has(item.path) ?
|
|
|
|
|
+ (this.isDarkMode ? '#2E3238' : '#FFFFFF') :
|
|
|
|
|
+ (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .border({
|
|
|
|
|
+ width: this.selectedBrowserItems.has(item.path) ? 2 : 1,
|
|
|
|
|
+ color: this.selectedBrowserItems.has(item.path) ?
|
|
|
|
|
+ this.themeColor :
|
|
|
|
|
+ (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ if (!item.isDirectory) {
|
|
|
|
|
+ this.toggleItemSelection(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }, (item: LocalFileItem, index: number) => `${index}-${item.path}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(400)
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 底部按钮
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Text(`已选择 ${this.selectedBrowserItems.size} 项`)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+
|
|
|
|
|
+ Button('取消')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .padding({ left: 20, right: 20 })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
|
|
|
|
|
+ .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.closeFileBrowser();
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Button('确定')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .padding({ left: 20, right: 20 })
|
|
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .enabled(this.selectedBrowserItems.size > 0)
|
|
|
|
|
+ .opacity(this.selectedBrowserItems.size > 0 ? 1.0 : 0.4)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.confirmFileSelection();
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .constraintSize({ maxWidth: 700, maxHeight: '80%' })
|
|
|
|
|
+ .backgroundColor(this.isDarkMode ? '#191A1C' : '#FFFFFF')
|
|
|
|
|
+ .borderRadius(16)
|
|
|
|
|
+ .shadow({
|
|
|
|
|
+ radius: 24,
|
|
|
|
|
+ color: this.isDarkMode ? '#33000000' : '#19000000',
|
|
|
|
|
+ offsetX: 0,
|
|
|
|
|
+ offsetY: 8
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ FileBrowserDialogBuilder() {
|
|
|
|
|
+ Stack() {
|
|
|
|
|
+ // 背景层(不添加 onClick,避免误关闭)
|
|
|
|
|
+ Column()
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .backgroundColor('rgba(0, 0, 0, 0.5)')
|
|
|
|
|
+
|
|
|
|
|
+ // 对话框内容(居中显示)
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ this.FileBrowserDialog()
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|