UploadMusicPage.ets 71 KB

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