WebDavMainPage.ets 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. import { WebdavManager } from '../common/util/WebdavManager';
  2. import { WebDavAccount } from '../viewmodel/WebDavAccount';
  3. import { Song } from '../viewmodel/Song';
  4. import { WebdavManagerStates } from '../common/enums/WebdavManagerStates';
  5. import Logger from '../common/util/Logger';
  6. import { promptAction, 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. /**
  16. * 歌单播放事件数据
  17. */
  18. interface PlaylistEventData {
  19. playlistId: string;
  20. playlistName: string;
  21. songCount: number;
  22. startIndex: number;
  23. songFilePaths: string[];
  24. webDavAuthInfo?: WebDavAuthInfo; // 新增WebDAV认证信息
  25. }
  26. /**
  27. * WebDAV认证信息
  28. */
  29. interface WebDavAuthInfo {
  30. accountId: number;
  31. host: string;
  32. port: number;
  33. account: string;
  34. password: string;
  35. enableHttps: boolean;
  36. }
  37. const TAG = 'heanup WebDavMainPage';
  38. // URL解码函数
  39. function decodeUrlEncodedString(encodedStr: string): string {
  40. try {
  41. return decodeURIComponent(encodedStr);
  42. } catch (error) {
  43. // 如果解码失败,返回原始字符串
  44. return encodedStr;
  45. }
  46. }
  47. @Preview
  48. @Entry
  49. @Component
  50. export struct WebDavMainPage {
  51. @State webdavManager: WebdavManager = WebdavManager.getInstance();
  52. @State accounts: WebDavAccount[] = [];
  53. @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
  54. @State songs: Song[] = [];
  55. @State dataSource:LazyDataSource<Song> = new LazyDataSource(this.songs)
  56. @Link mType: number;
  57. @Link offsetX: number;
  58. @Link isShowDrawer: boolean;
  59. @State isLoading: boolean = false;
  60. @State webDavFiles: FileInfo[] = []; // 直接在页面中保存文件列表副本
  61. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  62. @StorageProp('isDarkMode') isDarkMode: boolean = false;
  63. @State topRectHeight: number = 0; // 顶部安全区高度
  64. @State breadcrumbs:string[] = []//面包屑导航
  65. @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
  66. onSwitchAccount(){
  67. console.log('onecold 切换账户:', this.selectedAccount.name);
  68. this.songs = [];
  69. this.updateListData(this.songs)
  70. this.isLoading = true;
  71. this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
  72. .catch((error: Error) => {
  73. Logger.error(TAG, '加载文件失败: ' + error.message);
  74. this.isLoading = false;
  75. });
  76. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  77. }
  78. updateListData(mList:Array<Song>){
  79. this.dataSource.pushArrayData(mList)
  80. }
  81. // 更新可见文件夹列表
  82. private updateVisibleFolders(): void {
  83. try {
  84. // 安全检查webDavFiles
  85. if (!this.webDavFiles || !Array.isArray(this.webDavFiles)) {
  86. this.visibleFoldersState = [];
  87. return;
  88. }
  89. const allFolders = this.webDavFiles.filter(f => f.isDirectory);
  90. const visible: FileInfo[] = [];
  91. for (let i = 0; i < allFolders.length; i++) {
  92. const folder = allFolders[i];
  93. // 安全检查folder对象
  94. if (!folder || typeof folder.fileName !== 'string') {
  95. continue;
  96. }
  97. const shouldShow = this.isDirectChildOfCurrentPath(folder);
  98. if (shouldShow) {
  99. visible.push(folder);
  100. }
  101. }
  102. this.visibleFoldersState = visible;
  103. } catch (error) {
  104. Logger.error(TAG, '更新文件夹列表失败:', error.toString());
  105. this.visibleFoldersState = [];
  106. }
  107. }
  108. // 对话框控制器
  109. private accountDialogController: CustomDialogController | null = null;
  110. // 保存事件处理器引用,用于取消订阅
  111. private eventHandler: (event: string) => void = (event: string) => {
  112. this.handleWebdavEvent(event);
  113. };
  114. aboutToAppear(): void {
  115. // 获取顶部安全区高度
  116. this.getTopRectHeight();
  117. // 加载账户列表
  118. this.loadAccounts();
  119. this.loadFiles()
  120. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  121. // 订阅WebDAV状态变化
  122. this.webdavManager.subscribe(this.eventHandler);
  123. }
  124. // 获取顶部安全区高度
  125. private getTopRectHeight(): void {
  126. window.getLastWindow(getContext(this), (err, data) => {
  127. if (err.code) {
  128. Logger.error(TAG, '获取窗口失败: ' + JSON.stringify(err));
  129. return;
  130. }
  131. const area = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
  132. this.topRectHeight = px2vp(area.topRect.height);
  133. Logger.info(TAG, '顶部安全区高度: ' + this.topRectHeight);
  134. });
  135. }
  136. aboutToDisappear(): void {
  137. // 取消订阅
  138. this.webdavManager.unsubscribe(this.eventHandler);
  139. }
  140. // 处理WebDAV事件
  141. private handleWebdavEvent(event: string): void {
  142. switch (event) {
  143. case WebdavManagerStates.LoadFilesInfoSucceed:
  144. this.songs = this.webdavManager.webDavSongs;
  145. this.updateListData(this.songs)
  146. // 直接引用webdavManager的数组,避免@Observed序列化问题
  147. this.webDavFiles = this.webdavManager.webDavFiles;
  148. this.isLoading = false;
  149. // 更新可见文件夹列表
  150. this.updateVisibleFolders();
  151. // promptAction.showToast({
  152. // message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
  153. // });
  154. break;
  155. case WebdavManagerStates.LoadFilesInfoFailed:
  156. this.isLoading = false;
  157. this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
  158. break;
  159. case WebdavManagerStates.InsertAccountSucceed:
  160. case WebdavManagerStates.EditAccountSucceed:
  161. case WebdavManagerStates.RemoveAccountSucceed:
  162. this.loadAccounts();
  163. break;
  164. }
  165. }
  166. // 加载账户列表
  167. private loadAccounts(): void {
  168. this.accounts = this.webdavManager.getAllWebDavAccounts();
  169. }
  170. // 加载文件列表
  171. private loadFiles(): void {
  172. if (!this.selectedAccount) {
  173. this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
  174. return;
  175. }
  176. this.isLoading = true;
  177. this.webdavManager.loadFilesInfoFromWebdav()
  178. .catch((error: Error) => {
  179. Logger.error(TAG, '加载文件失败: ' + error.message);
  180. this.isLoading = false;
  181. });
  182. }
  183. // 进入文件夹
  184. private enterFolder(folder: FileInfo): void {
  185. this.isLoading = true;
  186. this.webdavManager.enterFolder(folder)
  187. .catch((error: Error) => {
  188. Logger.error(TAG, '进入文件夹失败: ' + error.message);
  189. this.isLoading = false;
  190. });
  191. }
  192. // 检查是否为当前目录的直接子项
  193. private isDirectChildOfCurrentPath(folder: FileInfo): boolean {
  194. const currentPath = this.webdavManager.currentPath || '';
  195. // 如果在根目录(currentPath为空或'/'),显示所有第一级文件夹
  196. if (currentPath === '' || currentPath === '/') {
  197. // 根目录情况下,显示所有第一级文件夹(href格式为/foldername)
  198. return folder.href.startsWith('/') &&
  199. folder.href !== '/' &&
  200. !folder.href.substring(1).includes('/');
  201. }
  202. // 非根目录情况,计算相对路径
  203. let relativePath = folder.href;
  204. if (currentPath !== '/') {
  205. relativePath = folder.href.replace(currentPath, '');
  206. }
  207. relativePath = relativePath.replace(/^\//, '').replace(/\/$/, '');
  208. // 只有相对路径不为空且不包含/时才认为是直接子项
  209. return relativePath !== '' && !relativePath.includes('/');
  210. }
  211. // 返回上级目录
  212. private goBack(): void {
  213. if(this.webdavManager.canGoBack()){
  214. this.isLoading = true;
  215. this.webdavManager.goBack()
  216. .catch((error: Error) => {
  217. Logger.error(TAG, '返回失败: ' + error.message);
  218. this.isLoading = false;
  219. });
  220. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  221. }
  222. }
  223. // 切换账户
  224. private switchAccount(account: WebDavAccount): void {
  225. this.selectedAccount = account;
  226. this.songs = [];
  227. this.updateListData(this.songs)
  228. }
  229. // 将Song转换为VideoItem
  230. private convertSongToVideoItem(song: Song, index: number): VideoItem {
  231. const videoItem = new VideoItem(
  232. song.title,
  233. index.toString(),
  234. song.src, // WebDAV URL作为文件路径
  235. CommonConstants.TYPE_INTERNET, // 使用网络类型
  236. song.fileSize,
  237. song.time.toString(),
  238. undefined, // pixelMap
  239. undefined, // size
  240. typeof song.img === 'string' ? song.img : undefined, // pixelMapPath
  241. song.artist,
  242. undefined, // album
  243. song.name // fileName
  244. );
  245. return videoItem;
  246. }
  247. // 播放WebDAV歌曲
  248. private playSong(song: Song, index: number): void {
  249. try {
  250. Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
  251. Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.title + ', 索引: ' + index);
  252. Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length);
  253. // 保存WebDAV认证信息到全局上下文(用于播放器认证)
  254. const globalContext = GlobalContext.getContext();
  255. if (this.selectedAccount && this.selectedAccount.account && this.selectedAccount.password) {
  256. const webDavAuthInfo: WebDavAuthInfo = {
  257. accountId: this.selectedAccount.id,
  258. host: this.selectedAccount.host,
  259. port: this.selectedAccount.port,
  260. account: this.selectedAccount.account,
  261. password: this.selectedAccount.password,
  262. enableHttps: this.selectedAccount.enableHttps
  263. };
  264. globalContext.setObject('webDavAuthInfo', webDavAuthInfo);
  265. Logger.info(TAG, 'heanup 已保存WebDAV认证信息到全局上下文'+JSON.stringify(webDavAuthInfo));
  266. }
  267. // 将当前歌曲列表转换为VideoItem数组
  268. const videoItems: VideoItem[] = [];
  269. const songFilePaths: string[] = [];
  270. for (let i = 0; i < this.songs.length; i++) {
  271. const item = this.convertSongToVideoItem(this.songs[i], i);
  272. videoItems.push(item);
  273. songFilePaths.push(item.filePath); // 使用filePath作为文件路径
  274. }
  275. Logger.info(TAG, 'heanup 所有WebDAV歌曲文件路径: ' + JSON.stringify(songFilePaths));
  276. // 保存WebDAV歌曲数据到全局上下文
  277. globalContext.setObject('videoItems', videoItems);
  278. globalContext.setObject('currentPlayIndex', index);
  279. Logger.info(TAG, 'heanup 已将WebDAV歌曲列表保存到全局上下文,长度:' + videoItems.length);
  280. // 验证保存是否成功
  281. const savedVideoItems = globalContext.getObject('videoItems') as VideoItem[];
  282. const savedIndex = globalContext.getObject('currentPlayIndex') as number;
  283. Logger.info(TAG, 'heanup 验证保存结果 - videoItems长度: ' + (savedVideoItems?.length || 0) + ', currentPlayIndex: ' + savedIndex);
  284. // 检查认证信息是否还在
  285. const savedAuthInfo = globalContext.getObject('webDavAuthInfo') as WebDavAuthInfo;
  286. if (savedAuthInfo) {
  287. Logger.info(TAG, 'heanup 认证信息验证成功,账户ID: ' + savedAuthInfo.accountId);
  288. } else {
  289. Logger.error(TAG, 'heanup 认证信息验证失败,webDavAuthInfo为空');
  290. }
  291. // 发送播放事件,类似歌单播放的方式
  292. const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
  293. // 准备WebDAV认证信息用于传递
  294. let authInfoForEvent: WebDavAuthInfo | undefined = undefined;
  295. if (this.selectedAccount && this.selectedAccount.account && this.selectedAccount.password) {
  296. authInfoForEvent = {
  297. accountId: this.selectedAccount.id,
  298. host: this.selectedAccount.host,
  299. port: this.selectedAccount.port,
  300. account: this.selectedAccount.account,
  301. password: this.selectedAccount.password,
  302. enableHttps: this.selectedAccount.enableHttps
  303. };
  304. Logger.info(TAG, 'heanup 将WebDAV认证信息包含在播放事件中');
  305. }
  306. const playlistData: PlaylistEventData = {
  307. playlistId: 'webdav-playlist', // 使用特殊的ID标识WebDAV播放列表
  308. playlistName: 'WebDAV - ' + (this.selectedAccount?.name || '未知账户'),
  309. songCount: this.songs.length,
  310. startIndex: index,
  311. songFilePaths: songFilePaths,
  312. webDavAuthInfo: authInfoForEvent // 直接传递认证信息
  313. };
  314. Logger.info(TAG, 'heanup 准备发送WebDAV播放事件,eventId: ' + eventPlaylistPlay.eventId);
  315. Logger.info(TAG, 'heanup 发送WebDAV播放事件数据: ' + JSON.stringify(playlistData));
  316. const eventData: emitter.EventData = {
  317. data: playlistData
  318. };
  319. emitter.emit(eventPlaylistPlay, eventData);
  320. // 跳转到首页播放器
  321. router.pushUrl({
  322. url: 'pages/NewIndex',
  323. params: {
  324. fromWebDAV: true
  325. }
  326. }).catch((error: Error) => {
  327. Logger.error(TAG, '跳转首页失败: ' + error.message);
  328. this.getUIContext().getPromptAction().showToast({ message: '跳转失败' });
  329. });
  330. } catch (error) {
  331. const err = error as Error;
  332. Logger.error(TAG, '播放歌曲失败: ' + err.message);
  333. this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
  334. }
  335. }
  336. // 导航到指定层级的面包屑路径
  337. private navigateToBreadcrumb(breadcrumbIndex: number): void {
  338. try {
  339. this.isLoading = true;
  340. this.webdavManager.navigateToBreadcrumb(breadcrumbIndex).then((path: string) => {
  341. this.webdavManager.enterFolderFromPath(path)
  342. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  343. })
  344. .catch((error: Error) => {
  345. Logger.error(TAG, '导航到面包屑路径失败: ' + error.message);
  346. this.isLoading = false;
  347. });
  348. } catch (error) {
  349. Logger.error(TAG, '导航到面包屑路径异常: ' + (error as Error).message);
  350. this.isLoading = false;
  351. }
  352. }
  353. build() {
  354. Column() {
  355. // 顶部安全区和标题栏
  356. Column() {
  357. Blank()
  358. .height(this.topRectHeight + 5)
  359. .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
  360. .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  361. // 标题栏
  362. Row() {
  363. Image($r('app.media.menu'))
  364. .width(24)
  365. .height(24)
  366. .margin({ left: 12, right: 8 })
  367. .onClick(() => {
  368. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  369. // this.mType =0
  370. this.isShowDrawer = !this.isShowDrawer
  371. this.offsetX = 0
  372. })
  373. });
  374. Text('WebDAV网盘')
  375. .fontSize(18)
  376. .fontColor(Color.White)
  377. .fontWeight(FontWeight.Medium)
  378. .layoutWeight(1)
  379. .textAlign(TextAlign.Center)
  380. }
  381. .height(48)
  382. .width('100%')
  383. .alignItems(VerticalAlign.Center)
  384. .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
  385. }
  386. // 内容区域
  387. if (this.accounts.length === 0) {
  388. this.buildEmptyView();
  389. } else {
  390. this.buildContentView();
  391. }
  392. }
  393. .width('100%')
  394. .height('100%')
  395. .backgroundColor($r('app.color.start_window_background'))
  396. }
  397. // 空状态视图
  398. @Builder
  399. buildEmptyView() {
  400. Column({ space: 20 }) {
  401. Image($r('app.media.cloudDisk'))
  402. .width(120)
  403. .height(120)
  404. .opacity(0.3)
  405. Text('暂无WebDAV账户')
  406. .fontSize(16)
  407. .fontColor($r('app.color.index_tab_font_color'))
  408. .opacity(0.6)
  409. }
  410. .justifyContent(FlexAlign.Center)
  411. .width('100%')
  412. .layoutWeight(1)
  413. }
  414. // 内容视图
  415. @Builder
  416. buildContentView() {
  417. Column() {
  418. // 加载按钮和面包屑导航
  419. Column({ space: 8 }) {
  420. // 面包屑导航
  421. if (this.webdavManager.currentPath !== '') {
  422. Row({ space: 8 }) {
  423. Button({ type: ButtonType.Circle }) {
  424. Image(this.webdavManager.canGoBack()?$r('app.media.back'):$r('app.media.cloudDisk'))
  425. .width(15)
  426. .height(15)
  427. .fillColor(Color.White)
  428. }
  429. .width(20)
  430. .height(20)
  431. .backgroundColor(this.themeColor)
  432. .onClick(() => this.goBack())
  433. Row({ space: 4 }) {
  434. ForEach(this.breadcrumbs, (crumb: string, index: number) => {
  435. Row() {
  436. Text(crumb)
  437. .fontSize(15)
  438. .fontColor(this.themeColor)
  439. .maxLines(1)
  440. .textOverflow({ overflow: TextOverflow.Ellipsis })
  441. }
  442. .onClick(() => {
  443. this.navigateToBreadcrumb(index);
  444. })
  445. // 添加分隔符(除了最后一个元素)
  446. if (index < this.breadcrumbs.length - 1) {
  447. Text('/')
  448. .fontSize(15)
  449. .fontColor($r('app.color.index_tab_font_color'))
  450. .opacity(0.6)
  451. }
  452. })
  453. }
  454. .layoutWeight(1)
  455. }
  456. .width('100%')
  457. .padding({ left: 4, right: 4 })
  458. }
  459. // 统计信息
  460. if (this.webDavFiles.length > 0) {
  461. Row() {
  462. Text(this.visibleFoldersState.length + ' 个文件夹, ' + this.songs.length + ' 首歌曲')
  463. .fontSize(13)
  464. .fontColor($r('app.color.index_tab_font_color'))
  465. .opacity(0.6)
  466. .layoutWeight(1)
  467. .textAlign(TextAlign.Start)
  468. Blank()
  469. }
  470. .padding({ left: 4, right: 4 })
  471. }
  472. }
  473. .width('100%')
  474. .padding(12)
  475. .margin({ top: 8 })
  476. // 加载状态
  477. Row() {
  478. LoadingProgress()
  479. .width(30)
  480. .height(30)
  481. .color(this.themeColor)
  482. Text('加载中...')
  483. .fontSize(14)
  484. .fontColor($r('app.color.index_tab_font_color'))
  485. .margin({ left: 12 })
  486. }
  487. .padding(20)
  488. .visibility(this.isLoading?Visibility.Visible:Visibility.None)
  489. .opacity(this.isLoading ? 1 : 0)
  490. .animation({
  491. duration: 500,
  492. curve: 'ease-in-out' // 可选动画曲线
  493. })
  494. // 文件列表(文件夹 + 歌曲)
  495. if (this.webDavFiles.length > 0) {
  496. List({ space: 0 }) {
  497. // 显示文件夹 - 只显示当前目录下的直接子文件夹
  498. ForEach(this.visibleFoldersState, (folder: FileInfo) => {
  499. ListItem() {
  500. this.buildFolderItem(folder)
  501. }
  502. })
  503. // 显示歌曲
  504. LazyForEach(this.dataSource, (song: Song, index: number) => {
  505. ListItem() {
  506. this.buildSongItem(song, index)
  507. }
  508. })
  509. }
  510. .layoutWeight(1)
  511. .divider({ strokeWidth: 1, color: '#EEEEEE' })
  512. .margin({ top: 4 })
  513. } else if (!this.isLoading) {
  514. Column() {
  515. Text('暂无内容')
  516. .fontSize(14)
  517. .fontColor($r('app.color.index_tab_font_color'))
  518. .opacity(0.6)
  519. Text('点击"加载文件列表"按钮加载')
  520. .fontSize(12)
  521. .fontColor($r('app.color.index_tab_font_color'))
  522. .opacity(0.4)
  523. .margin({ top: 8 })
  524. }
  525. .justifyContent(FlexAlign.Center)
  526. .layoutWeight(1)
  527. }
  528. }
  529. .layoutWeight(1)
  530. }
  531. // 文件夹列表项
  532. @Builder
  533. buildFolderItem(folder: FileInfo) {
  534. Row({ space: 2 }) {
  535. SymbolGlyph($r('sys.symbol.folder'))
  536. .fontSize(48)
  537. .fontColor([this.themeColor])
  538. .alignSelf(ItemAlign.Center)
  539. .margin({ left: 8, right: 6 })
  540. // 文件夹信息
  541. Column({ space: 4 }) {
  542. Text(decodeUrlEncodedString(folder.fileName.replace('/', '')))
  543. .fontSize(15)
  544. .fontColor($r('app.color.index_tab_font_color'))
  545. .maxLines(1)
  546. .textOverflow({ overflow: TextOverflow.Ellipsis })
  547. Text('文件夹')
  548. .fontSize(13)
  549. .fontColor($r('app.color.index_tab_font_color'))
  550. .opacity(0.6)
  551. }
  552. .alignItems(HorizontalAlign.Start)
  553. .layoutWeight(1)
  554. }
  555. .width('100%')
  556. .padding(12)
  557. .backgroundColor($r('app.color.start_window_background'))
  558. .onClick(() => {
  559. this.enterFolder(folder);
  560. this.breadcrumbs = this.webdavManager.getBreadcrumbs();
  561. })
  562. }
  563. // 歌曲列表项
  564. @Builder
  565. buildSongItem(song: Song, index: number) {
  566. Row({ space: 12 }) {
  567. // 序号
  568. // 歌曲封面
  569. Image(song.img)
  570. .width(48)
  571. .height(48)
  572. .borderRadius(4)
  573. .objectFit(ImageFit.Cover)
  574. // 歌曲信息
  575. Column({ space: 4 }) {
  576. Text(decodeUrlEncodedString(song.title))
  577. .fontSize(15)
  578. .fontColor($r('app.color.index_tab_font_color'))
  579. .maxLines(1)
  580. .textOverflow({ overflow: TextOverflow.Ellipsis })
  581. Text(decodeUrlEncodedString(song.artist))
  582. .fontSize(13)
  583. .fontColor($r('app.color.index_tab_font_color'))
  584. .opacity(0.6)
  585. .maxLines(1)
  586. .textOverflow({ overflow: TextOverflow.Ellipsis })
  587. }
  588. .alignItems(HorizontalAlign.Start)
  589. .layoutWeight(1)
  590. // 播放图标
  591. Image($r('app.media.ic_play'))
  592. .width(20)
  593. .height(20)
  594. .fillColor($r('app.color.index_tab_font_color'))
  595. .opacity(0.4)
  596. }
  597. .width('100%')
  598. .padding(12)
  599. .backgroundColor($r('app.color.start_window_background'))
  600. .onClick(() => {
  601. this.playSong(song, index);
  602. })
  603. }
  604. }