WebDavMainPage.ets 45 KB

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