ScanFilePage.ets 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import TitleBar from '../view/TitleBar'
  2. import { router, window } from '@kit.ArkUI'
  3. import { AppUtil, DeviceUtil, DisplayUtil, FileUtil, LogUtil, PreferencesUtil, ToastUtil } from '@pura/harmony-utils'
  4. import lottie from '@ohos/lottie'
  5. import { AnimationItem } from '@ohos/lottie'
  6. import { JSON, MessageEvents, taskpool, worker } from '@kit.ArkTS'
  7. import { fileUri, picker } from '@kit.CoreFileKit'
  8. import { Utility } from '../common/util/Utility'
  9. import MediaTable from '../common/util/MediaTable'
  10. import { VideoItem } from '../viewmodel/VideoItem'
  11. import Logger from '../common/util/Logger'
  12. import { CommonConstants, STR_LOCK_VIDEO } from '../common/constants/CommonConstants'
  13. import { EventConstants } from '../common/constants/EventConstants'
  14. import { BusinessError, emitter } from '@kit.BasicServicesKit'
  15. import { BreakpointTypeEnum } from '../common/util/BreakpointSystem'
  16. import { resourceManager } from '@kit.LocalizationKit'
  17. import { common, ConfigurationConstant } from '@kit.AbilityKit'
  18. import { DialogHelper } from '@pura/harmony-dialog'
  19. @Preview
  20. // @Entry
  21. @Component
  22. export struct ScanFilePage{
  23. // 新增状态变量
  24. @State currentInsertCount: number = 0 // 当前已扫描并插入的歌曲数量
  25. @State currentFilePath: string = '' // 当前正在处理的文件路径
  26. context = this.getUIContext().getHostContext() as common.UIAbilityContext
  27. // @StorageProp('mediaKuList') mediaKuList: Array<VideoItem> = []; //媒体库文件
  28. @State rootPath:string = '' //音频根目录
  29. @State lockPath:string = ''
  30. @Consume isShowDrawer: boolean;
  31. @Consume offsetX: number;
  32. @State textVisi:Visibility=Visibility.Hidden
  33. @State isStart:boolean = false
  34. @State isStartCover:boolean = false
  35. @State isStartSync:boolean = false
  36. @State appName:string = ''
  37. @State packName: string = ''
  38. @Consume mType: number;
  39. //lottie动画构建渲染上下文
  40. private mainRenderingSettings: RenderingContextSettings = new RenderingContextSettings(true)
  41. private path:string = "common/lottie/leida.json"
  42. private successpath:string = "common/lottie/success2.json"
  43. private mainCanvasRenderingContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.mainRenderingSettings)
  44. private animateItem: AnimationItem | null = null;
  45. /** 当前断点类型(如大屏/小屏) */
  46. @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
  47. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  48. @State isDarkMode: boolean = false
  49. @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
  50. ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
  51. private windowClass: window.Window = globalThis.windowClass
  52. @StorageProp('topRectHeight') topRectHeight: number = px2vp(AppUtil.getStatusBarHeight());
  53. initLottie(lpath:string,isLoop:boolean){
  54. lottie.destroy(); //加载动画前先销毁之前加载的动画
  55. this.animateItem = lottie.loadAnimation({
  56. container: this.mainCanvasRenderingContext, // 渲染上下文
  57. renderer: 'canvas', // 渲染方式
  58. loop: isLoop, // 是否循环播放,默认true
  59. autoplay: true, // 是否自动播放,默认true
  60. name: '2024', // 动画名称
  61. contentMode: 'Contain', // 填充的模式
  62. frameRate: 30, //设置animator的刷帧率为30
  63. imagePath: 'lottie/images/', // 加载读取指定路径下的图片资源
  64. path: lpath, // json路径
  65. // initialSegment: [0,70]
  66. })
  67. lottie.setSpeed(0.7)
  68. }
  69. onPageShow() {
  70. }
  71. // 组件生命周期
  72. async aboutToAppear() {
  73. this.packName = AppUtil.getBundleName()
  74. this.topRectHeight = px2vp(AppUtil.getStatusBarHeight());
  75. Utility.getAppName(getContext(this)).then((appName:string)=>{
  76. this.appName = appName
  77. })
  78. let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
  79. AppStorage.setOrCreate('themeColor', themeColor);
  80. this.themeColor = themeColor;
  81. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  82. const documentViewPicker = new picker.DocumentViewPicker()
  83. let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD })
  84. this.rootPath = new fileUri.FileUri(documentSaveResult[0]).path
  85. this.lockPath = this.rootPath +'/'+ STR_LOCK_VIDEO
  86. LogUtil.info('onecold 文件扫描 aboutToAppear')
  87. this.initLottie(this.path,true)
  88. setTimeout(()=>{
  89. lottie.pause()
  90. },88)
  91. }
  92. onColorModeChange() {
  93. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  94. }
  95. endScan(isOnekey:boolean){
  96. const eventData: emitter.EventData = {};
  97. emitter.emit({ eventId: EventConstants.EVENT_SCAN_UPDATE }, eventData); // 发送视频打开广播事件
  98. this.watchStatus(false)
  99. this.currentFilePath = '扫描文件入库成功!'
  100. if(isOnekey){
  101. this.isStartCover = false
  102. }else{
  103. if(this.isStartSync){
  104. this.isStartSync = false
  105. }else{
  106. this.isStart = false
  107. }
  108. }
  109. //结束动画
  110. lottie.pause()
  111. lottie.stop()
  112. this.initLottie(this.successpath,false)
  113. lottie.play()
  114. }
  115. //扫描过程中屏幕要保存常亮
  116. watchStatus(isKeep:boolean) {
  117. this.windowClass.setWindowKeepScreenOn(isKeep);
  118. }
  119. // 组件生命周期
  120. aboutToDisappear() {
  121. lottie.destroy();
  122. }
  123. initState(isOnekey:boolean){
  124. this.textVisi = Visibility.Visible
  125. this.initLottie(this.path,true)
  126. if(isOnekey){
  127. this.isStartCover = true
  128. }else{
  129. this.isStart = true
  130. }
  131. lottie.play()
  132. }
  133. doOptimize(isOnekey:boolean){
  134. this.watchStatus(true)
  135. this.initState(isOnekey)
  136. const task = new taskpool.Task(scanDirectoryTask, this.context, this.rootPath,
  137. this.lockPath,PreferencesUtil.getStringSync('COVER_API',''),
  138. PreferencesUtil.getBooleanSync('autoParseMusicName', true));
  139. task.onReceiveData((path: string, count: number) => {
  140. this.currentFilePath = path
  141. this.currentInsertCount = count
  142. })
  143. taskpool.execute(task, taskpool.Priority.HIGH).then(()=>{
  144. this.endScan(isOnekey)
  145. taskpool.terminateTask(task);
  146. }).catch((e:object)=>{
  147. console.info("onecold task1 catch e: " + JSON.stringify(e));
  148. })
  149. }
  150. // 更新进度回调函数
  151. private updateProgress = (path: string, count: number) => {
  152. this.currentFilePath = path
  153. this.currentInsertCount = count
  154. }
  155. build() {
  156. Column() {
  157. // 顶部安全区和自定义标题栏
  158. Column() {
  159. // 顶部安全区
  160. Blank()
  161. .height(this.topRectHeight)
  162. .backgroundColor(this.isDarkMode?$r('app.color.title_bar_bg'):this.themeColor)
  163. .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  164. // 自定义标题栏(Stack实现绝对居中)
  165. Stack() {
  166. // 居中标题
  167. Text('文件扫描')
  168. .fontSize(18)
  169. .fontColor(Color.White)
  170. .align(Alignment.Center)
  171. // 左右按钮
  172. Row() {
  173. Image($r('app.media.menu'))
  174. .width(28)
  175. .height(28)
  176. .margin({ left: 12, right: 8 })
  177. .onClick(() => {
  178. animateTo({ duration: 555 }, () => {
  179. // 动画闭包内控制Image组件的出现和消失
  180. this.isShowDrawer = !this.isShowDrawer
  181. this.offsetX = 0
  182. })
  183. })
  184. Blank().flexGrow(1)
  185. Blank().width(32)
  186. }
  187. .height(48)
  188. .width('100%')
  189. .alignItems(VerticalAlign.Center)
  190. }
  191. .height(48)
  192. .width('100%')
  193. .backgroundColor(this.isDarkMode?$r('app.color.title_bar_bg'):this.themeColor)
  194. }
  195. Scroll(){
  196. Column() {
  197. Stack(){
  198. Image($r('app.media.file_search'))
  199. .width(48)
  200. .height(48)
  201. .fillColor(this.themeColor)
  202. .alignSelf(ItemAlign.Center)
  203. //lottie动画
  204. Canvas(this.mainCanvasRenderingContext)
  205. .width(160)
  206. .height(160)
  207. .onReady(()=>{
  208. //抗锯齿的设置
  209. this.mainCanvasRenderingContext.imageSmoothingEnabled = true;
  210. this.mainCanvasRenderingContext.imageSmoothingQuality = 'medium'
  211. })
  212. }
  213. .margin({top:10})
  214. .transition(TransitionEffect.move(TransitionEdge.END)
  215. .animation({ duration: 380, curve: Curve.Ease,delay:300 }))
  216. Text(`${this.currentInsertCount}首歌`)
  217. .height(38)
  218. .fontSize(16)
  219. .fontColor($r('app.color.text_color'))
  220. .fontWeight(480)
  221. .visibility(this.textVisi)
  222. .animation({
  223. duration: 666,
  224. curve: 'ease-in-out' // 可选动画曲线
  225. })
  226. Text(this.currentFilePath)
  227. .height(38)
  228. .fontSize(!this.isStart?15:12)
  229. .maxLines(3)
  230. .margin({bottom:6})
  231. .width('86%')
  232. .textAlign(this.isStart?TextAlign.Start:TextAlign.Center)
  233. .fontColor($r('app.color.text_color'))
  234. .fontWeight(480)
  235. .visibility(this.textVisi)
  236. .animation({
  237. duration: 666,
  238. curve: 'ease-in-out' // 可选动画曲线
  239. })
  240. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  241. Row(){
  242. SymbolGlyph($r('sys.symbol.magnifyingglass'))
  243. .fontSize(22)
  244. .fontColor([Color.White])
  245. Text($r('app.string.start_scan'))
  246. .margin({ left: 8 })
  247. .fontSize(18)
  248. .fontColor(Color.White)
  249. .fontWeight(480)
  250. .textAlign(TextAlign.Center)
  251. }
  252. .justifyContent(FlexAlign.Center)
  253. }
  254. .width(180)
  255. .height(55)
  256. .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
  257. .margin({ top: 10, bottom: 10 })
  258. .backgroundColor(this.themeColor)
  259. .enabled(this.isStart ?false:true)
  260. .onClick(() => {
  261. this.doOptimize(false)
  262. })
  263. .alignSelf(ItemAlign.Center)
  264. .transition(TransitionEffect.move(TransitionEdge.END)
  265. .animation({ duration: 380, curve: Curve.Ease,delay:300 }))
  266. Column() {
  267. Row() {
  268. Image($r('app.media.selected'))
  269. .width(16)
  270. .height(16)
  271. .fillColor(this.themeColor)
  272. .margin({ left:10})
  273. .alignSelf(ItemAlign.Center)
  274. Text(Utility.resourceToString(getContext(this),$r('app.string.file_scan_tip_one'))
  275. +`${this.appName} `+
  276. Utility.resourceToString(getContext(this),$r('app.string.file_scan_tip_one_1')))
  277. .margin({ left: 10, right: 20 })
  278. .fontSize(15)
  279. .fontColor(Color.Gray)
  280. .fontWeight(480)
  281. .layoutWeight(1)
  282. }
  283. // .width('100%')
  284. .margin({ left:25,right: 25 ,top:20 })
  285. Row() {
  286. Image($r('app.media.selected'))
  287. .width(16)
  288. .height(16)
  289. .fillColor(this.themeColor)
  290. .margin({ left:10})
  291. .alignSelf(ItemAlign.Center)
  292. Text($r('app.string.file_scan_tip_two'))
  293. .margin({ left: 10, right: 20 })
  294. .fontSize(15)
  295. .fontColor(Color.Gray)
  296. .fontWeight(480)
  297. .layoutWeight(1)
  298. }
  299. // .width('100%')
  300. .margin({ left:25,right: 25 ,top:20,bottom:20 })
  301. Row() {
  302. Image($r('app.media.selected'))
  303. .width(16)
  304. .height(16)
  305. .fillColor(this.themeColor)
  306. .margin({ left:10})
  307. .alignSelf(ItemAlign.Center)
  308. Text(Utility.resourceToString(getContext(this),$r('app.string.pcfile_scan_tip_one'))
  309. +`${this.packName} `+
  310. Utility.resourceToString(getContext(this),$r('app.string.pcfile_scan_tip_one_1')))
  311. .margin({ left: 10, right: 20 })
  312. .fontSize(15)
  313. .fontColor(Color.Gray)
  314. .fontWeight(480)
  315. .layoutWeight(1)
  316. }
  317. .margin({ left:25,right: 25 ,bottom:20 })
  318. }
  319. .width(this.currentBreakpoint !== BreakpointTypeEnum.SM?450:350)
  320. .margin({ left:25,right: 25,top:20,bottom:20 })
  321. .borderRadius(20)
  322. .border({
  323. color: Color.Gray,
  324. width: 1.8
  325. })
  326. .justifyContent(FlexAlign.Center)
  327. .transition(TransitionEffect.move(TransitionEdge.END)
  328. .animation({ duration: 380, curve: Curve.Ease,delay:300 }))
  329. Row() {
  330. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  331. Row(){
  332. SymbolGlyph($r('sys.symbol.picture'))
  333. .fontSize(19)
  334. .fontColor([Color.White])
  335. Text($r('app.string.onekey_cover'))
  336. .margin({ left: 4 })
  337. .fontSize(15)
  338. .fontColor(Color.White)
  339. .fontWeight(480)
  340. .textAlign(TextAlign.Center)
  341. }
  342. .justifyContent(FlexAlign.Center)
  343. }
  344. .width(150)
  345. .height(55)
  346. .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
  347. .margin({ top: 10, bottom: 10,right:10 })
  348. .backgroundColor(this.themeColor)
  349. .enabled(this.isStartCover ?false:true)
  350. .onClick(() => {
  351. if(PreferencesUtil.getStringSync('COVER_API','')===''){
  352. this.showTipsDialog()
  353. return
  354. }
  355. this.doOptimize(true)
  356. })
  357. .alignSelf(ItemAlign.Center)
  358. }
  359. .transition(TransitionEffect.move(TransitionEdge.END)
  360. .animation({ duration: 380, curve: Curve.Ease,delay:300 }))
  361. .visibility(Visibility.None)
  362. }
  363. .height('100%')
  364. .width('100%')
  365. .align(Alignment.Top)
  366. }
  367. .height(1000)
  368. }
  369. }
  370. showTipsDialog() {
  371. DialogHelper.showCustomContentDialog({
  372. dialogId: 'tips',
  373. title: "友情提示",
  374. autoCancel: false, //点击遮障层时,不关闭弹窗
  375. backCancel: true, //点击返回键,不关闭弹窗
  376. contentBuilder: () => {
  377. this.customTipsBuilder("请到设置界面配置封面服务器地址!")
  378. },
  379. buttons: [],
  380. })
  381. }
  382. @Builder
  383. customTipsBuilder(content: string) {
  384. Column() {
  385. Text(content)
  386. .fontColor(Color.Gray)
  387. .fontSize(16)
  388. .alignSelf(ItemAlign.Start)
  389. .margin({ bottom: 15 })
  390. .fontSize(16)
  391. Row() {
  392. Button('取消')
  393. .fontColor(Color.White)
  394. .backgroundColor($r('app.color.title_bar_bg'))//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]})
  395. .height(50)
  396. .layoutWeight(1)
  397. .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7})
  398. .margin({ right: 6 })
  399. .onClick(() => {
  400. DialogHelper.closeDialog('tips'); //关闭弹框
  401. })
  402. Button('跳转')
  403. .fontColor(Color.White)//.linearGradient( { direction: GradientDirection.Right, colors:[['#ff37a0fc',0.0],['#67e667',0.5],['#f5856e',1.0]]})
  404. .layoutWeight(1)
  405. .height(50)
  406. .backgroundColor($r('app.color.title_bar_bg'))
  407. .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7})
  408. .margin({ left: 6 })
  409. .onClick(() => {
  410. DialogHelper.closeDialog('tips'); //关闭弹框
  411. this.mType = 3
  412. // router.pushUrl({
  413. // url: 'pages/SettingPage'
  414. // }, router.RouterMode.Single);
  415. })
  416. }
  417. }
  418. .width("100%")
  419. .padding(10)
  420. }
  421. }
  422. //扫描数据库校正对比数据是否真实存在,不存在在删除数据
  423. @Concurrent
  424. async function syncDataTask(context: Context, mediaKuList: Array<string>) {
  425. const table: MediaTable = new MediaTable(context);
  426. for (const item of mediaKuList) {
  427. const filePath = item;
  428. // 检查 filePath 是否存在
  429. const exists:boolean = await FileUtil.accessSync(filePath);
  430. // 如果 filePath 不存在,则删除数据库中的数据
  431. if (!exists) {
  432. table.getRdbStore(context, async (err:Error) => {
  433. if (err) {
  434. return;
  435. }
  436. Logger.info('xiaozheng 校正数据开始 this.exists = ' + exists+' filepath = ' +filePath)
  437. await table.deleteDataFilePath(filePath, (result:boolean) => {
  438. if(result){
  439. Logger.info(`xiaozheng Deleted item success with filePath: ${filePath}`);
  440. }
  441. });
  442. })
  443. }
  444. }
  445. }
  446. //扫描文件入库
  447. @Concurrent
  448. async function scanDirectoryTask(context: Context, dirPath: string,
  449. lockPath: string,cover_api:string,autoParseMusicName:boolean) {
  450. const stack: string[] = [dirPath];
  451. const table: MediaTable = new MediaTable(context);
  452. let insertCount = 0
  453. while (stack.length > 0) {
  454. const currentPath = stack.pop()!;
  455. const files = FileUtil.listFileSync(currentPath);
  456. await Promise.all(files.map(async (file) => {
  457. const fPath = `${currentPath}/${file}`;
  458. // console.info('onecold scanDirectoryTask fPath = ' + fPath)
  459. if (fPath === lockPath) return;
  460. if (FileUtil.isDirectory(fPath)) {
  461. stack.push(fPath); // 使用栈结构代替递归
  462. } else {
  463. if (fPath.endsWith('.lrc') || fPath.endsWith('.srt')) return;
  464. // console.info('onecold scanDirectoryTask fPath2 = ' + fPath)
  465. if (Utility.isMeidaByExtension(fPath)) {
  466. const mediaItem = await Utility.uriGetMusicAssetsFromFile(
  467. context, fPath, CommonConstants.TYPE_LOCAL, autoParseMusicName
  468. );
  469. insertCount++
  470. try {
  471. // console.info('onecold scanDirectoryTask fPath = ' + fPath)
  472. // console.info('onecold scanDirectoryTask insertCount = ' + insertCount)
  473. taskpool.Task.sendData(fPath,insertCount);
  474. } catch (error) {
  475. let err = error as BusinessError;
  476. console.warn( `onecold sendData failed, code=${err.code}, message=${err.message}`);
  477. }
  478. table.insert(mediaItem, (id: number) => {
  479. },cover_api);
  480. }
  481. }
  482. }));
  483. }
  484. }