ScanFilePage.ets 17 KB

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