FixMessyView.ets 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import { common, ConfigurationConstant } from '@kit.AbilityKit';
  2. import { CommonConstants } from '../common/constants/CommonConstants';
  3. import { VideoItem } from '../viewmodel/VideoItem';
  4. import { LazyDataSource } from '../common/util/LazyDataSource';
  5. import { AppUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
  6. import fs from '@ohos.file.fs';
  7. import { changeMusicCover, findLocalCoverImage, getApiLyric, repairAudioMetadata,
  8. searchCover,
  9. syncLyricToDB} from '../common/util/MusicTagUtils';
  10. import { util } from '@kit.ArkTS';
  11. import { extractHwMediaMetadata, FFMpegTags, Utility } from '../common/util/Utility';
  12. import { DialogHelper } from '@pura/harmony-dialog';
  13. import MediaTable from '../common/util/MediaTable';
  14. // 批量乱码修正,元数据接口用华为的接口获取
  15. @Component
  16. export struct FixMessyView {
  17. private listScroller: ListScroller = new ListScroller()
  18. onFixResult = (_result: boolean) => {
  19. }
  20. @Link isFixMessy: boolean;
  21. @State bundleName: string = ''
  22. @State dataSource: LazyDataSource<VideoItem> = new LazyDataSource([])
  23. @Prop selectedFiles: Array<VideoItem>
  24. @StorageProp('topRectHeight') topRectHeight: number = 0;
  25. @State isDarkMode: boolean = false
  26. @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
  27. ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
  28. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  29. context = this.getUIContext().getHostContext() as common.UIAbilityContext
  30. // 新增状态用于进度显示
  31. @State currentProgress: number = 0;
  32. @State totalFiles: number = 0;
  33. @State currentFileIndex: number = 0;
  34. @State currentFileName: string = '';
  35. @State isEmbedding: boolean = false;
  36. @State embedSuccessCount: number = 0;
  37. @State embedFailedCount: number = 0;
  38. private table: MediaTable = new MediaTable(this.context);
  39. // 新增状态用于跟踪每个文件的嵌入状态
  40. @State embedStatusMap: Map<string, boolean> = new Map<string, boolean>();
  41. onColorModeChange() {
  42. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  43. }
  44. async aboutToAppear() {
  45. this.bundleName = AppUtil.getBundleName()
  46. // 确保 selectedFiles 有默认值后再初始化 dataSource
  47. if (this.selectedFiles && this.selectedFiles.length > 0) {
  48. this.dataSource = new LazyDataSource(this.selectedFiles)
  49. } else {
  50. this.dataSource = new LazyDataSource([])
  51. }
  52. await new Promise<void>((resolve, reject) => {
  53. this.table.getRdbStore(this.context, (err:Error) => {
  54. err ? reject(err) : resolve();
  55. });
  56. });
  57. }
  58. build() {
  59. Column(){
  60. this.topTitleBar()
  61. this.startButton()
  62. this.listView()
  63. }
  64. .height('100%')
  65. .width('100%')
  66. .backgroundColor($r('app.color.index_background'))
  67. }
  68. @Builder
  69. listView() {
  70. Column(){
  71. List({ scroller: this.listScroller }) {
  72. ListItemGroup({ header: this.listHeader() }) {
  73. LazyForEach(this.dataSource, (item: VideoItem, index: number) => {
  74. ListItem() {
  75. Column() {
  76. this.MusicItem(item, index)
  77. }
  78. }
  79. .transition(TransitionEffect.asymmetric(
  80. TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
  81. TransitionEffect.scale({ x: 0, y: 0 })
  82. ))
  83. .clickEffect({ level: ClickEffectLevel.LIGHT })
  84. }, (item: VideoItem) => item.filePath)
  85. }
  86. }
  87. .cachedCount(2)
  88. // .layoutWeight(1)
  89. // .height('100%')
  90. .transition(TransitionEffect.asymmetric(
  91. TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
  92. TransitionEffect.scale({ x: 0, y: 0 })
  93. ))
  94. }
  95. .borderRadius(20)
  96. .padding({top:10,bottom:6})
  97. .height('auto')
  98. .backgroundColor($r('app.color.start_window_background'))
  99. .margin({top:20,right:20,left:20})
  100. .layoutWeight(1)
  101. .height('100%')
  102. }
  103. @Builder
  104. listHeader() {
  105. Stack({alignContent:Alignment.Start}){
  106. Text(`共${this.selectedFiles.length}首歌`)
  107. .fontSize(14)
  108. .maxLines(1)
  109. .textAlign(TextAlign.Start)
  110. .padding({ left: 25 })
  111. .fontColor($r('app.color.text_color'))
  112. .margin({bottom:6,top:4,left: 6 })
  113. }
  114. .height('auto').width('100%')
  115. }
  116. @Builder
  117. startButton(){
  118. Stack(){
  119. Progress({ value: this.currentProgress, total: this.totalFiles,
  120. type: ProgressType.Capsule }).height(55)
  121. .margin({top:20,right:20,left:20})
  122. .backgroundColor($r('app.color.index_background'))
  123. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  124. Row() {
  125. // 菜单图标
  126. SymbolGlyph($r('sys.symbol.star_trophy'))// .size({ width: 22, height: 22 })
  127. .fontSize(22)
  128. .fontColor([this.themeColor])
  129. .alignSelf(ItemAlign.Center)
  130. .margin({ left: 25 })
  131. // 菜单标题
  132. Text('开始乱码修正')
  133. .margin({ left: 10, right: 20 })
  134. .fontSize(15)
  135. .fontWeight(480)
  136. Blank()
  137. // 右侧箭头
  138. Image($r('app.media.arrow_right'))
  139. .width(22)
  140. .height(22)
  141. .margin({ left: 0, right: 20 })
  142. .align(Alignment.Center)
  143. }
  144. .width('100%')
  145. }
  146. .margin({top:20,right:20,left:20})
  147. .height(55)
  148. .enabled(!this.isEmbedding)
  149. .backgroundColor($r('app.color.start_window_background'))
  150. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  151. .onClick(()=>{
  152. this.startEmbed()
  153. })
  154. }
  155. }
  156. /**
  157. * 开始修复
  158. */
  159. async startEmbed() {
  160. if (this.isEmbedding || !this.selectedFiles || this.selectedFiles.length === 0) {
  161. return;
  162. }
  163. this.isEmbedding = true;
  164. this.currentProgress = 0;
  165. this.currentFileIndex = 0;
  166. this.embedSuccessCount = 0;
  167. this.embedFailedCount = 0;
  168. this.totalFiles = this.selectedFiles.length;
  169. // 清空之前的嵌入状态
  170. this.embedStatusMap = new Map<string, boolean>();
  171. // 创建异步任务数组
  172. const embedTasks = this.selectedFiles.map(async (item: VideoItem, index: number) => {
  173. try {
  174. this.currentFileIndex = index + 1;
  175. this.currentFileName = item.name;
  176. let success = true;
  177. let hwTags = await extractHwMediaMetadata(item.filePath)
  178. let artist = hwTags?.artist|| '';
  179. let title = hwTags?.title|| '';
  180. let album = hwTags?.album|| '';
  181. console.info('onecold 乱码修正 artist='+artist);
  182. console.info('onecold 乱码修正 title='+title);
  183. console.info('onecold 乱码修正 album='+album);
  184. const metadata:FFMpegTags = {
  185. title: title,
  186. artist: artist,
  187. album: album,
  188. };
  189. //内嵌下标签的值,设置overwrite为true会直接修改原文件
  190. const result:boolean = await repairAudioMetadata(
  191. item.filePath,
  192. item.lyricContent||'',
  193. metadata,
  194. true
  195. );
  196. //入库
  197. if (result) {
  198. this.table.updateMediaInfo(item.filePath,title, artist, album,
  199. item.lyricContent||'','','','','','','','','', (success: boolean, error?: string) => {
  200. if (success) {
  201. console.info('onecold 乱码修正 更新库成功')
  202. }else{
  203. console.info('onecold 乱码修正 更新库失败')
  204. }
  205. });
  206. }
  207. // 更新嵌入状态
  208. if (item.filePath) {
  209. this.embedStatusMap.set(item.filePath, success);
  210. }
  211. if (success) {
  212. this.embedSuccessCount++;
  213. } else {
  214. this.embedFailedCount++;
  215. }
  216. // 滚动到当前处理的项目位置
  217. // this.listScroller.scrollToIndex(index);
  218. this.currentProgress = Math.floor(((index + 1) / this.totalFiles) * 100);
  219. return success;
  220. } catch (error) {
  221. console.error(`处理文件 ${item.name} 失败: ${JSON.stringify(error)}`);
  222. this.embedFailedCount++;
  223. return false;
  224. }
  225. });
  226. try {
  227. // 并发执行所有嵌入任务
  228. await Promise.all(embedTasks);
  229. } catch (error) {
  230. this.onFixResult(false)
  231. console.error(`批量修复过程出错: ${JSON.stringify(error)}`);
  232. } finally {
  233. ToastUtil.showToast(`修复完成: 成功 ${this.embedSuccessCount}, 失败 ${this.embedFailedCount}`)
  234. this.isEmbedding = false;
  235. this.currentProgress = 0;
  236. this.totalFiles = this.selectedFiles.length;
  237. this.onFixResult(true)
  238. // 可以在这里添加完成后的提示或回调
  239. console.info(`修复完成: 成功 ${this.embedSuccessCount}, 失败 ${this.embedFailedCount}`);
  240. }
  241. }
  242. @Builder
  243. topTitleBar() {
  244. // 顶部安全区和自定义标题栏
  245. Column() {
  246. // 顶部安全区
  247. Blank()
  248. .height(this.topRectHeight)
  249. .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
  250. .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  251. // 自定义标题栏(Stack实现绝对居中)
  252. Stack() {
  253. // 居中标题
  254. Text('批量乱码修正')
  255. .fontSize(18)
  256. .fontColor(Color.White)
  257. .align(Alignment.Center)
  258. // 左右按钮
  259. Row() {
  260. Image($r('app.media.left_back_white'))
  261. .width(26)
  262. .height(26)
  263. .margin({ left: 12, right: 8 })
  264. .onClick(() => {
  265. this.getUIContext().animateTo({ duration: 666 }, () => {
  266. // 动画闭包内控制Image组件的出现和消失
  267. this.isFixMessy = !this.isFixMessy
  268. })
  269. })
  270. Blank().flexGrow(1)
  271. Blank().width(32)
  272. }
  273. .height(48)
  274. .width('100%')
  275. .alignItems(VerticalAlign.Center)
  276. }
  277. .height(48)
  278. .width('100%')
  279. .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
  280. }
  281. }
  282. @Builder
  283. private MusicItem(item: VideoItem, index?: number) {
  284. Button({ type: ButtonType.Normal, stateEffect: true }) {
  285. Column() {
  286. Row() {
  287. SymbolGlyph($r('sys.symbol.music'))
  288. .fontSize(20)
  289. .fontColor([this.themeColor])
  290. .alignSelf(ItemAlign.Center)
  291. .padding({ left: 25 })
  292. Text(item.name)
  293. .fontSize(14)
  294. .maxLines(1)
  295. .padding({ left: 5 })
  296. .textOverflow({ overflow: TextOverflow.MARQUEE })
  297. .animation({
  298. duration: 555,
  299. curve: 'Linear',
  300. })
  301. .fontColor(Color.Gray)
  302. .margin({ left: 8 })
  303. Blank()
  304. // 根据嵌入状态显示勾选图标
  305. SymbolGlyph($r('sys.symbol.checkmark_circle'))
  306. .fontSize(20)
  307. .fontColor([this.themeColor])
  308. .alignSelf(ItemAlign.Center)
  309. .padding({ right: 25 })
  310. .animation({
  311. duration: 666,
  312. curve: 'ease-in-out' // 可选动画曲线
  313. })
  314. .visibility(this.embedStatusMap.get(item.filePath)?Visibility.Visible:Visibility.None)
  315. }
  316. .layoutWeight(1)
  317. .height('100%')
  318. .width('100%')
  319. }
  320. }
  321. .backgroundColor(Color.Transparent)
  322. .width('100%')
  323. .height(40)
  324. }
  325. }