WebDavMainPage.ets 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. import { 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 } 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 { PreferencesUtil, StrUtil } from '@pura/harmony-utils';
  16. import { ButtonFancyModifier,
  17. MenuModifier,
  18. ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
  19. import { getRemoteDriveAccountLabel, getRemoteDriveDisplayLabel, getRemoteDrivePlaylistPrefix } from '../common/util/RemoteDriveLabel';
  20. import { RemoteDriveType } from '../common/enums/RemoteDriveType';
  21. import { Utility } from '../common/util/Utility';
  22. /**
  23. * 歌单播放事件数据
  24. */
  25. interface PlaylistEventData {
  26. playlistId: string;
  27. playlistName: string;
  28. songCount: number;
  29. startIndex: number;
  30. songFilePaths: string[];
  31. // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id
  32. }
  33. const TAG = 'heanup WebDavMainPage';
  34. // WebDAV歌曲数据全局内存存储
  35. let globalWebdavVideoItems: VideoItem[] = [];
  36. let globalWebdavCurrentPlayIndex: number = 0;
  37. // 导出函数供LocalMusic访问
  38. export function getWebdavVideoItems(): VideoItem[] {
  39. return globalWebdavVideoItems;
  40. }
  41. export function getWebdavCurrentPlayIndex(): number {
  42. return globalWebdavCurrentPlayIndex;
  43. }
  44. export function clearWebdavVideoItems(): void {
  45. globalWebdavVideoItems = [];
  46. globalWebdavCurrentPlayIndex = 0;
  47. }
  48. // URL解码函数
  49. function decodeUrlEncodedString(encodedStr: string): string {
  50. try {
  51. return decodeURIComponent(encodedStr);
  52. } catch (error) {
  53. // 如果解码失败,返回原始字符串
  54. return encodedStr;
  55. }
  56. }
  57. @Preview
  58. @Entry
  59. @Component
  60. export struct WebDavMainPage {
  61. @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
  62. searchController: SearchController = new SearchController()
  63. @StorageProp('animationState') animationState: AnimationStatus= AnimationStatus.Running
  64. @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
  65. @State isSearchMode: boolean = false
  66. @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
  67. @StorageProp('topSafeHeight') topSafeHeight: number = 0;
  68. @State webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
  69. @State accounts: WebDavAccount[] = [];
  70. @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
  71. @State songs: VideoItem[] = [];
  72. @State dataSource:LazyDataSource<VideoItem> = new LazyDataSource(this.songs)
  73. @Link mType: number;
  74. @Link offsetX: number;
  75. @Link isShowDrawer: boolean;
  76. @State isLoading: boolean = false;
  77. @State webDavFiles: FileInfo[] = []; // 直接在页面中保存文件列表副本
  78. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  79. @StorageProp('isDarkMode') isDarkMode: boolean = false;
  80. @State topRectHeight: number = 0; // 顶部安全区高度
  81. @State breadcrumbs:string[] = []//面包屑导航
  82. @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
  83. @State isShowFileName: boolean = false//是否显示文件名
  84. @State isLongNameRoLL: boolean = true//长歌名滚动
  85. @State sortType: number = 0 //默认排序方式
  86. async onSwitchAccount(){
  87. console.log('onecold 切换账户:', this.selectedAccount.name);
  88. this.songs = [];
  89. this.visibleFoldersState = [];
  90. this.updateListData(this.songs)
  91. // 注意:不再需要清空全局上下文,因为LocalMusic已改用实例变量缓存
  92. // 当新账户加载时,新的认证信息会自动覆盖旧的
  93. Logger.info(TAG, 'heanup 切换账户,准备加载新账户文件');
  94. this.isLoading = true;
  95. await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
  96. .catch((error: Error) => {
  97. Logger.error(TAG, '加载文件失败: ' + error.message);
  98. this.isLoading = false;
  99. });
  100. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  101. }
  102. updateListData(mList:Array<VideoItem>, noSort?: boolean){
  103. if (!noSort) {
  104. this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
  105. this.doSortType(this.sortType)
  106. }
  107. this.dataSource.pushArrayData(mList)
  108. }
  109. // 更新可见文件夹列表
  110. private updateVisibleFolders(): void {
  111. try {
  112. // 安全检查webDavFiles
  113. if (!this.webDavFiles || !Array.isArray(this.webDavFiles)) {
  114. this.visibleFoldersState = [];
  115. return;
  116. }
  117. const allFolders = this.webDavFiles.filter(f => f.isDirectory);
  118. const isNavAccount = this.selectedAccount?.webType === RemoteDriveType.Navidrome;
  119. if (isNavAccount) {
  120. const sortedNavFolders = allFolders.sort((a, b) => a.fileName.localeCompare(b.fileName));
  121. this.visibleFoldersState = sortedNavFolders;
  122. return;
  123. }
  124. const visible: FileInfo[] = [];
  125. for (let i = 0; i < allFolders.length; i++) {
  126. const folder = allFolders[i];
  127. // 安全检查folder对象
  128. if (!folder || typeof folder.fileName !== 'string') {
  129. continue;
  130. }
  131. let shouldShow = this.isDirectChildOfCurrentPath(folder);
  132. if (shouldShow) {
  133. visible.push(folder);
  134. }
  135. }
  136. console.log('更新文件夹列表:', visible);
  137. this.visibleFoldersState = visible;
  138. } catch (error) {
  139. Logger.error(TAG, '更新文件夹列表失败:', error.toString());
  140. this.visibleFoldersState = [];
  141. }
  142. }
  143. // 对话框控制器
  144. private accountDialogController: CustomDialogController | null = null;
  145. // 保存事件处理器引用,用于取消订阅
  146. private eventHandler: (event: string) => void = (event: string) => {
  147. this.handleWebdavEvent(event);
  148. };
  149. aboutToAppear(): void {
  150. this.isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false)
  151. this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
  152. this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
  153. this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true)
  154. // 获取顶部安全区高度
  155. this.getTopRectHeight();
  156. // 加载账户列表
  157. this.loadAccounts();
  158. this.loadFiles()
  159. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  160. // 订阅WebDAV状态变化
  161. this.webdavManager.subscribe(this.eventHandler);
  162. }
  163. // 获取顶部安全区高度
  164. private getTopRectHeight(): void {
  165. window.getLastWindow(getContext(this), (err, data) => {
  166. if (err.code) {
  167. Logger.error(TAG, '获取窗口失败: ' + JSON.stringify(err));
  168. return;
  169. }
  170. const area = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
  171. this.topRectHeight = px2vp(area.topRect.height);
  172. Logger.info(TAG, '顶部安全区高度: ' + this.topRectHeight);
  173. });
  174. }
  175. aboutToDisappear(): void {
  176. // 取消订阅
  177. this.webdavManager.unsubscribe(this.eventHandler);
  178. }
  179. // 处理WebDAV事件
  180. private handleWebdavEvent(event: string): void {
  181. switch (event) {
  182. case RemoteDriveManagerStates.LoadFilesInfoSucceed:
  183. this.songs = this.webdavManager.webDavSongs;
  184. this.updateListData(this.songs)
  185. // 直接引用webdavManager的数组,避免@Observed序列化问题
  186. this.webDavFiles = this.webdavManager.webDavFiles;
  187. this.isLoading = false;
  188. // 更新可见文件夹列表
  189. this.updateVisibleFolders();
  190. // promptAction.showToast({
  191. // message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
  192. // });
  193. break;
  194. case RemoteDriveManagerStates.LoadFilesInfoFailed:
  195. this.isLoading = false;
  196. this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
  197. break;
  198. case RemoteDriveManagerStates.InsertAccountSucceed:
  199. case RemoteDriveManagerStates.EditAccountSucceed:
  200. case RemoteDriveManagerStates.RemoveAccountSucceed:
  201. this.loadAccounts();
  202. break;
  203. }
  204. }
  205. // 加载账户列表
  206. private loadAccounts(): void {
  207. this.accounts = this.webdavManager.getAllWebDavAccounts();
  208. }
  209. // 加载文件列表
  210. private loadFiles(): void {
  211. if (!this.selectedAccount) {
  212. this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
  213. return;
  214. }
  215. this.isLoading = true;
  216. this.webdavManager.loadFilesInfoFromWebdav()
  217. .catch((error: Error) => {
  218. Logger.error(TAG, '加载文件失败: ' + error.message);
  219. this.isLoading = false;
  220. });
  221. }
  222. // 进入文件夹
  223. private enterFolder(folder: FileInfo): void {
  224. this.isLoading = true;
  225. this.webdavManager.enterFolder(folder)
  226. .catch((error: Error) => {
  227. Logger.error(TAG, '进入文件夹失败: ' + error.message);
  228. this.isLoading = false;
  229. });
  230. }
  231. // 检查是否为当前目录的直接子项
  232. private isDirectChildOfCurrentPath(folder: FileInfo): boolean {
  233. const currentPath = this.webdavManager.currentPath || '';
  234. // 如果在根目录(currentPath为空或'/'),显示所有第一级文件夹
  235. if (currentPath === '' || currentPath === '/') {
  236. const folderPath = folder.href.replace(/\/$/, ''); // 去掉尾部斜杠
  237. return folder.href.startsWith('/') &&
  238. folder.href !== '/' &&
  239. !folderPath.substring(1).includes('/');
  240. }
  241. // 非根目录情况,计算相对路径
  242. let relativePath = folder.href;
  243. if (currentPath !== '/') {
  244. relativePath = folder.href.replace(currentPath, '');
  245. }
  246. relativePath = relativePath.replace(/^\//, '').replace(/\/$/, '');
  247. // 只有相对路径不为空且不包含/时才认为是直接子项
  248. return relativePath !== '' && !relativePath.includes('/');
  249. }
  250. // 返回上级目录
  251. private goBack(): void {
  252. if(this.webdavManager.canGoBack()){
  253. this.isLoading = true;
  254. this.webdavManager.goBack()
  255. .catch((error: Error) => {
  256. Logger.error(TAG, '返回失败: ' + error.message);
  257. this.isLoading = false;
  258. });
  259. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  260. }
  261. }
  262. // 切换账户
  263. private switchAccount(account: WebDavAccount): void {
  264. this.selectedAccount = account;
  265. this.songs = [];
  266. this.updateListData(this.songs)
  267. }
  268. // 播放WebDAV歌曲
  269. private playSong(song: VideoItem, index: number): void {
  270. try {
  271. Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
  272. Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index);
  273. Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length);
  274. // 检查歌曲是否有webdav_account_id
  275. Logger.info(TAG, `heanup 播放歌曲的webdav_account_id: ${song.webdav_account_id || '未设置'}`);
  276. // 确保所有歌曲都设置了正确的webdav_account_id
  277. if (this.selectedAccount && this.selectedAccount.id) {
  278. const accountIds = this.songs.map(s => s.webdav_account_id).filter(id => id);
  279. Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${this.songs.length} 首歌曲设置了webdav_account_id`);
  280. // 如果发现歌曲缺少webdav_account_id,立即设置
  281. this.songs.forEach((item, idx) => {
  282. if (!item.webdav_account_id) {
  283. item.webdav_account_id = this.selectedAccount!.id.toString();
  284. Logger.info(TAG, `heanup 为歌曲 "${item.name}" 设置webdav_account_id: ${item.webdav_account_id}`);
  285. }
  286. });
  287. }
  288. // 直接使用当前的VideoItem数组
  289. const videoItems: VideoItem[] = this.songs;
  290. const songFilePaths: string[] = [];
  291. for (let i = 0; i < this.songs.length; i++) {
  292. const item = this.songs[i];
  293. songFilePaths.push(item.filePath); // 使用filePath作为文件路径
  294. }
  295. // 直接通过事件传递videoItems数据,不使用GlobalContext
  296. const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
  297. const playlistData: PlaylistEventData = {
  298. playlistId: 'webdav-playlist', // 使用特殊的ID标识网盘播放列表
  299. playlistName: `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
  300. songCount: this.songs.length,
  301. startIndex: index,
  302. songFilePaths: songFilePaths
  303. };
  304. // 保存videoItems到全局内存
  305. globalWebdavVideoItems = videoItems;
  306. globalWebdavCurrentPlayIndex = index;
  307. Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${index}`);
  308. // 发送播放请求事件,只传递索引信息
  309. emitter.emit(eventPlaylistPlay, { data: playlistData });
  310. Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${index}`);
  311. if(!this.isNoJumpToHome){
  312. // 跳转到首页播放器
  313. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  314. this.mType = 0
  315. })
  316. }
  317. } catch (error) {
  318. const err = error as Error;
  319. Logger.error(TAG, '播放歌曲失败: ' + err.message);
  320. this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
  321. }
  322. }
  323. /**
  324. * 一键创建歌单:将当前WebDAV歌曲全部加入新歌单
  325. */
  326. private async createPlaylistFromCurrentWebDav(): Promise<void> {
  327. try {
  328. Logger.info(TAG, 'heanup 一键创建歌单开始');
  329. if (!this.selectedAccount || !this.selectedAccount.id) {
  330. this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
  331. return;
  332. }
  333. if (!this.songs || this.songs.length === 0) {
  334. // 回退到manager内的歌曲(可能还未复制到页面state)
  335. this.songs = this.webdavManager.webDavSongs;
  336. Logger.info(TAG, `heanup 页面songs为空,回退webdavManager.webDavSongs,长度=${this.songs.length}`);
  337. if (this.songs.length === 0) {
  338. this.getUIContext().getPromptAction().showToast({ message: '当前目录没有可添加的歌曲' });
  339. return;
  340. }
  341. }
  342. Logger.info(TAG, `heanup 歌单创建前歌曲数量: ${this.songs.length}`);
  343. // 确保每首歌都有webdav_account_id
  344. for (let i = 0; i < this.songs.length; i++) {
  345. if (!this.songs[i].webdav_account_id) {
  346. this.songs[i].webdav_account_id = this.selectedAccount.id.toString();
  347. }
  348. }
  349. // 构建歌单名称:账户名 + 当前路径(简化)
  350. const rawPath = this.webdavManager.currentPath || '/';
  351. const shortPath = rawPath === '/' ? '根目录' : rawPath.replace(/\/$/, '').split('/').slice(-1)[0];
  352. const playlistName = `${getRemoteDrivePlaylistPrefix(this.selectedAccount.webType)}-${this.selectedAccount.name}-${shortPath}`;
  353. // 创建歌单
  354. // 安全获取HostContext
  355. const uiContext = this.getUIContext();
  356. const hostCtx = uiContext ? uiContext.getHostContext() : undefined;
  357. if (!hostCtx) {
  358. this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' });
  359. return;
  360. }
  361. const playlistModule = await import('../common/util/PlaylistTable');
  362. const mediaModule = await import('../common/util/MediaTable');
  363. const playlistTable = new playlistModule.default(hostCtx);
  364. const mediaTable = new mediaModule.default(hostCtx);
  365. // 等待MediaTable底层RDB初始化完成
  366. await new Promise<void>((resolve) => {
  367. mediaTable.getRdbStore(hostCtx, () => {
  368. Logger.info(TAG, 'heanup mediaTable RDB 初始化完成');
  369. resolve();
  370. });
  371. });
  372. // 先将WebDAV歌曲入库(若不存在)
  373. let upsertSuccess = 0;
  374. for (let i = 0; i < this.songs.length; i++) {
  375. const v = this.songs[i];
  376. if (!v.id || v.id === '') {
  377. // 使用filePath作为唯一ID
  378. v.id = v.filePath;
  379. }
  380. if (!v.parentPath) {
  381. const idxp = v.filePath.lastIndexOf('/');
  382. if (idxp > 0) {
  383. v.parentPath = v.filePath.substring(0, idxp);
  384. }
  385. }
  386. const ok = await mediaTable.upsertWebDavVideoItem(v);
  387. Logger.info(TAG, `heanup upsert 第${i+1}/${this.songs.length}首: ${v.filePath} => ${ok}`);
  388. if (ok) {
  389. upsertSuccess++;
  390. }
  391. }
  392. Logger.info(TAG, `heanup WebDAV歌曲入库完成: 成功 ${upsertSuccess}/${this.songs.length}`);
  393. // 先查询是否已有同名歌单,避免重复创建导致混淆
  394. const existing = (await playlistTable.queryAllPlaylists()).find(p => p.name === playlistName);
  395. if (existing) {
  396. this.getUIContext().getPromptAction().showToast({ message: '歌单已存在,直接追加歌曲' });
  397. const filePathsExist: string[] = this.songs.map(s => s.filePath);
  398. await playlistTable.addSongsToPlaylist(existing.id, filePathsExist);
  399. router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: existing } });
  400. return;
  401. }
  402. Logger.info(TAG, `heanup 准备创建歌单: ${playlistName}`);
  403. const created = await playlistTable.createPlaylist(playlistName, `来自${getRemoteDriveDisplayLabel(this.selectedAccount.webType)}: ${this.selectedAccount.name} 路径: ${rawPath}`);
  404. if (!created) {
  405. this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' });
  406. return;
  407. }
  408. // 查询刚创建的歌单ID
  409. const playlists = await playlistTable.queryAllPlaylists();
  410. const target = playlists.reverse().find(p => p.name === playlistName); // 取最近创建的同名歌单
  411. if (!target) {
  412. this.getUIContext().getPromptAction().showToast({ message: '无法找到新建歌单' });
  413. return;
  414. }
  415. // 批量添加歌曲
  416. const filePaths: string[] = this.songs.map(s => s.filePath);
  417. Logger.info(TAG, `heanup 开始批量添加歌曲到歌单: ${target.id}`);
  418. const addResult = await playlistTable.addSongsToPlaylist(target.id, filePaths);
  419. Logger.info(TAG, `heanup 批量添加结果: ${addResult}`);
  420. if (!addResult) {
  421. Logger.warn(TAG, '批量添加歌曲返回false,可能全部已存在或写入失败');
  422. }
  423. this.getUIContext().getPromptAction().showToast({ message: `歌单创建成功: ${playlistName}` });
  424. Logger.info(TAG, `heanup 一键创建歌单成功: ${playlistName}, 添加 ${filePaths.length} 首歌曲`);
  425. // 跳转到歌单详情页面
  426. router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: target } });
  427. } catch (error) {
  428. Logger.error(TAG, '一键创建歌单失败: ' + (error as Error).message);
  429. this.getUIContext().getPromptAction().showToast({ message: '一键创建歌单失败' });
  430. }
  431. }
  432. // 导航到指定层级的面包屑路径
  433. private navigateToBreadcrumb(breadcrumbIndex: number): void {
  434. try {
  435. this.isLoading = true;
  436. this.webdavManager.navigateToBreadcrumb(breadcrumbIndex).then((path: string) => {
  437. this.webdavManager.enterFolderFromPath(path)
  438. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  439. console.info('onecold this.breadcrumbs = ' + JSON.stringify(this.breadcrumbs))
  440. console.info('onecold this.webdavManager.currentPath = ' + this.webdavManager.currentPath)
  441. })
  442. .catch((error: Error) => {
  443. Logger.error(TAG, '导航到面包屑路径失败: ' + error.message);
  444. this.isLoading = false;
  445. });
  446. } catch (error) {
  447. Logger.error(TAG, '导航到面包屑路径异常: ' + (error as Error).message);
  448. this.isLoading = false;
  449. }
  450. }
  451. @Builder
  452. SortMenuBuilder() {
  453. Menu() {
  454. MenuItem({
  455. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')),
  456. content: $r('app.string.sort_by_name')
  457. })
  458. .onClick(async () => {
  459. this.doSortType(0)
  460. PreferencesUtil.put("webDavSortType", 0)
  461. })
  462. MenuItem({
  463. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')),
  464. content: '按名称降序'
  465. })
  466. .onClick(async () => {
  467. this.doSortType(1)
  468. PreferencesUtil.put("webDavSortType", 1)
  469. })
  470. MenuItem({
  471. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')),
  472. content:'按时间升序'
  473. })
  474. .onClick(async () => {
  475. this.doSortType(2)
  476. PreferencesUtil.put("webDavSortType", 2)
  477. })
  478. MenuItem({
  479. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')),
  480. content: '按时间降序'
  481. })
  482. .onClick(async () => {
  483. this.doSortType(3)
  484. PreferencesUtil.put("webDavSortType", 3)
  485. })
  486. MenuItem({
  487. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')),
  488. content:'按大小升序'
  489. })
  490. .onClick(async () => {
  491. this.doSortType(4)
  492. PreferencesUtil.put("webDavSortType", 4)
  493. })
  494. MenuItem({
  495. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')),
  496. content: '按大小降序'
  497. })
  498. .onClick(async () => {
  499. this.doSortType(5)
  500. PreferencesUtil.put("webDavSortType", 5)
  501. })
  502. }.attributeModifier(new MenuModifier())
  503. }
  504. doSortType(index: number) {
  505. // 对歌曲列表进行排序
  506. switch (index) {
  507. case 0:
  508. Utility.doSortListAscending(this.songs,this.isShowFileName)
  509. this.visibleFoldersState.sort((a, b) => {
  510. return a.fileName.localeCompare(b.fileName);
  511. });
  512. break;
  513. case 1:
  514. Utility.doSortListDescending(this.songs,this.isShowFileName)
  515. this.visibleFoldersState.sort((a, b) => {
  516. return b.fileName.localeCompare(a.fileName);
  517. });
  518. break;
  519. case 2:
  520. this.songs.sort((a, b) => {
  521. return a.cTime.localeCompare(b.cTime);
  522. });
  523. this.visibleFoldersState.sort((a, b) => {
  524. return a.time-b.time;
  525. });
  526. break;
  527. case 3:
  528. this.songs.sort((a, b) => {
  529. return b.cTime.localeCompare(a.cTime);
  530. });
  531. this.visibleFoldersState.sort((a, b) => {
  532. return b.time-a.time;
  533. });
  534. break;
  535. case 4:
  536. this.songs.sort((a, b) => {
  537. return a.videoSize - b.videoSize;
  538. });
  539. break;
  540. case 5:
  541. this.songs.sort((a, b) => {
  542. return b.videoSize - a.videoSize;
  543. });
  544. break;
  545. }
  546. this.updateListData(this.songs,true)
  547. }
  548. @Builder
  549. topTitleBar(){
  550. Column() {
  551. Row({ space: 15 }) {
  552. if (!this.isSearchMode ) {
  553. //左侧滑动按钮
  554. Button({ type: ButtonType.Circle, stateEffect: true }) {
  555. SymbolGlyph($r('sys.symbol.sort'))
  556. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  557. }
  558. .attributeModifier(new ButtonFancyModifier(40, 40))
  559. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  560. .animation({ duration: 300, curve: Curve.Ease })
  561. .onClick(() => {
  562. this.getUIContext().animateTo({ duration: 555 }, () => {
  563. // 动画闭包内控制Image组件的出现和消失
  564. this.isShowDrawer = !this.isShowDrawer
  565. this.offsetX = 0
  566. })
  567. })
  568. .attributeModifier(new ShadowModifier())
  569. .zIndex(0)
  570. Text(this.selectedAccount.name)
  571. .margin({left:3,right:10})
  572. .fontColor($r('app.color.text_color'))
  573. .fontSize(19)
  574. .maxLines(1)
  575. .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
  576. .layoutWeight(1)
  577. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  578. }else{
  579. //左侧搜索返回按钮
  580. Button({ type: ButtonType.Circle, stateEffect: true }) {
  581. SymbolGlyph($r('sys.symbol.chevron_left'))
  582. .attributeModifier(new SymbolGlyphFancyModifier(24, '', ''))
  583. }
  584. .attributeModifier(new ButtonFancyModifier(40, 40))
  585. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  586. .animation({ duration: 300, curve: Curve.Ease })
  587. .onClick(() => {
  588. this.isSearchMode = false
  589. this.onSearchInput('')
  590. })
  591. .attributeModifier(new ShadowModifier())
  592. .zIndex(0)
  593. }
  594. //搜索框
  595. Search({ controller: this.searchController,value: this.searchText, placeholder: '搜索标题、艺术家...' })
  596. .searchButton('搜索',{fontColor:this.themeColor})
  597. .searchIcon({
  598. src: $r('sys.media.ohos_ic_public_search_filled')
  599. })
  600. .cancelButton({
  601. style: CancelButtonStyle.CONSTANT,
  602. icon: {
  603. src: $r('sys.media.ohos_ic_public_cancel_filled')
  604. }
  605. })
  606. .layoutWeight(1)
  607. .height(35)
  608. .maxLength(20)
  609. .backgroundColor(this.isDarkMode?Color.Black:'#F5F5F5')
  610. .placeholderColor(Color.Grey)
  611. .placeholderFont({ size: 14, weight: 400 })
  612. .textFont({ size: 14, weight: 400 })
  613. .onSubmit((value: string) => {
  614. console.log('onecold onSubmit ='+value)
  615. this.searchController.stopEditing()
  616. this.onSearchInput(this.searchText);
  617. })
  618. .onChange((value: string) => {
  619. console.log('onecold onChange ='+value)
  620. this.onSearchInput(value);
  621. })
  622. .visibility(this.isSearchMode?Visibility.Visible:Visibility.None)
  623. .animation({ duration: 300, curve: Curve.Ease })
  624. //搜索按钮
  625. if (!this.isSearchMode) {
  626. Button({ type: ButtonType.Circle, stateEffect: true }) {
  627. SymbolGlyph($r('sys.symbol.magnifyingglass'))
  628. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  629. }
  630. .attributeModifier(new ButtonFancyModifier(40, 40))
  631. .animation({ duration: 300, curve: Curve.Ease })
  632. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  633. .attributeModifier(new ShadowModifier())
  634. .zIndex(0)
  635. .onClick(()=>{
  636. this.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹
  637. this.isSearchMode = true
  638. })
  639. //排序按钮
  640. Button({ type: ButtonType.Circle, stateEffect: true }) {
  641. SymbolGlyph($r('sys.symbol.list_number'))
  642. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  643. }
  644. .attributeModifier(new ButtonFancyModifier(40, 40))
  645. .animation({ duration: 300, curve: Curve.Ease })
  646. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  647. .bindMenu(this.SortMenuBuilder)
  648. .attributeModifier(new ShadowModifier())
  649. .zIndex(0)
  650. //添加按钮
  651. Button({ type: ButtonType.Circle, stateEffect: true }) {
  652. SymbolGlyph($r('sys.symbol.plus'))
  653. .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
  654. }
  655. .attributeModifier(new ButtonFancyModifier(40, 40))
  656. .animation({ duration: 300, curve: Curve.Ease })
  657. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  658. .onClick(() => {
  659. this.createPlaylistFromCurrentWebDav();
  660. })
  661. .attributeModifier(new ShadowModifier())
  662. .zIndex(0)
  663. }
  664. }
  665. }
  666. .padding({ top: this.topSafeHeight+10, left: 10, right: 10,bottom:2 })
  667. .width('100%')
  668. }
  669. //搜索功能的实现
  670. @State searchText: string = ''; // 用户输入内容
  671. @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
  672. @State filteredFolderList: Array<FileInfo> = []; // 过滤后的文件夹结果
  673. // 实时搜索逻辑(带防抖)
  674. // 实时搜索逻辑(带防抖)
  675. private onSearchInput(value: string) {
  676. this.searchText = value.trim();
  677. let mSearchList: Array<VideoItem> = []
  678. mSearchList = this.songs
  679. // 新增条件判断:空输入时显示所有数据
  680. if (this.searchText === '') {
  681. this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新
  682. this.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹
  683. } else {
  684. this.filteredList = mSearchList.filter((item: VideoItem) => {
  685. //支持模糊匹配和艺术家 专辑匹配
  686. const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i');
  687. return regex.test(item.name.toLowerCase())||
  688. regex.test(item.fileName?.toLowerCase() ?? "") ||
  689. regex.test(item.artist?.toLowerCase() ?? "") ||
  690. regex.test(item.album?.toLowerCase() ?? "")
  691. });
  692. // 对文件夹进行过滤
  693. this.filteredFolderList = this.visibleFoldersState.filter((folder: FileInfo) => {
  694. const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i');
  695. return regex.test(folder.fileName?.toLowerCase() ?? "") ||
  696. regex.test(decodeUrlEncodedString(folder.fileName?.replace('/', '') ?? "").toLowerCase());
  697. });
  698. }
  699. this.updateListData(this.filteredList);
  700. }
  701. build() {
  702. Stack() {
  703. if (this.accounts.length === 0) {
  704. this.buildEmptyView();
  705. } else {
  706. this.buildContentView();
  707. }
  708. Column() {
  709. this.topTitleBar()
  710. this.breaker()
  711. }
  712. }
  713. .alignContent(Alignment.Top)
  714. .width('100%')
  715. .height('100%')
  716. }
  717. @Builder
  718. breaker() {
  719. // 加载按钮和面包屑导航
  720. Column({ space: 8 }) {
  721. // 面包屑导航
  722. if (this.webdavManager.currentPath !== '') {
  723. Row({ space: 8 }) {
  724. Button({ type: ButtonType.Circle }) {
  725. Image(this.webdavManager.canGoBack()?$r('app.media.back'):this.selectedAccount.coverPath)
  726. .width(15)
  727. .height(15)
  728. .borderRadius(10)
  729. .alt($r('app.media.cloudDisk'))
  730. .fillColor(Color.White)
  731. }
  732. .width(20)
  733. .height(20)
  734. .margin({left:5})
  735. .backgroundColor(this.themeColor)
  736. .onClick(() => this.goBack())
  737. Row({ space: 4 }) {
  738. ForEach(this.breadcrumbs, (crumb: string, index: number) => {
  739. Row() {
  740. Text(crumb)
  741. .fontSize(15)
  742. .fontColor(this.themeColor)
  743. .maxLines(1)
  744. .textOverflow({ overflow: TextOverflow.Ellipsis })
  745. }
  746. .onClick(() => {
  747. this.navigateToBreadcrumb(index);
  748. })
  749. // 添加分隔符(除了最后一个元素)
  750. if (index < this.breadcrumbs.length - 1) {
  751. Text('/')
  752. .fontSize(15)
  753. .fontColor($r('app.color.index_tab_font_color'))
  754. .opacity(0.6)
  755. }
  756. })
  757. }
  758. .layoutWeight(1)
  759. }
  760. .width('100%')
  761. .padding({ left: 4, right: 4 })
  762. }
  763. }
  764. .width('100%')
  765. .padding({ left: 12, right: 12,top: 10,bottom: 5 })
  766. }
  767. // 空状态视图
  768. @Builder
  769. buildEmptyView() {
  770. Column({ space: 20 }) {
  771. Image($r('app.media.cloudDisk'))
  772. .width(120)
  773. .height(120)
  774. .opacity(0.3)
  775. Text(`暂无${getRemoteDriveAccountLabel()}`)
  776. .fontSize(16)
  777. .fontColor($r('app.color.index_tab_font_color'))
  778. .opacity(0.6)
  779. }
  780. .justifyContent(FlexAlign.Center)
  781. .width('100%')
  782. .padding({top: this.topSafeHeight+50})
  783. .layoutWeight(1)
  784. }
  785. // 内容视图
  786. @Builder
  787. buildContentView() {
  788. Column() {
  789. // 加载状态
  790. Row() {
  791. LoadingProgress()
  792. .width(30)
  793. .height(30)
  794. .color(this.themeColor)
  795. Text('加载中...')
  796. .fontSize(14)
  797. .fontColor($r('app.color.index_tab_font_color'))
  798. .margin({ left: 12 })
  799. }
  800. .padding({top: this.topSafeHeight + 90, bottom: 20, left: 20, right: 20})
  801. .visibility(this.isLoading?Visibility.Visible:Visibility.None)
  802. .opacity(this.isLoading ? 1 : 0)
  803. .animation({
  804. duration: 500,
  805. curve: 'ease-in-out' // 可选动画曲线
  806. })
  807. // 文件列表(文件夹 + 歌曲)
  808. if (this.webDavFiles.length > 0) {
  809. List({ space: 0 }) {
  810. // 显示文件夹 - 只显示当前目录下的直接子文件夹
  811. ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => {
  812. ListItem() {
  813. this.buildFolderItem(folder)
  814. }
  815. .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
  816. TransitionEffect.scale({ x: 0, y: 0 })))
  817. .clickEffect({ level: ClickEffectLevel.MIDDLE })
  818. }, (folder: FileInfo) => folder.name+folder.fileName)
  819. // 显示歌曲
  820. LazyForEach(this.dataSource, (song: VideoItem, index: number) => {
  821. ListItem() {
  822. this.buildSongItem(song, index)
  823. }
  824. .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
  825. TransitionEffect.scale({ x: 0, y: 0 })))
  826. .clickEffect({ level: ClickEffectLevel.MIDDLE })
  827. }, (item: VideoItem) => item.filePath)
  828. }
  829. .contentStartOffset(this.topSafeHeight + 80)
  830. .contentEndOffset(this.bottomSafeHeight)
  831. .layoutWeight(1)
  832. .margin({ top: 4 })
  833. } else if (!this.isLoading) {
  834. Column() {
  835. Text('暂无内容')
  836. .fontSize(14)
  837. .fontColor($r('app.color.index_tab_font_color'))
  838. .opacity(0.6)
  839. Text('点击"左侧菜单"网盘加载')
  840. .fontSize(12)
  841. .fontColor($r('app.color.index_tab_font_color'))
  842. .opacity(0.4)
  843. .margin({ top: 8 })
  844. }
  845. .justifyContent(FlexAlign.Center)
  846. .layoutWeight(1)
  847. }
  848. }
  849. .layoutWeight(1)
  850. }
  851. // 文件夹列表项
  852. @Builder
  853. buildFolderItem(folder: FileInfo) {
  854. Button({ type: ButtonType.Normal, stateEffect: false }) {
  855. Row({ space: 2 }) {
  856. SymbolGlyph($r('sys.symbol.folder'))
  857. .fontSize(48)
  858. .fontColor([this.themeColor])
  859. .alignSelf(ItemAlign.Center)
  860. .margin({ left: 8, right: 6 })
  861. // 文件夹信息
  862. Column({ space: 4 }) {
  863. Text(decodeUrlEncodedString(folder.fileName.replace('/', '')))
  864. .fontSize(15)
  865. .fontColor($r('app.color.index_tab_font_color'))
  866. .maxLines(1)
  867. .textOverflow({ overflow: TextOverflow.Ellipsis })
  868. Text('文件夹')
  869. .fontSize(13)
  870. .fontColor($r('app.color.index_tab_font_color'))
  871. .opacity(0.6)
  872. }
  873. .alignItems(HorizontalAlign.Start)
  874. .layoutWeight(1)
  875. }
  876. }
  877. .reuseId('dir_item')
  878. .width('100%')
  879. .padding(12)
  880. .backgroundColor(Color.Transparent)
  881. // .backgroundColor($r('app.color.start_window_background'))
  882. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
  883. .onClick(() => {
  884. this.enterFolder(folder);
  885. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  886. })
  887. }
  888. // 歌曲列表项
  889. @Builder
  890. buildSongItem(song: VideoItem, index: number) {
  891. Button({ type: ButtonType.Normal, stateEffect: false }) {
  892. Row({ space: 12 }) {
  893. // 序号
  894. // 歌曲封面
  895. Image(song.pixelMap)
  896. .width(48)
  897. .height(48)
  898. .borderRadius(4)
  899. .alt($r('app.media.music_red'))
  900. .fillColor(this.themeColor)
  901. .objectFit(ImageFit.Cover)
  902. .margin({ left: 8 })
  903. // 歌曲信息
  904. Column({ space: 4 }) {
  905. Text(this.isShowFileName?song.fileName :song.name)
  906. .fontSize(15)
  907. .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
  908. .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
  909. .maxLines(1)
  910. .textOverflow({ overflow: TextOverflow.Ellipsis })
  911. Row(){
  912. Text(song.artist+" ")
  913. .fontSize(13)
  914. .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
  915. .opacity(0.6)
  916. .maxLines(1)
  917. .visibility(song.artist?Visibility.Visible:Visibility.None)
  918. .textOverflow({ overflow: TextOverflow.Ellipsis })
  919. Text(decodeUrlEncodedString(song.size||""))
  920. .fontSize(13)
  921. .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:
  922. $r('app.color.index_tab_font_color'))
  923. .opacity(0.6)
  924. .maxLines(1)
  925. .textOverflow({ overflow: TextOverflow.Ellipsis })
  926. Text(song.cTime)
  927. .fontSize(13)
  928. .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:
  929. $r('app.color.index_tab_font_color'))
  930. .opacity(0.6)
  931. .maxLines(1)
  932. .padding({left: 10})
  933. .visibility(StrUtil.isNotEmpty(song.artist)?Visibility.None:Visibility.Visible)
  934. .textOverflow({ overflow: TextOverflow.Ellipsis })
  935. }
  936. .width('90%')
  937. }
  938. .alignItems(HorizontalAlign.Start)
  939. .layoutWeight(1)
  940. .padding({ right: 20 })
  941. Column() {
  942. //多选按钮的Checkbox 先注释掉
  943. // Checkbox({ name: 'checkbox' + index })
  944. // .select(this.selectedFiles.some(x => x.filePath === item.filePath))
  945. // .selectedColor(this.themeColor)
  946. // .shape(CheckBoxShape.CIRCLE)
  947. // .opacity(this.isMultiSelect ? 1 : 0)
  948. // .animation({
  949. // duration: 666,
  950. // curve: 'Smooth' // 可选动画曲线
  951. // })
  952. // .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
  953. // .onChange((checked: boolean) => this.handleFileSelection(item, checked))
  954. // .margin({ left: 20, top: 8, bottom: 8,right:18 })
  955. // .width(22)
  956. // .height(22)
  957. ImageAnimator()
  958. .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组
  959. .duration(1000)// 持续
  960. .state(this.animationState)// 动画状态
  961. .fillMode(FillMode.Forwards)
  962. .width(18)
  963. .margin({ right: 12, top: 8, bottom: 8 })
  964. .visibility(this.currentSong?.filePath==song.filePath ? Visibility.Visible :
  965. Visibility.None)
  966. .height(18)
  967. .iterations(-1) // 播放次数
  968. }
  969. }
  970. }
  971. .width('100%')
  972. .padding(12)
  973. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
  974. .backgroundColor(Color.Transparent)
  975. .onClick(() => {
  976. this.playSong(song, index);
  977. })
  978. }
  979. }