| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064 |
- 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<VideoItem> = []; //媒体库文件
- private mediaTable: MediaTable = new MediaTable(getContext(this))
- private playlistTable: PlaylistTable = new PlaylistTable(getContext(this))
- @State dataSource: LazyDataSource<VideoItem> = 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<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,
- 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<string> = []
- 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)&&!fullPath.endsWith('.lrc')&&!fullPath.endsWith('.srt')) {
- 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()
- }
- 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<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) : ''
- }
- }
- 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: 2 }) {
- Image($r('app.media.ic_action_search'))
- .width(20)
- .height(20)
- .padding({ left: 5})
- .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(11)
- .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
- .placeholderColor(this.isDarkMode ? '#8E8E93' : '#999999')
- .placeholderFont({ size:11 })
- .onChange((value: string) => {
- this.searchText = value
- this.filterSongs()
- })
- }
- .width('100%')
- .padding(2)
- .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
- .borderRadius(20)
- // 视图模式切换 SegmentButton
- SegmentButton({
- options: this.viewModeOptions,
- selectedIndexes: $viewMode
- })
- .width('100%')
- .margin({ top: 2 })
- // 已选择歌曲数量(包含目录展开后的歌曲)
- 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(11)
- .fontColor(this.isDarkMode ? '#8E8E93' : '#999999')
- .maxLines(1)
- }
- }
- .alignItems(HorizontalAlign.Start)
- .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)
- }
|