import { DialogHelper } from '@pura/harmony-dialog'; import { Playlist } from '../viewmodel/Playlist'; import { ToastUtil, LogUtil, StrUtil, FileUtil, PreferencesUtil } from '@pura/harmony-utils'; import { VideoItem } from '../viewmodel/VideoItem'; import MediaTable from '../common/util/MediaTable'; import PlaylistTable from '../common/util/PlaylistTable'; import { LazyDataSource } from '../common/util/LazyDataSource'; import { ConfigurationConstant, common } from '@kit.AbilityKit'; import { CommonConstants } from '../common/constants/CommonConstants'; import { SegmentButton } from '@kit.ArkUI'; import { SegmentButtonOptions, SegmentButtonItemTuple } from '@kit.ArkUI'; import { Utility } from '../common/util/Utility'; import fs from '@ohos.file.fs'; // 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体 function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string { if (isDarkMode) { // 深色模式下返回更深的灰色或半透明黑色 return `rgba(34,34,34,${alpha + 0.2 > 1 ? 1 : alpha + 0.2})`; } const color = themeColor.replace('#', ''); const r = parseInt(color.substring(0, 2), 16); const g = parseInt(color.substring(2, 4), 16); const b = parseInt(color.substring(4, 6), 16); return `rgba(${r},${g},${b},${alpha})`; } // 定义SegmentButton按钮元组类型 const viewModeButtons: SegmentButtonItemTuple = [{ text: '全部' }, { text: '目录' }]; /** * 添加歌曲到歌单对话框内容组件 */ @Component struct AddSongsToPlaylistDialogContent { context = this.getUIContext().getHostContext() as common.UIAbilityContext @State opacityItem: number = 1; // 控制透明度的状态变量 @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; @State isDarkMode: boolean = false @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_DARK; onColorModeChange() { this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK } @State selectedSongs: VideoItem[] = [] @State isLoading: boolean = false @State searchText: string = '' @State filteredSongs: VideoItem[] = [] private listScroller: ListScroller = new ListScroller() @State isSearchMode: boolean = false @Prop playlist: Playlist @StorageProp('mediaKuList') mediaKuList: Array = []; //媒体库文件 private mediaTable: MediaTable = new MediaTable(getContext(this)) private playlistTable: PlaylistTable = new PlaylistTable(getContext(this)) @State dataSource: LazyDataSource = new LazyDataSource([]) // 分页相关状态 @State currentPage: number = 0 @State hasMoreData: boolean = true private readonly PAGE_SIZE: number = 50 // 用于存储完整数据的引用 private allSongs: VideoItem[] = [] private baseDownloadPath: string = '' // 回调函数 onConfirm?: (songs: VideoItem[]) => void onCancel?: () => void // 添加目录浏览相关状态 @State @Watch('onViewModeChange') viewMode: number[] = [0]; // 0: 全部歌曲, 1: 目录模式 @State currentPath: string = ''; @State folderItems: VideoItem[] = []; @State selectedFolders: Set = new Set(); @State isFolderLoading: boolean = false; private table: MediaTable = new MediaTable(getContext(this)) // 用于高效判断歌曲选中状态(避免在大列表中频繁遍历) @State totalSelectedCount: number = 0; private selectedSongIds: Set = new Set(); private updateTimer: number = -1; // SegmentButton选项配置 @State viewModeOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({ buttons: viewModeButtons, backgroundColor: $r('app.color.index_background'), selectedBackgroundColor: $r('app.color.start_window_background'), selectedFontColor: $r('app.color.text_color'), buttonPadding: { top: 10, bottom: 10 }, multiply: false }); async aboutToAppear() { // 初始化深色模式状态 this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK LogUtil.info('heanup AddSongsToPlaylistDialogContent aboutToAppear 开始') LogUtil.info('heanup 初始化深色模式状态: ' + this.isDarkMode + ', currentMode: ' + this.currentMode) LogUtil.info('heanup playlist: ' + (this.playlist ? JSON.stringify({ id: this.playlist.id, name: this.playlist.name }) : 'null')) console.info('onecold mediaKuList 对话框 aboutToAppear=' + this.mediaKuList.length) this.allSongs = [...this.mediaKuList] this.filteredSongs = [...this.mediaKuList] this.loadInitialData() this.clearSelection() this.baseDownloadPath = this.normalizeFsPath(PreferencesUtil.getStringSync('download_path', '/storage/Users/currentUser')) this.currentPath = this.baseDownloadPath // 异步加载目录数据 await this.loadFile(this.currentPath) } aboutToDisappear() { // 清理定时器,防止内存泄漏 if (this.updateTimer !== -1) { clearTimeout(this.updateTimer) this.updateTimer = -1 } } /** * 加载初始数据 */ private loadInitialData() { this.currentPage = 0 this.hasMoreData = true this.updateListData(false) } @State fileList: Array = [] async loadFile(curPath: string) { const fsPath = this.normalizeFsPath(curPath) this.isFolderLoading = true this.currentPath = fsPath let directories: Array = [] let files: Array = [] const normalizedCurrent = this.normalizeLocalPath(fsPath) try { const entries: Array = fs.listFileSync(fsPath) as Array this.fileList = entries for (let i = 0; i < entries.length; i++) { const name = entries[i] if (name.startsWith('.')) { continue } const fullPath = `${fsPath}/${name}` try { const stat: fs.Stat = fs.statSync(fullPath) as fs.Stat if (stat.isDirectory()) { directories.push(new VideoItem(name, name, fullPath, CommonConstants.TYPE_IS_DIR, 0, '')) } else { // 仅显示音频文件 if (!Utility.isMeidaByExtension(fullPath)) { continue } const normalized = this.normalizeLocalPath(fullPath) const matched = this.allSongs.find((item: VideoItem) => this.normalizeLocalPath(item.filePath) === normalized) if (matched) { files.push(matched) } else { // 构造最小信息的 VideoItem 兜底显示 const fallback = new VideoItem(name, normalized, fullPath, CommonConstants.TYPE_LOCAL, stat.size, '') fallback.fileName = name fallback.parentPath = fsPath files.push(fallback) } } } catch (error) { LogUtil.warn('heanup AddSongsToPlaylistDialog', `无法访问: ${fullPath}`) } } // 目录优先,其次按名称排序 this.folderItems = directories.concat(files).sort((a: VideoItem, b: VideoItem) => { if (a.type === CommonConstants.TYPE_IS_DIR && b.type !== CommonConstants.TYPE_IS_DIR) { return -1 } if (a.type !== CommonConstants.TYPE_IS_DIR && b.type === CommonConstants.TYPE_IS_DIR) { return 1 } return a.name.localeCompare(b.name) }) } catch (error) { LogUtil.error('heanup AddSongsToPlaylistDialog', `加载文件列表失败: ${(error as Error).message}`) this.fileList = [] this.folderItems = directories } finally { this.isFolderLoading = false } } /** * 用于文件系统访问的路径清洗:去掉多余 //,保留单个前导 / */ private normalizeFsPath(path: string): string { if (!path) { return '' } let cleaned = path.replace('file://', '').replace('file://docs', '').replace('docs://', '') cleaned = cleaned.replace(/\/{2,}/g, '/') if (!cleaned.startsWith('/')) { cleaned = '/' + cleaned } if (cleaned.endsWith('/') && cleaned.length > 1) { cleaned = cleaned.slice(0, -1) } return cleaned } /** * 安全获取 parentPath,避免因异常抛出导致目录加载中断 */ private safeGetParentPath(item: VideoItem): string { if (item.parentPath) { return item.parentPath } try { return FileUtil.getParentPath(item.filePath) } catch (error) { const idx = item.filePath?.lastIndexOf('/') ?? -1 if (idx > 0) { return item.filePath.substring(0, idx) } LogUtil.warn('heanup AddSongsToPlaylistDialog', `fallback parentPath for ${item.filePath}: ${(error as Error).message}`) return '' } } private querySongsByParentPath(path: string): Promise { return new Promise((resolve) => { try { this.table.queryByParentPath(path, (result: VideoItem[]) => resolve(result)) } catch (error) { LogUtil.error('heanup AddSongsToPlaylistDialog', `queryByParentPath 异常: ${(error as Error).message}`) resolve([]) } }) } onViewModeChange() { if (this.viewMode && this.viewMode[0] === 1) { this.loadFile(this.currentPath) } else { this.updateSelectedCount() } } private enterFolder(path: string) { this.loadFile(path) } private normalizeLocalPath(path: string): string { if (!path) { return '' } let normalized = path.replace('file://docs', '').replace('file://', '') if (normalized.startsWith('/docs/')) { normalized = normalized.replace('/docs', '') } else if (normalized.startsWith('docs/')) { normalized = normalized.substring(4) if (!normalized.startsWith('/')) { normalized = '/' + normalized } } if (!normalized.startsWith('/')) { normalized = '/' + normalized } if (normalized.endsWith('/') && normalized.length > 1) { normalized = normalized.slice(0, -1) } return normalized } private navigateToParent() { if (!this.currentPath) { return } // 限制在下载目录之下 if (this.baseDownloadPath && this.normalizeFsPath(this.currentPath) === this.baseDownloadPath) { LogUtil.info('heanup AddSongsToPlaylistDialog', 'navigateToParent blocked at baseDownloadPath') return } const idx = this.currentPath.lastIndexOf('/') if (idx <= 0) { return } const parent = this.currentPath.substring(0, idx) if (!parent || parent === this.currentPath) { return } this.loadFile(parent) } private canNavigateUp(): boolean { if (!this.currentPath) { return false } const current = this.normalizeFsPath(this.currentPath) if (!this.baseDownloadPath) { return current.lastIndexOf('/') > 0 } return current.startsWith(this.baseDownloadPath) && current !== this.baseDownloadPath } private toggleSongSelection(song: VideoItem) { if (song.type === CommonConstants.TYPE_IS_DIR) { this.toggleFolderSelection(song.filePath) return } const exists = this.selectedSongIds.has(song.id) if (exists) { this.selectedSongs = this.selectedSongs.filter((s: VideoItem) => s.id !== song.id) this.selectedSongIds.delete(song.id) } else { this.selectedSongs = [...this.selectedSongs, song] this.selectedSongIds.add(song.id) } this.updateSelectedCount() } private toggleFolderSelection(folderPath: string) { const normalized = this.normalizeLocalPath(folderPath) const next = new Set(this.selectedFolders) if (next.has(normalized)) { next.delete(normalized) } else { next.add(normalized) } LogUtil.info('heanup AddSongsToPlaylistDialog', `toggleFolderSelection folder=${normalized}, now=${Array.from(next).join(',')}`) this.selectedFolders = next this.updateSelectedCount() } private clearSelection() { this.selectedSongs = [] this.selectedFolders = new Set() this.selectedSongIds.clear() this.totalSelectedCount = 0 } private mergeSongs(primary: VideoItem[], extra: VideoItem[]): VideoItem[] { const map = new Map() primary.forEach((item: VideoItem) => { map.set(this.normalizeLocalPath(item.filePath), item) }) extra.forEach((item: VideoItem) => { const key = this.normalizeLocalPath(item.filePath) if (!map.has(key)) { map.set(key, item) } }) return Array.from(map.values()) } private safeParentFromPath(path: string): string { if (!path) { return '' } try { return FileUtil.getParentPath(path) } catch (error) { const idx = path.lastIndexOf('/') return idx > 0 ? path.substring(0, idx) : '' } } private getSongsFromFolders(folderPaths: string[]): VideoItem[] { if (!folderPaths || folderPaths.length === 0) { LogUtil.info('heanup AddSongsToPlaylistDialog', 'getSongsFromFolders empty folderPaths') return [] } try { // 使用 Set 优化查找性能,并统一路径格式 const normalizedSet = new Set(folderPaths.map((path: string) => this.normalizeLocalPath(path))) const result: VideoItem[] = [] // 单次遍历,避免嵌套循环 for (let i = 0; i < this.allSongs.length; i++) { const item = this.allSongs[i] if (!item || item.type === CommonConstants.TYPE_IS_DIR || !item.filePath) { continue } const parentPath = this.normalizeLocalPath(item.parentPath || this.safeParentFromPath(item.filePath)) const filePath = this.normalizeLocalPath(item.filePath) // 直接使用 Set 查找,O(1) 时间复杂度 if (normalizedSet.has(parentPath)) { result.push(item) continue } // 检查是否在子目录中(这部分仍需遍历,但通常文件夹数量不多) for (const folder of normalizedSet) { if (filePath.startsWith(folder + '/')) { result.push(item) break } } } // 额外兜底:将当前目录列表中展示的文件也纳入(防止媒体表尚未入库时无法计数) this.folderItems.forEach((item: VideoItem) => { if (!item || item.type === CommonConstants.TYPE_IS_DIR) { return } const parentPath = this.normalizeLocalPath(item.parentPath || this.safeParentFromPath(item.filePath)) const filePath = this.normalizeLocalPath(item.filePath) if (normalizedSet.has(parentPath) || Array.from(normalizedSet).some(folder => filePath.startsWith(folder + '/'))) { result.push(item) } }) LogUtil.info('heanup AddSongsToPlaylistDialog', `getSongsFromFolders folders=${Array.from(normalizedSet).join(',')}, result=${result.length}`) return result } catch (error) { LogUtil.error('heanup AddSongsToPlaylistDialog', `getSongsFromFolders error: ${(error as Error).message}`) return [] } } private updateSelectedCount() { // 清除之前的定时器,实现防抖 if (this.updateTimer !== -1) { clearTimeout(this.updateTimer) } // 延迟执行计算,避免阻塞主线程 this.updateTimer = setTimeout(() => { try { const folderSongs = this.getSongsFromFolders(Array.from(this.selectedFolders)) const merged = this.mergeSongs( this.selectedSongs.filter((item: VideoItem) => item.type !== CommonConstants.TYPE_IS_DIR), folderSongs ) this.totalSelectedCount = merged.length LogUtil.info('heanup AddSongsToPlaylistDialog', `updateSelectedCount folders=${Array.from(this.selectedFolders).join(',')}, folderSongs=${folderSongs.length}, directSongs=${this.selectedSongs.length}, total=${this.totalSelectedCount}`) } catch (error) { LogUtil.error('heanup AddSongsToPlaylistDialog', `更新选中数量失败: ${(error as Error).message}`) // 发生错误时至少显示已选歌曲数量 this.totalSelectedCount = this.selectedSongs.filter( (item: VideoItem) => item.type !== CommonConstants.TYPE_IS_DIR ).length } this.updateTimer = -1 }, 50) } /** * 异步全选歌曲,分批处理避免阻塞主线程 */ private selectAllSongsAsync() { const allSongs = this.filteredSongs && this.filteredSongs.length > 0 ? this.filteredSongs : this.allSongs LogUtil.info('heanup AddSongsToPlaylistDialog', `selectAllSongsAsync start, total=${allSongs.length}`) const BATCH_SIZE = 500 // 单批进一步增大,减少全选等待时间 let currentIndex = 0 const tempSelected: VideoItem[] = [...this.selectedSongs] // 保留已选歌曲 const tempIds = new Set(this.selectedSongIds) const processBatch = () => { const endIndex = Math.min(currentIndex + BATCH_SIZE, allSongs.length) const batch = allSongs.slice(currentIndex, endIndex) // 批量添加到临时列表和 Set batch.forEach((song: VideoItem) => { if (!tempIds.has(song.id)) { tempSelected.push(song) tempIds.add(song.id) } }) // 重新赋值触发响应式更新 this.selectedSongs = [...tempSelected] this.selectedSongIds = new Set(tempIds) currentIndex = endIndex if (currentIndex < allSongs.length) { // 还有更多数据,继续处理下一批 setTimeout(processBatch, 0) // 使用 setTimeout 让出主线程 } else { // 全部处理完成,更新计数 this.updateSelectedCount() LogUtil.info('AddSongsToPlaylistDialog', `全选完成,共选中 ${this.selectedSongs.length} 首歌曲`) } } // 开始第一批处理 processBatch() } /** * 搜索过滤歌曲 */ filterSongs() { this.isSearchMode = true if (!this.searchText.trim()) { this.filteredSongs = [...this.allSongs] } else { // 添加空值检查以防止TypeError if (!this.allSongs || !Array.isArray(this.allSongs)) { this.filteredSongs = [] } else { this.filteredSongs = this.allSongs.filter(item => { //支持模糊匹配和艺术家 专辑匹配 const regex = new RegExp(this.searchText.replace(/\s+/g, '.*'), 'i'); return regex.test(item.name.toLowerCase()) || regex.test(item.fileName?.toLowerCase() ?? "") || regex.test(item.artist?.toLowerCase() ?? "") || regex.test(item.album?.toLowerCase() ?? "") }) } } // 重置分页状态并重新加载数据 this.loadInitialData() } // 修改 updateListData 方法,使其更清晰 updateListData(append: boolean = false) { this.getUIContext().animateTo({ duration: 666 }, () => { this.opacityItem = 0; }) setTimeout(() => { const sourceData = this.filteredSongs let dataToShow: VideoItem[] = [] if (append) { // 追加数据模式 const currentData = this.dataSource.dataArray const startIndex = this.currentPage * this.PAGE_SIZE const endIndex = Math.min(startIndex + this.PAGE_SIZE, sourceData.length) const newData = sourceData.slice(startIndex, endIndex) if (newData.length > 0) { dataToShow = [...currentData, ...newData] this.currentPage++ } } else { // 初始加载模式 this.currentPage = 1 const endIndex = Math.min(this.PAGE_SIZE, sourceData.length) dataToShow = sourceData.slice(0, endIndex) } // 更新数据源 this.dataSource.pushArrayData(dataToShow) // 检查是否还有更多数据 const totalLoaded = dataToShow.length this.hasMoreData = totalLoaded < sourceData.length this.getUIContext().animateTo({ duration: 666 }, () => { this.opacityItem = 1 }) }, 200) } loadMoreData() { console.info('loadMoreData called, hasMoreData:', this.hasMoreData, 'isLoading:', this.isLoading) if (!this.hasMoreData || this.isLoading) { console.info('loadMoreData skipped - no more data or already loading') return } console.info('loadMoreData executing') this.isLoading = true // 使用 setTimeout 模拟异步加载 setTimeout(() => { this.updateListData(true) // append mode this.isLoading = false console.info('loadMoreData completed, current data length:', this.dataSource.dataArray.length) }, 50) } build() { Column({ space: 16 }) { // 搜索框 Row({ space: 8 }) { Image($r('app.media.ic_action_search')) .width(20) .height(20) .fillColor(this.isDarkMode ? '#8E8E93' : $r('app.color.text_color')) .opacity(0.6) TextInput({ placeholder: '搜索歌曲、歌手或专辑', text: this.searchText }) .layoutWeight(1) .height(35) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .borderRadius(8) .padding({ left: 8, right: 8 }) .fontSize(14) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .placeholderColor(this.isDarkMode ? '#8E8E93' : '#999999') .onChange((value: string) => { this.searchText = value this.filterSongs() }) } .width('100%') .padding(12) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .borderRadius(20) // 视图模式切换 SegmentButton SegmentButton({ options: this.viewModeOptions, selectedIndexes: $viewMode }) .width('100%') .margin({ top: 8 }) // 已选择歌曲数量(包含目录展开后的歌曲) if (this.totalSelectedCount > 0) { Row() { Text(`已选择 ${this.totalSelectedCount} 首歌曲`) .fontSize(14) .fontColor(this.isDarkMode ? '#FFFFFF' : this.themeColor) .fontWeight(FontWeight.Medium) Blank() if (this.viewMode[0] === 0) { Button((this.selectedSongs.length === this.dataSource.dataArray.length && this.dataSource.dataArray.length > 0) ? '全不选' : '全选') .fontSize(12) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .backgroundColor(Color.Transparent) .height(30) .padding({ left: 8, right: 8 }) .onClick(() => { if (this.selectedSongs.length === this.dataSource.dataArray.length && this.dataSource.dataArray.length > 0) { // 全不选:清空所有选择 this.selectedSongs = [] this.selectedSongIds.clear() this.updateSelectedCount() } else { // 全选:异步分批处理,避免阻塞主线程 this.selectAllSongsAsync() } }) } Button('清空') .fontSize(12) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .backgroundColor(Color.Transparent) .height(30) .padding({ left: 8, right: 8 }) .onClick(() => { this.clearSelection() }) } .width('100%') .padding({ left: 4, right: 4 }) } // 歌曲列表或目录列表 if (this.viewMode[0] === 0) { // 全部歌曲模式 if (this.dataSource.dataArray.length === 0 && this.isSearchMode) { Column({ space: 12 }) { Image($r('app.media.music_red')) .width(64) .height(64) .opacity(0.3) Text(this.searchText ? '没有找到匹配的歌曲' : '没有可添加的歌曲') .fontSize(14) .fontColor(this.isDarkMode ? '#8E8E93' : '#999999') } .width('100%') .height(300) .justifyContent(FlexAlign.Center) } else { this.getListView() } } else { // 目录模式 this.getFolderView() } // 按钮区域 Row({ space: 12 }) { Button('取消') .width('45%') .height(40) .borderRadius(8) .fontSize(14) .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode) :$r('app.color.cancel_button_background') ) .fontColor($r('app.color.cancel_button_text')) .onClick(() => { this.onCancel?.() DialogHelper.closeDialog('addSongsToPlaylistDialog') }) Button(`添加${this.totalSelectedCount > 0 ? `(${this.totalSelectedCount})` : ''}`) .width('45%') .height(40) .backgroundColor(this.isDarkMode ? $r('app.color.silvery'):this.themeColor) .borderRadius(8) .fontSize(14) .fontColor(Color.White) .enabled(this.totalSelectedCount > 0) .opacity(this.totalSelectedCount > 0 ? 1 : 0.5) .onClick(() => { this.handleConfirm() }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .margin({ top: 10, bottom: 10 }) } .width('100%') .constraintSize({ maxWidth: 400 }) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.dialog_background')) .borderRadius(12) .padding({ left: 20, right: 20 }) } /** * 处理确认操作 */ private handleConfirm() { const folderSongs = this.getSongsFromFolders(Array.from(this.selectedFolders)) const submitSongs = this.mergeSongs( this.selectedSongs.filter((item: VideoItem) => item.type !== CommonConstants.TYPE_IS_DIR), folderSongs ) LogUtil.info('heanup AddSongsToPlaylistDialog', `handleConfirm selectedFolders=${Array.from(this.selectedFolders).join(',')}, folderSongs=${folderSongs.length}, directSongs=${this.selectedSongs.length}, submit=${submitSongs.length}`) if (submitSongs.length === 0) { ToastUtil.showToast(' 请选择至少一首歌曲') return } this.onConfirm?.(submitSongs) DialogHelper.closeDialog('addSongsToPlaylistDialog') } @Builder getListView() { Scroll() { Column({ space: 0 }) { List({ scroller: this.listScroller }) { LazyForEach(this.dataSource, (song: VideoItem, index: number) => { ListItem() { Row({ space: 12 }) { Image(StrUtil.isEmpty(song.pixelMapPath) ? $r('app.media.music_red') : song.pixelMapPath) .fillColor(StrUtil.isEmpty(song.pixelMapPath) ? (this.isDarkMode ? Color.White : this.themeColor) : undefined) .height(40) .width(40) .alt($r('app.media.music_red')) .borderRadius('100%') .clip(true) .draggable(false) .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿 .autoResize(true) // 重采样,可减少内存占用 .opacity(this.opacityItem) // 绑定透明度 // 歌曲信息 Column({ space: 4 }) { Text(song.name || '未知歌曲') .fontSize(14) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') Row() { if (song.artist) { Text(song.artist+' '+song.duration) .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#999999') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) } } .width('100%') } .alignItems(HorizontalAlign.Start) .layoutWeight(1) // 选择框 Checkbox({ name: 'song_' + song.id }) .select(this.selectedSongIds.has(song.id)) .selectedColor($r('app.color.theme_color')) .shape(CheckBoxShape.ROUNDED_SQUARE) .onChange(() => { this.toggleSongSelection(song) }) } .width('100%') .padding(12) .borderRadius(10) .onClick(() => { this.toggleSongSelection(song) }) } .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }) .animation({ duration: 500 }), TransitionEffect.scale({ x: 0, y: 0 }))) .clickEffect({ level: ClickEffectLevel.LIGHT }) }, (song: VideoItem) => song.id) // 添加 footer 来显示加载状态 ListItem() { this.footer() } .visibility(this.hasMoreData ? Visibility.Visible : Visibility.None) } .cachedCount(6) .height('100%') .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true }) .onReachEnd(() => { // 滚动到底部时加载更多数据 console.info('List onReachEnd triggered') this.loadMoreData() }) } } .scrollBar(BarState.Auto) .scrollable(ScrollDirection.Vertical) .height(300) } @Builder getFolderView() { Column({ space: 12 }) { Row({ space: 8 }) { Text('目录视图') .fontSize(16) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) Blank() Button('上一级') .fontSize(12) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .height(28) .padding({ left: 10, right: 10 }) .backgroundColor(Color.Transparent) .border({ width: 1, color: this.isDarkMode ? '#444444' : '#dddddd' }) .borderRadius(14) .enabled(this.canNavigateUp()) .opacity(this.canNavigateUp() ? 1 : 0.4) .onClick(() => { this.navigateToParent() }) Button('刷新') .fontSize(12) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .height(28) .padding({ left: 10, right: 10 }) .backgroundColor(Color.Transparent) .border({ width: 1, color: this.isDarkMode ? '#444444' : '#dddddd' }) .borderRadius(14) .onClick(() => { this.loadFile(this.currentPath) }) } .width('100%') Text(this.normalizeLocalPath(this.currentPath) || '未选择路径') .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#666666') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .padding({ left: 4, right: 4 }) if (this.isFolderLoading) { Row() { LoadingProgress() .width(20) .height(20) Text('正在加载目录...') .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#666666') .margin({ left: 8 }) } .width('100%') .height(80) .justifyContent(FlexAlign.Center) } else { Scroll() { Column() { ForEach(this.folderItems, (item: VideoItem, index: number) => { Row() { Text(item.type === CommonConstants.TYPE_IS_DIR ? '📁' : '🎵') .fontSize(20) .margin({ right: 6 }) Column({ space: 2 }) { Text(item.name || '未知歌曲') .fontSize(14) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (item.artist && item.type !== CommonConstants.TYPE_IS_DIR) { Text(item.artist + ' ' + (item.duration || '')) .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#999999') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } else if (item.type === CommonConstants.TYPE_IS_DIR) { Text('包含子目录内的所有音乐将一起添加') .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#999999') .maxLines(1) } } .layoutWeight(1) .margin({ left: 8 }) if (item.type === CommonConstants.TYPE_IS_DIR) { Button() { Text('进入') .fontSize(12) .fontColor(this.isDarkMode ? '#FFFFFF' : this.themeColor) } .height(30) .padding({ left: 10, right: 10 }) .backgroundColor(this.isDarkMode ? '#3a3a3c' : '#f1f1f1') .borderRadius(8) .margin({ right: 8 }) .onClick(() => { this.enterFolder(item.filePath) }) } Checkbox({ name: 'folder_item_' + item.id }) .select((item.type === CommonConstants.TYPE_IS_DIR) ? this.selectedFolders.has(this.normalizeLocalPath(item.filePath)) : this.selectedSongIds.has(item.id)) .selectedColor($r('app.color.theme_color')) .onChange(() => { if (item.type === CommonConstants.TYPE_IS_DIR) { this.toggleFolderSelection(item.filePath) } else { this.toggleSongSelection(item) } }) } .width('100%') .padding(12) .backgroundColor(this.isDarkMode ? '#2C2C2E' : '#F8F8F8') .borderRadius(8) .margin({ bottom: 4 }) .onClick(() => { if (item.type === CommonConstants.TYPE_IS_DIR) { this.toggleFolderSelection(item.filePath) } else { this.toggleSongSelection(item) } }) }, (item: VideoItem) => item.id) } } .height(300) } } .width('100%') } // 改进的 footer Builder @Builder footer() { Column() { if (this.isLoading) { Row() { LoadingProgress() .height(20) .width(20) Text('加载中...') .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#999999') .margin({ left: 8 }) } .width('100%') .height(40) .justifyContent(FlexAlign.Center) } else if (this.hasMoreData) { Row() { Text('上拉加载更多') .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#999999') } .width('100%') .height(40) .justifyContent(FlexAlign.Center) } } .width('100%') } } /** * 添加歌曲到歌单对话框管理器 */ @Component export struct AddSongsToPlaylistDialogManager { /** * 添加歌曲到歌单对话框构建器 */ @Builder buildAddSongsToPlaylistDialog( playlist: Playlist, onConfirm: (songs: VideoItem[]) => void, onCancel?: () => void, ) { AddSongsToPlaylistDialogContent({ playlist: playlist, onConfirm: onConfirm, onCancel: onCancel }) } /** * 显示添加歌曲到歌单对话框 */ showAddSongsToPlaylistDialog( playlist: Playlist, onConfirm: (songs: VideoItem[]) => void, onCancel?: () => void, ) { DialogHelper.showCustomContentDialog({ dialogId: 'addSongsToPlaylistDialog', title: '添加歌曲到歌单', autoCancel: true, contentBuilder: () => { this.buildAddSongsToPlaylistDialog(playlist, onConfirm,onCancel) }, buttons: [] }) } build() { } } // 创建全局实例 const dialogManager = new AddSongsToPlaylistDialogManager() /** * 显示添加歌曲到歌单对话框 */ export function showAddSongsToPlaylistDialog( playlist: Playlist, onConfirm: (songs: VideoItem[]) => void, onCancel?: () => void, ) { dialogManager.showAddSongsToPlaylistDialog(playlist, onConfirm, onCancel) }