AddSongsToPlaylistDialog.ets 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. import { DialogHelper } from '@pura/harmony-dialog';
  2. import { Playlist } from '../viewmodel/Playlist';
  3. import { ToastUtil, LogUtil, StrUtil } from '@pura/harmony-utils';
  4. import { VideoItem } from '../viewmodel/VideoItem';
  5. import MediaTable from '../common/util/MediaTable';
  6. import PlaylistTable from '../common/util/PlaylistTable';
  7. import { LazyDataSource } from '../common/util/LazyDataSource';
  8. import { ConfigurationConstant, common } from '@kit.AbilityKit';
  9. import { CommonConstants } from '../common/constants/CommonConstants';
  10. // 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
  11. function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
  12. if (isDarkMode) {
  13. // 深色模式下返回更深的灰色或半透明黑色
  14. return `rgba(34,34,34,${alpha + 0.2 > 1 ? 1 : alpha + 0.2})`;
  15. }
  16. const color = themeColor.replace('#', '');
  17. const r = parseInt(color.substring(0, 2), 16);
  18. const g = parseInt(color.substring(2, 4), 16);
  19. const b = parseInt(color.substring(4, 6), 16);
  20. return `rgba(${r},${g},${b},${alpha})`;
  21. }
  22. /**
  23. * 添加歌曲到歌单对话框内容组件
  24. */
  25. @Component
  26. struct AddSongsToPlaylistDialogContent {
  27. context = this.getUIContext().getHostContext() as common.UIAbilityContext
  28. @State opacityItem: number = 1; // 控制透明度的状态变量
  29. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  30. @State isDarkMode: boolean = false
  31. @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
  32. ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
  33. onColorModeChange() {
  34. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  35. }
  36. @State selectedSongs: VideoItem[] = []
  37. @State isLoading: boolean = false
  38. @State searchText: string = ''
  39. @State filteredSongs: VideoItem[] = []
  40. private listScroller: ListScroller = new ListScroller()
  41. @State isSearchMode: boolean = false
  42. @Prop playlist: Playlist
  43. @StorageProp('mediaKuList') mediaKuList: Array<VideoItem> = []; //媒体库文件
  44. private mediaTable: MediaTable = new MediaTable(getContext(this))
  45. private playlistTable: PlaylistTable = new PlaylistTable(getContext(this))
  46. @State dataSource: LazyDataSource<VideoItem> = new LazyDataSource([])
  47. // 分页相关状态
  48. @State currentPage: number = 0
  49. @State hasMoreData: boolean = true
  50. private readonly PAGE_SIZE: number = 50
  51. // 用于存储完整数据的引用
  52. private allSongs: VideoItem[] = []
  53. // 回调函数
  54. onConfirm?: (songs: VideoItem[]) => void
  55. onCancel?: () => void
  56. aboutToAppear() {
  57. // 初始化深色模式状态
  58. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  59. LogUtil.info('heanup AddSongsToPlaylistDialogContent aboutToAppear 开始')
  60. LogUtil.info('heanup 初始化深色模式状态: ' + this.isDarkMode + ', currentMode: ' + this.currentMode)
  61. LogUtil.info('heanup playlist: ' +
  62. (this.playlist ? JSON.stringify({ id: this.playlist.id, name: this.playlist.name }) : 'null'))
  63. console.info('onecold mediaKuList 对话框 aboutToAppear=' + this.mediaKuList.length)
  64. this.allSongs = [...this.mediaKuList]
  65. this.filteredSongs = [...this.mediaKuList]
  66. this.loadInitialData()
  67. }
  68. /**
  69. * 加载初始数据
  70. */
  71. private loadInitialData() {
  72. this.currentPage = 0
  73. this.hasMoreData = true
  74. this.updateListData(false)
  75. }
  76. /**
  77. * 搜索过滤歌曲
  78. */
  79. filterSongs() {
  80. this.isSearchMode = true
  81. if (!this.searchText.trim()) {
  82. this.filteredSongs = [...this.allSongs]
  83. } else {
  84. // 添加空值检查以防止TypeError
  85. if (!this.allSongs || !Array.isArray(this.allSongs)) {
  86. this.filteredSongs = []
  87. } else {
  88. this.filteredSongs = this.allSongs.filter(item => {
  89. //支持模糊匹配和艺术家 专辑匹配
  90. const regex = new RegExp(this.searchText.replace(/\s+/g, '.*'), 'i');
  91. return regex.test(item.name.toLowerCase()) ||
  92. regex.test(item.fileName?.toLowerCase() ?? "") ||
  93. regex.test(item.artist?.toLowerCase() ?? "") ||
  94. regex.test(item.album?.toLowerCase() ?? "")
  95. })
  96. }
  97. }
  98. // 重置分页状态并重新加载数据
  99. this.loadInitialData()
  100. }
  101. // 修改 updateListData 方法,使其更清晰
  102. updateListData(append: boolean = false) {
  103. this.getUIContext().animateTo({ duration: 666 }, () => {
  104. this.opacityItem = 0;
  105. })
  106. setTimeout(() => {
  107. const sourceData = this.filteredSongs
  108. let dataToShow: VideoItem[] = []
  109. if (append) {
  110. // 追加数据模式
  111. const currentData = this.dataSource.dataArray
  112. const startIndex = this.currentPage * this.PAGE_SIZE
  113. const endIndex = Math.min(startIndex + this.PAGE_SIZE, sourceData.length)
  114. const newData = sourceData.slice(startIndex, endIndex)
  115. if (newData.length > 0) {
  116. dataToShow = [...currentData, ...newData]
  117. this.currentPage++
  118. }
  119. } else {
  120. // 初始加载模式
  121. this.currentPage = 1
  122. const endIndex = Math.min(this.PAGE_SIZE, sourceData.length)
  123. dataToShow = sourceData.slice(0, endIndex)
  124. }
  125. // 更新数据源
  126. this.dataSource.pushArrayData(dataToShow)
  127. // 检查是否还有更多数据
  128. const totalLoaded = dataToShow.length
  129. this.hasMoreData = totalLoaded < sourceData.length
  130. this.getUIContext().animateTo({ duration: 666 }, () => {
  131. this.opacityItem = 1
  132. })
  133. }, 200)
  134. }
  135. loadMoreData() {
  136. console.info('loadMoreData called, hasMoreData:', this.hasMoreData, 'isLoading:', this.isLoading)
  137. if (!this.hasMoreData || this.isLoading) {
  138. console.info('loadMoreData skipped - no more data or already loading')
  139. return
  140. }
  141. console.info('loadMoreData executing')
  142. this.isLoading = true
  143. // 使用 setTimeout 模拟异步加载
  144. setTimeout(() => {
  145. this.updateListData(true) // append mode
  146. this.isLoading = false
  147. console.info('loadMoreData completed, current data length:', this.dataSource.dataArray.length)
  148. }, 50)
  149. }
  150. build() {
  151. Column({ space: 16 }) {
  152. // 搜索框
  153. Row({ space: 8 }) {
  154. Image($r('app.media.ic_action_search'))
  155. .width(20)
  156. .height(20)
  157. .fillColor(this.isDarkMode ? '#8E8E93' : $r('app.color.text_color'))
  158. .opacity(0.6)
  159. TextInput({ placeholder: '搜索歌曲、歌手或专辑', text: this.searchText })
  160. .layoutWeight(1)
  161. .height(35)
  162. .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
  163. .borderRadius(8)
  164. .padding({ left: 8, right: 8 })
  165. .fontSize(14)
  166. .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
  167. .placeholderColor(this.isDarkMode ? '#8E8E93' : '#999999')
  168. .onChange((value: string) => {
  169. this.searchText = value
  170. this.filterSongs()
  171. })
  172. }
  173. .width('100%')
  174. .padding(12)
  175. .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
  176. .borderRadius(20)
  177. // 已选择歌曲数量
  178. if (this.selectedSongs.length > 0) {
  179. Row() {
  180. Text(`已选择 ${this.selectedSongs.length} 首歌曲`)
  181. .fontSize(14)
  182. .fontColor(this.isDarkMode ? '#FFFFFF' : this.themeColor)
  183. .fontWeight(FontWeight.Medium)
  184. Blank()
  185. Button(this.selectedSongs.length === this.dataSource.dataArray.length
  186. && this.dataSource.dataArray.length > 0 ? '全不选' : '全选')
  187. .fontSize(12)
  188. .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
  189. .backgroundColor(Color.Transparent)
  190. .height(30)
  191. .padding({ left: 8, right: 8 })
  192. .onClick(() => {
  193. // 判断是否已经全选
  194. if (this.selectedSongs.length === this.dataSource.dataArray.length && this.dataSource.dataArray.length > 0) {
  195. // 如果已经全选,则清空选择
  196. this.selectedSongs = []
  197. } else {
  198. // 如果没有全选,则选择当前显示的所有歌曲
  199. this.selectedSongs = [...this.dataSource.dataArray]
  200. }
  201. })
  202. Button('清空')
  203. .fontSize(12)
  204. .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
  205. .backgroundColor(Color.Transparent)
  206. .height(30)
  207. .padding({ left: 8, right: 8 })
  208. .onClick(() => {
  209. this.selectedSongs = []
  210. })
  211. }
  212. .width('100%')
  213. .padding({ left: 4, right: 4 })
  214. }
  215. // 歌曲列表
  216. if (this.dataSource.dataArray.length === 0 && this.isSearchMode) {
  217. Column({ space: 12 }) {
  218. Image($r('app.media.music_red'))
  219. .width(64)
  220. .height(64)
  221. .opacity(0.3)
  222. Text(this.searchText ? '没有找到匹配的歌曲' : '没有可添加的歌曲')
  223. .fontSize(14)
  224. .fontColor(this.isDarkMode ? '#8E8E93' : '#999999')
  225. }
  226. .width('100%')
  227. .height(300)
  228. .justifyContent(FlexAlign.Center)
  229. } else {
  230. this.getListView()
  231. }
  232. // 按钮区域
  233. Row({ space: 12 }) {
  234. Button('取消')
  235. .width('45%')
  236. .height(40)
  237. .backgroundColor($r('app.color.cancel_button_background'))
  238. .borderRadius(8)
  239. .fontSize(14)
  240. .fontColor($r('app.color.cancel_button_text'))
  241. .onClick(() => {
  242. this.onCancel?.()
  243. DialogHelper.closeDialog('addSongsToPlaylistDialog')
  244. })
  245. Button(`添加${this.selectedSongs.length > 0 ? `(${this.selectedSongs.length})` : ''}`)
  246. .width('45%')
  247. .height(40)
  248. .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode) : this.themeColor)
  249. .borderRadius(8)
  250. .fontSize(14)
  251. .fontColor(Color.White)
  252. .enabled(this.selectedSongs.length > 0)
  253. .opacity(this.selectedSongs.length > 0 ? 1 : 0.5)
  254. .onClick(() => {
  255. this.handleConfirm()
  256. })
  257. }
  258. .width('100%')
  259. .justifyContent(FlexAlign.SpaceBetween)
  260. .margin({ top: 10, bottom: 10 })
  261. }
  262. .width('100%')
  263. .constraintSize({ maxWidth: 400 })
  264. .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.dialog_background'))
  265. .borderRadius(12)
  266. .padding({ left: 20, right: 20 })
  267. }
  268. /**
  269. * 处理确认操作
  270. */
  271. private handleConfirm() {
  272. if (this.selectedSongs.length === 0) {
  273. ToastUtil.showToast(' 请选择至少一首歌曲')
  274. return
  275. }
  276. this.onConfirm?.(this.selectedSongs)
  277. DialogHelper.closeDialog('addSongsToPlaylistDialog')
  278. }
  279. @Builder
  280. getListView() {
  281. Scroll() {
  282. Column({ space: 0 }) {
  283. List({ scroller: this.listScroller }) {
  284. LazyForEach(this.dataSource, (song: VideoItem, index: number) => {
  285. ListItem() {
  286. Row({ space: 12 }) {
  287. Image(StrUtil.isEmpty(song.pixelMapPath) ? $r('app.media.music_red') : song.pixelMapPath)
  288. .fillColor(StrUtil.isEmpty(song.pixelMapPath) ? (this.isDarkMode ? Color.White : this.themeColor) : undefined)
  289. .height(40)
  290. .width(40)
  291. .alt($r('app.media.music_red'))
  292. .borderRadius('100%')
  293. .clip(true)
  294. .draggable(false)
  295. .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿
  296. .autoResize(true) // 重采样,可减少内存占用
  297. .opacity(this.opacityItem) // 绑定透明度
  298. // 歌曲信息
  299. Column({ space: 4 }) {
  300. Text(song.name || '未知歌曲')
  301. .fontSize(14)
  302. .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
  303. .maxLines(1)
  304. .textOverflow({ overflow: TextOverflow.Ellipsis })
  305. .width('100%')
  306. Row() {
  307. if (song.artist) {
  308. Text(song.artist+' '+song.duration)
  309. .fontSize(12)
  310. .fontColor(this.isDarkMode ? '#8E8E93' : '#999999')
  311. .maxLines(1)
  312. .textOverflow({ overflow: TextOverflow.Ellipsis })
  313. .layoutWeight(1)
  314. }
  315. }
  316. .width('100%')
  317. }
  318. .alignItems(HorizontalAlign.Start)
  319. .layoutWeight(1)
  320. // 选择框
  321. Checkbox({ name: 'song_' + song.id })
  322. .select(this.selectedSongs.some(s => s.id === song.id))
  323. .selectedColor($r('app.color.theme_color'))
  324. .shape(CheckBoxShape.ROUNDED_SQUARE)
  325. .onChange((checked: boolean) => {
  326. if (checked) {
  327. if (!this.selectedSongs.some(s => s.id === song.id)) {
  328. this.selectedSongs.push(song)
  329. }
  330. } else {
  331. const index = this.selectedSongs.findIndex(s => s.id === song.id)
  332. if (index > -1) {
  333. this.selectedSongs.splice(index, 1)
  334. }
  335. }
  336. })
  337. }
  338. .width('100%')
  339. .padding(12)
  340. .borderRadius(10)
  341. .onClick(() => {
  342. const isSelected = this.selectedSongs.some(s => s.id === song.id)
  343. if (isSelected) {
  344. const index = this.selectedSongs.findIndex(s => s.id === song.id)
  345. if (index > -1) {
  346. this.selectedSongs.splice(index, 1)
  347. }
  348. } else {
  349. this.selectedSongs.push(song)
  350. }
  351. })
  352. }
  353. .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 })
  354. .animation({ duration: 500 }),
  355. TransitionEffect.scale({ x: 0, y: 0 })))
  356. .clickEffect({ level: ClickEffectLevel.LIGHT })
  357. }, (song: VideoItem) => song.id)
  358. // 添加 footer 来显示加载状态
  359. ListItem() {
  360. this.footer()
  361. }
  362. .visibility(this.hasMoreData ? Visibility.Visible : Visibility.None)
  363. }
  364. .cachedCount(6)
  365. .height('100%')
  366. .scrollBar(BarState.Off)
  367. .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
  368. .onReachEnd(() => {
  369. // 滚动到底部时加载更多数据
  370. console.info('List onReachEnd triggered')
  371. this.loadMoreData()
  372. })
  373. }
  374. }
  375. .scrollBar(BarState.Auto)
  376. .scrollable(ScrollDirection.Vertical)
  377. .height(300)
  378. }
  379. // 改进的 footer Builder
  380. @Builder
  381. footer() {
  382. Column() {
  383. if (this.isLoading) {
  384. Row() {
  385. LoadingProgress()
  386. .height(20)
  387. .width(20)
  388. Text('加载中...')
  389. .fontSize(12)
  390. .fontColor(this.isDarkMode ? '#8E8E93' : '#999999')
  391. .margin({ left: 8 })
  392. }
  393. .width('100%')
  394. .height(40)
  395. .justifyContent(FlexAlign.Center)
  396. } else if (this.hasMoreData) {
  397. Row() {
  398. Text('上拉加载更多')
  399. .fontSize(12)
  400. .fontColor(this.isDarkMode ? '#8E8E93' : '#999999')
  401. }
  402. .width('100%')
  403. .height(40)
  404. .justifyContent(FlexAlign.Center)
  405. }
  406. }
  407. .width('100%')
  408. }
  409. }
  410. /**
  411. * 添加歌曲到歌单对话框管理器
  412. */
  413. @Component
  414. export struct AddSongsToPlaylistDialogManager {
  415. /**
  416. * 添加歌曲到歌单对话框构建器
  417. */
  418. @Builder
  419. buildAddSongsToPlaylistDialog(
  420. playlist: Playlist,
  421. onConfirm: (songs: VideoItem[]) => void,
  422. onCancel?: () => void,
  423. ) {
  424. AddSongsToPlaylistDialogContent({
  425. playlist: playlist,
  426. onConfirm: onConfirm,
  427. onCancel: onCancel
  428. })
  429. }
  430. /**
  431. * 显示添加歌曲到歌单对话框
  432. */
  433. showAddSongsToPlaylistDialog(
  434. playlist: Playlist,
  435. onConfirm: (songs: VideoItem[]) => void,
  436. onCancel?: () => void,
  437. ) {
  438. DialogHelper.showCustomContentDialog({
  439. dialogId: 'addSongsToPlaylistDialog',
  440. title: '添加歌曲到歌单',
  441. autoCancel: true,
  442. contentBuilder: () => {
  443. this.buildAddSongsToPlaylistDialog(playlist, onConfirm,onCancel)
  444. },
  445. buttons: []
  446. })
  447. }
  448. build() {
  449. }
  450. }
  451. // 创建全局实例
  452. const dialogManager = new AddSongsToPlaylistDialogManager()
  453. /**
  454. * 显示添加歌曲到歌单对话框
  455. */
  456. export function showAddSongsToPlaylistDialog(
  457. playlist: Playlist,
  458. onConfirm: (songs: VideoItem[]) => void,
  459. onCancel?: () => void,
  460. ) {
  461. dialogManager.showAddSongsToPlaylistDialog(playlist, onConfirm, onCancel)
  462. }