WebDavMainPage.ets 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739
  1. import { BreadcrumbItem, RemoteDriveManager } from '../common/util/RemoteDriveManager';
  2. import { WebDavAccount } from '../viewmodel/WebDavAccount';
  3. import { Song } from '../viewmodel/Song';
  4. import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
  5. import Logger from '../common/util/Logger';
  6. import { promptAction, SymbolGlyphModifier, router, window, curves } from '@kit.ArkUI';
  7. import { CommonConstants } from '../common/constants/CommonConstants';
  8. import { VideoItem } from '../viewmodel/VideoItem';
  9. import { GlobalContext } from '../common/util/GlobalContext';
  10. import { display } from '@kit.ArkUI';
  11. import { FileInfo } from '../viewmodel/FileInfo';
  12. import { emitter } from '@kit.BasicServicesKit';
  13. import { EventConstants } from '../common/constants/EventConstants';
  14. import { LazyDataSource } from '../common/util/LazyDataSource';
  15. import { ArrayUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
  16. import { DialogHelper } from '@pura/harmony-dialog';
  17. import { ButtonFancyModifier,
  18. MenuModifier,
  19. ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
  20. import { getRemoteDriveAccountLabel, getRemoteDriveDisplayLabel, getRemoteDrivePlaylistPrefix } from '../common/util/RemoteDriveLabel';
  21. import { RemoteDriveType } from '../common/enums/RemoteDriveType';
  22. import { Utility } from '../common/util/Utility';
  23. import { SettingPage } from './SettingPage';
  24. import { getCloudDiskIcon } from '../dialog/RemoteDriveAccountDialog';
  25. import { CreateFolderDialog } from '../dialog/CreateFolderDialog';
  26. import { UploadMusicPage } from './UploadMusicPage';
  27. /**
  28. * 歌单播放事件数据
  29. */
  30. interface PlaylistEventData {
  31. playlistId: string;
  32. playlistName: string;
  33. songCount: number;
  34. startIndex: number;
  35. isJump: boolean;
  36. songFilePaths: string[];
  37. // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id
  38. }
  39. interface WebDavMetadataUpdatePayload {
  40. filePath: string;
  41. pixelMapPath?: string;
  42. name?: string;
  43. artist?: string;
  44. }
  45. const TAG = 'heanup WebDavMainPage';
  46. // WebDAV歌曲数据全局内存存储
  47. let globalWebdavVideoItems: VideoItem[] = [];
  48. let globalWebdavCurrentPlayIndex: number = 0;
  49. // 导出函数供LocalMusic访问
  50. export function getWebdavVideoItems(): VideoItem[] {
  51. return globalWebdavVideoItems;
  52. }
  53. export function getWebdavCurrentPlayIndex(): number {
  54. return globalWebdavCurrentPlayIndex;
  55. }
  56. export function clearWebdavVideoItems(): void {
  57. globalWebdavVideoItems = [];
  58. globalWebdavCurrentPlayIndex = 0;
  59. }
  60. // URL解码函数
  61. function decodeUrlEncodedString(encodedStr: string): string {
  62. try {
  63. return decodeURIComponent(encodedStr);
  64. } catch (error) {
  65. // 如果解码失败,返回原始字符串
  66. return encodedStr;
  67. }
  68. }
  69. @Preview
  70. @Entry
  71. @Component
  72. export struct WebDavMainPage {
  73. @State appName: string = ''
  74. @State isShowUploadFile: boolean = false
  75. @State isShowTitleBar: boolean = true //是否显示分类导航条
  76. private prevOffsetY: number = 0; // 记录上一次的Y轴偏移量
  77. @State autoHideTitle: boolean = true //滚动自动隐藏标题栏
  78. @State isCustomizeBg: boolean = false //自定义背景界面
  79. @StorageProp('isLandscape') isLandscape: boolean = false;
  80. @State blurValue: number = 0 //背景模糊
  81. @State bgBrightness: number = 0 //背景亮度
  82. @State customizeBgPath: string | undefined = '';
  83. private listScroller: ListScroller = new ListScroller()
  84. @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
  85. searchController: SearchController = new SearchController()
  86. @StorageProp('animationState') animationState: AnimationStatus= AnimationStatus.Running
  87. @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
  88. @State isSearchMode: boolean = false
  89. @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
  90. @StorageProp('topSafeHeight') topSafeHeight: number = 0;
  91. @State webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
  92. @State accounts: WebDavAccount[] = [];
  93. @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
  94. @State songs: VideoItem[] = [];
  95. @State dataSource:LazyDataSource<VideoItem> = new LazyDataSource(this.songs)
  96. @Link mType: number;
  97. @Link offsetX: number;
  98. @Link isShowDrawer: boolean;
  99. @State isLoading: boolean = false;
  100. @State webDavFiles: FileInfo[] = []; // 直接在页面中保存文件列表副本
  101. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  102. @StorageProp('isDarkMode') isDarkMode: boolean = false;
  103. @State breadcrumbs:BreadcrumbItem[] = []//面包屑导航
  104. @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
  105. @State isShowFileName: boolean = false//是否显示文件名
  106. @State isLongNameRoLL: boolean = true//长歌名滚动
  107. @State sortType: number = 0 //默认排序方式
  108. @State listRefreshKey: number = 0 // 列表刷新标识
  109. @Consume isMultiSelect: boolean
  110. @State selectedSongs: VideoItem[] = []
  111. @State isAllSelected: boolean = false
  112. @State isDeletingSelection: boolean = false
  113. private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
  114. async onSwitchAccount(){
  115. console.log('onecold 切换账户:', this.selectedAccount.name);
  116. this.songs = [];
  117. this.visibleFoldersState = [];
  118. this.updateListData(this.songs)
  119. this.exitMultiSelect();
  120. // 注意:不再需要清空全局上下文,因为LocalMusic已改用实例变量缓存
  121. // 当新账户加载时,新的认证信息会自动覆盖旧的
  122. Logger.info(TAG, 'heanup 切换账户,准备加载新账户文件');
  123. this.isLoading = true;
  124. await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
  125. .catch((error: Error) => {
  126. Logger.error(TAG, '加载文件失败: ' + error.message);
  127. this.isLoading = false;
  128. });
  129. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  130. }
  131. updateListData(mList:Array<VideoItem>, noSort?: boolean){
  132. if (!noSort) {
  133. this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
  134. this.doSortType(this.sortType)
  135. }
  136. this.dataSource.pushArrayData(mList)
  137. if(mList.length > 0){
  138. setTimeout(() => {
  139. this.listScroller.scrollToIndex(0)
  140. },200)
  141. }
  142. }
  143. // 更新可见文件夹列表
  144. private updateVisibleFolders(): void {
  145. try {
  146. // 安全检查webDavFiles
  147. if (!this.webDavFiles || !Array.isArray(this.webDavFiles)) {
  148. this.visibleFoldersState = [];
  149. return;
  150. }
  151. const allFolders = this.webDavFiles.filter(f => f.isDirectory);
  152. const isFlatFolderAccount = this.selectedAccount?.webType === RemoteDriveType.Navidrome
  153. || this.selectedAccount?.webType === RemoteDriveType.Jellyfin
  154. || this.selectedAccount?.webType === RemoteDriveType.Emby;
  155. if (isFlatFolderAccount) {
  156. const sortedNavFolders = allFolders.sort((a, b) => a.fileName.localeCompare(b.fileName));
  157. this.visibleFoldersState = sortedNavFolders;
  158. return;
  159. }
  160. const visible: FileInfo[] = [];
  161. for (let i = 0; i < allFolders.length; i++) {
  162. const folder = allFolders[i];
  163. // 安全检查folder对象
  164. if (!folder || typeof folder.fileName !== 'string') {
  165. continue;
  166. }
  167. let shouldShow = this.isDirectChildOfCurrentPath(folder);
  168. if (shouldShow) {
  169. visible.push(folder);
  170. }
  171. }
  172. console.log('更新文件夹列表:', visible);
  173. this.visibleFoldersState = visible;
  174. } catch (error) {
  175. Logger.error(TAG, '更新文件夹列表失败:', error.toString());
  176. this.visibleFoldersState = [];
  177. }
  178. this.isShowTitleBar = true
  179. }
  180. private isSongSelected(song: VideoItem): boolean {
  181. return this.selectedSongs.some(item => item.filePath === song.filePath);
  182. }
  183. private enterMultiSelect(song: VideoItem): void {
  184. if (!this.isMultiSelect) {
  185. this.isMultiSelect = true;
  186. this.selectedSongs = [song];
  187. this.isAllSelected = this.selectedSongs.length === this.songs.length && this.songs.length > 0;
  188. return;
  189. }
  190. if (!this.isSongSelected(song)) {
  191. this.toggleSongSelection(song);
  192. }
  193. }
  194. private toggleSongSelection(song: VideoItem): void {
  195. if (!this.isMultiSelect) {
  196. this.isMultiSelect = true;
  197. }
  198. const exists = this.isSongSelected(song);
  199. if (exists) {
  200. const next = this.selectedSongs.filter(item => item.filePath !== song.filePath);
  201. this.selectedSongs = next;
  202. this.isAllSelected = next.length > 0 && next.length === this.songs.length;
  203. if (next.length === 0) {
  204. this.exitMultiSelect();
  205. }
  206. } else {
  207. const next = [...this.selectedSongs, song];
  208. this.selectedSongs = next;
  209. this.isAllSelected = next.length === this.songs.length && this.songs.length > 0;
  210. }
  211. }
  212. private handleCheckboxSelection(song: VideoItem, checked: boolean): void {
  213. if (!this.isMultiSelect) {
  214. this.isMultiSelect = true;
  215. }
  216. const exists = this.isSongSelected(song);
  217. if (checked && !exists) {
  218. this.toggleSongSelection(song);
  219. } else if (!checked && exists) {
  220. this.toggleSongSelection(song);
  221. }
  222. }
  223. private toggleSelectAll(): void {
  224. if (this.isAllSelected) {
  225. this.selectedSongs = [];
  226. this.isAllSelected = false;
  227. return;
  228. }
  229. if (!this.isMultiSelect) {
  230. this.isMultiSelect = true;
  231. }
  232. this.selectedSongs = this.songs.slice();
  233. this.isAllSelected = this.selectedSongs.length > 0 && this.selectedSongs.length === this.songs.length;
  234. }
  235. private exitMultiSelect(): void {
  236. this.isMultiSelect = false;
  237. this.selectedSongs = [];
  238. this.isAllSelected = false;
  239. console.info('onecold this.isMultiSelect '+this.isMultiSelect)
  240. }
  241. private syncSelectionAfterRefresh(): void {
  242. if (!this.isMultiSelect) {
  243. return;
  244. }
  245. const currentKeys: Set<string> = new Set(this.songs.map(item => item.filePath));
  246. const filtered = this.selectedSongs.filter(item => currentKeys.has(item.filePath));
  247. if (filtered.length !== this.selectedSongs.length) {
  248. this.selectedSongs = filtered;
  249. }
  250. this.isAllSelected = filtered.length > 0 && filtered.length === this.songs.length;
  251. if (filtered.length === 0) {
  252. this.exitMultiSelect();
  253. }
  254. }
  255. private confirmDeleteSelected(): void {
  256. if (!this.selectedAccount) {
  257. this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
  258. return;
  259. }
  260. if (!this.isMultiSelect || this.selectedSongs.length === 0) {
  261. this.getUIContext().getPromptAction().showToast({ message: '请先选择要删除的文件' });
  262. return;
  263. }
  264. this.getUIContext().showAlertDialog({
  265. title: '删除文件',
  266. message: `确定删除选中的${this.selectedSongs.length}个文件吗?`,
  267. primaryButton: {
  268. value: '取消',
  269. action: () => {}
  270. },
  271. secondaryButton: {
  272. value: '删除',
  273. fontColor: Color.Red,
  274. action: () => {
  275. void this.performDeleteSelected();
  276. }
  277. }
  278. });
  279. }
  280. private async performDeleteSelected(): Promise<void> {
  281. if (!this.selectedAccount || this.selectedSongs.length === 0) {
  282. return;
  283. }
  284. this.isDeletingSelection = true;
  285. try {
  286. await this.webdavManager.deleteRemoteSongs(this.selectedAccount, this.selectedSongs.slice());
  287. await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
  288. this.exitMultiSelect();
  289. this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
  290. this.exitMultiSelect()
  291. } catch (error) {
  292. const err = error as Error;
  293. this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
  294. } finally {
  295. this.isDeletingSelection = false;
  296. }
  297. }
  298. private buildSongMetaLine(song: VideoItem): string {
  299. const parts: string[] = [];
  300. if (StrUtil.isNotEmpty(song.duration)) {
  301. parts.push(song.duration as string);
  302. } else if (StrUtil.isNotEmpty(song.size)) {
  303. parts.push(decodeUrlEncodedString(song.size as string));
  304. }
  305. if (parts.length === 0 && StrUtil.isNotEmpty(song.cTime)) {
  306. parts.push(song.cTime as string);
  307. }
  308. return parts.join(' · ');
  309. }
  310. // 对话框控制器
  311. private accountDialogController: CustomDialogController | null = null;
  312. // 保存事件处理器引用,用于取消订阅
  313. private eventHandler: (event: string) => void = (event: string) => {
  314. this.handleWebdavEvent(event);
  315. };
  316. initSetting(){
  317. this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false)
  318. this.customizeBgPath = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '')
  319. this.blurValue = PreferencesUtil.getNumberSync(SettingPage.CUSTOMIZE_BG_BLUR, 0)
  320. this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0)
  321. this.isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false)
  322. this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
  323. this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
  324. this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true)
  325. this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
  326. this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true)
  327. }
  328. aboutToAppear(): void {
  329. Utility.getAppName(getContext(this)).then((appName: string) => {
  330. this.appName = appName
  331. })
  332. this.initSetting()
  333. let eventSetting: emitter.InnerEvent = { eventId: 333 }
  334. // 监听广播事件(通用设置配置更新)
  335. emitter.on(eventSetting, (eventData: emitter.EventData) => {
  336. this.initSetting()
  337. });
  338. // 加载账户列表
  339. this.loadAccounts();
  340. this.loadFiles()
  341. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  342. // 订阅WebDAV状态变化
  343. this.webdavManager.subscribe(this.eventHandler);
  344. this.subscribeWebDavMetadataUpdates();
  345. // 监听手势返回事件
  346. let eventBackSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_DISK }
  347. emitter.on(eventBackSwipeBack, (eventData: emitter.EventData) => {
  348. console.info('onecold', ' 收到 EVENT_SWIPE_BACK_DISK 事件');
  349. // 如果在详情视图模式,退出详情视图
  350. this.goBack()
  351. });
  352. }
  353. aboutToDisappear(): void {
  354. // 取消订阅
  355. this.webdavManager.unsubscribe(this.eventHandler);
  356. emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED);
  357. }
  358. // 处理WebDAV事件
  359. private handleWebdavEvent(event: string): void {
  360. switch (event) {
  361. case RemoteDriveManagerStates.LoadFilesInfoSucceed:
  362. this.songs = this.webdavManager.webDavSongs;
  363. this.updateListData(this.songs)
  364. // 直接引用webdavManager的数组,避免@Observed序列化问题
  365. this.webDavFiles = this.webdavManager.webDavFiles;
  366. this.isLoading = false;
  367. // 更新可见文件夹列表
  368. this.updateVisibleFolders();
  369. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  370. this.syncSelectionAfterRefresh();
  371. // promptAction.showToast({
  372. // message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
  373. // });
  374. break;
  375. case RemoteDriveManagerStates.LoadFilesInfoFailed:
  376. this.isLoading = false;
  377. // this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
  378. break;
  379. case RemoteDriveManagerStates.InsertAccountSucceed:
  380. case RemoteDriveManagerStates.EditAccountSucceed:
  381. case RemoteDriveManagerStates.RemoveAccountSucceed:
  382. this.loadAccounts();
  383. break;
  384. }
  385. }
  386. private subscribeWebDavMetadataUpdates(): void {
  387. emitter.on(this.metadataUpdateEvent, (eventData: emitter.EventData) => {
  388. const payloads = eventData?.data as WebDavMetadataUpdatePayload[] | undefined;
  389. if (!payloads || payloads.length === 0) {
  390. Logger.info(TAG, 'WebDav metadata event received but payload empty');
  391. return;
  392. }
  393. Logger.info(TAG, `WebDav metadata event payload count: ${payloads.length}`);
  394. this.handleWebDavMetadataUpdates(payloads);
  395. });
  396. }
  397. private handleWebDavMetadataUpdates(payloads: WebDavMetadataUpdatePayload[]): void {
  398. if (!payloads || payloads.length === 0) {
  399. return;
  400. }
  401. Logger.info(TAG, 'handleWebDavMetadataUpdates start');
  402. let hasChanges = false;
  403. for (let i = 0; i < payloads.length; i++) {
  404. const payload = payloads[i];
  405. if (!payload || !payload.filePath) {
  406. continue;
  407. }
  408. Logger.info(TAG, `WebDav metadata detail path=${payload.pixelMapPath ?? 'null'}`);
  409. const songUpdated = this.updateSongMetadata(payload);
  410. if (songUpdated) {
  411. hasChanges = true;
  412. Logger.info(TAG, `metadata updated for ${payload.filePath}`);
  413. }
  414. }
  415. if (!hasChanges) {
  416. Logger.info(TAG, 'handleWebDavMetadataUpdates no changes detected');
  417. return;
  418. }
  419. const clonedSongs: VideoItem[] = [];
  420. for (let i = 0; i < this.songs.length; i++) {
  421. clonedSongs.push(this.cloneSong(this.songs[i]));
  422. }
  423. this.songs = clonedSongs;
  424. this.dataSource.pushArrayData(clonedSongs);
  425. this.listRefreshKey++;
  426. Logger.info(TAG, 'handleWebDavMetadataUpdates trigger refresh');
  427. }
  428. private updateSongMetadata(payload: WebDavMetadataUpdatePayload): boolean {
  429. if (!payload.filePath) {
  430. return false;
  431. }
  432. const targetIndex = this.findSongIndex(payload.filePath);
  433. if (targetIndex < 0) {
  434. return false;
  435. }
  436. const targetSong = this.songs[targetIndex];
  437. let mutated = false;
  438. if (payload.pixelMapPath && payload.pixelMapPath.length > 0) {
  439. targetSong.pixelMapPath = payload.pixelMapPath;
  440. mutated = true;
  441. }
  442. if (payload.name && payload.name.length > 0) {
  443. targetSong.name = payload.name;
  444. mutated = true;
  445. }
  446. if (payload.artist && payload.artist.length > 0) {
  447. targetSong.artist = payload.artist;
  448. mutated = true;
  449. }
  450. return mutated;
  451. }
  452. private findSongIndex(filePath: string): number {
  453. for (let i = 0; i < this.songs.length; i++) {
  454. if (this.songs[i].filePath === filePath) {
  455. return i;
  456. }
  457. }
  458. return -1;
  459. }
  460. private cloneSong(item: VideoItem): VideoItem {
  461. const clone = new VideoItem(
  462. item.name,
  463. item.id,
  464. item.filePath,
  465. item.type,
  466. item.videoSize,
  467. item.cTime,
  468. item.size,
  469. item.pixelMapPath,
  470. item.artist,
  471. item.album,
  472. item.fileName,
  473. item.lastPlayed
  474. );
  475. clone.duration = item.duration;
  476. clone.mimeType = item.mimeType;
  477. clone.trackCount = item.trackCount;
  478. clone.sampleRate = item.sampleRate;
  479. clone.size = item.size;
  480. clone.webdav_account_id = item.webdav_account_id;
  481. clone.remote_rel_path = item.remote_rel_path;
  482. clone.artist = item.artist;
  483. clone.album = item.album;
  484. clone.lyricContent = item.lyricContent;
  485. clone.pixelMapPath = item.pixelMapPath;
  486. clone.cTime = item.cTime;
  487. clone.fileName = item.fileName;
  488. clone.md5Str = item.md5Str;
  489. clone.bit_rate = item.bit_rate;
  490. clone.probe_score = item.probe_score;
  491. clone.year = item.year;
  492. clone.nb_streams = item.nb_streams;
  493. clone.nb_programs = item.nb_programs;
  494. clone.genre = item.genre;
  495. clone.track = item.track;
  496. clone.disc = item.disc;
  497. clone.channels = item.channels;
  498. clone.channel_layout = item.channel_layout;
  499. clone.start_time = item.start_time;
  500. clone.ALBUMARTIST = item.ALBUMARTIST;
  501. clone.COMPOSER = item.COMPOSER;
  502. clone.COMMENT = item.COMMENT;
  503. clone.LYRICIST = item.LYRICIST;
  504. clone.pyStr = item.pyStr;
  505. clone.extra_json = item.extra_json;
  506. clone.parentPath = item.parentPath;
  507. clone.isFav = item.isFav;
  508. clone.playCount = item.playCount;
  509. clone.lastPlayed = item.lastPlayed;
  510. clone.videoSize = item.videoSize;
  511. clone.isFav = item.isFav;
  512. return clone;
  513. }
  514. // 加载账户列表
  515. private loadAccounts(): void {
  516. this.accounts = this.webdavManager.getAllWebDavAccounts();
  517. }
  518. // 加载文件列表
  519. private loadFiles(): void {
  520. if (!this.selectedAccount) {
  521. this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
  522. return;
  523. }
  524. this.isLoading = true;
  525. this.webdavManager.loadFilesInfoFromWebdav()
  526. .catch((error: Error) => {
  527. Logger.error(TAG, '加载文件失败: ' + error.message);
  528. this.isLoading = false;
  529. });
  530. }
  531. // 进入文件夹
  532. private enterFolder(folder: FileInfo): void {
  533. this.isLoading = true;
  534. this.webdavManager.enterFolder(folder)
  535. .catch((error: Error) => {
  536. Logger.error(TAG, '进入文件夹失败: ' + error.message);
  537. this.isLoading = false;
  538. });
  539. }
  540. // 检查是否为当前目录的直接子项
  541. private isDirectChildOfCurrentPath(folder: FileInfo): boolean {
  542. const currentPath = this.webdavManager.currentPath || '';
  543. // 如果在根目录(currentPath为空或'/'),显示所有第一级文件夹
  544. if (currentPath === '' || currentPath === '/') {
  545. const folderPath = folder.href.replace(/\/$/, ''); // 去掉尾部斜杠
  546. return folder.href.startsWith('/') &&
  547. folder.href !== '/' &&
  548. !folderPath.substring(1).includes('/');
  549. }
  550. // 非根目录情况,计算相对路径
  551. let relativePath = folder.href;
  552. if (currentPath !== '/') {
  553. relativePath = folder.href.replace(currentPath, '');
  554. }
  555. relativePath = relativePath.replace(/^\//, '').replace(/\/$/, '');
  556. // 只有相对路径不为空且不包含/时才认为是直接子项
  557. return relativePath !== '' && !relativePath.includes('/');
  558. }
  559. // 返回上级目录
  560. private goBack(): void {
  561. if(this.webdavManager.canGoBack()){
  562. this.isLoading = true;
  563. this.webdavManager.goBack()
  564. .catch((error: Error) => {
  565. Logger.error(TAG, '返回失败: ' + error.message);
  566. this.isLoading = false;
  567. });
  568. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  569. }else{
  570. this.getUIContext().animateTo({ duration: 555 }, () => {
  571. // 动画闭包内控制Image组件的出现和消失
  572. this.isShowDrawer = !this.isShowDrawer
  573. this.offsetX = 0
  574. })
  575. }
  576. }
  577. // 切换账户
  578. private switchAccount(account: WebDavAccount): void {
  579. this.selectedAccount = account;
  580. this.songs = [];
  581. this.updateListData(this.songs)
  582. }
  583. // 播放WebDAV歌曲
  584. private playSong(song: VideoItem, index: number,isJump:boolean=false): void {
  585. try {
  586. Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
  587. Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index);
  588. Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length);
  589. // 检查歌曲是否有webdav_account_id
  590. Logger.info(TAG, `heanup 播放歌曲的webdav_account_id: ${song.webdav_account_id || '未设置'}`);
  591. // 确保所有歌曲都设置了正确的webdav_account_id
  592. if (this.selectedAccount && this.selectedAccount.id) {
  593. const accountIds = this.songs.map(s => s.webdav_account_id).filter(id => id);
  594. Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${this.songs.length} 首歌曲设置了webdav_account_id`);
  595. // 如果发现歌曲缺少webdav_account_id,立即设置
  596. this.songs.forEach((item, idx) => {
  597. if (!item.webdav_account_id) {
  598. item.webdav_account_id = this.selectedAccount!.id.toString();
  599. Logger.info(TAG, `heanup 为歌曲 "${item.name}" 设置webdav_account_id: ${item.webdav_account_id}`);
  600. }
  601. });
  602. }
  603. // 直接使用当前的VideoItem数组
  604. const videoItems: VideoItem[] = this.songs;
  605. const songFilePaths: string[] = [];
  606. for (let i = 0; i < this.songs.length; i++) {
  607. const item = this.songs[i];
  608. songFilePaths.push(item.filePath); // 使用filePath作为文件路径
  609. }
  610. // 直接通过事件传递videoItems数据,不使用GlobalContext
  611. const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
  612. const playlistData: PlaylistEventData = {
  613. playlistId: 'webdav-playlist', // 使用特殊的ID标识网盘播放列表
  614. playlistName: `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
  615. songCount: this.songs.length,
  616. startIndex: index,
  617. isJump: isJump,//设置true会弹出播放页
  618. songFilePaths: songFilePaths
  619. };
  620. // 保存videoItems到全局内存
  621. globalWebdavVideoItems = videoItems;
  622. globalWebdavCurrentPlayIndex = index;
  623. Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${index}`);
  624. // 发送播放请求事件,只传递索引信息
  625. emitter.emit(eventPlaylistPlay, { data: playlistData });
  626. Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${index}`);
  627. if(!this.isNoJumpToHome){
  628. // 跳转到首页播放器
  629. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  630. this.mType = 0
  631. })
  632. }
  633. } catch (error) {
  634. const err = error as Error;
  635. Logger.error(TAG, '播放歌曲失败: ' + err.message);
  636. this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
  637. }
  638. }
  639. /**
  640. * 一键创建歌单:将当前WebDAV歌曲全部加入新歌单
  641. */
  642. private async createPlaylistFromCurrentWebDav(): Promise<void> {
  643. try {
  644. Logger.info(TAG, 'heanup 一键创建歌单开始');
  645. if (!this.selectedAccount || !this.selectedAccount.id) {
  646. this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
  647. return;
  648. }
  649. if (!this.songs || this.songs.length === 0) {
  650. // 回退到manager内的歌曲(可能还未复制到页面state)
  651. this.songs = this.webdavManager.webDavSongs;
  652. Logger.info(TAG, `heanup 页面songs为空,回退webdavManager.webDavSongs,长度=${this.songs.length}`);
  653. if (this.songs.length === 0) {
  654. this.getUIContext().getPromptAction().showToast({ message: '当前目录没有可添加的歌曲' });
  655. return;
  656. }
  657. }
  658. Logger.info(TAG, `heanup 歌单创建前歌曲数量: ${this.songs.length}`);
  659. // 确保每首歌都有webdav_account_id
  660. for (let i = 0; i < this.songs.length; i++) {
  661. if (!this.songs[i].webdav_account_id) {
  662. this.songs[i].webdav_account_id = this.selectedAccount.id.toString();
  663. }
  664. }
  665. // 构建歌单名称:账户名 + 当前路径(简化)
  666. const rawPath = this.webdavManager.currentPath || '/';
  667. const shortPath = rawPath === '/' ? '根目录' : rawPath.replace(/\/$/, '').split('/').slice(-1)[0];
  668. const playlistName = `${getRemoteDrivePlaylistPrefix(this.selectedAccount.webType)}-${this.selectedAccount.name}-${shortPath}`;
  669. // 创建歌单
  670. // 安全获取HostContext
  671. const uiContext = this.getUIContext();
  672. const hostCtx = uiContext ? uiContext.getHostContext() : undefined;
  673. if (!hostCtx) {
  674. this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' });
  675. return;
  676. }
  677. const playlistModule = await import('../common/util/PlaylistTable');
  678. const mediaModule = await import('../common/util/MediaTable');
  679. const playlistTable = new playlistModule.default(hostCtx);
  680. const mediaTable = new mediaModule.default(hostCtx);
  681. // 等待MediaTable底层RDB初始化完成
  682. await new Promise<void>((resolve) => {
  683. mediaTable.getRdbStore(hostCtx, () => {
  684. Logger.info(TAG, 'heanup mediaTable RDB 初始化完成');
  685. resolve();
  686. });
  687. });
  688. // 先将WebDAV歌曲入库(若不存在)
  689. let upsertSuccess = 0;
  690. for (let i = 0; i < this.songs.length; i++) {
  691. const v = this.songs[i];
  692. if (!v.id || v.id === '') {
  693. // 使用filePath作为唯一ID
  694. v.id = v.filePath;
  695. }
  696. if (!v.parentPath) {
  697. const idxp = v.filePath.lastIndexOf('/');
  698. if (idxp > 0) {
  699. v.parentPath = v.filePath.substring(0, idxp);
  700. }
  701. }
  702. const ok = await mediaTable.upsertWebDavVideoItem(v);
  703. Logger.info(TAG, `heanup upsert 第${i+1}/${this.songs.length}首: ${v.filePath} => ${ok}`);
  704. if (ok) {
  705. upsertSuccess++;
  706. }
  707. }
  708. Logger.info(TAG, `heanup WebDAV歌曲入库完成: 成功 ${upsertSuccess}/${this.songs.length}`);
  709. // 先查询是否已有同名歌单,避免重复创建导致混淆
  710. const existing = (await playlistTable.queryAllPlaylists()).find(p => p.name === playlistName);
  711. if (existing) {
  712. this.getUIContext().getPromptAction().showToast({ message: '歌单已存在,直接追加歌曲' });
  713. const filePathsExist: string[] = this.songs.map(s => s.filePath);
  714. await playlistTable.addSongsToPlaylist(existing.id, filePathsExist);
  715. router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: existing } });
  716. return;
  717. }
  718. Logger.info(TAG, `heanup 准备创建歌单: ${playlistName}`);
  719. const created = await playlistTable.createPlaylist(playlistName, `来自${getRemoteDriveDisplayLabel(this.selectedAccount.webType)}: ${this.selectedAccount.name} 路径: ${rawPath}`);
  720. if (!created) {
  721. this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' });
  722. return;
  723. }
  724. // 查询刚创建的歌单ID
  725. const playlists = await playlistTable.queryAllPlaylists();
  726. const target = playlists.reverse().find(p => p.name === playlistName); // 取最近创建的同名歌单
  727. if (!target) {
  728. this.getUIContext().getPromptAction().showToast({ message: '无法找到新建歌单' });
  729. return;
  730. }
  731. // 批量添加歌曲
  732. const filePaths: string[] = this.songs.map(s => s.filePath);
  733. Logger.info(TAG, `heanup 开始批量添加歌曲到歌单: ${target.id}`);
  734. const addResult = await playlistTable.addSongsToPlaylist(target.id, filePaths);
  735. Logger.info(TAG, `heanup 批量添加结果: ${addResult}`);
  736. if (!addResult) {
  737. Logger.warn(TAG, '批量添加歌曲返回false,可能全部已存在或写入失败');
  738. }
  739. this.getUIContext().getPromptAction().showToast({ message: `歌单创建成功: ${playlistName}` });
  740. Logger.info(TAG, `heanup 一键创建歌单成功: ${playlistName}, 添加 ${filePaths.length} 首歌曲`);
  741. // 跳转到歌单详情页面
  742. router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: target } });
  743. } catch (error) {
  744. Logger.error(TAG, '一键创建歌单失败: ' + (error as Error).message);
  745. this.getUIContext().getPromptAction().showToast({ message: '一键创建歌单失败' });
  746. }
  747. }
  748. // 导航到指定层级的面包屑路径
  749. private async navigateToBreadcrumb(crumb: BreadcrumbItem): Promise<void> {
  750. if (!crumb) {
  751. return;
  752. }
  753. try {
  754. this.isLoading = true;
  755. await this.webdavManager.enterFolderFromPath(crumb.path);
  756. } catch (error) {
  757. Logger.error(TAG, '导航到面包屑路径失败: ' + (error as Error).message);
  758. this.isLoading = false;
  759. }
  760. }
  761. @Builder
  762. MoreMenuBuilder() {
  763. Menu() {
  764. MenuItem({
  765. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')),
  766. content: $r('app.string.onekey_add_playlist')
  767. })
  768. .onClick(async () => {
  769. this.createPlaylistFromCurrentWebDav();
  770. })
  771. MenuItem({
  772. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_up_to_line')),
  773. content: $r('app.string.upload_music_file')
  774. })
  775. .onClick(async () => {
  776. // this.navigateToUploadPage();
  777. this.isShowUploadFile = !this.isShowUploadFile
  778. })
  779. MenuItem({
  780. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.folder_badge_plus')),
  781. content: $r('app.string.create_foder')
  782. })
  783. .onClick(async () => {
  784. this.showCreateFolderDialog();
  785. })
  786. }.attributeModifier(new MenuModifier())
  787. }
  788. @Builder
  789. SortMenuBuilder() {
  790. Menu() {
  791. MenuItem({
  792. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')),
  793. content: $r('app.string.sort_by_name')
  794. })
  795. .onClick(async () => {
  796. this.doSortType(0)
  797. PreferencesUtil.put("webDavSortType", 0)
  798. })
  799. MenuItem({
  800. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')),
  801. content: '按名称降序'
  802. })
  803. .onClick(async () => {
  804. this.doSortType(1)
  805. PreferencesUtil.put("webDavSortType", 1)
  806. })
  807. MenuItem({
  808. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')),
  809. content:'按时间升序'
  810. })
  811. .onClick(async () => {
  812. this.doSortType(2)
  813. PreferencesUtil.put("webDavSortType", 2)
  814. })
  815. MenuItem({
  816. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')),
  817. content: '按时间降序'
  818. })
  819. .onClick(async () => {
  820. this.doSortType(3)
  821. PreferencesUtil.put("webDavSortType", 3)
  822. })
  823. MenuItem({
  824. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')),
  825. content:'按大小升序'
  826. })
  827. .onClick(async () => {
  828. this.doSortType(4)
  829. PreferencesUtil.put("webDavSortType", 4)
  830. })
  831. MenuItem({
  832. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')),
  833. content: '按大小降序'
  834. })
  835. .onClick(async () => {
  836. this.doSortType(5)
  837. PreferencesUtil.put("webDavSortType", 5)
  838. })
  839. }.attributeModifier(new MenuModifier())
  840. }
  841. doSortType(index: number) {
  842. // 对歌曲列表进行排序
  843. switch (index) {
  844. case 0:
  845. Utility.doSortListAscending(this.songs,this.isShowFileName)
  846. this.visibleFoldersState.sort((a, b) => {
  847. return a.fileName.localeCompare(b.fileName);
  848. });
  849. break;
  850. case 1:
  851. Utility.doSortListDescending(this.songs,this.isShowFileName)
  852. this.visibleFoldersState.sort((a, b) => {
  853. return b.fileName.localeCompare(a.fileName);
  854. });
  855. break;
  856. case 2:
  857. this.songs.sort((a, b) => {
  858. return a.cTime.localeCompare(b.cTime);
  859. });
  860. this.visibleFoldersState.sort((a, b) => {
  861. return a.time-b.time;
  862. });
  863. break;
  864. case 3:
  865. this.songs.sort((a, b) => {
  866. return b.cTime.localeCompare(a.cTime);
  867. });
  868. this.visibleFoldersState.sort((a, b) => {
  869. return b.time-a.time;
  870. });
  871. break;
  872. case 4:
  873. this.songs.sort((a, b) => {
  874. return a.videoSize - b.videoSize;
  875. });
  876. break;
  877. case 5:
  878. this.songs.sort((a, b) => {
  879. return b.videoSize - a.videoSize;
  880. });
  881. break;
  882. }
  883. this.updateListData(this.songs,true)
  884. }
  885. @Builder
  886. topTitleBar(){
  887. Column() {
  888. Row({ space: 15 }) {
  889. if (!this.isSearchMode ) {
  890. //左侧滑动按钮
  891. Button({ type: ButtonType.Circle, stateEffect: true }) {
  892. SymbolGlyph($r('sys.symbol.sort'))
  893. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  894. }
  895. .attributeModifier(new ButtonFancyModifier(40, 40))
  896. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  897. .animation({ duration: 300, curve: Curve.Ease })
  898. .onClick(() => {
  899. this.getUIContext().animateTo({ duration: 555 }, () => {
  900. // 动画闭包内控制Image组件的出现和消失
  901. this.isShowDrawer = !this.isShowDrawer
  902. this.offsetX = 0
  903. })
  904. })
  905. .attributeModifier(new ShadowModifier())
  906. .zIndex(0)
  907. Text(this.selectedAccount.name)
  908. .margin({left:3,right:10})
  909. .fontColor($r('app.color.text_color'))
  910. .fontSize(19)
  911. .maxLines(1)
  912. .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
  913. .layoutWeight(1)
  914. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  915. }else{
  916. //左侧搜索返回按钮
  917. Button({ type: ButtonType.Circle, stateEffect: true }) {
  918. SymbolGlyph($r('sys.symbol.chevron_left'))
  919. .attributeModifier(new SymbolGlyphFancyModifier(24, '', ''))
  920. }
  921. .attributeModifier(new ButtonFancyModifier(40, 40))
  922. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  923. .animation({ duration: 300, curve: Curve.Ease })
  924. .onClick(() => {
  925. this.isSearchMode = false
  926. this.onSearchInput('')
  927. })
  928. .attributeModifier(new ShadowModifier())
  929. .zIndex(0)
  930. }
  931. //搜索框
  932. Search({ controller: this.searchController,value: this.searchText, placeholder: '搜索标题、艺术家...' })
  933. .searchButton('搜索',{fontColor:this.themeColor})
  934. .searchIcon({
  935. src: $r('sys.media.ohos_ic_public_search_filled')
  936. })
  937. .cancelButton({
  938. style: CancelButtonStyle.CONSTANT,
  939. icon: {
  940. src: $r('sys.media.ohos_ic_public_cancel_filled')
  941. }
  942. })
  943. .layoutWeight(1)
  944. .height(35)
  945. .maxLength(20)
  946. .backgroundColor(this.isDarkMode?Color.Black:'#F5F5F5')
  947. .placeholderColor(Color.Grey)
  948. .placeholderFont({ size: 14, weight: 400 })
  949. .textFont({ size: 14, weight: 400 })
  950. .onSubmit((value: string) => {
  951. console.log('onecold onSubmit ='+value)
  952. this.searchController.stopEditing()
  953. this.onSearchInput(this.searchText);
  954. })
  955. .onChange((value: string) => {
  956. console.log('onecold onChange ='+value)
  957. this.onSearchInput(value);
  958. })
  959. .visibility(this.isSearchMode?Visibility.Visible:Visibility.None)
  960. .animation({ duration: 300, curve: Curve.Ease })
  961. //搜索按钮
  962. if (!this.isSearchMode) {
  963. Button({ type: ButtonType.Circle, stateEffect: true }) {
  964. SymbolGlyph($r('sys.symbol.magnifyingglass'))
  965. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  966. }
  967. .attributeModifier(new ButtonFancyModifier(40, 40))
  968. .animation({ duration: 300, curve: Curve.Ease })
  969. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  970. .attributeModifier(new ShadowModifier())
  971. .zIndex(0)
  972. .onClick(()=>{
  973. this.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹
  974. this.isSearchMode = true
  975. })
  976. //排序按钮
  977. Button({ type: ButtonType.Circle, stateEffect: true }) {
  978. SymbolGlyph($r('sys.symbol.list_number'))
  979. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  980. }
  981. .attributeModifier(new ButtonFancyModifier(40, 40))
  982. .animation({ duration: 300, curve: Curve.Ease })
  983. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  984. .bindMenu(this.SortMenuBuilder)
  985. .attributeModifier(new ShadowModifier())
  986. .zIndex(0)
  987. //添加/上传综合按钮
  988. Button({ type: ButtonType.Circle, stateEffect: true }) {
  989. SymbolGlyph($r('sys.symbol.plus'))
  990. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  991. }
  992. .attributeModifier(new ButtonFancyModifier(40, 40))
  993. .animation({ duration: 300, curve: Curve.Ease })
  994. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  995. .bindMenu(this.MoreMenuBuilder)
  996. .attributeModifier(new ShadowModifier())
  997. .zIndex(0)
  998. .bindContentCover($$this.isShowUploadFile, this.UploadFielBuilder(), {
  999. transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
  1000. })
  1001. }
  1002. }
  1003. }
  1004. .padding({ top: this.topSafeHeight + 10, left: 10, right: 10, bottom: 2 })
  1005. .width('100%')
  1006. }
  1007. @Builder
  1008. UploadFielBuilder() {
  1009. Scroll() {
  1010. Column() {
  1011. UploadMusicPage({
  1012. accountId: this.selectedAccount.id,
  1013. isShowUploadFile:this.isShowUploadFile,
  1014. onResult:(result:boolean)=>{
  1015. console.info("heanup onResult,上传成功,刷新列表")
  1016. if(result){
  1017. // 上传文件成功后刷新当前路径的列表
  1018. if(this.selectedAccount.webType==RemoteDriveType.Baidu){
  1019. ToastUtil.showToast(`由于百度网盘限制,百度网盘的上传路径是apps/${this.appName}`)
  1020. }
  1021. this.refreshFileListAfterUpload()
  1022. }
  1023. },
  1024. })
  1025. }
  1026. }
  1027. .width('100%')
  1028. .height('100%')
  1029. }
  1030. //搜索功能的实现
  1031. @State searchText: string = ''; // 用户输入内容
  1032. @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
  1033. @State filteredFolderList: Array<FileInfo> = []; // 过滤后的文件夹结果
  1034. // 实时搜索逻辑(带防抖)
  1035. // 实时搜索逻辑(带防抖)
  1036. private onSearchInput(value: string) {
  1037. this.searchText = value.trim();
  1038. let mSearchList: Array<VideoItem> = []
  1039. mSearchList = this.songs
  1040. // 新增条件判断:空输入时显示所有数据
  1041. if (this.searchText === '') {
  1042. this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新
  1043. this.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹
  1044. } else {
  1045. this.filteredList = mSearchList.filter((item: VideoItem) => {
  1046. //支持模糊匹配和艺术家 专辑匹配
  1047. const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i');
  1048. return regex.test(item.name.toLowerCase())||
  1049. regex.test(item.fileName?.toLowerCase() ?? "") ||
  1050. regex.test(item.artist?.toLowerCase() ?? "") ||
  1051. regex.test(item.album?.toLowerCase() ?? "")
  1052. });
  1053. // 对文件夹进行过滤
  1054. this.filteredFolderList = this.visibleFoldersState.filter((folder: FileInfo) => {
  1055. const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i');
  1056. return regex.test(folder.fileName?.toLowerCase() ?? "") ||
  1057. regex.test(decodeUrlEncodedString(folder.fileName?.replace('/', '') ?? "").toLowerCase());
  1058. });
  1059. }
  1060. this.updateListData(this.filteredList);
  1061. }
  1062. build() {
  1063. Stack() {
  1064. if (this.accounts.length === 0) {
  1065. this.buildEmptyView();
  1066. } else {
  1067. this.buildContentView();
  1068. }
  1069. // 顶部区域:标题栏在上,面包屑在下
  1070. Column() {
  1071. this.topTitleBar()
  1072. this.breaker()
  1073. }
  1074. .width('100%')
  1075. .visibility(this.isShowTitleBar?Visibility.Visible:
  1076. this.autoHideTitle?Visibility.None:Visibility.Visible)
  1077. .animation({
  1078. duration: 500,
  1079. curve: Curve.Friction // 可选动画曲线
  1080. })
  1081. .position({ top: 0, left: 0 })
  1082. // .backgroundColor($r('app.color.start_window_background'))
  1083. // 多选操作条,位于播放条之上(仅在多选模式下显示)
  1084. if (this.isMultiSelect || this.isDeletingSelection) {
  1085. this.buildSelectionOverlay()
  1086. }
  1087. }
  1088. .width('100%')
  1089. .height('100%')
  1090. .alignContent(Alignment.Bottom)
  1091. .backgroundImage(this.isCustomizeBg?this.customizeBgPath:$r('app.color.start_window_background'))
  1092. .backgroundImageSize(this.isLandscape?{width:'100%'}:{ height: '100%'})
  1093. .backgroundImagePosition(Alignment.Center)
  1094. .backdropBlur(this.blurValue)
  1095. .backgroundBrightness({rate:this.isCustomizeBg?0.1:0,lightUpDegree:this.bgBrightness})
  1096. .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
  1097. }
  1098. @Builder
  1099. breaker() {
  1100. // 面包屑导航
  1101. Column({ space: 8 }) {
  1102. // 面包屑导航
  1103. if (this.webdavManager.currentPath !== '') {
  1104. Row({ space: 8 }) {
  1105. Button({ type: ButtonType.Circle }) {
  1106. Image(this.webdavManager.canGoBack()?$r('app.media.back'):
  1107. this.selectedAccount.coverPath?this.selectedAccount.coverPath:getCloudDiskIcon(this.selectedAccount.webType))
  1108. .width(15)
  1109. .height(15)
  1110. .borderRadius(10)
  1111. .alt($r('app.media.cloudDisk'))
  1112. .fillColor(Color.White)
  1113. }
  1114. .width(20)
  1115. .height(20)
  1116. .margin({left:5})
  1117. .backgroundColor(this.themeColor)
  1118. .onClick(() => this.goBack())
  1119. Row({ space: 4 }) {
  1120. ForEach(this.breadcrumbs, (crumb: BreadcrumbItem, index: number) => {
  1121. Row() {
  1122. Text(crumb.label)
  1123. .fontSize(15)
  1124. .fontColor($r('app.color.text_color'))
  1125. .maxLines(1)
  1126. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1127. }
  1128. .onClick(() => {
  1129. this.navigateToBreadcrumb(crumb);
  1130. })
  1131. // 添加分隔符(除了最后一个元素)
  1132. if (index < this.breadcrumbs.length - 1) {
  1133. Text('/')
  1134. .fontSize(15)
  1135. .fontColor($r('app.color.index_tab_font_color'))
  1136. .opacity(0.6)
  1137. }
  1138. })
  1139. }
  1140. .layoutWeight(1)
  1141. }
  1142. .width('100%')
  1143. .padding({ left: 4, right: 4 })
  1144. }
  1145. }
  1146. .width('100%')
  1147. .padding({ left: 12, right: 12, top: 8, bottom: 5 })
  1148. }
  1149. // 空状态视图
  1150. @Builder
  1151. buildEmptyView() {
  1152. Column({ space: 20 }) {
  1153. Image($r('app.media.cloudDisk'))
  1154. .width(120)
  1155. .height(120)
  1156. .opacity(0.3)
  1157. Text(`暂无${getRemoteDriveAccountLabel()}`)
  1158. .fontSize(16)
  1159. .fontColor($r('app.color.index_tab_font_color'))
  1160. .opacity(0.6)
  1161. }
  1162. .justifyContent(FlexAlign.Center)
  1163. .width('100%')
  1164. .padding({top: this.topSafeHeight+160})
  1165. .layoutWeight(1)
  1166. }
  1167. // 内容视图
  1168. @Builder
  1169. buildContentView() {
  1170. Column() {
  1171. // 加载状态
  1172. Row() {
  1173. LoadingProgress()
  1174. .width(30)
  1175. .height(30)
  1176. .color(this.themeColor)
  1177. Text('加载中...')
  1178. .fontSize(14)
  1179. .fontColor($r('app.color.index_tab_font_color'))
  1180. .margin({ left: 12 })
  1181. }
  1182. .padding({top: this.topSafeHeight + 90, left: 20, right: 20})
  1183. .visibility(this.isLoading?Visibility.Visible:Visibility.None)
  1184. .opacity(this.isLoading ? 1 : 0)
  1185. .animation({
  1186. duration: 800,
  1187. curve: Curve.Smooth // 可选动画曲线
  1188. })
  1189. // 文件列表(文件夹 + 歌曲)
  1190. if (this.webDavFiles.length > 0 || this.songs.length > 0) {
  1191. List({ scroller: this.listScroller ,space: 0 }) {
  1192. // 显示文件夹 - 只显示当前目录下的直接子文件夹
  1193. ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => {
  1194. ListItem() {
  1195. this.buildFolderItem(folder)
  1196. }
  1197. .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
  1198. TransitionEffect.scale({ x: 0, y: 0 })))
  1199. .clickEffect({ level: ClickEffectLevel.MIDDLE })
  1200. }, (folder: FileInfo) => folder.name+folder.fileName)
  1201. // 显示歌曲
  1202. LazyForEach(this.dataSource, (song: VideoItem, index: number) => {
  1203. ListItem() {
  1204. this.buildSongItem(song, index)
  1205. }
  1206. .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
  1207. TransitionEffect.scale({ x: 0, y: 0 })))
  1208. .clickEffect({ level: ClickEffectLevel.MIDDLE })
  1209. }, (item: VideoItem) => item.filePath + '_' + this.listRefreshKey)
  1210. }
  1211. .scrollBar(BarState.Off)
  1212. .onScrollFrameBegin((offset: number) => {
  1213. // 获取当前滚动偏移量
  1214. if(this.autoHideTitle){
  1215. const currentOffsetY = this.listScroller.currentOffset().yOffset;
  1216. // 判断滚动方向
  1217. if (currentOffsetY > this.prevOffsetY) {
  1218. this.isShowTitleBar = false
  1219. } else if (currentOffsetY < this.prevOffsetY) {
  1220. this.isShowTitleBar = true
  1221. }
  1222. // 更新前一次偏移量
  1223. this.prevOffsetY = currentOffsetY;
  1224. }
  1225. return { offsetRemain: offset };
  1226. })
  1227. .contentStartOffset(this.topSafeHeight + 85)
  1228. .contentEndOffset(this.bottomSafeHeight+70)
  1229. .layoutWeight(1)
  1230. .margin({ top: 4 })
  1231. } else if (!this.isLoading) {
  1232. Column() {
  1233. Text('暂无内容')
  1234. .fontSize(14)
  1235. .fontColor($r('app.color.index_tab_font_color'))
  1236. .opacity(0.6)
  1237. Text('点击"左侧菜单"网盘加载')
  1238. .fontSize(12)
  1239. .fontColor($r('app.color.index_tab_font_color'))
  1240. .opacity(0.4)
  1241. .margin({ top: 8 })
  1242. }
  1243. .justifyContent(FlexAlign.Center)
  1244. .layoutWeight(1)
  1245. }
  1246. }
  1247. .layoutWeight(1)
  1248. }
  1249. @Builder
  1250. private buildMultiSelectBar() {
  1251. Row({ space: 12 }) {
  1252. Button(this.isAllSelected ? '反选' : '全选', { type: ButtonType.Circle, stateEffect: true })
  1253. .width(55)
  1254. .height(55)
  1255. .fontSize(13)
  1256. .backgroundColor(this.themeColor)
  1257. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
  1258. .onClick(() => this.toggleSelectAll())
  1259. Button('删除', { type: ButtonType.Circle, stateEffect: true })
  1260. .width(55)
  1261. .height(55)
  1262. .fontSize(13)
  1263. .backgroundColor(this.themeColor)
  1264. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
  1265. .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
  1266. .onClick(() => this.confirmDeleteSelected())
  1267. Button('取消', { type: ButtonType.Circle, stateEffect: true })
  1268. .width(55)
  1269. .height(55)
  1270. .fontSize(13)
  1271. .backgroundColor(this.themeColor)
  1272. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
  1273. .onClick(() => this.exitMultiSelect())
  1274. }
  1275. .width('100%')
  1276. .padding({ bottom: 10, top: 12 })
  1277. .justifyContent(FlexAlign.Center)
  1278. .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
  1279. .opacity(this.isMultiSelect ? 1 : 0)
  1280. .animation({ duration: 300, curve: 'ease-in-out' })
  1281. }
  1282. @Builder
  1283. private buildSelectionOverlay() {
  1284. Column() {
  1285. Row({ space: 10 }) {
  1286. LoadingProgress()
  1287. .width(22)
  1288. .height(22)
  1289. .color(this.themeColor)
  1290. Text('正在删除...')
  1291. .fontSize(14)
  1292. .fontColor($r('app.color.index_tab_font_color'))
  1293. }
  1294. .padding({ top: 12, bottom: 6 })
  1295. .justifyContent(FlexAlign.Center)
  1296. .visibility(this.isDeletingSelection ? Visibility.Visible : Visibility.None)
  1297. this.buildMultiSelectBar()
  1298. }
  1299. .width('100%')
  1300. .padding({ left: 12, right: 12, bottom: this.bottomSafeHeight })
  1301. }
  1302. // 文件夹列表项
  1303. @Builder
  1304. buildFolderItem(folder: FileInfo) {
  1305. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  1306. Row({ space: 2 }) {
  1307. SymbolGlyph($r('sys.symbol.folder'))
  1308. .fontSize(48)
  1309. .fontColor([this.themeColor])
  1310. .alignSelf(ItemAlign.Center)
  1311. .margin({ left: 8, right: 6 })
  1312. // 文件夹信息
  1313. Column({ space: 4 }) {
  1314. Text(decodeUrlEncodedString(folder.fileName.replace('/', '')))
  1315. .fontSize(15)
  1316. .fontColor($r('app.color.index_tab_font_color'))
  1317. .maxLines(1)
  1318. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1319. Text('文件夹')
  1320. .fontSize(13)
  1321. .fontColor($r('app.color.index_tab_font_color'))
  1322. .opacity(0.6)
  1323. }
  1324. .alignItems(HorizontalAlign.Start)
  1325. .layoutWeight(1)
  1326. }
  1327. }
  1328. .reuseId('dir_item')
  1329. .width('100%')
  1330. .padding(12)
  1331. .backgroundColor(Color.Transparent)
  1332. // .backgroundColor($r('app.color.start_window_background'))
  1333. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
  1334. .onClick(() => {
  1335. this.enterFolder(folder);
  1336. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  1337. })
  1338. }
  1339. // 歌曲列表项
  1340. @Builder
  1341. buildSongItem(song: VideoItem, index: number) {
  1342. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  1343. Row({ space: 12 }) {
  1344. // 序号
  1345. // 歌曲封面
  1346. Image(song.pixelMapPath ? song.pixelMapPath : $r('app.media.alt'))
  1347. .width(52)
  1348. .height(52)
  1349. .borderRadius(9)
  1350. .sourceSize({width:38, height:38})
  1351. .alt($r('app.media.alt'))
  1352. .fillColor(this.themeColor)
  1353. .objectFit(ImageFit.Cover)
  1354. .shadow({
  1355. radius: StrUtil.isEmpty(song.pixelMapPath) ?6:14,
  1356. type: ShadowType.BLUR,
  1357. color: 'on_primary'
  1358. })
  1359. .margin({ left: 8 })
  1360. .onClick(() => {
  1361. if (this.isMultiSelect) {
  1362. this.toggleSongSelection(song);
  1363. } else {
  1364. this.playSong(song, index, true);
  1365. }
  1366. })
  1367. // 歌曲信息
  1368. Column({ space: 4 }) {
  1369. Text(this.isShowFileName?song.fileName :song.name)
  1370. .fontSize(15)
  1371. .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
  1372. .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
  1373. .maxLines(1)
  1374. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1375. Row(){
  1376. Text((song.artist ?? '') + " ")
  1377. .fontSize(13)
  1378. .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
  1379. .opacity(0.6)
  1380. .maxLines(1)
  1381. .visibility(song.artist?Visibility.Visible:Visibility.None)
  1382. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1383. Text(this.buildSongMetaLine(song))
  1384. .fontSize(13)
  1385. .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:
  1386. $r('app.color.index_tab_font_color'))
  1387. .opacity(0.6)
  1388. .maxLines(1)
  1389. .textOverflow({ overflow: TextOverflow.Ellipsis })
  1390. }
  1391. .width('90%')
  1392. }
  1393. .alignItems(HorizontalAlign.Start)
  1394. .layoutWeight(1)
  1395. .padding({ right: 20 })
  1396. Column() {
  1397. Checkbox({ name: 'checkbox_' + index })
  1398. .select(this.isSongSelected(song))
  1399. .selectedColor(this.themeColor)
  1400. .opacity(this.isMultiSelect ? 1 : 0)
  1401. .animation({
  1402. duration: 300,
  1403. curve: 'Smooth'
  1404. })
  1405. .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
  1406. .onChange((checked: boolean) => this.handleCheckboxSelection(song, checked))
  1407. .margin({ left: 10, top: 8, bottom: 8, right: 12 })
  1408. .width(22)
  1409. .height(22)
  1410. ImageAnimator()
  1411. .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组
  1412. .duration(1000)// 持续
  1413. //.state(this.animationState)// 动画状态
  1414. .state(AnimationStatus.Running)// 动画状态
  1415. .fillMode(FillMode.Forwards)
  1416. .width(18)
  1417. .margin({ right: 12, top: 8, bottom: 8 })
  1418. .visibility(this.currentSong?.filePath==song.filePath ? (this.isMultiSelect ? Visibility.None : Visibility.Visible) :
  1419. Visibility.None)
  1420. .height(18)
  1421. .iterations(-1) // 播放次数
  1422. }
  1423. }
  1424. }
  1425. .width('100%')
  1426. .padding(12)
  1427. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
  1428. .backgroundColor(Color.Transparent)
  1429. .onClick(() => {
  1430. if (this.isMultiSelect) {
  1431. this.toggleSongSelection(song);
  1432. } else {
  1433. this.playSong(song, index);
  1434. }
  1435. })
  1436. .gesture(LongPressGesture().onAction(() => {
  1437. this.enterMultiSelect(song);
  1438. }))
  1439. }
  1440. // 对话框控制器
  1441. private createFolderDialogController: CustomDialogController | null = null;
  1442. /**
  1443. * 显示创建文件夹对话框
  1444. */
  1445. private showCreateFolderDialog(): void {
  1446. if (!this.selectedAccount) {
  1447. this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
  1448. return;
  1449. }
  1450. // 获取当前主题色
  1451. const currentThemeColor = this.themeColor;
  1452. // 创建自定义对话框
  1453. this.createFolderDialogController = new CustomDialogController({
  1454. builder: CreateFolderDialog({
  1455. onConfirm: (folderName: string) => {
  1456. if (folderName.trim().length > 0) {
  1457. void this.createFolder(folderName.trim());
  1458. } else {
  1459. this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
  1460. }
  1461. },
  1462. onCancel: () => {
  1463. // 用户取消创建
  1464. },
  1465. initialName: '',
  1466. themeColor: currentThemeColor
  1467. }),
  1468. autoCancel: true,
  1469. alignment: DialogAlignment.Center,
  1470. customStyle: false
  1471. });
  1472. this.createFolderDialogController.open();
  1473. }
  1474. /**
  1475. * 上传成功后刷新文件列表
  1476. */
  1477. private async refreshFileListAfterUpload(): Promise<void> {
  1478. if (!this.selectedAccount) {
  1479. Logger.warn(TAG, '刷新失败:未选择账户');
  1480. return;
  1481. }
  1482. try {
  1483. Logger.info(TAG, 'heanup 开始刷新上传后的文件列表');
  1484. // 显示加载状态
  1485. this.isLoading = true;
  1486. // 重新加载当前账户的文件信息
  1487. await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
  1488. Logger.info(TAG, 'heanup 文件列表刷新完成');
  1489. } catch (error) {
  1490. const err = error as Error;
  1491. Logger.error(TAG, `heanup 刷新文件列表失败: ${err.message}`);
  1492. // 显示错误提示
  1493. this.getUIContext().getPromptAction().showToast({
  1494. message: `刷新失败: ${err.message}`
  1495. });
  1496. } finally {
  1497. this.isLoading = false;
  1498. }
  1499. }
  1500. /**
  1501. * 创建文件夹的实际方法
  1502. * @param folderName 文件夹名称
  1503. */
  1504. private async createFolder(folderName: string): Promise<void> {
  1505. if (!this.selectedAccount) {
  1506. this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
  1507. return;
  1508. }
  1509. if (folderName.length === 0) {
  1510. this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
  1511. return;
  1512. }
  1513. try {
  1514. Logger.info(TAG, `heanup 开始创建文件夹: ${folderName}`);
  1515. // 显示加载状态
  1516. this.isLoading = true;
  1517. if(this.selectedAccount.webType==RemoteDriveType.Baidu){
  1518. // 调用RemoteDriveManager的createBaiduFolder方法
  1519. await this.webdavManager.createBaiduFolder(this.selectedAccount, folderName);
  1520. }else if(this.selectedAccount.webType==RemoteDriveType.WebDav){
  1521. // 调用RemoteDriveManager的createWebDavFolder方法
  1522. await this.webdavManager.createWebDavFolder(this.selectedAccount, folderName);
  1523. }else if(this.selectedAccount.webType==RemoteDriveType.Smb){
  1524. // 调用RemoteDriveManager的createSMBFolder方法
  1525. await this.webdavManager.createSMBFolder(this.selectedAccount, folderName);
  1526. }else if(this.selectedAccount.webType==RemoteDriveType.Ftp){
  1527. // 调用RemoteDriveManager的createFTPFolder方法
  1528. this.getUIContext().getPromptAction().showToast({ message: 'FTP目前不支持创建文件夹' });
  1529. }else{
  1530. this.getUIContext().getPromptAction().showToast({ message: '当前账户类型不支持创建文件夹' });
  1531. }
  1532. // 创建成功后刷新当前目录
  1533. await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
  1534. this.getUIContext().getPromptAction().showToast({
  1535. message: `文件夹 "${folderName}" 创建成功`
  1536. });
  1537. Logger.info(TAG, `heanup 文件夹创建成功`);
  1538. } catch (error) {
  1539. const err = error as Error;
  1540. Logger.error(TAG, `heanup 创建文件夹失败: ${err.message}`);
  1541. // 显示具体的错误信息
  1542. this.getUIContext().getPromptAction().showToast({
  1543. message: `创建失败: ${err.message}`
  1544. });
  1545. } finally {
  1546. this.isLoading = false;
  1547. }
  1548. }
  1549. }