UploadMusicPage.ets 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267
  1. import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
  2. import { WebDavAccount } from '../viewmodel/WebDavAccount';
  3. import { VideoItem } from '../viewmodel/VideoItem';
  4. import { FileInfo } from '../viewmodel/FileInfo';
  5. import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
  6. import Logger from '../common/util/Logger';
  7. import { router } from '@kit.ArkUI';
  8. import { CommonConstants } from '../common/constants/CommonConstants';
  9. import { Constants } from '../Constants';
  10. import { picker, fileUri } from '@kit.CoreFileKit';
  11. import { BusinessError } from '@kit.BasicServicesKit';
  12. import { PreferencesUtil } from '@pura/harmony-utils';
  13. import { UploadTaskDataSource } from '../viewmodel/UploadTask';
  14. import { promptAction } from '@kit.ArkUI';
  15. import fs from '@ohos.file.fs';
  16. import ReqPermissionUtil from '../common/util/ReqPermissionUtil';
  17. import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
  18. import { SegmentButton } from '@kit.ArkUI';
  19. import { SegmentButtonOptions, SegmentButtonItemTuple } from '@kit.ArkUI';
  20. const TAG = 'heanup UploadMusicPage';
  21. export interface LocalFileItem {
  22. name: string;
  23. path: string;
  24. isDirectory: boolean;
  25. isAudioFile: boolean;
  26. }
  27. interface LocalAudioFile {
  28. fullPath: string;
  29. relativePath: string;
  30. }
  31. @Entry
  32. @Component
  33. export struct UploadMusicPage {
  34. @StorageProp('topSafeHeight') topSafeHeight: number = 0;
  35. @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
  36. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  37. @StorageProp('isDarkMode') isDarkMode: boolean = false;
  38. // 页面状态变量
  39. @State selectedFiles: string[] = [];
  40. @State selectedFileNames: string[] = [];
  41. @State selectedFileTypes: string[] = []; // 'file' 或 'folder'
  42. @State accounts: WebDavAccount[] = [];
  43. // 文件浏览器状态
  44. @State isShowFileBrowser: boolean = false;
  45. @State fileBrowserPath: string = '';
  46. @State browserFiles: LocalFileItem[] = [];
  47. @State selectedBrowserItems: Set<string> = new Set();
  48. @State isLoadingFiles: boolean = false;
  49. @State selectedAccount: WebDavAccount | null = null;
  50. @State uploadPath: string = '/';
  51. @State availablePaths: string[] = [];
  52. @State duplicateAction: string = 'skip';
  53. // SegmentButton 重复文件处理选项
  54. @State duplicateOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
  55. buttons: [{ text: '跳过' }, { text: '覆盖' }, { text: '重命名' }] as SegmentButtonItemTuple,
  56. backgroundColor: $r('app.color.index_background'),
  57. selectedBackgroundColor: $r('app.color.start_window_background'),
  58. selectedFontColor: $r('app.color.text_color'),
  59. buttonPadding: { top: 10, bottom: 10 },
  60. multiply: false
  61. });
  62. @State @Watch('onDuplicateActionChange') selectedDuplicateIndex: number[] = [0];
  63. @State uploadProgress: number = 0;
  64. @State uploadSpeed: string = '0 KB/s';
  65. @State uploadedCount: number = 0;
  66. @State totalUploadCount: number = 0;
  67. @State isUploading: boolean = false;
  68. @State uploadQueue: VideoItem[] = [];
  69. @State finishQueue: VideoItem[] = [];
  70. @State currentTask: VideoItem | null = null;
  71. @State isShowPathBrowser: boolean = false;
  72. @State currentBrowsePath: string = '/';
  73. @State browseFolders: FileInfo[] = [];
  74. @State isLoadingFolders: boolean = false;
  75. @State pendingCount: number = 0;
  76. @State finishedCount: number = 0;
  77. // RemoteDriveManager实例
  78. private webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
  79. // 性能优化:使用LazyDataSource优化队列列表渲染
  80. // LazyForEach配合IDataSource可以实现按需加载,避免一次性渲染大量列表项
  81. // 对于包含数百个上传任务的队列,可以显著提升UI响应性能
  82. private uploadQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource();
  83. private finishQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource();
  84. // 事件处理器引用
  85. private eventHandler: (event: string) => void = (event: string) => {
  86. this.handleUploadEvent(event);
  87. };
  88. private cachedDownloadRoot: string = '';
  89. private localBrowseRoot: string = '';
  90. // 重复文件处理方式变化监听
  91. onDuplicateActionChange() {
  92. if (this.selectedDuplicateIndex.length > 0) {
  93. const actionMap = ['skip', 'overwrite', 'rename'];
  94. this.duplicateAction = actionMap[this.selectedDuplicateIndex[0]];
  95. Logger.info(TAG, `重复文件处理方式变更为: ${this.duplicateAction}`);
  96. }
  97. }
  98. // 获取当前重复文件处理方式的描述
  99. private getActionDescription(): string {
  100. switch (this.duplicateAction) {
  101. case 'skip':
  102. return '保留远程文件,不上传';
  103. case 'overwrite':
  104. return '替换远程文件';
  105. case 'rename':
  106. return '自动重命名后上传';
  107. default:
  108. return '保留远程文件,不上传';
  109. }
  110. }
  111. aboutToAppear(): void {
  112. Logger.info(TAG, '页面即将显示');
  113. // 加载用户设置
  114. this.loadUserSettings();
  115. // 加载WebDAV账户列表
  116. this.loadAccounts();
  117. // 从路由参数获取预选账户ID
  118. const params = router.getParams() as Record<string, Object>;
  119. if (params && params['accountId']) {
  120. const accountId = params['accountId'] as number;
  121. this.preselectAccount(accountId);
  122. }
  123. // 订阅RemoteDriveManager的上传事件
  124. this.subscribeUploadEvents();
  125. }
  126. /**
  127. * 加载用户设置
  128. */
  129. private loadUserSettings(): void {
  130. try {
  131. // 加载默认重复文件处理方式
  132. this.duplicateAction = PreferencesUtil.getStringSync('webdavUploadDuplicateAction', 'skip');
  133. // 根据加载的设置初始化SegmentButton选择索引
  134. const actionMap = ['skip', 'overwrite', 'rename'];
  135. const index = actionMap.indexOf(this.duplicateAction);
  136. this.selectedDuplicateIndex = [index >= 0 ? index : 0];
  137. Logger.info(TAG, `加载用户设置 - 重复文件处理: ${this.duplicateAction}`);
  138. } catch (error) {
  139. const err = error as Error;
  140. Logger.error(TAG, `加载用户设置失败: ${err.message}`);
  141. }
  142. }
  143. aboutToDisappear(): void {
  144. Logger.info(TAG, '页面即将销毁');
  145. // 取消订阅事件
  146. this.unsubscribeUploadEvents();
  147. }
  148. /**
  149. * 加载WebDAV账户列表
  150. */
  151. private loadAccounts(): void {
  152. try {
  153. this.accounts = this.webdavManager.getAllWebDavAccounts();
  154. Logger.info(TAG, `加载了 ${this.accounts.length} 个WebDAV账户`);
  155. // 如果没有选中账户且有可用账户,默认选择第一个
  156. if (!this.selectedAccount && this.accounts.length > 0) {
  157. this.selectedAccount = this.accounts[0];
  158. this.applyDefaultUploadPath(this.selectedAccount);
  159. }
  160. } catch (error) {
  161. const err = error as Error;
  162. Logger.error(TAG, `加载账户列表失败: ${err.message}`);
  163. }
  164. }
  165. private getPreferredDownloadRoot(): string {
  166. if (this.cachedDownloadRoot) {
  167. return this.cachedDownloadRoot;
  168. }
  169. try {
  170. let saved = PreferencesUtil.getStringSync('download_path', '');
  171. if (saved && saved.length > 0) {
  172. this.cachedDownloadRoot = this.normalizeLocalDirectory(saved);
  173. if (this.cachedDownloadRoot) {
  174. return this.cachedDownloadRoot;
  175. }
  176. }
  177. } catch (error) {
  178. const err = error as Error;
  179. Logger.warn(TAG, `读取download_path失败: ${err.message}`);
  180. }
  181. return '';
  182. }
  183. private normalizeLocalDirectory(path: string): string {
  184. if (!path || path.length === 0) {
  185. return '';
  186. }
  187. let normalized = path.replace(/\\/g, '/');
  188. normalized = normalized.replace(/\/+$/, '');
  189. return normalized;
  190. }
  191. private ensureDirectoryAccessible(path: string): boolean {
  192. if (!path || path.length === 0) {
  193. return false;
  194. }
  195. try {
  196. const stat = fs.statSync(path);
  197. return stat.isDirectory();
  198. } catch (_error) {
  199. try {
  200. fs.mkdirSync(path);
  201. return true;
  202. } catch (createErr) {
  203. const err = createErr as Error;
  204. Logger.warn(TAG, `创建目录失败(${path}): ${err.message}`);
  205. return false;
  206. }
  207. }
  208. }
  209. private async requestDownloadRootFromSystem(force: boolean = false): Promise<string> {
  210. try {
  211. const documentViewPicker = new picker.DocumentViewPicker();
  212. const documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD });
  213. if (documentSaveResult && documentSaveResult.length > 0) {
  214. const resolvedPath = new fileUri.FileUri(documentSaveResult[0]).path;
  215. const normalized = this.normalizeLocalDirectory(resolvedPath);
  216. if (normalized) {
  217. this.cachedDownloadRoot = normalized;
  218. const storedValue = normalized.endsWith('/') ? normalized : `${normalized}/`;
  219. PreferencesUtil.putSync('download_path', storedValue);
  220. await ReqPermissionUtil.persistPermission(documentSaveResult[0]);
  221. return normalized;
  222. }
  223. }
  224. } catch (error) {
  225. const err = error as Error;
  226. Logger.error(TAG, `请求系统下载目录失败: ${err.message}`);
  227. }
  228. return '';
  229. }
  230. private async resolveInitialBrowserPath(): Promise<string> {
  231. let preferred = this.getPreferredDownloadRoot();
  232. if (preferred && this.ensureDirectoryAccessible(preferred)) {
  233. return preferred;
  234. }
  235. const requested = await this.requestDownloadRootFromSystem();
  236. if (requested && this.ensureDirectoryAccessible(requested)) {
  237. return requested;
  238. }
  239. promptAction.showToast({
  240. message: '请先在本地音乐页授权下载目录后再使用此功能',
  241. duration: 2000
  242. });
  243. throw new Error('未授权任何本地目录');
  244. }
  245. private normalizeBrowsePath(path: string): string {
  246. if (!path || path.trim().length === 0) {
  247. return '/';
  248. }
  249. let normalized = path.trim().replace(/\\/g, '/');
  250. if (!normalized.startsWith('/')) {
  251. normalized = `/${normalized}`;
  252. }
  253. normalized = normalized.replace(/\/+/g, '/');
  254. if (normalized.length > 1 && normalized.endsWith('/')) {
  255. normalized = normalized.slice(0, -1);
  256. }
  257. return normalized || '/';
  258. }
  259. /**
  260. * 预选指定账户
  261. * @param accountId 账户ID
  262. */
  263. private preselectAccount(accountId: number): void {
  264. try {
  265. for (let i = 0; i < this.accounts.length; i++) {
  266. const account = this.accounts[i];
  267. if (account.id === accountId) {
  268. this.selectedAccount = account;
  269. this.applyDefaultUploadPath(account);
  270. Logger.info(TAG, `预选账户: ${account.name}`);
  271. break;
  272. }
  273. }
  274. } catch (error) {
  275. const err = error as Error;
  276. Logger.error(TAG, `预选账户失败: ${err.message}`);
  277. }
  278. }
  279. /**
  280. * 订阅RemoteDriveManager的上传事件
  281. */
  282. private subscribeUploadEvents(): void {
  283. this.webdavManager.subscribe(this.eventHandler);
  284. Logger.info(TAG, '已订阅上传事件');
  285. }
  286. /**
  287. * 取消订阅RemoteDriveManager的上传事件
  288. */
  289. private unsubscribeUploadEvents(): void {
  290. this.webdavManager.unsubscribe(this.eventHandler);
  291. Logger.info(TAG, '已取消订阅上传事件');
  292. }
  293. /**
  294. * 处理上传事件
  295. * @param event 事件类型
  296. */
  297. private handleUploadEvent(event: string): void {
  298. Logger.info(TAG, `收到上传事件: ${event}`);
  299. switch (event) {
  300. case RemoteDriveManagerStates.UploadStart:
  301. this.handleUploadStart();
  302. break;
  303. case RemoteDriveManagerStates.UploadProgress:
  304. this.handleUploadProgress();
  305. break;
  306. case RemoteDriveManagerStates.UploadSuccess:
  307. this.handleUploadSuccess();
  308. break;
  309. case RemoteDriveManagerStates.UploadFailed:
  310. this.handleUploadFailed();
  311. break;
  312. case RemoteDriveManagerStates.UploadPaused:
  313. this.handleUploadPaused();
  314. break;
  315. case RemoteDriveManagerStates.UploadResumed:
  316. this.handleUploadResumed();
  317. break;
  318. case RemoteDriveManagerStates.SetCurrentUploadTask:
  319. this.handleCurrentTaskChanged();
  320. break;
  321. case RemoteDriveManagerStates.ChangeUploadQueue:
  322. this.handleUploadQueueChanged();
  323. break;
  324. case RemoteDriveManagerStates.ChangeFinishUploadQueue:
  325. this.handleFinishQueueChanged();
  326. break;
  327. }
  328. }
  329. /**
  330. * 处理上传开始事件
  331. */
  332. private handleUploadStart(): void {
  333. this.isUploading = true;
  334. Logger.info(TAG, '上传开始');
  335. }
  336. /**
  337. * 处理上传进度更新事件
  338. */
  339. private handleUploadProgress(): void {
  340. const uploaded = this.webdavManager.uploadReceivedSize;
  341. const total = this.webdavManager.uploadTotalSize;
  342. if (total > 0) {
  343. this.uploadProgress = Math.floor((uploaded / total) * 100);
  344. // 计算上传速度(简化版,实际应该基于时间差计算)
  345. const speedMBps = uploaded / (1024 * 1024);
  346. this.uploadSpeed = `${speedMBps.toFixed(2)} MB/s`;
  347. }
  348. Logger.info(TAG, `上传进度: ${this.uploadProgress}%`);
  349. }
  350. /**
  351. * 处理上传成功事件
  352. */
  353. private handleUploadSuccess(): void {
  354. this.uploadedCount++;
  355. Logger.info(TAG, `上传成功,已完成: ${this.uploadedCount}/${this.totalUploadCount}`);
  356. }
  357. /**
  358. * 处理上传失败事件
  359. */
  360. private handleUploadFailed(): void {
  361. Logger.error(TAG, '上传失败');
  362. }
  363. /**
  364. * 处理上传暂停事件
  365. */
  366. private handleUploadPaused(): void {
  367. Logger.info(TAG, '上传已暂停');
  368. }
  369. /**
  370. * 处理上传恢复事件
  371. */
  372. private handleUploadResumed(): void {
  373. Logger.info(TAG, '上传已恢复');
  374. }
  375. /**
  376. * 处理当前任务变更事件
  377. */
  378. private handleCurrentTaskChanged(): void {
  379. const task = this.webdavManager.currentUploadTask;
  380. if (task) {
  381. this.currentTask = task.song;
  382. Logger.info(TAG, `当前上传任务: ${task.song.name}`);
  383. } else {
  384. this.currentTask = null;
  385. }
  386. }
  387. /**
  388. * 处理上传队列变更事件
  389. */
  390. private handleUploadQueueChanged(): void {
  391. const tasks = this.webdavManager.uploadQueue;
  392. this.uploadQueue = [];
  393. for (let i = 0; i < tasks.length; i++) {
  394. this.uploadQueue.push(tasks[i].song);
  395. }
  396. this.pendingCount = this.uploadQueue.length;
  397. if (this.pendingCount === 0) {
  398. this.isUploading = false;
  399. this.currentTask = null;
  400. this.uploadProgress = 0;
  401. this.uploadSpeed = '0 KB/s';
  402. }
  403. this.uploadQueueDataSource.updateTasks(this.uploadQueue);
  404. Logger.info(TAG, `上传队列更新,当前数量: ${this.pendingCount}`);
  405. }
  406. /**
  407. * 处理完成队列变更事件
  408. */
  409. private handleFinishQueueChanged(): void {
  410. const tasks = this.webdavManager.finishUploadQueue;
  411. this.finishQueue = [];
  412. for (let i = 0; i < tasks.length; i++) {
  413. this.finishQueue.push(tasks[i].song);
  414. }
  415. this.finishedCount = this.finishQueue.length;
  416. this.finishQueueDataSource.updateTasks(this.finishQueue);
  417. Logger.info(TAG, `完成队列更新,当前数量: ${this.finishedCount}`);
  418. }
  419. /**
  420. * 打开文件选择器
  421. */
  422. private async openFilePicker(): Promise<void> {
  423. try {
  424. const documentSelectOptions = new picker.DocumentSelectOptions();
  425. // 设置音频文件过滤器
  426. documentSelectOptions.fileSuffixFilters = Constants.AUDIO_EXTENSIONS;
  427. // 支持多选
  428. documentSelectOptions.maxSelectNumber = 100;
  429. const documentPicker = new picker.DocumentViewPicker();
  430. const documentSelectResult = await documentPicker.select(documentSelectOptions);
  431. if (documentSelectResult && documentSelectResult.length > 0) {
  432. for (let i = 0; i < documentSelectResult.length; i++) {
  433. try {
  434. await ReqPermissionUtil.persistPermission(documentSelectResult[i]);
  435. } catch (error) {
  436. const err = error as Error;
  437. Logger.error(TAG, `持久化授权失败: ${err.message}`);
  438. }
  439. }
  440. Logger.info(TAG, `选择了 ${documentSelectResult.length} 个文件`);
  441. // 保存选中的文件URI
  442. this.selectedFiles = documentSelectResult;
  443. // 提取文件名用于显示
  444. this.selectedFileNames = [];
  445. this.selectedFileTypes = [];
  446. for (let i = 0; i < documentSelectResult.length; i++) {
  447. const uri = documentSelectResult[i];
  448. try {
  449. const fileUriObj = new fileUri.FileUri(uri);
  450. const fileName = fileUriObj.name;
  451. this.selectedFileNames.push(fileName);
  452. this.selectedFileTypes.push('file');
  453. } catch (error) {
  454. const err = error as Error;
  455. Logger.error(TAG, `解析文件URI失败: ${err.message}`);
  456. this.selectedFileNames.push('未知文件');
  457. this.selectedFileTypes.push('file');
  458. }
  459. }
  460. Logger.info(TAG, `文件列表: ${JSON.stringify(this.selectedFileNames)}`);
  461. }
  462. } catch (error) {
  463. const err = error as BusinessError;
  464. Logger.error(TAG, `文件选择失败: ${err.message}`);
  465. }
  466. }
  467. /**
  468. * 打开文件浏览器
  469. */
  470. private async openFileBrowser(): Promise<void> {
  471. try {
  472. const initialPath = await this.resolveInitialBrowserPath();
  473. if (!initialPath) {
  474. promptAction.showToast({
  475. message: '无法定位到下载目录,请授予文件访问权限',
  476. duration: 2000
  477. });
  478. return;
  479. }
  480. this.fileBrowserPath = initialPath;
  481. this.localBrowseRoot = initialPath;
  482. this.selectedBrowserItems.clear();
  483. this.isShowFileBrowser = true;
  484. await this.loadLocalFiles(initialPath);
  485. Logger.info(TAG, `文件浏览器已定位到: ${initialPath}`);
  486. } catch (error) {
  487. const err = error as Error;
  488. Logger.error(TAG, `打开文件浏览器失败: ${err.message}`);
  489. promptAction.showToast({
  490. message: err.message || '打开文件浏览器失败',
  491. duration: 2000
  492. });
  493. }
  494. }
  495. /**
  496. * 加载本地文件列表
  497. */
  498. private async loadLocalFiles(path: string): Promise<void> {
  499. try {
  500. this.isLoadingFiles = true;
  501. this.browserFiles = [];
  502. let targetPath = path;
  503. if (this.localBrowseRoot && !targetPath.startsWith(this.localBrowseRoot)) {
  504. targetPath = this.localBrowseRoot;
  505. }
  506. Logger.info(TAG, `加载目录: ${targetPath}`);
  507. const files = fs.listFileSync(targetPath);
  508. for (let i = 0; i < files.length; i++) {
  509. const fileName = files[i];
  510. const fullPath = `${targetPath}/${fileName}`;
  511. try {
  512. const stat = fs.statSync(fullPath);
  513. const isDirectory = stat.isDirectory();
  514. if (fileName.startsWith('.')) {
  515. continue;
  516. }
  517. const lowerFileName = fileName.toLowerCase();
  518. const isAudioFile = !isDirectory && Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext));
  519. // 只显示文件夹和音频文件
  520. if (isDirectory || isAudioFile) {
  521. this.browserFiles.push({
  522. name: fileName,
  523. path: fullPath,
  524. isDirectory: isDirectory,
  525. isAudioFile: isAudioFile
  526. });
  527. }
  528. } catch (error) {
  529. Logger.warn(TAG, `无法访问: ${fullPath}`);
  530. }
  531. }
  532. // 排序:文件夹在前,文件在后
  533. this.browserFiles.sort((a, b) => {
  534. if (a.isDirectory && !b.isDirectory) return -1;
  535. if (!a.isDirectory && b.isDirectory) return 1;
  536. return a.name.localeCompare(b.name);
  537. });
  538. Logger.info(TAG, `加载了 ${this.browserFiles.length} 个项目`);
  539. } catch (error) {
  540. const err = error as Error;
  541. Logger.error(TAG, `加载文件列表失败: ${err.message}`);
  542. promptAction.showToast({
  543. message: `加载失败: ${err.message}`,
  544. duration: 2000
  545. });
  546. } finally {
  547. this.isLoadingFiles = false;
  548. }
  549. }
  550. /**
  551. * 进入子目录
  552. */
  553. private async enterDirectory(item: LocalFileItem): Promise<void> {
  554. if (!item.isDirectory) {
  555. return;
  556. }
  557. if (this.localBrowseRoot && !item.path.startsWith(this.localBrowseRoot)) {
  558. promptAction.showToast({
  559. message: '没有此目录的访问权限',
  560. duration: 2000
  561. });
  562. return;
  563. }
  564. this.fileBrowserPath = item.path;
  565. await this.loadLocalFiles(item.path);
  566. }
  567. /**
  568. * 返回上级目录
  569. */
  570. private async goBackDirectory(): Promise<void> {
  571. if (!this.localBrowseRoot) {
  572. return;
  573. }
  574. if (this.fileBrowserPath === this.localBrowseRoot) {
  575. return;
  576. }
  577. const lastSlash = this.fileBrowserPath.lastIndexOf('/');
  578. if (lastSlash > 0) {
  579. const parent = this.fileBrowserPath.substring(0, lastSlash);
  580. if (!parent.startsWith(this.localBrowseRoot)) {
  581. this.fileBrowserPath = this.localBrowseRoot;
  582. await this.loadLocalFiles(this.localBrowseRoot);
  583. return;
  584. }
  585. this.fileBrowserPath = parent;
  586. await this.loadLocalFiles(this.fileBrowserPath);
  587. }
  588. }
  589. /**
  590. * 切换项目选中状态
  591. */
  592. private toggleItemSelection(item: LocalFileItem): void {
  593. if (this.selectedBrowserItems.has(item.path)) {
  594. this.selectedBrowserItems.delete(item.path);
  595. } else {
  596. this.selectedBrowserItems.add(item.path);
  597. }
  598. // 触发UI更新
  599. this.selectedBrowserItems = new Set(this.selectedBrowserItems);
  600. }
  601. /**
  602. * 确认选择文件
  603. */
  604. private async confirmFileSelection(): Promise<void> {
  605. if (this.selectedBrowserItems.size === 0) {
  606. promptAction.showToast({
  607. message: '请至少选择一个项目',
  608. duration: 2000
  609. });
  610. return;
  611. }
  612. // 将选中的项目添加到已选列表
  613. const selectedItems: LocalFileItem[] = [];
  614. const selectedSet = new Set<string>();
  615. this.selectedBrowserItems.forEach((path) => {
  616. const found = this.browserFiles.find((f) => f.path === path);
  617. if (found && !selectedSet.has(found.path)) {
  618. selectedSet.add(found.path);
  619. selectedItems.push(found);
  620. }
  621. });
  622. for (let i = 0; i < selectedItems.length; i++) {
  623. const item = selectedItems[i];
  624. const uri = `file://${item.path}`;
  625. try {
  626. await ReqPermissionUtil.persistPermission(uri);
  627. } catch (error) {
  628. const err = error as Error;
  629. Logger.error(TAG, `持久化授权失败: ${err.message}`);
  630. }
  631. this.selectedFiles.push(uri);
  632. this.selectedFileNames.push(item.name);
  633. this.selectedFileTypes.push(item.isDirectory ? 'folder' : 'file');
  634. }
  635. promptAction.showToast({
  636. message: `已添加 ${this.selectedBrowserItems.size} 个项目`,
  637. duration: 2000
  638. });
  639. this.closeFileBrowser();
  640. }
  641. /**
  642. * 关闭文件浏览器
  643. */
  644. private closeFileBrowser(): void {
  645. this.isShowFileBrowser = false;
  646. this.selectedBrowserItems.clear();
  647. this.browserFiles = [];
  648. this.localBrowseRoot = '';
  649. }
  650. /**
  651. * 递归扫描文件夹,获取所有音频文件
  652. */
  653. private async scanFolderForAudioFiles(folderPath: string, relativePrefix: string = ''): Promise<LocalAudioFile[]> {
  654. const audioFiles: LocalAudioFile[] = [];
  655. try {
  656. const files = fs.listFileSync(folderPath);
  657. for (let i = 0; i < files.length; i++) {
  658. const fileName = files[i];
  659. const fullPath = `${folderPath}/${fileName}`;
  660. try {
  661. const stat = fs.statSync(fullPath);
  662. if (stat.isDirectory()) {
  663. const nextPrefix = relativePrefix ? `${relativePrefix}/${fileName}` : fileName;
  664. const subFiles = await this.scanFolderForAudioFiles(fullPath, nextPrefix);
  665. audioFiles.push(...subFiles);
  666. } else {
  667. const lowerFileName = fileName.toLowerCase();
  668. const isAudio = Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext));
  669. if (isAudio) {
  670. const relativePath = relativePrefix ? `${relativePrefix}/${fileName}` : fileName;
  671. audioFiles.push({
  672. fullPath,
  673. relativePath
  674. });
  675. }
  676. }
  677. } catch (error) {
  678. const err = error as Error;
  679. Logger.warn(TAG, `无法访问: ${fullPath}, 错误: ${err.message}`);
  680. }
  681. }
  682. } catch (error) {
  683. const err = error as Error;
  684. Logger.error(TAG, `扫描文件夹失败: ${err.message}`);
  685. }
  686. return audioFiles;
  687. }
  688. private getFileNameFromPath(fullPath: string): string {
  689. if (!fullPath) {
  690. return '';
  691. }
  692. const normalized = fullPath.replace(/\\/g, '/');
  693. const lastSlash = normalized.lastIndexOf('/');
  694. return lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized;
  695. }
  696. /**
  697. * 移除已选文件
  698. * @param index 文件索引
  699. */
  700. private removeSelectedFile(index: number): void {
  701. if (index >= 0 && index < this.selectedFiles.length) {
  702. this.selectedFiles.splice(index, 1);
  703. this.selectedFileNames.splice(index, 1);
  704. this.selectedFileTypes.splice(index, 1);
  705. Logger.info(TAG, `移除项目,剩余 ${this.selectedFiles.length} 个`);
  706. }
  707. }
  708. /**
  709. * 选择上传账户
  710. * @param account 选中的账户
  711. */
  712. private selectUploadAccount(account: WebDavAccount): void {
  713. this.selectedAccount = account;
  714. this.applyDefaultUploadPath(account);
  715. Logger.info(TAG, `选择账户: ${account.name}, 上传路径: ${this.uploadPath}`);
  716. }
  717. private applyDefaultUploadPath(account: WebDavAccount | null): void {
  718. if (!account) {
  719. return;
  720. }
  721. const managerAccount = this.webdavManager.currentAccount;
  722. const managerPath = this.webdavManager.currentPath;
  723. if (
  724. managerPath &&
  725. managerPath.length > 0 &&
  726. managerAccount &&
  727. managerAccount.id === account.id
  728. ) {
  729. this.uploadPath = managerPath;
  730. } else {
  731. this.uploadPath = account.uploadFilePath || '/';
  732. }
  733. }
  734. /**
  735. * 打开路径浏览器
  736. */
  737. private openPathBrowser(): void {
  738. if (!this.selectedAccount) {
  739. Logger.warn(TAG, '请先选择账户');
  740. return;
  741. }
  742. Logger.info(TAG, '========== 打开路径浏览器 ==========');
  743. Logger.info(TAG, `当前上传路径: ${this.uploadPath}`);
  744. Logger.info(TAG, `选中账户: ${this.selectedAccount.name}`);
  745. const managerAccount = this.webdavManager.currentAccount;
  746. const defaultPath =
  747. managerAccount &&
  748. managerAccount.id === this.selectedAccount.id &&
  749. this.webdavManager.currentPath &&
  750. this.webdavManager.currentPath.length > 0
  751. ? this.webdavManager.currentPath
  752. : this.uploadPath;
  753. this.currentBrowsePath = defaultPath;
  754. this.isShowPathBrowser = true;
  755. Logger.info(TAG, `isShowPathBrowser 设置为: ${this.isShowPathBrowser}`);
  756. Logger.info(TAG, '开始加载文件夹列表...');
  757. this.loadWebDavFolders(this.currentBrowsePath);
  758. }
  759. /**
  760. * 关闭路径浏览器
  761. */
  762. private closePathBrowser(): void {
  763. Logger.info(TAG, '========== 关闭路径浏览器 ==========');
  764. this.isShowPathBrowser = false;
  765. this.browseFolders = [];
  766. }
  767. /**
  768. * 加载WebDAV文件夹列表
  769. * @param path 路径
  770. */
  771. private async loadWebDavFolders(path: string): Promise<void> {
  772. if (!this.selectedAccount) {
  773. return;
  774. }
  775. try {
  776. this.isLoadingFolders = true;
  777. Logger.info(TAG, `加载文件夹列表: ${path}`);
  778. await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, path);
  779. // 过滤出文件夹
  780. const allFiles = this.webdavManager.webDavFiles;
  781. this.browseFolders = [];
  782. const normalizedCurrent = this.normalizeBrowsePath(this.currentBrowsePath);
  783. for (let i = 0; i < allFiles.length; i++) {
  784. const file = allFiles[i];
  785. if (file.isDirectory) {
  786. const folderPath = this.normalizeBrowsePath(file.href);
  787. if (folderPath !== normalizedCurrent) {
  788. this.browseFolders.push(file);
  789. }
  790. }
  791. }
  792. Logger.info(TAG, `加载了 ${this.browseFolders.length} 个文件夹`);
  793. } catch (error) {
  794. const err = error as Error;
  795. Logger.error(TAG, `加载文件夹失败: ${err.message}`);
  796. } finally {
  797. this.isLoadingFolders = false;
  798. }
  799. }
  800. /**
  801. * 进入子文件夹
  802. * @param folder 文件夹信息
  803. */
  804. private async enterBrowseFolder(folder: FileInfo): Promise<void> {
  805. this.currentBrowsePath = folder.href;
  806. await this.loadWebDavFolders(folder.href);
  807. }
  808. /**
  809. * 返回上级文件夹
  810. */
  811. private async goBackBrowseFolder(): Promise<void> {
  812. if (this.currentBrowsePath === '/') {
  813. return;
  814. }
  815. const lastSlashIndex = this.currentBrowsePath.lastIndexOf('/');
  816. if (lastSlashIndex > 0) {
  817. this.currentBrowsePath = this.currentBrowsePath.substring(0, lastSlashIndex);
  818. } else {
  819. this.currentBrowsePath = '/';
  820. }
  821. await this.loadWebDavFolders(this.currentBrowsePath);
  822. }
  823. /**
  824. * 确认选择路径
  825. */
  826. private confirmPathSelection(): void {
  827. this.uploadPath = this.currentBrowsePath;
  828. Logger.info(TAG, `选择上传路径: ${this.uploadPath}`);
  829. this.closePathBrowser();
  830. }
  831. /**
  832. * 开始上传
  833. */
  834. private async startUpload(): Promise<void> {
  835. // 验证
  836. if (!this.selectedAccount) {
  837. Logger.warn(TAG, '请先选择账户');
  838. return;
  839. }
  840. if (this.selectedFiles.length === 0) {
  841. Logger.warn(TAG, '请先选择文件');
  842. return;
  843. }
  844. if (!this.uploadPath) {
  845. Logger.warn(TAG, '请先选择上传路径');
  846. return;
  847. }
  848. try {
  849. Logger.info(TAG, '开始上传任务');
  850. // 显示处理提示
  851. promptAction.showToast({
  852. message: '正在处理文件...',
  853. duration: 2000
  854. });
  855. // 构建VideoItem列表
  856. const songs: VideoItem[] = [];
  857. let totalFiles = 0;
  858. let totalFolders = 0;
  859. for (let i = 0; i < this.selectedFiles.length; i++) {
  860. const selectedUri = this.selectedFiles[i];
  861. const displayName = this.selectedFileNames[i];
  862. const itemType = this.selectedFileTypes[i];
  863. try {
  864. const fileUriObj = new fileUri.FileUri(selectedUri);
  865. const resolvedPath = fileUriObj.path || '';
  866. if (!resolvedPath) {
  867. Logger.error(TAG, `无法获取本地路径,跳过: ${selectedUri}`);
  868. continue;
  869. }
  870. if (itemType === 'folder') {
  871. totalFolders++;
  872. const folderName = displayName || this.getFileNameFromPath(resolvedPath);
  873. Logger.info(TAG, `扫描文件夹: ${folderName}`);
  874. const audioFiles = await this.scanFolderForAudioFiles(resolvedPath, folderName);
  875. Logger.info(TAG, `文件夹 ${folderName} 包含 ${audioFiles.length} 个音频文件`);
  876. for (let j = 0; j < audioFiles.length; j++) {
  877. const audioFile = audioFiles[j];
  878. const audioFileName = this.getFileNameFromPath(audioFile.fullPath);
  879. const videoItem = new VideoItem(
  880. audioFileName,
  881. `file://${audioFile.fullPath}`,
  882. audioFile.fullPath,
  883. CommonConstants.TYPE_LOCAL,
  884. 0,
  885. '',
  886. undefined,
  887. undefined,
  888. undefined,
  889. undefined,
  890. undefined,
  891. undefined
  892. );
  893. videoItem.remote_rel_path = audioFile.relativePath;
  894. songs.push(videoItem);
  895. }
  896. } else {
  897. // 处理单个文件
  898. totalFiles++;
  899. const videoItem = new VideoItem(
  900. displayName,
  901. selectedUri,
  902. resolvedPath,
  903. CommonConstants.TYPE_LOCAL,
  904. 0,
  905. '',
  906. undefined,
  907. undefined,
  908. undefined,
  909. undefined,
  910. undefined,
  911. undefined
  912. );
  913. videoItem.remote_rel_path = displayName;
  914. songs.push(videoItem);
  915. }
  916. } catch (error) {
  917. const err = error as Error;
  918. Logger.error(TAG, `处理项目失败: ${err.message}`);
  919. continue;
  920. }
  921. }
  922. if (songs.length === 0) {
  923. promptAction.showToast({
  924. message: '未找到可上传的音频文件',
  925. duration: 2000
  926. });
  927. Logger.warn(TAG, '未找到可上传的有效文件,取消任务');
  928. return;
  929. }
  930. Logger.info(TAG, `处理完成: ${totalFiles} 个文件, ${totalFolders} 个文件夹, 共 ${songs.length} 个音频文件`);
  931. promptAction.showToast({
  932. message: `准备上传 ${songs.length} 个文件`,
  933. duration: 2000
  934. });
  935. // 添加到上传队列,传递用户选择的上传路径
  936. Logger.info(TAG, `使用上传路径: ${this.uploadPath}`);
  937. const uniqueSongs = this.deduplicateSongs(songs);
  938. this.webdavManager.addToUploadQueue(uniqueSongs, this.selectedAccount, this.uploadPath);
  939. this.totalUploadCount = uniqueSongs.length;
  940. this.uploadedCount = 0;
  941. // 开始处理上传队列
  942. await this.webdavManager.startUploadQueue();
  943. Logger.info(TAG, '上传任务已启动');
  944. this.selectedFiles = [];
  945. this.selectedFileNames = [];
  946. this.selectedFileTypes = [];
  947. this.pendingCount = this.webdavManager.uploadQueue.length;
  948. this.selectedFiles = [];
  949. this.selectedFileNames = [];
  950. this.selectedFileTypes = [];
  951. } catch (error) {
  952. const err = error as Error;
  953. Logger.error(TAG, `启动上传失败: ${err.message}`);
  954. promptAction.showToast({
  955. message: `上传失败: ${err.message}`,
  956. duration: 2000
  957. });
  958. }
  959. }
  960. private deduplicateSongs(songs: VideoItem[]): VideoItem[] {
  961. const unique: VideoItem[] = [];
  962. const seen = new Set<string>();
  963. for (let i = 0; i < songs.length; i++) {
  964. const song = songs[i];
  965. const key = song.filePath || song.name;
  966. if (!key) {
  967. continue;
  968. }
  969. if (seen.has(key)) {
  970. Logger.warn(TAG, `跳过重复文件: ${song.name}`);
  971. continue;
  972. }
  973. seen.add(key);
  974. unique.push(song);
  975. }
  976. return unique;
  977. }
  978. /**
  979. * 暂停上传
  980. */
  981. private pauseUpload(): void {
  982. this.webdavManager.pauseUploadQueue();
  983. Logger.info(TAG, '暂停上传');
  984. }
  985. /**
  986. * 恢复上传
  987. */
  988. private async resumeUpload(): Promise<void> {
  989. await this.webdavManager.resumeUploadQueue();
  990. Logger.info(TAG, '恢复上传');
  991. }
  992. /**
  993. * 取消上传
  994. */
  995. private cancelUpload(): void {
  996. this.webdavManager.pauseUploadQueue();
  997. this.webdavManager.clearUploadQueue();
  998. this.isUploading = false;
  999. this.currentTask = null;
  1000. Logger.info(TAG, '取消上传');
  1001. }
  1002. /**
  1003. * 上传进度显示组件
  1004. */
  1005. @Builder
  1006. UploadProgressView() {
  1007. if (this.isUploading && this.currentTask) {
  1008. Column({ space: 16 }) {
  1009. // 标题行
  1010. Row({ space: 8 }) {
  1011. Text('⏫')
  1012. .fontSize(20)
  1013. Text('上传中')
  1014. .fontSize(16)
  1015. .fontWeight(FontWeight.Medium)
  1016. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1017. }
  1018. .alignSelf(ItemAlign.Start)
  1019. // 当前文件信息卡片
  1020. Column({ space: 10 }) {
  1021. Row({ space: 8 }) {
  1022. Text('🎵')
  1023. .fontSize(18)
  1024. Text(this.currentTask.name)
  1025. .fontSize(14)
  1026. .fontWeight(FontWeight.Medium)
  1027. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
  1028. .layoutWeight(1)
  1029. .maxLines(1)
  1030. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1031. }
  1032. .width('100%')
  1033. // 进度条
  1034. Progress({ value: this.uploadProgress, total: 100, type: ProgressType.Linear })
  1035. .width('100%')
  1036. .color(this.themeColor)
  1037. .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
  1038. .style({ strokeWidth: 6 })
  1039. // 进度信息行
  1040. Row() {
  1041. Text(`${this.uploadProgress}%`)
  1042. .fontSize(15)
  1043. .fontWeight(FontWeight.Bold)
  1044. .fontColor(this.themeColor)
  1045. Blank()
  1046. Text(`${this.uploadSpeed}`)
  1047. .fontSize(13)
  1048. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
  1049. }
  1050. .width('100%')
  1051. // 已上传数量
  1052. Row({ space: 6 }) {
  1053. Text('✓')
  1054. .fontSize(14)
  1055. .fontColor(this.themeColor)
  1056. Text(`已完成 ${this.uploadedCount}/${this.totalUploadCount}`)
  1057. .fontSize(13)
  1058. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
  1059. }
  1060. }
  1061. .width('100%')
  1062. .padding(14)
  1063. .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
  1064. .borderRadius(10)
  1065. // 控制按钮
  1066. Row({ space: 10 }) {
  1067. Button({ type: ButtonType.Normal }) {
  1068. Row({ space: 6 }) {
  1069. Text(this.webdavManager.isPauseUpload ? '▶️' : '⏸️')
  1070. .fontSize(16)
  1071. Text(this.webdavManager.isPauseUpload ? '恢复' : '暂停')
  1072. .fontSize(14)
  1073. .fontWeight(FontWeight.Medium)
  1074. }
  1075. }
  1076. .height(44)
  1077. .layoutWeight(1)
  1078. .backgroundColor(this.themeColor)
  1079. .fontColor('#FFFFFF')
  1080. .borderRadius(10)
  1081. .onClick(() => {
  1082. if (this.webdavManager.isPauseUpload) {
  1083. this.resumeUpload();
  1084. } else {
  1085. this.pauseUpload();
  1086. }
  1087. })
  1088. Button({ type: ButtonType.Normal }) {
  1089. Row({ space: 6 }) {
  1090. Text('❌')
  1091. .fontSize(16)
  1092. Text('取消')
  1093. .fontSize(14)
  1094. .fontWeight(FontWeight.Medium)
  1095. }
  1096. }
  1097. .height(44)
  1098. .layoutWeight(1)
  1099. .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30')
  1100. .fontColor('#FFFFFF')
  1101. .borderRadius(10)
  1102. .onClick(() => {
  1103. this.cancelUpload();
  1104. })
  1105. }
  1106. .width('100%')
  1107. }
  1108. .width('100%')
  1109. .padding(16)
  1110. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1111. .borderRadius(12)
  1112. .shadow({
  1113. radius: this.isDarkMode ? 8 : 12,
  1114. color: this.isDarkMode ? '#0C000000' : '#19000000',
  1115. offsetX: 0,
  1116. offsetY: 2
  1117. })
  1118. .margin({ bottom: 16 })
  1119. }
  1120. }
  1121. /**
  1122. * 上传队列管理组件(使用LazyDataSource优化性能)
  1123. */
  1124. @Builder
  1125. UploadQueueView() {
  1126. Column({ space: 16 }) {
  1127. // 标题行
  1128. Row({ space: 8 }) {
  1129. Text('📊')
  1130. .fontSize(20)
  1131. Text('任务队列')
  1132. .fontSize(16)
  1133. .fontWeight(FontWeight.Medium)
  1134. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1135. }
  1136. .alignSelf(ItemAlign.Start)
  1137. Tabs({ barPosition: BarPosition.Start }) {
  1138. TabContent() {
  1139. this.QueueList(this.uploadQueueDataSource, 'pending')
  1140. }
  1141. .tabBar(this.TabBarBuilder('⏳', '待上传', this.pendingCount))
  1142. TabContent() {
  1143. this.QueueList(this.finishQueueDataSource, 'finished')
  1144. }
  1145. .tabBar(this.TabBarBuilder('✅', '已完成', this.finishedCount))
  1146. }
  1147. .width('100%')
  1148. .height(300)
  1149. .barMode(BarMode.Fixed)
  1150. .barBackgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1151. .animationDuration(300)
  1152. }
  1153. .width('100%')
  1154. .padding(16)
  1155. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1156. .borderRadius(12)
  1157. .shadow({
  1158. radius: this.isDarkMode ? 8 : 12,
  1159. color: this.isDarkMode ? '#0C000000' : '#19000000',
  1160. offsetX: 0,
  1161. offsetY: 2
  1162. })
  1163. .margin({ bottom: 16 })
  1164. }
  1165. /**
  1166. * 自定义Tab标签构建器
  1167. */
  1168. @Builder
  1169. TabBarBuilder(icon: string, title: string, count: number) {
  1170. Row({ space: 6 }) {
  1171. Text(icon)
  1172. .fontSize(16)
  1173. Text(title)
  1174. .fontSize(14)
  1175. .fontWeight(FontWeight.Medium)
  1176. Text(`${count}`)
  1177. .fontSize(12)
  1178. .fontWeight(FontWeight.Medium)
  1179. .fontColor('#FFFFFF')
  1180. .padding({
  1181. left: 6,
  1182. right: 6,
  1183. top: 2,
  1184. bottom: 2
  1185. })
  1186. .backgroundColor(this.themeColor)
  1187. .borderRadius(8)
  1188. }
  1189. .padding({ left: 4, right: 4 })
  1190. }
  1191. /**
  1192. * 队列列表组件(使用LazyForEach优化性能)
  1193. */
  1194. @Builder
  1195. QueueList(dataSource: UploadTaskDataSource, type: string) {
  1196. if (dataSource.totalCount() === 0) {
  1197. Column() {
  1198. Text(type === 'pending' ? '暂无待上传任务' : '暂无已完成任务')
  1199. .fontSize(14)
  1200. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
  1201. }
  1202. .width('100%')
  1203. .height('100%')
  1204. .justifyContent(FlexAlign.Center)
  1205. } else {
  1206. List({ space: 8 }) {
  1207. LazyForEach(dataSource, (item: VideoItem, index: number) => {
  1208. ListItem() {
  1209. Row({ space: 12 }) {
  1210. // 文件图标
  1211. Text('🎵')
  1212. .fontSize(20)
  1213. Text(item.name)
  1214. .fontSize(14)
  1215. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1216. .layoutWeight(1)
  1217. .maxLines(1)
  1218. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1219. if (type === 'pending') {
  1220. Button('移除')
  1221. .fontSize(12)
  1222. .height(32)
  1223. .padding({ left: 12, right: 12 })
  1224. .backgroundColor(this.isDarkMode ? '#D94838' : '#E84026')
  1225. .fontColor('#FFFFFF')
  1226. .borderRadius(6)
  1227. .onClick(() => {
  1228. // 从队列移除
  1229. const tasks = this.webdavManager.uploadQueue;
  1230. if (index < tasks.length) {
  1231. this.webdavManager.removeFromUploadQueue(tasks[index]);
  1232. }
  1233. })
  1234. } else {
  1235. Text('✓')
  1236. .fontSize(20)
  1237. .fontColor(this.isDarkMode ? '#5BA854' : '#64BB5C')
  1238. }
  1239. }
  1240. .width('100%')
  1241. .padding(12)
  1242. .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
  1243. .borderRadius(8)
  1244. }
  1245. }, (item: VideoItem, index: number) => `${type}-${index}-${item.name}`)
  1246. }
  1247. .width('100%')
  1248. .height('100%')
  1249. .cachedCount(5) // 缓存5个列表项以提升滚动性能
  1250. .divider({
  1251. strokeWidth: 1,
  1252. color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
  1253. })
  1254. }
  1255. }
  1256. /**
  1257. * 开始上传按钮组件
  1258. */
  1259. @Builder
  1260. StartUploadButton() {
  1261. Button({ type: ButtonType.Normal }) {
  1262. Row({ space: 10 }) {
  1263. Text('🚀')
  1264. .fontSize(20)
  1265. Text('开始上传')
  1266. .fontSize(16)
  1267. .fontWeight(FontWeight.Bold)
  1268. }
  1269. }
  1270. .width('100%')
  1271. .height(54)
  1272. .backgroundColor(this.themeColor)
  1273. .fontColor('#FFFFFF')
  1274. .borderRadius(12)
  1275. .enabled(!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null)
  1276. .opacity((!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null) ? 1.0 : 0.4)
  1277. .shadow({
  1278. radius: 12,
  1279. color: this.themeColor + '50',
  1280. offsetX: 0,
  1281. offsetY: 4
  1282. })
  1283. .onClick(() => {
  1284. this.startUpload();
  1285. })
  1286. .margin({ bottom: 16 })
  1287. }
  1288. /**
  1289. * 远程配置区域组件
  1290. */
  1291. @Builder
  1292. UploadConfigSection() {
  1293. Column({ space: 16 }) {
  1294. // 标题行
  1295. Row({ space: 8 }) {
  1296. Text('📁')
  1297. .fontSize(20)
  1298. Text('远程配置')
  1299. .fontSize(16)
  1300. .fontWeight(FontWeight.Medium)
  1301. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1302. }
  1303. .alignSelf(ItemAlign.Start)
  1304. // 上传路径选择卡片
  1305. Column({ space: 12 }) {
  1306. Row({ space: 8 }) {
  1307. Text('远程目录')
  1308. .fontSize(13)
  1309. .fontWeight(FontWeight.Medium)
  1310. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000')
  1311. Row({ space: 8 }) {
  1312. Text('📂')
  1313. .fontSize(18)
  1314. Text(this.uploadPath || '请选择远程目录')
  1315. .fontSize(15)
  1316. .fontColor(this.uploadPath ?
  1317. (this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') :
  1318. (this.isDarkMode ? '#66FFFFFF' : '#99000000'))
  1319. .fontWeight(this.uploadPath ? FontWeight.Medium : FontWeight.Regular)
  1320. .maxLines(1)
  1321. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1322. }
  1323. Blank()
  1324. Button('浏览')
  1325. .fontSize(13)
  1326. .height(32)
  1327. .padding({ left: 12, right: 12 })
  1328. .backgroundColor(this.themeColor)
  1329. .fontColor('#FFFFFF')
  1330. .borderRadius(6)
  1331. .enabled(this.selectedAccount !== null)
  1332. .opacity(this.selectedAccount !== null ? 1.0 : 0.4)
  1333. .bindSheet($$this.isShowPathBrowser, this.PathBrowserDialog(), {
  1334. height: '90%',
  1335. dragBar: true,
  1336. showClose: true,
  1337. preferType: SheetType.BOTTOM,
  1338. blurStyle: BlurStyle.Thin,
  1339. title: { title: '选择上传路径' }
  1340. })
  1341. .onClick(() => {
  1342. this.openPathBrowser();
  1343. })
  1344. }
  1345. .width('100%')
  1346. }
  1347. .width('100%')
  1348. .padding(14)
  1349. .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
  1350. .borderRadius(10)
  1351. .border({
  1352. width: 1,
  1353. color: this.uploadPath ?
  1354. (this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30') :
  1355. (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
  1356. })
  1357. // 重复文件处理卡片
  1358. Column({ space: 12 }) {
  1359. Text('重复文件处理')
  1360. .fontSize(13)
  1361. .fontWeight(FontWeight.Medium)
  1362. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000')
  1363. .alignSelf(ItemAlign.Start)
  1364. // SegmentButton 选项
  1365. SegmentButton({
  1366. options: this.duplicateOptions,
  1367. selectedIndexes: $selectedDuplicateIndex
  1368. })
  1369. .width('100%')
  1370. .margin({ top: 8 })
  1371. }
  1372. .width('100%')
  1373. .padding(14)
  1374. .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
  1375. .borderRadius(10)
  1376. }
  1377. .width('100%')
  1378. .padding(16)
  1379. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1380. .borderRadius(12)
  1381. .shadow({
  1382. radius: this.isDarkMode ? 8 : 12,
  1383. color: this.isDarkMode ? '#0C000000' : '#19000000',
  1384. offsetX: 0,
  1385. offsetY: 2
  1386. })
  1387. .margin({ bottom: 16 })
  1388. }
  1389. /**
  1390. * 路径浏览器对话框
  1391. */
  1392. @Builder
  1393. PathBrowserDialog() {
  1394. Column() {
  1395. // 当前路径显示
  1396. Row({ space: 12 }) {
  1397. Column({ space: 4 }) {
  1398. Text('当前路径')
  1399. .fontSize(12)
  1400. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
  1401. Text(this.currentBrowsePath)
  1402. .fontSize(14)
  1403. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1404. .maxLines(1)
  1405. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1406. }
  1407. .alignItems(HorizontalAlign.Start)
  1408. .layoutWeight(1)
  1409. if (this.currentBrowsePath !== '/') {
  1410. Button('返回上级')
  1411. .fontSize(12)
  1412. .height(36)
  1413. .padding({ left: 12, right: 12 })
  1414. .backgroundColor(this.themeColor)
  1415. .fontColor('#FFFFFF')
  1416. .borderRadius(8)
  1417. .onClick(() => {
  1418. this.goBackBrowseFolder();
  1419. })
  1420. }
  1421. }
  1422. .width('100%')
  1423. .padding(16)
  1424. .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
  1425. // 文件夹列表
  1426. if (this.isLoadingFolders) {
  1427. Column({ space: 12 }) {
  1428. LoadingProgress()
  1429. .width(48)
  1430. .height(48)
  1431. .color(this.themeColor)
  1432. Text('加载中...')
  1433. .fontSize(14)
  1434. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
  1435. }
  1436. .width('100%')
  1437. .height(300)
  1438. .justifyContent(FlexAlign.Center)
  1439. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1440. } else if (this.browseFolders.length === 0) {
  1441. Column() {
  1442. Text('📂')
  1443. .fontSize(48)
  1444. .margin({ bottom: 12 })
  1445. Text('当前目录没有子文件夹')
  1446. .fontSize(14)
  1447. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
  1448. }
  1449. .width('100%')
  1450. .height(300)
  1451. .justifyContent(FlexAlign.Center)
  1452. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1453. } else {
  1454. List({ space: 8 }) {
  1455. ForEach(this.browseFolders, (folder: FileInfo, index: number) => {
  1456. ListItem() {
  1457. Row({ space: 12 }) {
  1458. Text('📁')
  1459. .fontSize(24)
  1460. Text(folder.fileName)
  1461. .fontSize(14)
  1462. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1463. .layoutWeight(1)
  1464. .maxLines(1)
  1465. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1466. Text('→')
  1467. .fontSize(18)
  1468. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
  1469. }
  1470. .width('100%')
  1471. .padding(16)
  1472. .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
  1473. .borderRadius(8)
  1474. .onClick(() => {
  1475. this.enterBrowseFolder(folder);
  1476. })
  1477. }
  1478. }, (folder: FileInfo, index: number) => `folder-${index}-${folder.fileName}`)
  1479. }
  1480. .width('100%')
  1481. .layoutWeight(1)
  1482. .padding(16)
  1483. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1484. .divider({
  1485. strokeWidth: 1,
  1486. color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
  1487. })
  1488. }
  1489. // 底部按钮
  1490. Row({ space: 12 }) {
  1491. Button('取消')
  1492. .fontSize(14)
  1493. .height(48)
  1494. .layoutWeight(1)
  1495. .backgroundColor(this.isDarkMode ? '#2E3033' : '#E5E5EA')
  1496. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1497. .borderRadius(8)
  1498. .onClick(() => {
  1499. this.closePathBrowser();
  1500. })
  1501. Button('选择此路径')
  1502. .fontSize(14)
  1503. .height(48)
  1504. .layoutWeight(1)
  1505. .backgroundColor(this.themeColor)
  1506. .fontColor('#FFFFFF')
  1507. .borderRadius(8)
  1508. .onClick(() => {
  1509. this.confirmPathSelection();
  1510. })
  1511. }
  1512. .width('100%')
  1513. .padding({ left:16,right:16 })
  1514. .margin({bottom:25})
  1515. }
  1516. }
  1517. /**
  1518. * 账户选择区域组件
  1519. */
  1520. @Builder
  1521. AccountSelectorSection() {
  1522. Column({ space: 16 }) {
  1523. // 标题行
  1524. Row({ space: 8 }) {
  1525. Text('☁️')
  1526. .fontSize(20)
  1527. Text('选择账户')
  1528. .fontSize(16)
  1529. .fontWeight(FontWeight.Medium)
  1530. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1531. }
  1532. .alignSelf(ItemAlign.Start)
  1533. if (this.accounts.length === 0) {
  1534. Column({ space: 12 }) {
  1535. Text('📦')
  1536. .fontSize(48)
  1537. Text('暂无账户')
  1538. .fontSize(16)
  1539. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
  1540. Text('请先添加账户')
  1541. .fontSize(14)
  1542. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
  1543. }
  1544. .width('100%')
  1545. .padding(32)
  1546. .justifyContent(FlexAlign.Center)
  1547. } else {
  1548. List({ space: 8 }) {
  1549. ForEach(this.accounts, (account: WebDavAccount, index: number) => {
  1550. ListItem() {
  1551. Row({ space: 12 }) {
  1552. // 账户图标
  1553. Text('☁️')
  1554. .fontSize(24)
  1555. Column({ space: 3 }) {
  1556. Text(account.name)
  1557. .fontSize(14)
  1558. .fontWeight(FontWeight.Medium)
  1559. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
  1560. Text(`${account.host}:${account.port}`)
  1561. .fontSize(11)
  1562. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1563. }
  1564. .alignItems(HorizontalAlign.Start)
  1565. .layoutWeight(1)
  1566. if (this.selectedAccount && this.selectedAccount.id === account.id) {
  1567. Text('✓')
  1568. .fontSize(20)
  1569. .fontColor(this.themeColor)
  1570. }
  1571. }
  1572. .width('100%')
  1573. .padding(8)
  1574. .backgroundColor(this.selectedAccount && this.selectedAccount.id === account.id
  1575. ? (this.isDarkMode ? '#2E3238' : '#FFFFFF')
  1576. : (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
  1577. .borderRadius(10)
  1578. .border({
  1579. width: this.selectedAccount && this.selectedAccount.id === account.id ? 2 : 1,
  1580. color: this.selectedAccount && this.selectedAccount.id === account.id
  1581. ? this.themeColor
  1582. : (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
  1583. })
  1584. .onClick(() => {
  1585. this.selectUploadAccount(account);
  1586. })
  1587. }
  1588. }, (account: WebDavAccount) => `account-${account.id}`)
  1589. }
  1590. .width('100%')
  1591. }
  1592. }
  1593. .width('100%')
  1594. .padding(10)
  1595. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1596. .borderRadius(12)
  1597. .shadow({
  1598. radius: this.isDarkMode ? 8 : 12,
  1599. color: this.isDarkMode ? '#0C000000' : '#19000000',
  1600. offsetX: 0,
  1601. offsetY: 2
  1602. })
  1603. .margin({ bottom: 16 })
  1604. }
  1605. /**
  1606. * 文件选择区域组件
  1607. */
  1608. @Builder
  1609. FilePickerSection() {
  1610. Column({ space: 16 }) {
  1611. // 标题行
  1612. Row({ space: 8 }) {
  1613. Text('🎵')
  1614. .fontSize(20)
  1615. Text('选择文件')
  1616. .fontSize(16)
  1617. .fontWeight(FontWeight.Medium)
  1618. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1619. }
  1620. .alignSelf(ItemAlign.Start)
  1621. // 按钮组
  1622. Row({ space: 10 }) {
  1623. Button({ type: ButtonType.Normal }) {
  1624. Row({ space: 6 }) {
  1625. Text('📁')
  1626. .fontSize(16)
  1627. Text('从文件管理器')
  1628. .fontSize(14)
  1629. .fontWeight(FontWeight.Medium)
  1630. }
  1631. }
  1632. .layoutWeight(1)
  1633. .height(48)
  1634. .backgroundColor(this.themeColor)
  1635. .fontColor('#FFFFFF')
  1636. .borderRadius(10)
  1637. .shadow({
  1638. radius: 8,
  1639. color: this.themeColor + '40',
  1640. offsetX: 0,
  1641. offsetY: 3
  1642. })
  1643. .onClick(() => {
  1644. this.openFilePicker();
  1645. })
  1646. Button({ type: ButtonType.Normal }) {
  1647. Row({ space: 6 }) {
  1648. Text('🎵')
  1649. .fontSize(16)
  1650. Text('从本地目录')
  1651. .fontSize(14)
  1652. .fontWeight(FontWeight.Medium)
  1653. }
  1654. }
  1655. .layoutWeight(1)
  1656. .height(48)
  1657. .backgroundColor(this.themeColor)
  1658. .fontColor('#FFFFFF')
  1659. .borderRadius(10)
  1660. .shadow({
  1661. radius: 8,
  1662. color: this.themeColor + '40',
  1663. offsetX: 0,
  1664. offsetY: 3
  1665. })
  1666. .onClick(() => {
  1667. this.openFileBrowser();
  1668. })
  1669. }
  1670. .width('100%')
  1671. if (this.selectedFiles.length > 0) {
  1672. Row({ space: 10 }) {
  1673. Text('✓')
  1674. .fontSize(18)
  1675. .fontColor(this.themeColor)
  1676. Column({ space: 2 }) {
  1677. Text(`已选择 ${this.selectedFiles.length} 项`)
  1678. .fontSize(14)
  1679. .fontWeight(FontWeight.Medium)
  1680. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
  1681. Row({ space: 8 }) {
  1682. if (this.selectedFileTypes.filter(t => t === 'file').length > 0) {
  1683. Text(`${this.selectedFileTypes.filter(t => t === 'file').length} 个文件`)
  1684. .fontSize(12)
  1685. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1686. }
  1687. if (this.selectedFileTypes.filter(t => t === 'folder').length > 0) {
  1688. Text(`${this.selectedFileTypes.filter(t => t === 'folder').length} 个文件夹`)
  1689. .fontSize(12)
  1690. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1691. }
  1692. }
  1693. }
  1694. .alignItems(HorizontalAlign.Start)
  1695. }
  1696. .width('100%')
  1697. .padding(12)
  1698. .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
  1699. .borderRadius(8)
  1700. .border({
  1701. width: 1,
  1702. color: this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30'
  1703. })
  1704. }
  1705. }
  1706. .width('100%')
  1707. .padding(16)
  1708. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1709. .borderRadius(12)
  1710. .shadow({
  1711. radius: this.isDarkMode ? 8 : 12,
  1712. color: this.isDarkMode ? '#0C000000' : '#19000000',
  1713. offsetX: 0,
  1714. offsetY: 2
  1715. })
  1716. .margin({ bottom: 16 })
  1717. }
  1718. /**
  1719. * 已选文件列表组件
  1720. */
  1721. @Builder
  1722. SelectedFilesList() {
  1723. Column({ space: 16 }) {
  1724. // 标题行
  1725. Row({ space: 8 }) {
  1726. Text('📋')
  1727. .fontSize(20)
  1728. Text('已选文件')
  1729. .fontSize(16)
  1730. .fontWeight(FontWeight.Medium)
  1731. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
  1732. Blank()
  1733. Text(`${this.selectedFiles.length}`)
  1734. .fontSize(13)
  1735. .fontWeight(FontWeight.Medium)
  1736. .fontColor('#FFFFFF')
  1737. .padding({
  1738. left: 8,
  1739. right: 8,
  1740. top: 4,
  1741. bottom: 4
  1742. })
  1743. .backgroundColor(this.themeColor)
  1744. .borderRadius(10)
  1745. }
  1746. .width('100%')
  1747. .alignSelf(ItemAlign.Start)
  1748. List({ space: 8 }) {
  1749. ForEach(this.selectedFileNames, (fileName: string, index: number) => {
  1750. ListItem() {
  1751. Row({ space: 12 }) {
  1752. // 文件/文件夹图标
  1753. Text(this.selectedFileTypes[index] === 'folder' ? '📁' : '🎵')
  1754. .fontSize(20)
  1755. Column({ space: 2 }) {
  1756. Text(fileName)
  1757. .fontSize(14)
  1758. .fontWeight(FontWeight.Medium)
  1759. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
  1760. .maxLines(1)
  1761. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1762. if (this.selectedFileTypes[index] === 'folder') {
  1763. Text('文件夹')
  1764. .fontSize(12)
  1765. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1766. }
  1767. }
  1768. .alignItems(HorizontalAlign.Start)
  1769. .layoutWeight(1)
  1770. Button({ type: ButtonType.Normal }) {
  1771. Text('移除')
  1772. .fontSize(12)
  1773. .fontWeight(FontWeight.Medium)
  1774. }
  1775. .height(32)
  1776. .padding({ left: 12, right: 12 })
  1777. .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30')
  1778. .fontColor('#FFFFFF')
  1779. .borderRadius(6)
  1780. .onClick(() => {
  1781. this.removeSelectedFile(index);
  1782. })
  1783. }
  1784. .width('100%')
  1785. .padding(12)
  1786. .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
  1787. .borderRadius(8)
  1788. .border({
  1789. width: 1,
  1790. color: this.isDarkMode ? '#19FFFFFF' : '#E5E5EA'
  1791. })
  1792. }
  1793. }, (fileName: string, index: number) => `${index}-${fileName}`)
  1794. }
  1795. .width('100%')
  1796. .constraintSize({ maxHeight: 300 })
  1797. }
  1798. .width('100%')
  1799. .padding(16)
  1800. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1801. .borderRadius(12)
  1802. .shadow({
  1803. radius: this.isDarkMode ? 8 : 12,
  1804. color: this.isDarkMode ? '#0C000000' : '#19000000',
  1805. offsetX: 0,
  1806. offsetY: 2
  1807. })
  1808. .margin({ bottom: 16 })
  1809. }
  1810. build() {
  1811. Column() {
  1812. // 标题栏
  1813. this.topTitleBar()
  1814. // 页面内容区域
  1815. Scroll() {
  1816. Column({ space: 0 }) {
  1817. // 账户选择区域
  1818. // this.AccountSelectorSection()
  1819. // 上传配置区域
  1820. this.UploadConfigSection()
  1821. // 文件选择区域
  1822. this.FilePickerSection()
  1823. // 已选文件列表
  1824. if (this.selectedFiles.length > 0) {
  1825. this.SelectedFilesList()
  1826. }
  1827. // 开始上传按钮
  1828. if (!this.isUploading) {
  1829. this.StartUploadButton()
  1830. }
  1831. // 上传进度显示
  1832. this.UploadProgressView()
  1833. // 上传队列管理
  1834. if (this.uploadQueue.length > 0 || this.finishQueue.length > 0) {
  1835. this.UploadQueueView()
  1836. }
  1837. }
  1838. .width('100%')
  1839. .constraintSize({ minHeight: '100%' })
  1840. }
  1841. .layoutWeight(1)
  1842. .width('100%')
  1843. .padding(16)
  1844. .scrollBar(BarState.Auto)
  1845. // 底部安全区
  1846. Row()
  1847. .height(this.bottomSafeHeight)
  1848. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1849. }
  1850. .width('100%')
  1851. .height('100%')
  1852. .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
  1853. .bindSheet($$this.isShowFileBrowser, this.FileBrowserDialog(), {
  1854. height: '95%',
  1855. dragBar: true,
  1856. showClose: true,
  1857. preferType: SheetType.BOTTOM,
  1858. blurStyle: BlurStyle.Thin,
  1859. title: { title: '浏览选择文件' }
  1860. })
  1861. }
  1862. @Builder
  1863. topTitleBar() {
  1864. Column() {
  1865. Row({ space: 15 }) {
  1866. //左侧滑动按钮
  1867. Button({ type: ButtonType.Circle, stateEffect: true }) {
  1868. SymbolGlyph($r('sys.symbol.chevron_left'))
  1869. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  1870. }
  1871. .attributeModifier(new ButtonFancyModifier(40, 40))
  1872. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
  1873. .animation({ duration: 300, curve: Curve.Ease })
  1874. .onClick(() => {
  1875. this.getUIContext().getRouter().back();
  1876. })
  1877. .attributeModifier(new ShadowModifier())
  1878. .zIndex(0)
  1879. Text('上传音乐')
  1880. .margin({ left: 3, right: 10 })
  1881. .fontColor($r('app.color.text_color'))
  1882. .fontSize(19)
  1883. .maxLines(1)
  1884. .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
  1885. .layoutWeight(1)
  1886. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
  1887. }
  1888. }
  1889. .padding({
  1890. top: this.topSafeHeight + 10,
  1891. left: 10,
  1892. right: 10,
  1893. bottom: 12
  1894. })
  1895. .width('100%')
  1896. }
  1897. /**
  1898. * 文件浏览器对话框
  1899. */
  1900. @Builder
  1901. FileBrowserDialog() {
  1902. Column() {
  1903. // 当前路径和返回按钮
  1904. Row({ space: 12 }) {
  1905. Column({ space: 4 }) {
  1906. Text('当前目录')
  1907. .fontSize(12)
  1908. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1909. Text(this.fileBrowserPath)
  1910. .fontSize(13)
  1911. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
  1912. .maxLines(1)
  1913. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1914. }
  1915. .alignItems(HorizontalAlign.Start)
  1916. .layoutWeight(1)
  1917. if (this.fileBrowserPath.lastIndexOf('/') > 0) {
  1918. Button('返回上级')
  1919. .fontSize(12)
  1920. .height(36)
  1921. .padding({ left: 12, right: 12 })
  1922. .backgroundColor(this.themeColor)
  1923. .fontColor('#FFFFFF')
  1924. .borderRadius(8)
  1925. .onClick(() => {
  1926. this.goBackDirectory();
  1927. })
  1928. }
  1929. }
  1930. .width('100%')
  1931. .padding(16)
  1932. .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
  1933. // 文件列表
  1934. if (this.isLoadingFiles) {
  1935. Column({ space: 12 }) {
  1936. LoadingProgress()
  1937. .width(48)
  1938. .height(48)
  1939. .color(this.themeColor)
  1940. Text('加载中...')
  1941. .fontSize(14)
  1942. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
  1943. }
  1944. .width('100%')
  1945. .height(400)
  1946. .justifyContent(FlexAlign.Center)
  1947. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1948. } else if (this.browserFiles.length === 0) {
  1949. Column({ space: 12 }) {
  1950. Text('📂')
  1951. .fontSize(48)
  1952. Text('当前目录为空')
  1953. .fontSize(14)
  1954. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1955. }
  1956. .width('100%')
  1957. .height(400)
  1958. .justifyContent(FlexAlign.Center)
  1959. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  1960. } else {
  1961. List({ space: 8 }) {
  1962. ForEach(this.browserFiles, (item: LocalFileItem, index: number) => {
  1963. ListItem() {
  1964. Row({ space: 12 }) {
  1965. // 复选框
  1966. Checkbox()
  1967. .select(this.selectedBrowserItems.has(item.path))
  1968. .selectedColor(this.themeColor)
  1969. .onChange((checked: boolean) => {
  1970. this.toggleItemSelection(item);
  1971. })
  1972. // 图标
  1973. Text(item.isDirectory ? '📁' : '🎵')
  1974. .fontSize(24)
  1975. // 文件名
  1976. Column({ space: 2 }) {
  1977. Text(item.name)
  1978. .fontSize(14)
  1979. .fontWeight(FontWeight.Medium)
  1980. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
  1981. .maxLines(1)
  1982. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1983. if (item.isDirectory) {
  1984. Text('文件夹')
  1985. .fontSize(12)
  1986. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1987. }
  1988. }
  1989. .alignItems(HorizontalAlign.Start)
  1990. .layoutWeight(1)
  1991. // 进入按钮(仅文件夹)
  1992. if (item.isDirectory) {
  1993. Text('→')
  1994. .fontSize(20)
  1995. .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
  1996. .onClick(() => {
  1997. this.enterDirectory(item);
  1998. })
  1999. }
  2000. }
  2001. .width('100%')
  2002. .padding(12)
  2003. .backgroundColor(this.selectedBrowserItems.has(item.path) ?
  2004. (this.isDarkMode ? '#2E3238' : '#FFFFFF') :
  2005. (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
  2006. .borderRadius(8)
  2007. .border({
  2008. width: this.selectedBrowserItems.has(item.path) ? 2 : 1,
  2009. color: this.selectedBrowserItems.has(item.path) ?
  2010. this.themeColor :
  2011. (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
  2012. })
  2013. .onClick(() => {
  2014. if (!item.isDirectory) {
  2015. this.toggleItemSelection(item);
  2016. }
  2017. })
  2018. }
  2019. }, (item: LocalFileItem, index: number) => `${index}-${item.path}`)
  2020. }
  2021. .width('100%')
  2022. .layoutWeight(1)
  2023. .padding(16)
  2024. .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
  2025. }
  2026. // 底部按钮
  2027. Row({ space: 12 }) {
  2028. Text(`已选择 ${this.selectedBrowserItems.size} 项`)
  2029. .fontSize(14)
  2030. .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
  2031. .layoutWeight(1)
  2032. Button('取消')
  2033. .fontSize(14)
  2034. .height(48)
  2035. .padding({ left: 30, right: 30 })
  2036. .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
  2037. .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
  2038. .borderRadius(8)
  2039. .onClick(() => {
  2040. this.closeFileBrowser();
  2041. })
  2042. Button('确定')
  2043. .fontSize(14)
  2044. .height(48)
  2045. .padding({ left: 30, right: 30 })
  2046. .backgroundColor(this.themeColor)
  2047. .fontColor('#FFFFFF')
  2048. .borderRadius(8)
  2049. .enabled(this.selectedBrowserItems.size > 0)
  2050. .opacity(this.selectedBrowserItems.size > 0 ? 1.0 : 0.4)
  2051. .onClick(() => {
  2052. this.confirmFileSelection();
  2053. })
  2054. }
  2055. .width('100%')
  2056. .padding({ left:16,right:16 })
  2057. .margin({bottom:12})
  2058. }
  2059. }
  2060. }