|
|
@@ -10,6 +10,7 @@ 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 {
|
|
|
@@ -59,17 +60,22 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
private readonly PAGE_SIZE: number = 50
|
|
|
// 用于存储完整数据的引用
|
|
|
private allSongs: VideoItem[] = []
|
|
|
+ private baseDownloadPath: string = ''
|
|
|
// 回调函数
|
|
|
onConfirm?: (songs: VideoItem[]) => void
|
|
|
onCancel?: () => void
|
|
|
|
|
|
// 添加目录浏览相关状态
|
|
|
- @State viewMode: number[] = [0]; // 0: 全部歌曲, 1: 目录模式
|
|
|
+ @State @Watch('onViewModeChange') viewMode: number[] = [0]; // 0: 全部歌曲, 1: 目录模式
|
|
|
@State currentPath: string = '';
|
|
|
@State folderItems: VideoItem[] = [];
|
|
|
@State selectedFolders: Set<string> = new Set();
|
|
|
@State isFolderLoading: boolean = false;
|
|
|
private table: MediaTable = new MediaTable(getContext(this))
|
|
|
+ // 用于高效判断歌曲选中状态(避免在大列表中频繁遍历)
|
|
|
+ @State totalSelectedCount: number = 0;
|
|
|
+ private selectedSongIds: Set<string> = new Set();
|
|
|
+ private updateTimer: number = -1;
|
|
|
// SegmentButton选项配置
|
|
|
@State viewModeOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
|
|
|
buttons: viewModeButtons,
|
|
|
@@ -80,7 +86,7 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
multiply: false
|
|
|
});
|
|
|
|
|
|
- aboutToAppear() {
|
|
|
+ async aboutToAppear() {
|
|
|
// 初始化深色模式状态
|
|
|
this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
|
|
|
LogUtil.info('heanup AddSongsToPlaylistDialogContent aboutToAppear 开始')
|
|
|
@@ -91,8 +97,19 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
this.allSongs = [...this.mediaKuList]
|
|
|
this.filteredSongs = [...this.mediaKuList]
|
|
|
this.loadInitialData()
|
|
|
- this.currentPath = PreferencesUtil.getStringSync('download_path','/storage/Users/currentUser');
|
|
|
- this.loadFile(this.currentPath)
|
|
|
+ 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
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -105,30 +122,370 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
}
|
|
|
@State fileList: Array<string> = []
|
|
|
|
|
|
- loadFile(curPath: string,){
|
|
|
- this.fileList = FileUtil.listFileSync(curPath);
|
|
|
+ async loadFile(curPath: string) {
|
|
|
+ const fsPath = this.normalizeFsPath(curPath)
|
|
|
+ this.isFolderLoading = true
|
|
|
+ this.currentPath = fsPath
|
|
|
+ let directories: Array<VideoItem> = []
|
|
|
+ let files: Array<VideoItem> = []
|
|
|
+ const normalizedCurrent = this.normalizeLocalPath(fsPath)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const entries: Array<string> = fs.listFileSync(fsPath) as Array<string>
|
|
|
+ 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<VideoItem[]> {
|
|
|
+ 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()
|
|
|
+ }
|
|
|
|
|
|
- let directories: Array<VideoItem> = [];
|
|
|
- let files: Array<VideoItem> = [];
|
|
|
+ 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
|
|
|
+ }
|
|
|
|
|
|
- for (let i = 0; i < this.fileList.length; i++) {
|
|
|
- console.info(`The name of file: ${this.fileList[i]}`);
|
|
|
- let path = curPath + '/' + this.fileList[i]
|
|
|
- if (FileUtil.isDirectory(path)) {
|
|
|
- let item: VideoItem =
|
|
|
- new VideoItem(this.fileList[i].toString(), this.fileList[i].toString(), path, CommonConstants.TYPE_IS_DIR, 0,
|
|
|
- '')
|
|
|
- directories.push(item);
|
|
|
+ private mergeSongs(primary: VideoItem[], extra: VideoItem[]): VideoItem[] {
|
|
|
+ const map = new Map<string, VideoItem>()
|
|
|
+ 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) : ''
|
|
|
}
|
|
|
- this.table.queryByParentPath(curPath, async (result: VideoItem[]) => {
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- files = result;
|
|
|
- this.folderItems = directories.concat(files);
|
|
|
+ // 额外兜底:将当前目录列表中展示的文件也纳入(防止媒体表尚未入库时无法计数)
|
|
|
+ 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()
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -254,33 +611,37 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
.width('100%')
|
|
|
.margin({ top: 8 })
|
|
|
|
|
|
- // 已选择歌曲数量
|
|
|
- if (this.selectedSongs.length > 0) {
|
|
|
+ // 已选择歌曲数量(包含目录展开后的歌曲)
|
|
|
+ if (this.totalSelectedCount > 0) {
|
|
|
Row() {
|
|
|
- Text(`已选择 ${this.selectedSongs.length} 首歌曲`)
|
|
|
+ Text(`已选择 ${this.totalSelectedCount} 首歌曲`)
|
|
|
.fontSize(14)
|
|
|
.fontColor(this.isDarkMode ? '#FFFFFF' : this.themeColor)
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
- 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 = []
|
|
|
- } else {
|
|
|
- // 如果没有全选,则选择当前显示的所有歌曲
|
|
|
- this.selectedSongs = [...this.dataSource.dataArray]
|
|
|
- }
|
|
|
- })
|
|
|
+ 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)
|
|
|
@@ -289,7 +650,7 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
.height(30)
|
|
|
.padding({ left: 8, right: 8 })
|
|
|
.onClick(() => {
|
|
|
- this.selectedSongs = []
|
|
|
+ this.clearSelection()
|
|
|
})
|
|
|
}
|
|
|
.width('100%')
|
|
|
@@ -336,15 +697,15 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
DialogHelper.closeDialog('addSongsToPlaylistDialog')
|
|
|
})
|
|
|
|
|
|
- Button(`添加${this.selectedSongs.length > 0 ? `(${this.selectedSongs.length})` : ''}`)
|
|
|
+ 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.selectedSongs.length > 0)
|
|
|
- .opacity(this.selectedSongs.length > 0 ? 1 : 0.5)
|
|
|
+ .enabled(this.totalSelectedCount > 0)
|
|
|
+ .opacity(this.totalSelectedCount > 0 ? 1 : 0.5)
|
|
|
.onClick(() => {
|
|
|
this.handleConfirm()
|
|
|
})
|
|
|
@@ -364,12 +725,18 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
* 处理确认操作
|
|
|
*/
|
|
|
private handleConfirm() {
|
|
|
- if (this.selectedSongs.length === 0) {
|
|
|
+ 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?.(this.selectedSongs)
|
|
|
+ this.onConfirm?.(submitSongs)
|
|
|
DialogHelper.closeDialog('addSongsToPlaylistDialog')
|
|
|
}
|
|
|
|
|
|
@@ -420,35 +787,18 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
|
|
|
// 选择框
|
|
|
Checkbox({ name: 'song_' + song.id })
|
|
|
- .select(this.selectedSongs.some(s => s.id === song.id))
|
|
|
+ .select(this.selectedSongIds.has(song.id))
|
|
|
.selectedColor($r('app.color.theme_color'))
|
|
|
.shape(CheckBoxShape.ROUNDED_SQUARE)
|
|
|
- .onChange((checked: boolean) => {
|
|
|
- if (checked) {
|
|
|
- if (!this.selectedSongs.some(s => s.id === song.id)) {
|
|
|
- this.selectedSongs.push(song)
|
|
|
- }
|
|
|
- } else {
|
|
|
- const index = this.selectedSongs.findIndex(s => s.id === song.id)
|
|
|
- if (index > -1) {
|
|
|
- this.selectedSongs.splice(index, 1)
|
|
|
- }
|
|
|
- }
|
|
|
+ .onChange(() => {
|
|
|
+ this.toggleSongSelection(song)
|
|
|
})
|
|
|
}
|
|
|
.width('100%')
|
|
|
.padding(12)
|
|
|
.borderRadius(10)
|
|
|
.onClick(() => {
|
|
|
- const isSelected = this.selectedSongs.some(s => s.id === song.id)
|
|
|
- if (isSelected) {
|
|
|
- const index = this.selectedSongs.findIndex(s => s.id === song.id)
|
|
|
- if (index > -1) {
|
|
|
- this.selectedSongs.splice(index, 1)
|
|
|
- }
|
|
|
- } else {
|
|
|
- this.selectedSongs.push(song)
|
|
|
- }
|
|
|
+ this.toggleSongSelection(song)
|
|
|
})
|
|
|
}
|
|
|
.transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 })
|
|
|
@@ -484,75 +834,136 @@ struct AddSongsToPlaylistDialogContent {
|
|
|
@Builder
|
|
|
getFolderView() {
|
|
|
Column({ space: 12 }) {
|
|
|
- Text('目录视图')
|
|
|
- .fontSize(16)
|
|
|
- .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
|
|
|
-
|
|
|
- // 这里将实现目录视图的布局
|
|
|
- Scroll() {
|
|
|
- Column() {
|
|
|
- ForEach(this.folderItems, (item: VideoItem, index: number) => {
|
|
|
- Row() {
|
|
|
- // 根据是否有parentPath判断是目录还是文件
|
|
|
- Text(item.parentPath ? '📁' : '🎵')
|
|
|
- .fontSize(20)
|
|
|
-
|
|
|
- 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) {
|
|
|
- Text(item.artist + ' ' + (item.duration || ''))
|
|
|
- .fontSize(12)
|
|
|
- .fontColor(this.isDarkMode ? '#8E8E93' : '#999999')
|
|
|
+ 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 })
|
|
|
-
|
|
|
- // 显示选择框
|
|
|
- Checkbox({ name: 'folder_item_' + item.id })
|
|
|
- .select(this.selectedSongs.some((s: VideoItem) => s.id === item.id))
|
|
|
- .selectedColor($r('app.color.theme_color'))
|
|
|
- .onChange((checked: boolean) => {
|
|
|
- if (checked) {
|
|
|
- if (!this.selectedSongs.some((s: VideoItem) => s.id === item.id)) {
|
|
|
- this.selectedSongs.push(item);
|
|
|
- }
|
|
|
- } else {
|
|
|
- const index: number = this.selectedSongs.findIndex((s: VideoItem) => s.id === item.id);
|
|
|
- if (index > -1) {
|
|
|
- this.selectedSongs.splice(index, 1);
|
|
|
- }
|
|
|
+ .layoutWeight(1)
|
|
|
+ .margin({ left: 8 })
|
|
|
+
|
|
|
+ if (item.type === CommonConstants.TYPE_IS_DIR) {
|
|
|
+ Button() {
|
|
|
+ Text('进入')
|
|
|
+ .fontSize(12)
|
|
|
+ .fontColor(this.isDarkMode ? '#FFFFFF' : this.themeColor)
|
|
|
}
|
|
|
- })
|
|
|
- }
|
|
|
- .width('100%')
|
|
|
- .padding(12)
|
|
|
- .backgroundColor(this.isDarkMode ? '#2C2C2E' : '#F8F8F8')
|
|
|
- .borderRadius(8)
|
|
|
- .margin({ bottom: 4 })
|
|
|
- .onClick(() => {
|
|
|
- const isSelected: boolean = this.selectedSongs.some((s: VideoItem) => s.id === item.id);
|
|
|
- if (isSelected) {
|
|
|
- const index: number = this.selectedSongs.findIndex((s: VideoItem) => s.id === item.id);
|
|
|
- if (index > -1) {
|
|
|
- this.selectedSongs.splice(index, 1);
|
|
|
+ .height(30)
|
|
|
+ .padding({ left: 10, right: 10 })
|
|
|
+ .backgroundColor(this.isDarkMode ? '#3a3a3c' : '#f1f1f1')
|
|
|
+ .borderRadius(8)
|
|
|
+ .margin({ right: 8 })
|
|
|
+ .onClick(() => {
|
|
|
+ this.enterFolder(item.filePath)
|
|
|
+ })
|
|
|
}
|
|
|
- } else {
|
|
|
- this.selectedSongs.push(item);
|
|
|
+
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
- })
|
|
|
- }, (item: VideoItem) => item.id)
|
|
|
+ .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)
|
|
|
}
|
|
|
- .height(300)
|
|
|
}
|
|
|
.width('100%')
|
|
|
}
|