1/** 2 * 歌单管理 3 */ 4import {Immer} from 'immer'; 5import {useEffect, useMemo, useState} from 'react'; 6import {nanoid} from 'nanoid'; 7import {isSameMediaItem} from '@/utils/mediaItem'; 8import storage from '@/core/musicSheet/storage.ts'; 9import migrate, {migrateV2} from '@/core/musicSheet/migrate.ts'; 10import {getDefaultStore, useAtomValue} from 'jotai'; 11import { 12 musicListMap, 13 musicSheetsBaseAtom, 14 starredMusicSheetsAtom, 15} from '@/core/musicSheet/atoms.ts'; 16import {ResumeMode, SortType} from '@/constants/commonConst.ts'; 17import SortedMusicList from '@/core/musicSheet/sortedMusicList.ts'; 18import ee from '@/core/musicSheet/ee.ts'; 19import Config from '@/core/config.ts'; 20 21const produce = new Immer({ 22 autoFreeze: false, 23}).produce; 24 25const defaultSheet: IMusic.IMusicSheetItemBase = { 26 id: 'favorite', 27 coverImg: undefined, 28 title: '我喜欢', 29 worksNum: 0, 30}; 31 32async function setup() { 33 // 升级逻辑 - 从 AsyncStorage 升级到 MMKV 34 await migrate(); 35 try { 36 const allSheets: IMusic.IMusicSheetItemBase[] = storage.getSheets(); 37 38 if (!Array.isArray(allSheets)) { 39 throw new Error('not exist'); 40 } 41 42 let needRestore = false; 43 if (!allSheets.length) { 44 allSheets.push({ 45 ...defaultSheet, 46 }); 47 needRestore = true; 48 } 49 if (allSheets[0].id !== defaultSheet.id) { 50 const defaultSheetIndex = allSheets.findIndex( 51 it => it.id === defaultSheet.id, 52 ); 53 54 if (defaultSheetIndex === -1) { 55 allSheets.unshift({ 56 ...defaultSheet, 57 }); 58 } else { 59 const firstSheet = allSheets.splice(defaultSheetIndex, 1); 60 allSheets.unshift(firstSheet[0]); 61 } 62 needRestore = true; 63 } 64 65 if (needRestore) { 66 await storage.setSheets(allSheets); 67 } 68 69 for (let sheet of allSheets) { 70 const musicList = storage.getMusicList(sheet.id); 71 const sortType = storage.getSheetMeta(sheet.id, 'sort') as SortType; 72 sheet.worksNum = musicList.length; 73 migrateV2.migrate(sheet.id, musicList); 74 musicListMap.set( 75 sheet.id, 76 new SortedMusicList(musicList, sortType, true), 77 ); 78 sheet.worksNum = musicList.length; 79 ee.emit('UpdateMusicList', { 80 sheetId: sheet.id, 81 updateType: 'length', 82 }); 83 } 84 migrateV2.done(); 85 getDefaultStore().set(musicSheetsBaseAtom, allSheets); 86 setupStarredMusicSheets(); 87 } catch (e: any) { 88 if (e.message === 'not exist') { 89 await storage.setSheets([defaultSheet]); 90 await storage.setMusicList(defaultSheet.id, []); 91 getDefaultStore().set(musicSheetsBaseAtom, [defaultSheet]); 92 musicListMap.set( 93 defaultSheet.id, 94 new SortedMusicList([], SortType.None, true), 95 ); 96 } 97 } 98} 99 100// 获取音乐 101function getSortedMusicListBySheetId(sheetId: string) { 102 let musicList: SortedMusicList; 103 if (!musicListMap.has(sheetId)) { 104 musicList = new SortedMusicList([], SortType.None, true); 105 musicListMap.set(sheetId, musicList); 106 } else { 107 musicList = musicListMap.get(sheetId)!; 108 } 109 110 return musicList; 111} 112 113/** 114 * 更新基本信息 115 * @param sheetId 歌单ID 116 * @param data 歌单数据 117 */ 118async function updateMusicSheetBase( 119 sheetId: string, 120 data: Partial<IMusic.IMusicSheetItemBase>, 121) { 122 const musicSheets = getDefaultStore().get(musicSheetsBaseAtom); 123 const targetSheetIndex = musicSheets.findIndex(it => it.id === sheetId); 124 125 if (targetSheetIndex === -1) { 126 return; 127 } 128 129 const newMusicSheets = produce(musicSheets, draft => { 130 draft[targetSheetIndex] = { 131 ...draft[targetSheetIndex], 132 ...data, 133 id: sheetId, 134 }; 135 return draft; 136 }); 137 await storage.setSheets(newMusicSheets); 138 getDefaultStore().set(musicSheetsBaseAtom, newMusicSheets); 139 ee.emit('UpdateSheetBasic', { 140 sheetId, 141 }); 142} 143 144/** 145 * 新建歌单 146 * @param title 歌单名称 147 */ 148async function addSheet(title: string) { 149 const newId = nanoid(); 150 const musicSheets = getDefaultStore().get(musicSheetsBaseAtom); 151 152 const newSheets: IMusic.IMusicSheetItemBase[] = [ 153 musicSheets[0], 154 { 155 title, 156 id: newId, 157 coverImg: undefined, 158 worksNum: 0, 159 createAt: Date.now(), 160 }, 161 ...musicSheets.slice(1), 162 ]; 163 // 写入存储 164 await storage.setSheets(newSheets); 165 await storage.setMusicList(newId, []); 166 167 // 更新状态 168 getDefaultStore().set(musicSheetsBaseAtom, newSheets); 169 let defaultSortType = Config.get('setting.basic.musicOrderInLocalSheet'); 170 if ( 171 defaultSortType && 172 [ 173 SortType.Newest, 174 SortType.Artist, 175 SortType.Album, 176 SortType.Oldest, 177 SortType.Title, 178 ].includes(defaultSortType) 179 ) { 180 storage.setSheetMeta(newId, 'sort', defaultSortType); 181 } else { 182 defaultSortType = SortType.None; 183 } 184 musicListMap.set(newId, new SortedMusicList([], defaultSortType, true)); 185 return newId; 186} 187 188async function resumeSheets( 189 sheets: IMusic.IMusicSheetItem[], 190 resumeMode: ResumeMode, 191) { 192 if (resumeMode === ResumeMode.Append) { 193 // 逆序恢复,最新创建的在最上方 194 for (let i = sheets.length - 1; i >= 0; --i) { 195 const newSheetId = await addSheet(sheets[i].title || ''); 196 await addMusic(newSheetId, sheets[i].musicList || []); 197 } 198 return; 199 } 200 // 1. 分离默认歌单和其他歌单 201 const defaultSheetIndex = sheets.findIndex(it => it.id === defaultSheet.id); 202 203 let exportedDefaultSheet: IMusic.IMusicSheetItem | null = null; 204 205 if (defaultSheetIndex !== -1) { 206 exportedDefaultSheet = sheets.splice(defaultSheetIndex, 1)[0]; 207 } 208 209 // 2. 合并默认歌单 210 await addMusic(defaultSheet.id, exportedDefaultSheet?.musicList || []); 211 212 // 3. 合并其他歌单 213 if (resumeMode === ResumeMode.OverwriteDefault) { 214 // 逆序恢复,最新创建的在最上方 215 for (let i = sheets.length - 1; i >= 0; --i) { 216 const newSheetId = await addSheet(sheets[i].title || ''); 217 await addMusic(newSheetId, sheets[i].musicList || []); 218 } 219 } else { 220 // 合并同名 221 const existsSheetIdMap: Record<string, string> = {}; 222 const allSheets = getDefaultStore().get(musicSheetsBaseAtom); 223 allSheets.forEach(it => { 224 existsSheetIdMap[it.title!] = it.id; 225 }); 226 for (let i = sheets.length - 1; i >= 0; --i) { 227 let newSheetId = existsSheetIdMap[sheets[i].title || '']; 228 if (!newSheetId) { 229 newSheetId = await addSheet(sheets[i].title || ''); 230 } 231 await addMusic(newSheetId, sheets[i].musicList || []); 232 } 233 } 234 235 if (overwrite) { 236 } else { 237 const newSheetId = await addSheet( 238 exportedDefaultSheet?.title || defaultSheet.title!, 239 ); 240 await addMusic(newSheetId, exportedDefaultSheet?.musicList || []); 241 } 242} 243 244function backupSheets() { 245 const allSheets = getDefaultStore().get(musicSheetsBaseAtom); 246 return allSheets.map(it => ({ 247 ...it, 248 musicList: musicListMap.get(it.id)?.musicList || [], 249 })) as IMusic.IMusicSheetItem[]; 250} 251 252/** 253 * 删除歌单 254 * @param sheetId 歌单id 255 */ 256async function removeSheet(sheetId: string) { 257 // 只能删除非默认歌单 258 if (sheetId === defaultSheet.id) { 259 return; 260 } 261 const musicSheets = getDefaultStore().get(musicSheetsBaseAtom); 262 263 // 删除后的歌单 264 const newSheets = musicSheets.filter(item => item.id !== sheetId); 265 266 // 写入存储 267 storage.removeMusicList(sheetId); 268 await storage.setSheets(newSheets); 269 270 // 修改状态 271 getDefaultStore().set(musicSheetsBaseAtom, newSheets); 272 musicListMap.delete(sheetId); 273} 274 275/** 276 * 向歌单内添加音乐 277 * @param sheetId 歌单id 278 * @param musicItem 音乐 279 */ 280async function addMusic( 281 sheetId: string, 282 musicItem: IMusic.IMusicItem | Array<IMusic.IMusicItem>, 283) { 284 const now = Date.now(); 285 if (!Array.isArray(musicItem)) { 286 musicItem = [musicItem]; 287 } 288 const taggedMusicItems = musicItem.map((it, index) => ({ 289 ...it, 290 $timestamp: now, 291 $sortIndex: musicItem.length - index, 292 })); 293 294 let musicList = getSortedMusicListBySheetId(sheetId); 295 296 const addedCount = musicList.add(taggedMusicItems); 297 298 // Update 299 if (!addedCount) { 300 return; 301 } 302 const musicSheets = getDefaultStore().get(musicSheetsBaseAtom); 303 if ( 304 !musicSheets 305 .find(_ => _.id === sheetId) 306 ?.coverImg?.startsWith('file://') 307 ) { 308 await updateMusicSheetBase(sheetId, { 309 coverImg: musicList.at(0)?.artwork, 310 }); 311 } 312 313 // 更新音乐数量 314 getDefaultStore().set( 315 musicSheetsBaseAtom, 316 produce(draft => { 317 const musicSheet = draft.find(it => it.id === sheetId); 318 if (musicSheet) { 319 musicSheet.worksNum = musicList.length; 320 } 321 }), 322 ); 323 324 await storage.setMusicList(sheetId, musicList.musicList); 325 ee.emit('UpdateMusicList', { 326 sheetId, 327 updateType: 'length', 328 }); 329} 330 331async function removeMusicByIndex(sheetId: string, indices: number | number[]) { 332 if (!Array.isArray(indices)) { 333 indices = [indices]; 334 } 335 336 const musicList = getSortedMusicListBySheetId(sheetId); 337 338 musicList.removeByIndex(indices); 339 340 // Update 341 const musicSheets = getDefaultStore().get(musicSheetsBaseAtom); 342 if ( 343 !musicSheets 344 .find(_ => _.id === sheetId) 345 ?.coverImg?.startsWith('file://') 346 ) { 347 await updateMusicSheetBase(sheetId, { 348 coverImg: musicList.at(0)?.artwork, 349 }); 350 } 351 // 更新音乐数量 352 getDefaultStore().set( 353 musicSheetsBaseAtom, 354 produce(draft => { 355 const musicSheet = draft.find(it => it.id === sheetId); 356 if (musicSheet) { 357 musicSheet.worksNum = musicList.length; 358 } 359 }), 360 ); 361 await storage.setMusicList(sheetId, musicList.musicList); 362 ee.emit('UpdateMusicList', { 363 sheetId, 364 updateType: 'length', 365 }); 366} 367 368async function removeMusic( 369 sheetId: string, 370 musicItems: IMusic.IMusicItem | IMusic.IMusicItem[], 371) { 372 if (!Array.isArray(musicItems)) { 373 musicItems = [musicItems]; 374 } 375 376 const musicList = getSortedMusicListBySheetId(sheetId); 377 musicList.remove(musicItems); 378 379 // Update 380 const musicSheets = getDefaultStore().get(musicSheetsBaseAtom); 381 382 let patchData: Partial<IMusic.IMusicSheetItemBase> = {}; 383 if ( 384 !musicSheets 385 .find(_ => _.id === sheetId) 386 ?.coverImg?.startsWith('file://') 387 ) { 388 patchData.coverImg = musicList.at(0)?.artwork; 389 } 390 patchData.worksNum = musicList.length; 391 await updateMusicSheetBase(sheetId, { 392 coverImg: musicList.at(0)?.artwork, 393 }); 394 395 await storage.setMusicList(sheetId, musicList.musicList); 396 ee.emit('UpdateMusicList', { 397 sheetId, 398 updateType: 'length', 399 }); 400} 401 402async function setSortType(sheetId: string, sortType: SortType) { 403 const musicList = getSortedMusicListBySheetId(sheetId); 404 musicList.setSortType(sortType); 405 406 // update 407 await storage.setMusicList(sheetId, musicList.musicList); 408 storage.setSheetMeta(sheetId, 'sort', sortType); 409 ee.emit('UpdateMusicList', { 410 sheetId, 411 updateType: 'resort', 412 }); 413} 414 415async function manualSort( 416 sheetId: string, 417 musicListAfterSort: IMusic.IMusicItem[], 418) { 419 const musicList = getSortedMusicListBySheetId(sheetId); 420 musicList.manualSort(musicListAfterSort); 421 422 // update 423 await storage.setMusicList(sheetId, musicList.musicList); 424 storage.setSheetMeta(sheetId, 'sort', SortType.None); 425 426 ee.emit('UpdateMusicList', { 427 sheetId, 428 updateType: 'resort', 429 }); 430} 431 432function useSheetsBase() { 433 return useAtomValue(musicSheetsBaseAtom); 434} 435 436// sheetId should not change 437function useSheetItem(sheetId: string) { 438 const sheetsBase = useAtomValue(musicSheetsBaseAtom); 439 440 const [sheetItem, setSheetItem] = useState<IMusic.IMusicSheetItem>({ 441 ...(sheetsBase.find(it => it.id === sheetId) || 442 ({} as IMusic.IMusicSheetItemBase)), 443 musicList: musicListMap.get(sheetId)?.musicList || [], 444 }); 445 446 useEffect(() => { 447 const onUpdateMusicList = ({sheetId: updatedSheetId}) => { 448 if (updatedSheetId !== sheetId) { 449 return; 450 } 451 setSheetItem(prev => ({ 452 ...prev, 453 musicList: musicListMap.get(sheetId)?.musicList || [], 454 })); 455 }; 456 457 const onUpdateSheetBasic = ({sheetId: updatedSheetId}) => { 458 if (updatedSheetId !== sheetId) { 459 return; 460 } 461 setSheetItem(prev => ({ 462 ...prev, 463 ...(getDefaultStore() 464 .get(musicSheetsBaseAtom) 465 .find(it => it.id === sheetId) || {}), 466 })); 467 }; 468 ee.on('UpdateMusicList', onUpdateMusicList); 469 ee.on('UpdateSheetBasic', onUpdateSheetBasic); 470 471 return () => { 472 ee.off('UpdateMusicList', onUpdateMusicList); 473 ee.off('UpdateSheetBasic', onUpdateSheetBasic); 474 }; 475 }, []); 476 477 return sheetItem; 478} 479 480function useFavorite(musicItem: IMusic.IMusicItem | null) { 481 const [fav, setFav] = useState(false); 482 483 useEffect(() => { 484 const onUpdateMusicList = ({sheetId: updatedSheetId, updateType}) => { 485 if (updatedSheetId !== defaultSheet.id || updateType === 'resort') { 486 return; 487 } 488 setFav(musicListMap.get(defaultSheet.id)?.has(musicItem) || false); 489 }; 490 ee.on('UpdateMusicList', onUpdateMusicList); 491 492 setFav(musicListMap.get(defaultSheet.id)?.has(musicItem) || false); 493 return () => { 494 ee.off('UpdateMusicList', onUpdateMusicList); 495 }; 496 }, [musicItem]); 497 498 return fav; 499} 500 501async function setupStarredMusicSheets() { 502 const starredSheets: IMusic.IMusicSheetItem[] = 503 storage.getStarredSheets() || []; 504 getDefaultStore().set(starredMusicSheetsAtom, starredSheets); 505} 506 507async function starMusicSheet(musicSheet: IMusic.IMusicSheetItem) { 508 const store = getDefaultStore(); 509 const starredSheets: IMusic.IMusicSheetItem[] = store.get( 510 starredMusicSheetsAtom, 511 ); 512 513 const newVal = [musicSheet, ...starredSheets]; 514 515 store.set(starredMusicSheetsAtom, newVal); 516 await storage.setStarredSheets(newVal); 517} 518 519async function unstarMusicSheet(musicSheet: IMusic.IMusicSheetItemBase) { 520 const store = getDefaultStore(); 521 const starredSheets: IMusic.IMusicSheetItem[] = store.get( 522 starredMusicSheetsAtom, 523 ); 524 525 const newVal = starredSheets.filter( 526 it => 527 !isSameMediaItem( 528 it as ICommon.IMediaBase, 529 musicSheet as ICommon.IMediaBase, 530 ), 531 ); 532 store.set(starredMusicSheetsAtom, newVal); 533 await storage.setStarredSheets(newVal); 534} 535 536function useSheetIsStarred( 537 musicSheet: IMusic.IMusicSheetItem | null | undefined, 538) { 539 // TODO: 类型有问题 540 const musicSheets = useAtomValue(starredMusicSheetsAtom); 541 return useMemo(() => { 542 if (!musicSheet) { 543 return false; 544 } 545 return ( 546 musicSheets.findIndex(it => 547 isSameMediaItem( 548 it as ICommon.IMediaBase, 549 musicSheet as ICommon.IMediaBase, 550 ), 551 ) !== -1 552 ); 553 }, [musicSheet, musicSheets]); 554} 555 556function useStarredSheets() { 557 return useAtomValue(starredMusicSheetsAtom); 558} 559 560/********* MusicSheet Meta ****************/ 561 562const MusicSheet = { 563 setup, 564 addSheet, 565 defaultSheet, 566 addMusic, 567 removeSheet, 568 backupSheets, 569 resumeSheets, 570 removeMusicByIndex, 571 removeMusic, 572 starMusicSheet, 573 unstarMusicSheet, 574 useFavorite, 575 useSheetsBase, 576 useSheetItem, 577 setSortType, 578 useSheetIsStarred, 579 useStarredSheets, 580 updateMusicSheetBase, 581 manualSort, 582 getSheetMeta: storage.getSheetMeta, 583}; 584 585export default MusicSheet; 586