import TitleBar from '../view/TitleBar' import { router, window } from '@kit.ArkUI' import { AppUtil, DeviceUtil, DisplayUtil, FileUtil, LogUtil, PreferencesUtil, ToastUtil } from '@pura/harmony-utils' import lottie from '@ohos/lottie' import { AnimationItem } from '@ohos/lottie' import { JSON, MessageEvents, taskpool, worker } from '@kit.ArkTS' import { fileUri, picker } from '@kit.CoreFileKit' import { Utility } from '../common/util/Utility' import MediaTable from '../common/util/MediaTable' import { VideoItem } from '../viewmodel/VideoItem' import Logger from '../common/util/Logger' import { CommonConstants, STR_LOCK_VIDEO } from '../common/constants/CommonConstants' import { EventConstants } from '../common/constants/EventConstants' import { BusinessError, emitter } from '@kit.BasicServicesKit' import { BreakpointTypeEnum } from '../common/util/BreakpointSystem' import { resourceManager } from '@kit.LocalizationKit' import { common, ConfigurationConstant } from '@kit.AbilityKit' import { DialogHelper } from '@pura/harmony-dialog' import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil' @Preview // @Entry @Component export struct ScanFilePage{ @StorageProp('topSafeHeight') topSafeHeight: number = 0; // 新增状态变量 @State currentInsertCount: number = 0 // 当前已扫描并插入的歌曲数量 @State currentFilePath: string = '' // 当前正在处理的文件路径 @State invalidMusicCount: number = 0 // 无效音乐文件数量 @State isCorrecting: boolean = false // 是否正在校正数据 context = this.getUIContext().getHostContext() as common.UIAbilityContext // @StorageProp('mediaKuList') mediaKuList: Array = []; //媒体库文件 @State rootPath:string = '' //音频根目录 @State lockPath:string = '' @Consume isShowDrawer: boolean; @Consume offsetX: number; @State textVisi:Visibility=Visibility.Hidden @State isStart:boolean = false @State isStartCover:boolean = false @State isStartSync:boolean = false @State appName:string = '' @State packName: string = '' @Consume mType: number; //lottie动画构建渲染上下文 private mainRenderingSettings: RenderingContextSettings = new RenderingContextSettings(true) private path:string = "common/lottie/leida.json" private successpath:string = "common/lottie/success2.json" private mainCanvasRenderingContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.mainRenderingSettings) private animateItem: AnimationItem | null = null; /** 当前断点类型(如大屏/小屏) */ @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD; @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; @State isDarkMode: boolean = false @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT; private windowClass: window.Window = globalThis.windowClass @StorageProp('topRectHeight') topRectHeight: number = px2vp(AppUtil.getStatusBarHeight()); initLottie(lpath:string,isLoop:boolean){ lottie.destroy(); //加载动画前先销毁之前加载的动画 this.animateItem = lottie.loadAnimation({ container: this.mainCanvasRenderingContext, // 渲染上下文 renderer: 'canvas', // 渲染方式 loop: isLoop, // 是否循环播放,默认true autoplay: true, // 是否自动播放,默认true name: '2024', // 动画名称 contentMode: 'Contain', // 填充的模式 frameRate: 30, //设置animator的刷帧率为30 imagePath: 'lottie/images/', // 加载读取指定路径下的图片资源 path: lpath, // json路径 // initialSegment: [0,70] }) lottie.setSpeed(0.7) } onPageShow() { } // 组件生命周期 async aboutToAppear() { this.packName = AppUtil.getBundleName() this.topRectHeight = px2vp(AppUtil.getStatusBarHeight()); Utility.getAppName(getContext(this)).then((appName:string)=>{ this.appName = appName }) let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR); AppStorage.setOrCreate('themeColor', themeColor); this.themeColor = themeColor; this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK const documentViewPicker = new picker.DocumentViewPicker() let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD }) this.rootPath = new fileUri.FileUri(documentSaveResult[0]).path this.lockPath = this.rootPath +'/'+ STR_LOCK_VIDEO LogUtil.info('onecold 文件扫描 aboutToAppear') this.initLottie(this.path,true) setTimeout(()=>{ lottie.pause() },88) setTimeout(()=>{ // 显示校正数据对话框 this.showCorrectDataDialog(false) },30000) } onColorModeChange() { this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK } endScan(isOnekey:boolean){ const eventData: emitter.EventData = {}; emitter.emit({ eventId: EventConstants.EVENT_SCAN_UPDATE }, eventData); // 发送视频打开广播事件 this.watchStatus(false) this.currentFilePath = '扫描文件入库成功!' if(isOnekey){ this.isStartCover = false }else{ if(this.isStartSync){ this.isStartSync = false }else{ this.isStart = false } } //结束动画 lottie.pause() lottie.stop() this.initLottie(this.successpath,false) lottie.play() } //扫描过程中屏幕要保存常亮 watchStatus(isKeep:boolean) { this.windowClass.setWindowKeepScreenOn(isKeep); } // 组件生命周期 aboutToDisappear() { lottie.destroy(); } initState(isOnekey:boolean){ this.textVisi = Visibility.Visible this.initLottie(this.path,true) if(isOnekey){ this.isStartCover = true }else{ this.isStart = true } lottie.play() } doOptimize(isOnekey:boolean){ this.watchStatus(true) this.initState(isOnekey) const task = new taskpool.Task(scanDirectoryTask, this.context, this.rootPath, this.lockPath,PreferencesUtil.getStringSync('COVER_API',''), PreferencesUtil.getBooleanSync('autoParseMusicName', true)); task.onReceiveData((path: string, count: number) => { this.currentFilePath = path this.currentInsertCount = count }) taskpool.execute(task, taskpool.Priority.HIGH).then(()=>{ this.endScan(isOnekey) taskpool.terminateTask(task); }).catch((e:object)=>{ console.info("onecold task1 catch e: " + JSON.stringify(e)); }) } // 更新进度回调函数 private updateProgress = (path: string, count: number) => { this.currentFilePath = path this.currentInsertCount = count } @Builder topTitleBar(){ Column() { Row({ space: 15 }) { //左侧滑动按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.sort')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.getUIContext().animateTo({ duration: 555 }, () => { // 动画闭包内控制Image组件的出现和消失 this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) }) .attributeModifier(new ShadowModifier()) .zIndex(0) Text($r('app.string.file_scan')) .margin({left:3,right:10}) .fontColor($r('app.color.text_color')) .fontSize(19) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.exclamationmark_circle')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 }) .attributeModifier(new ShadowModifier()) .zIndex(0) .onClick(() => { // 显示校正数据对话框 this.showCorrectDataDialog(true) }) } } .padding({ top: this.topSafeHeight+10, left: 10, right: 10,bottom:12 }) .width('100%') } build() { Column() { // 顶部安全区和自定义标题栏 // 顶部安全区和自定义标题栏 Column() { this.topTitleBar() } Scroll(){ Column() { Stack(){ Image($r('app.media.file_search')) .width(48) .height(48) .fillColor(this.themeColor) .alignSelf(ItemAlign.Center) //lottie动画 Canvas(this.mainCanvasRenderingContext) .width(160) .height(160) .onReady(()=>{ //抗锯齿的设置 this.mainCanvasRenderingContext.imageSmoothingEnabled = true; this.mainCanvasRenderingContext.imageSmoothingQuality = 'medium' }) } .margin({top:10}) .transition(TransitionEffect.move(TransitionEdge.END) .animation({ duration: 380, curve: Curve.Ease,delay:300 })) Text(`${this.currentInsertCount}首歌`) .height(38) .fontSize(16) .fontColor($r('app.color.text_color')) .fontWeight(480) .visibility(this.textVisi) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) Text(this.currentFilePath) .height(38) .fontSize(!this.isStart?15:12) .maxLines(3) .margin({bottom:6}) .width('86%') .textAlign(this.isStart?TextAlign.Start:TextAlign.Center) .fontColor($r('app.color.text_color')) .fontWeight(480) .visibility(this.textVisi) .animation({ duration: 666, curve: 'ease-in-out' // 可选动画曲线 }) Button({ type: ButtonType.Capsule, stateEffect: true }) { Row(){ SymbolGlyph($r('sys.symbol.magnifyingglass')) .fontSize(22) .fontColor([Color.White]) Text($r('app.string.start_scan')) .margin({ left: 8 }) .fontSize(18) .fontColor(Color.White) .fontWeight(480) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) } .width(180) .height(55) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .margin({ top: 10, bottom: 10 }) .backgroundColor(this.themeColor) .enabled(this.isStart ?false:true) .onClick(() => { this.doOptimize(false) }) .alignSelf(ItemAlign.Center) .transition(TransitionEffect.move(TransitionEdge.END) .animation({ duration: 380, curve: Curve.Ease,delay:300 })) Column() { Row() { Image($r('app.media.selected')) .width(16) .height(16) .fillColor(this.themeColor) .margin({ left:10}) .alignSelf(ItemAlign.Center) Text(Utility.resourceToString(getContext(this),$r('app.string.file_scan_tip_one')) +`${this.appName} `+ Utility.resourceToString(getContext(this),$r('app.string.file_scan_tip_one_1'))) .margin({ left: 10, right: 20 }) .fontSize(15) .fontColor(Color.Gray) .fontWeight(480) .layoutWeight(1) } // .width('100%') .margin({ left:25,right: 25 ,top:20 }) Row() { Image($r('app.media.selected')) .width(16) .height(16) .fillColor(this.themeColor) .margin({ left:10}) .alignSelf(ItemAlign.Center) Text($r('app.string.file_scan_tip_two')) .margin({ left: 10, right: 20 }) .fontSize(15) .fontColor(Color.Gray) .fontWeight(480) .layoutWeight(1) } // .width('100%') .margin({ left:25,right: 25 ,top:20,bottom:20 }) Row() { Image($r('app.media.selected')) .width(16) .height(16) .fillColor(this.themeColor) .margin({ left:10}) .alignSelf(ItemAlign.Center) Text(Utility.resourceToString(getContext(this),$r('app.string.pcfile_scan_tip_one')) +`${this.packName} `+ Utility.resourceToString(getContext(this),$r('app.string.pcfile_scan_tip_one_1'))) .margin({ left: 10, right: 20 }) .fontSize(15) .fontColor(Color.Gray) .fontWeight(480) .layoutWeight(1) } .margin({ left:25,right: 25 ,bottom:20 }) } .width(this.currentBreakpoint !== BreakpointTypeEnum.SM?450:350) .margin({ left:25,right: 25,top:20,bottom:20 }) .borderRadius(20) .border({ color: Color.Gray, width: 1.8 }) .justifyContent(FlexAlign.Center) .transition(TransitionEffect.move(TransitionEdge.END) .animation({ duration: 380, curve: Curve.Ease,delay:300 })) Row() { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row(){ SymbolGlyph($r('sys.symbol.picture')) .fontSize(19) .fontColor([Color.White]) Text($r('app.string.onekey_cover')) .margin({ left: 4 }) .fontSize(15) .fontColor(Color.White) .fontWeight(480) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) } .width(150) .height(55) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5}) .margin({ top: 10, bottom: 10,right:10 }) .backgroundColor(this.themeColor) .enabled(this.isStartCover ?false:true) .onClick(() => { if(PreferencesUtil.getStringSync('COVER_API','')===''){ this.showTipsDialog() return } this.doOptimize(true) }) .alignSelf(ItemAlign.Center) } .transition(TransitionEffect.move(TransitionEdge.END) .animation({ duration: 380, curve: Curve.Ease,delay:300 })) .visibility(Visibility.None) } .height('100%') .width('100%') .align(Alignment.Top) } .height(1000) } } showTipsDialog() { DialogHelper.showCustomContentDialog({ dialogId: 'tips', title: "友情提示", autoCancel: false, //点击遮障层时,不关闭弹窗 backCancel: true, //点击返回键,不关闭弹窗 contentBuilder: () => { this.customTipsBuilder("请到设置界面配置封面服务器地址!") }, buttons: [], }) } /** * 显示校正数据对话框 */ showCorrectDataDialog(isEmptyShowDialog:boolean) { // 先查询无效音乐数量 this.checkInvalidMusicCount(isEmptyShowDialog) } /** * 检查无效音乐数量并显示对话框 */ async checkInvalidMusicCount(isEmptyShowDialog:boolean) { try { const table: MediaTable = new MediaTable(this.context); console.info('heanup ScanFilePage', `checkInvalidMusicCount`) await new Promise((resolve, reject) => { table.getRdbStore(this.context, (err:Error) => { err ? reject(err) : resolve(); }); }); const allMusic = await this.queryAllLocalMusic(table); console.info('heanup ScanFilePage', `查询到的所有音乐数量: ${allMusic.length}`) let invalidCount = 0; const invalidFilePaths: string[] = []; for (const item of allMusic) { // 检查是否是本地音乐(filePath包含包名) if (item.filePath&&item.type===CommonConstants.TYPE_LOCAL && item.filePath.includes(this.packName)) { const exists = await FileUtil.accessSync(item.filePath); if (!exists) { invalidCount++; invalidFilePaths.push(item.filePath); } } } this.invalidMusicCount = invalidCount; console.info('heanup ScanFilePage', `showCustomContentDialog`) // 显示校正对话框 if(this.invalidMusicCount>0||isEmptyShowDialog){ DialogHelper.showCustomContentDialog({ dialogId: 'correctData', title: "校正数据", autoCancel: true, backCancel: true, contentBuilder: () => { this.correctDataContentBuilder(invalidCount); }, buttons: [], }); } } catch (error) { Logger.error('heanup ScanFilePage', `检查无效音乐失败: ${JSON.stringify(error)}`); ToastUtil.showToast('检查失败,请稍后重试'); } } /** * 查询所有本地音乐 */ async queryAllLocalMusic(table: MediaTable): Promise { return new Promise((resolve, reject) => { table.query(0, (result: VideoItem[]) => { resolve(result); }, true); }); } /** * 执行校正数据 */ async doCorrectData() { if (this.isCorrecting) { return; } this.isCorrecting = true; DialogHelper.closeDialog('correctData'); try { const table: MediaTable = new MediaTable(this.context); await new Promise((resolve, reject) => { table.getRdbStore(this.context, (err:Error) => { err ? reject(err) : resolve(); }); }); const allMusic = await this.queryAllLocalMusic(table); let deletedCount = 0; this.currentFilePath = '正在校正数据...'; this.textVisi = Visibility.Visible; for (const item of allMusic) { // 检查是否是本地音乐(filePath包含包名) if (item.filePath && item.filePath.includes(this.packName)) { const exists = await FileUtil.accessSync(item.filePath); if (!exists) { // 文件不存在,删除数据库记录 await new Promise((resolve) => { table.deleteDataFilePath(item.filePath, (success: boolean) => { if (success) { deletedCount++; Logger.info('heanup ScanFilePage', `删除无效记录: ${item.filePath}`); } resolve(); }); }); } } } this.currentFilePath = `校正完成!删除了 ${deletedCount} 条无效记录`; this.currentInsertCount = deletedCount; // 显示成功提示 setTimeout(() => { ToastUtil.showToast(`校正完成,删除了 ${deletedCount} 条无效记录`); }, 500); } catch (error) { Logger.error('heanup ScanFilePage', `校正数据失败: ${JSON.stringify(error)}`); ToastUtil.showToast('校正失败,请稍后重试'); this.currentFilePath = '校正失败'; } finally { this.isCorrecting = false; setTimeout(() => { this.currentFilePath = ''; this.currentInsertCount = 0; this.textVisi = Visibility.Hidden; }, 3000); } } @Builder customTipsBuilder(content: string) { Column() { Text(content) .fontColor(Color.Gray) .fontSize(16) .alignSelf(ItemAlign.Start) .margin({ bottom: 15 }) .fontSize(16) Row() { Button('取消') .fontColor(Color.White) .backgroundColor($r('app.color.title_bar_bg'))//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]}) .height(50) .layoutWeight(1) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .margin({ right: 6 }) .onClick(() => { DialogHelper.closeDialog('tips'); //关闭弹框 }) Button('跳转') .fontColor(Color.White)//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]}) .layoutWeight(1) .height(50) .backgroundColor($r('app.color.title_bar_bg')) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .margin({ left: 6 }) .onClick(() => { DialogHelper.closeDialog('tips'); //关闭弹框 this.mType = 3 // router.pushUrl({ // url: 'pages/SettingPage' // }, router.RouterMode.Single); }) } } .width("100%") .padding(10) } @Builder correctDataContentBuilder(invalidCount: number) { Column() { if (invalidCount > 0) { Text(`发现 ${invalidCount} 条无效的音乐记录`) .fontColor($r('app.color.text_color')) .fontSize(16) .margin({ bottom: 10 }) Text('这些音乐文件已被删除或移动,是否要清理这些无效记录?') .fontColor(Color.Gray) .fontSize(14) .margin({ bottom: 20 }) Row() { Button('取消') .fontColor(Color.White) .backgroundColor(Color.Gray) .height(50) .layoutWeight(1) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .margin({ right: 10 }) .onClick(() => { DialogHelper.closeDialog('correctData'); }) Button('确定') .fontColor(Color.White) .backgroundColor(this.themeColor) .height(50) .layoutWeight(1) .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .onClick(() => { this.doCorrectData(); }) } .width('100%') } else { Text('未发现无效的音乐记录') .fontColor($r('app.color.text_color')) .fontSize(16) .margin({ bottom: 20 }) Button('确定') .fontColor(Color.White) .backgroundColor(this.themeColor) .height(50) .width('100%') .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7}) .onClick(() => { DialogHelper.closeDialog('correctData'); }) } } .width("100%") .padding(15) } } //扫描数据库校正对比数据是否真实存在,不存在在删除数据 @Concurrent async function syncDataTask(context: Context, mediaKuList: Array) { const table: MediaTable = new MediaTable(context); for (const item of mediaKuList) { const filePath = item; // 检查 filePath 是否存在 const exists:boolean = await FileUtil.accessSync(filePath); // 如果 filePath 不存在,则删除数据库中的数据 if (!exists) { table.getRdbStore(context, async (err:Error) => { if (err) { return; } Logger.info('xiaozheng 校正数据开始 this.exists = ' + exists+' filepath = ' +filePath) await table.deleteDataFilePath(filePath, (result:boolean) => { if(result){ Logger.info(`xiaozheng Deleted item success with filePath: ${filePath}`); } }); }) } } } //扫描文件入库 @Concurrent async function scanDirectoryTask(context: Context, dirPath: string, lockPath: string,cover_api:string,autoParseMusicName:boolean) { const stack: string[] = [dirPath]; const table: MediaTable = new MediaTable(context); let insertCount = 0 while (stack.length > 0) { const currentPath = stack.pop()!; const files = FileUtil.listFileSync(currentPath); await Promise.all(files.map(async (file) => { const fPath = `${currentPath}/${file}`; // console.info('onecold scanDirectoryTask fPath = ' + fPath) if (fPath === lockPath) return; if (FileUtil.isDirectory(fPath)) { stack.push(fPath); // 使用栈结构代替递归 } else { if (fPath.endsWith('.lrc') || fPath.endsWith('.srt')) return; // console.info('onecold scanDirectoryTask fPath2 = ' + fPath) if (Utility.isMeidaByExtension(fPath)) { const mediaItem = await Utility.uriGetMusicAssetsFromFile( context, fPath, CommonConstants.TYPE_LOCAL, autoParseMusicName ); insertCount++ try { // console.info('onecold scanDirectoryTask fPath = ' + fPath) // console.info('onecold scanDirectoryTask insertCount = ' + insertCount) taskpool.Task.sendData(fPath,insertCount); } catch (error) { let err = error as BusinessError; console.warn( `onecold sendData failed, code=${err.code}, message=${err.message}`); } table.insert(mediaItem, (id: number) => { },cover_api); } } })); } }