1import {SortType} from '@/constants/commonConst.ts'; 2import {isSameMediaItem} from '@/utils/mediaItem.ts'; 3import {createMediaIndexMap} from '@/utils/mediaIndexMap.ts'; 4 5// Bug: localeCompare is slow sometimes https://github.com/facebook/hermes/issues/867 6const collator = new Intl.Collator('zh'); 7 8/// Compare Functions 9 10const compareTitle = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) => 11 collator.compare(a.title, b.title); 12const compareArtist = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) => 13 collator.compare(a.artist, b.artist); 14const compareAlbum = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) => 15 collator.compare(a.album, b.album); 16const compareTimeNewToOld = (b: IMusic.IMusicItem, a: IMusic.IMusicItem) => { 17 const timestamp = (a.$timestamp || 0) - (b.$timestamp || 0); 18 if (timestamp === 0) { 19 return (a.$sortIndex || 0) - (b.$sortIndex || 0); 20 } else { 21 return timestamp; 22 } 23}; 24 25const compareTimeOldToNew = (a: IMusic.IMusicItem, b: IMusic.IMusicItem) => { 26 const timestamp = (a.$timestamp || 0) - (b.$timestamp || 0); 27 if (timestamp === 0) { 28 return (a.$sortIndex || 0) - (b.$sortIndex || 0); 29 } else { 30 return timestamp; 31 } 32}; 33 34const compareFunctionMap = { 35 [SortType.Title]: compareTitle, 36 [SortType.Artist]: compareArtist, 37 [SortType.Album]: compareAlbum, 38 [SortType.Newest]: compareTimeNewToOld, 39 [SortType.Oldest]: compareTimeOldToNew, 40} as const; 41 42export default class SortedMusicList { 43 private array: IMusic.IMusicItem[] = []; 44 45 private sortType: SortType = SortType.None; 46 47 private countMap = new Map<string, Set<string>>(); 48 49 get musicList() { 50 return this.array; 51 } 52 53 get firstMusic() { 54 return this.array[0] || null; 55 } 56 57 get platforms() { 58 return [...this.countMap.keys()]; 59 } 60 61 get length() { 62 return this.array.length; 63 } 64 65 constructor( 66 musicItems: IMusic.IMusicItem[], 67 sortType = SortType.None, 68 skipInitialSort = false, 69 ) { 70 this.array = [...musicItems]; 71 this.addToCountMap(this.array); 72 this.sortType = sortType; 73 74 if (!skipInitialSort) { 75 this.resort(); 76 } 77 } 78 79 at(index: number) { 80 return this.array[index] || null; 81 } 82 83 has(musicItem: IMusic.IMusicItem | null) { 84 if (!musicItem) { 85 return false; 86 } 87 const platform = musicItem.platform.toString(); 88 const id = musicItem.id.toString(); 89 90 return this.countMap.get(platform)?.has(id) || false; 91 } 92 93 // 设置排序类型 94 setSortType(sortType: SortType) { 95 if ( 96 this.sortType === sortType && 97 this.sortType !== SortType.None && 98 this.sortType 99 ) { 100 return; 101 } 102 this.sortType = sortType; 103 this.resort(); 104 } 105 106 manualSort(newMusicItems: IMusic.IMusicItem[]) { 107 this.array = newMusicItems; 108 this.sortType = SortType.None; 109 } 110 111 add(musicItems: IMusic.IMusicItem[]) { 112 musicItems = musicItems.filter(it => !this.has(it)); 113 if (!musicItems.length) { 114 return 0; 115 } 116 this.addToCountMap(musicItems); 117 118 if (!compareFunctionMap[this.sortType]) { 119 this.array = musicItems.concat(this.array); 120 return musicItems.length; 121 } 122 123 // 如果歌单内歌曲比较少 124 if ( 125 this.array.length + musicItems.length < 500 || 126 musicItems.length / (this.array.length + 1) > 10 127 ) { 128 this.array = musicItems.concat(this.array); 129 this.resort(); 130 return musicItems.length; 131 } 132 // 如果歌单内歌曲比较多 133 musicItems.sort(compareFunctionMap[this.sortType]); 134 this.array = this.mergeArray(musicItems, this.array, this.sortType); 135 return musicItems.length; 136 } 137 138 remove(musicItems: IMusic.IMusicItem[]) { 139 const indexMap = createMediaIndexMap(musicItems); 140 141 this.array = this.array.filter(it => !indexMap.has(it)); 142 this.removeFromCountMap(musicItems); 143 } 144 145 removeByIndex(indices: number[]) { 146 const indicesSet = new Set(indices); 147 const removedItems: IMusic.IMusicItem[] = []; 148 149 this.array = this.array.filter((it, index) => { 150 if (indicesSet.has(index)) { 151 removedItems.push(it); 152 return false; 153 } 154 return true; 155 }); 156 157 this.removeFromCountMap(removedItems); 158 } 159 160 clearAll() { 161 this.array = []; 162 } 163 164 private addToCountMap(musicItems: IMusic.IMusicItem[]) { 165 for (let musicItem of musicItems) { 166 const platform = musicItem.platform.toString(); 167 const id = musicItem.id.toString(); 168 169 if (this.countMap.has(platform)) { 170 this.countMap.get(platform)!.add(id); 171 } else { 172 this.countMap.set(platform, new Set([id])); 173 } 174 } 175 } 176 177 private removeFromCountMap(musicItems: IMusic.IMusicItem[]) { 178 for (let musicItem of musicItems) { 179 const platform = musicItem.platform.toString(); 180 const id = musicItem.id.toString(); 181 182 if (this.countMap.has(platform)) { 183 const set = this.countMap.get(platform)!; 184 set.delete(id); 185 if (set.size === 0) { 186 this.countMap.delete(platform); 187 } 188 } 189 } 190 } 191 192 /** 193 * 合并两个有序列表 194 * @param musicList1 195 * @param musicList2 196 * @param sortType 197 * @private 198 */ 199 private mergeArray( 200 musicList1: IMusic.IMusicItem[], 201 musicList2: IMusic.IMusicItem[], 202 sortType: SortType, 203 ) { 204 // 无序 205 const compareFn = compareFunctionMap[sortType]; 206 207 if (!compareFn) { 208 return musicList1.concat(musicList2); 209 } 210 211 let [p1, p2] = [0, 0]; 212 let resultArray: IMusic.IMusicItem[] = []; 213 let peek1: IMusic.IMusicItem, peek2: IMusic.IMusicItem; 214 while (p1 < musicList1.length && p2 < musicList2.length) { 215 peek1 = musicList1[p1]; 216 peek2 = musicList2[p2]; 217 218 if (compareFn(peek1, peek2) < 0) { 219 resultArray.push(peek1); 220 ++p1; 221 } else { 222 resultArray.push(peek2); 223 ++p2; 224 } 225 } 226 227 if (p1 < musicList1.length) { 228 return resultArray.concat(musicList1.slice(p1)); 229 } 230 if (p2 < musicList2.length) { 231 return resultArray.concat(musicList2.slice(p2)); 232 } 233 234 return resultArray; 235 } 236 237 /** 238 * 寻找musicItem 239 * @param musicItem 240 * @private 241 */ 242 public findIndex(musicItem: IMusic.IMusicItem) { 243 if (!compareFunctionMap[this.sortType]) { 244 return this.array.find(it => isSameMediaItem(it, musicItem)); 245 } 246 let [left, right] = [0, this.array.length]; 247 let mid: number; 248 249 while (left < right) { 250 mid = Math.floor((left + right) / 2); 251 let compareResult = compareFunctionMap[this.sortType]( 252 this.array[mid], 253 musicItem, 254 ); 255 256 if (compareResult < 0) { 257 left = mid + 1; 258 } else if (compareResult === 0) { 259 left = mid; 260 break; 261 } else { 262 right = mid; 263 } 264 } 265 266 return left === right ? -1 : left; 267 } 268 269 // 重新排序 270 private resort() { 271 const compareFn = compareFunctionMap[this.sortType]; 272 273 if (!compareFn) { 274 return; 275 } 276 this.array.sort(compareFn); 277 this.array = [...this.array]; 278 return; 279 } 280} 281