1import { 2 copyFile, 3 exists, 4 readDir, 5 readFile, 6 unlink, 7 writeFile, 8} from 'react-native-fs'; 9import CryptoJs from 'crypto-js'; 10import dayjs from 'dayjs'; 11import axios from 'axios'; 12import bigInt from 'big-integer'; 13import qs from 'qs'; 14import {InteractionManager, ToastAndroid} from 'react-native'; 15import pathConst from '@/constants/pathConst'; 16import {compare, satisfies} from 'compare-versions'; 17import DeviceInfo from 'react-native-device-info'; 18import StateMapper from '@/utils/stateMapper'; 19import MediaMeta from './mediaMeta'; 20import {nanoid} from 'nanoid'; 21import {devLog, errorLog, trace} from '../utils/log'; 22import Cache from './cache'; 23import { 24 getInternalData, 25 InternalDataType, 26 isSameMediaItem, 27 resetMediaItem, 28} from '@/utils/mediaItem'; 29import { 30 CacheControl, 31 emptyFunction, 32 internalSerializeKey, 33 localPluginHash, 34 localPluginPlatform, 35} from '@/constants/commonConst'; 36import delay from '@/utils/delay'; 37import * as cheerio from 'cheerio'; 38import CookieManager from '@react-native-cookies/cookies'; 39import he from 'he'; 40import Network from './network'; 41import LocalMusicSheet from './localMusicSheet'; 42import {FileSystem} from 'react-native-file-access'; 43import Mp3Util from '@/native/mp3Util'; 44import {PluginMeta} from './pluginMeta'; 45import {useEffect, useState} from 'react'; 46 47axios.defaults.timeout = 1500; 48 49const sha256 = CryptoJs.SHA256; 50 51export enum PluginStateCode { 52 /** 版本不匹配 */ 53 VersionNotMatch = 'VERSION NOT MATCH', 54 /** 无法解析 */ 55 CannotParse = 'CANNOT PARSE', 56} 57 58//#region 插件类 59export class Plugin { 60 /** 插件名 */ 61 public name: string; 62 /** 插件的hash,作为唯一id */ 63 public hash: string; 64 /** 插件状态:激活、关闭、错误 */ 65 public state: 'enabled' | 'disabled' | 'error'; 66 /** 插件支持的搜索类型 */ 67 public supportedSearchType?: string; 68 /** 插件状态信息 */ 69 public stateCode?: PluginStateCode; 70 /** 插件的实例 */ 71 public instance: IPlugin.IPluginInstance; 72 /** 插件路径 */ 73 public path: string; 74 /** 插件方法 */ 75 public methods: PluginMethods; 76 /** 用户输入 */ 77 public userEnv?: Record<string, string>; 78 79 constructor( 80 funcCode: string | (() => IPlugin.IPluginInstance), 81 pluginPath: string, 82 ) { 83 this.state = 'enabled'; 84 let _instance: IPlugin.IPluginInstance; 85 try { 86 if (typeof funcCode === 'string') { 87 // eslint-disable-next-line no-new-func 88 _instance = Function(` 89 'use strict'; 90 try { 91 return ${funcCode}; 92 } catch(e) { 93 return null; 94 } 95 `)()({ 96 CryptoJs, 97 axios, 98 dayjs, 99 cheerio, 100 bigInt, 101 qs, 102 he, 103 CookieManager: { 104 flush: CookieManager.flush, 105 get: CookieManager.get, 106 }, 107 }); 108 } else { 109 _instance = funcCode(); 110 } 111 this.checkValid(_instance); 112 } catch (e: any) { 113 this.state = 'error'; 114 this.stateCode = PluginStateCode.CannotParse; 115 if (e?.stateCode) { 116 this.stateCode = e.stateCode; 117 } 118 errorLog(`${pluginPath}插件无法解析 `, { 119 stateCode: this.stateCode, 120 message: e?.message, 121 stack: e?.stack, 122 }); 123 _instance = e?.instance ?? { 124 _path: '', 125 platform: '', 126 appVersion: '', 127 async getMediaSource() { 128 return null; 129 }, 130 async search() { 131 return {}; 132 }, 133 async getAlbumInfo() { 134 return null; 135 }, 136 }; 137 } 138 this.instance = _instance; 139 this.path = pluginPath; 140 this.name = _instance.platform; 141 if (this.instance.platform === '') { 142 this.hash = ''; 143 } else { 144 if (typeof funcCode === 'string') { 145 this.hash = sha256(funcCode).toString(); 146 } else { 147 this.hash = sha256(funcCode.toString()).toString(); 148 } 149 } 150 151 // 放在最后 152 this.methods = new PluginMethods(this); 153 } 154 155 private checkValid(_instance: IPlugin.IPluginInstance) { 156 /** 版本号校验 */ 157 if ( 158 _instance.appVersion && 159 !satisfies(DeviceInfo.getVersion(), _instance.appVersion) 160 ) { 161 throw { 162 instance: _instance, 163 stateCode: PluginStateCode.VersionNotMatch, 164 }; 165 } 166 return true; 167 } 168} 169//#endregion 170 171//#region 基于插件类封装的方法,供给APP侧直接调用 172/** 有缓存等信息 */ 173class PluginMethods implements IPlugin.IPluginInstanceMethods { 174 private plugin; 175 constructor(plugin: Plugin) { 176 this.plugin = plugin; 177 } 178 /** 搜索 */ 179 async search<T extends ICommon.SupportMediaType>( 180 query: string, 181 page: number, 182 type: T, 183 ): Promise<IPlugin.ISearchResult<T>> { 184 if (!this.plugin.instance.search) { 185 return { 186 isEnd: true, 187 data: [], 188 }; 189 } 190 191 const result = 192 (await this.plugin.instance.search(query, page, type)) ?? {}; 193 if (Array.isArray(result.data)) { 194 result.data.forEach(_ => { 195 resetMediaItem(_, this.plugin.name); 196 }); 197 return { 198 isEnd: result.isEnd ?? true, 199 data: result.data, 200 }; 201 } 202 return { 203 isEnd: true, 204 data: [], 205 }; 206 } 207 208 /** 获取真实源 */ 209 async getMediaSource( 210 musicItem: IMusic.IMusicItemBase, 211 quality: IMusic.IQualityKey = 'standard', 212 retryCount = 1, 213 notUpdateCache = false, 214 ): Promise<IPlugin.IMediaSourceResult | null> { 215 // 1. 本地搜索 其实直接读mediameta就好了 216 const localPath = 217 getInternalData<string>(musicItem, InternalDataType.LOCALPATH) ?? 218 getInternalData<string>( 219 LocalMusicSheet.isLocalMusic(musicItem), 220 InternalDataType.LOCALPATH, 221 ); 222 if (localPath && (await FileSystem.exists(localPath))) { 223 trace('本地播放', localPath); 224 return { 225 url: localPath, 226 }; 227 } 228 if (musicItem.platform === localPluginPlatform) { 229 throw new Error('本地音乐不存在'); 230 } 231 // 2. 缓存播放 232 const mediaCache = Cache.get(musicItem); 233 const pluginCacheControl = 234 this.plugin.instance.cacheControl ?? 'no-cache'; 235 if ( 236 mediaCache && 237 mediaCache?.qualities?.[quality]?.url && 238 (pluginCacheControl === CacheControl.Cache || 239 (pluginCacheControl === CacheControl.NoCache && 240 Network.isOffline())) 241 ) { 242 trace('播放', '缓存播放'); 243 const qualityInfo = mediaCache.qualities[quality]; 244 return { 245 url: qualityInfo.url, 246 headers: mediaCache.headers, 247 userAgent: 248 mediaCache.userAgent ?? mediaCache.headers?.['user-agent'], 249 }; 250 } 251 // 3. 插件解析 252 if (!this.plugin.instance.getMediaSource) { 253 return {url: musicItem?.qualities?.[quality]?.url ?? musicItem.url}; 254 } 255 try { 256 const {url, headers} = (await this.plugin.instance.getMediaSource( 257 musicItem, 258 quality, 259 )) ?? {url: musicItem?.qualities?.[quality]?.url}; 260 if (!url) { 261 throw new Error('NOT RETRY'); 262 } 263 trace('播放', '插件播放'); 264 const result = { 265 url, 266 headers, 267 userAgent: headers?.['user-agent'], 268 } as IPlugin.IMediaSourceResult; 269 270 if ( 271 pluginCacheControl !== CacheControl.NoStore && 272 !notUpdateCache 273 ) { 274 Cache.update(musicItem, [ 275 ['headers', result.headers], 276 ['userAgent', result.userAgent], 277 [`qualities.${quality}.url`, url], 278 ]); 279 } 280 281 return result; 282 } catch (e: any) { 283 if (retryCount > 0 && e?.message !== 'NOT RETRY') { 284 await delay(150); 285 return this.getMediaSource(musicItem, quality, --retryCount); 286 } 287 errorLog('获取真实源失败', e?.message); 288 devLog('error', '获取真实源失败', e, e?.message); 289 return null; 290 } 291 } 292 293 /** 获取音乐详情 */ 294 async getMusicInfo( 295 musicItem: ICommon.IMediaBase, 296 ): Promise<Partial<IMusic.IMusicItem> | null> { 297 if (!this.plugin.instance.getMusicInfo) { 298 return null; 299 } 300 try { 301 return ( 302 this.plugin.instance.getMusicInfo( 303 resetMediaItem(musicItem, undefined, true), 304 ) ?? null 305 ); 306 } catch (e: any) { 307 devLog('error', '获取音乐详情失败', e, e?.message); 308 return null; 309 } 310 } 311 312 /** 获取歌词 */ 313 async getLyric( 314 musicItem: IMusic.IMusicItemBase, 315 from?: IMusic.IMusicItemBase, 316 ): Promise<ILyric.ILyricSource | null> { 317 // 1.额外存储的meta信息 318 const meta = MediaMeta.get(musicItem); 319 if (meta && meta.associatedLrc) { 320 // 有关联歌词 321 if ( 322 isSameMediaItem(musicItem, from) || 323 isSameMediaItem(meta.associatedLrc, musicItem) 324 ) { 325 // 形成环路,断开当前的环 326 await MediaMeta.update(musicItem, { 327 associatedLrc: undefined, 328 }); 329 // 无歌词 330 return null; 331 } 332 // 获取关联歌词 333 const associatedMeta = MediaMeta.get(meta.associatedLrc) ?? {}; 334 const result = await this.getLyric( 335 {...meta.associatedLrc, ...associatedMeta}, 336 from ?? musicItem, 337 ); 338 if (result) { 339 // 如果有关联歌词,就返回关联歌词,深度优先 340 return result; 341 } 342 } 343 const cache = Cache.get(musicItem); 344 let rawLrc = meta?.rawLrc || musicItem.rawLrc || cache?.rawLrc; 345 let lrcUrl = meta?.lrc || musicItem.lrc || cache?.lrc; 346 // 如果存在文本 347 if (rawLrc) { 348 return { 349 rawLrc, 350 lrc: lrcUrl, 351 }; 352 } 353 // 2.本地缓存 354 const localLrc = 355 meta?.[internalSerializeKey]?.local?.localLrc || 356 cache?.[internalSerializeKey]?.local?.localLrc; 357 if (localLrc && (await exists(localLrc))) { 358 rawLrc = await readFile(localLrc, 'utf8'); 359 return { 360 rawLrc, 361 lrc: lrcUrl, 362 }; 363 } 364 // 3.优先使用url 365 if (lrcUrl) { 366 try { 367 // 需要超时时间 axios timeout 但是没生效 368 rawLrc = (await axios.get(lrcUrl, {timeout: 1500})).data; 369 return { 370 rawLrc, 371 lrc: lrcUrl, 372 }; 373 } catch { 374 lrcUrl = undefined; 375 } 376 } 377 // 4. 如果地址失效 378 if (!lrcUrl) { 379 // 插件获得url 380 try { 381 let lrcSource; 382 if (from) { 383 lrcSource = await PluginManager.getByMedia( 384 musicItem, 385 )?.instance?.getLyric?.( 386 resetMediaItem(musicItem, undefined, true), 387 ); 388 } else { 389 lrcSource = await this.plugin.instance?.getLyric?.( 390 resetMediaItem(musicItem, undefined, true), 391 ); 392 } 393 394 rawLrc = lrcSource?.rawLrc; 395 lrcUrl = lrcSource?.lrc; 396 } catch (e: any) { 397 trace('插件获取歌词失败', e?.message, 'error'); 398 devLog('error', '插件获取歌词失败', e, e?.message); 399 } 400 } 401 // 5. 最后一次请求 402 if (rawLrc || lrcUrl) { 403 const filename = `${pathConst.lrcCachePath}${nanoid()}.lrc`; 404 if (lrcUrl) { 405 try { 406 rawLrc = (await axios.get(lrcUrl, {timeout: 1500})).data; 407 } catch {} 408 } 409 if (rawLrc) { 410 await writeFile(filename, rawLrc, 'utf8'); 411 // 写入缓存 412 Cache.update(musicItem, [ 413 [`${internalSerializeKey}.local.localLrc`, filename], 414 ]); 415 // 如果有meta 416 if (meta) { 417 MediaMeta.update(musicItem, [ 418 [`${internalSerializeKey}.local.localLrc`, filename], 419 ]); 420 } 421 return { 422 rawLrc, 423 lrc: lrcUrl, 424 }; 425 } 426 } 427 // 6. 如果是本地文件 428 const isDownloaded = LocalMusicSheet.isLocalMusic(musicItem); 429 if (musicItem.platform !== localPluginPlatform && isDownloaded) { 430 const res = await localFilePlugin.instance!.getLyric!(isDownloaded); 431 if (res) { 432 return res; 433 } 434 } 435 devLog('warn', '无歌词'); 436 437 return null; 438 } 439 440 /** 获取歌词文本 */ 441 async getLyricText( 442 musicItem: IMusic.IMusicItem, 443 ): Promise<string | undefined> { 444 return (await this.getLyric(musicItem))?.rawLrc; 445 } 446 447 /** 获取专辑信息 */ 448 async getAlbumInfo( 449 albumItem: IAlbum.IAlbumItemBase, 450 ): Promise<IAlbum.IAlbumItem | null> { 451 if (!this.plugin.instance.getAlbumInfo) { 452 return {...albumItem, musicList: []}; 453 } 454 try { 455 const result = await this.plugin.instance.getAlbumInfo( 456 resetMediaItem(albumItem, undefined, true), 457 ); 458 if (!result) { 459 throw new Error(); 460 } 461 result?.musicList?.forEach(_ => { 462 resetMediaItem(_, this.plugin.name); 463 }); 464 465 return {...albumItem, ...result}; 466 } catch (e: any) { 467 trace('获取专辑信息失败', e?.message); 468 devLog('error', '获取专辑信息失败', e, e?.message); 469 470 return {...albumItem, musicList: []}; 471 } 472 } 473 474 /** 查询作者信息 */ 475 async getArtistWorks<T extends IArtist.ArtistMediaType>( 476 artistItem: IArtist.IArtistItem, 477 page: number, 478 type: T, 479 ): Promise<IPlugin.ISearchResult<T>> { 480 if (!this.plugin.instance.getArtistWorks) { 481 return { 482 isEnd: true, 483 data: [], 484 }; 485 } 486 try { 487 const result = await this.plugin.instance.getArtistWorks( 488 artistItem, 489 page, 490 type, 491 ); 492 if (!result.data) { 493 return { 494 isEnd: true, 495 data: [], 496 }; 497 } 498 result.data?.forEach(_ => resetMediaItem(_, this.plugin.name)); 499 return { 500 isEnd: result.isEnd ?? true, 501 data: result.data, 502 }; 503 } catch (e: any) { 504 trace('查询作者信息失败', e?.message); 505 devLog('error', '查询作者信息失败', e, e?.message); 506 507 throw e; 508 } 509 } 510 511 /** 导入歌单 */ 512 async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> { 513 try { 514 const result = 515 (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? []; 516 result.forEach(_ => resetMediaItem(_, this.plugin.name)); 517 return result; 518 } catch (e: any) { 519 console.log(e); 520 devLog('error', '导入歌单失败', e, e?.message); 521 522 return []; 523 } 524 } 525 /** 导入单曲 */ 526 async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> { 527 try { 528 const result = await this.plugin.instance?.importMusicItem?.( 529 urlLike, 530 ); 531 if (!result) { 532 throw new Error(); 533 } 534 resetMediaItem(result, this.plugin.name); 535 return result; 536 } catch (e: any) { 537 devLog('error', '导入单曲失败', e, e?.message); 538 539 return null; 540 } 541 } 542} 543//#endregion 544 545let plugins: Array<Plugin> = []; 546const pluginStateMapper = new StateMapper(() => plugins); 547 548//#region 本地音乐插件 549/** 本地插件 */ 550const localFilePlugin = new Plugin(function () { 551 return { 552 platform: localPluginPlatform, 553 _path: '', 554 async getMusicInfo(musicBase) { 555 const localPath = getInternalData<string>( 556 musicBase, 557 InternalDataType.LOCALPATH, 558 ); 559 if (localPath) { 560 const coverImg = await Mp3Util.getMediaCoverImg(localPath); 561 return { 562 artwork: coverImg, 563 }; 564 } 565 return null; 566 }, 567 async getLyric(musicBase) { 568 const localPath = getInternalData<string>( 569 musicBase, 570 InternalDataType.LOCALPATH, 571 ); 572 let rawLrc: string | null = null; 573 if (localPath) { 574 // 读取内嵌歌词 575 try { 576 rawLrc = await Mp3Util.getLyric(localPath); 577 } catch (e) { 578 console.log('e', e); 579 } 580 if (!rawLrc) { 581 // 读取配置歌词 582 const lastDot = localPath.lastIndexOf('.'); 583 const lrcPath = localPath.slice(0, lastDot) + '.lrc'; 584 585 try { 586 if (await exists(lrcPath)) { 587 rawLrc = await readFile(lrcPath, 'utf8'); 588 } 589 } catch {} 590 } 591 } 592 593 return rawLrc 594 ? { 595 rawLrc, 596 } 597 : null; 598 }, 599 }; 600}, ''); 601localFilePlugin.hash = localPluginHash; 602 603//#endregion 604 605async function setup() { 606 const _plugins: Array<Plugin> = []; 607 try { 608 // 加载插件 609 const pluginsPaths = await readDir(pathConst.pluginPath); 610 for (let i = 0; i < pluginsPaths.length; ++i) { 611 const _pluginUrl = pluginsPaths[i]; 612 trace('初始化插件', _pluginUrl); 613 if ( 614 _pluginUrl.isFile() && 615 (_pluginUrl.name?.endsWith?.('.js') || 616 _pluginUrl.path?.endsWith?.('.js')) 617 ) { 618 const funcCode = await readFile(_pluginUrl.path, 'utf8'); 619 const plugin = new Plugin(funcCode, _pluginUrl.path); 620 const _pluginIndex = _plugins.findIndex( 621 p => p.hash === plugin.hash, 622 ); 623 if (_pluginIndex !== -1) { 624 // 重复插件,直接忽略 625 return; 626 } 627 plugin.hash !== '' && _plugins.push(plugin); 628 } 629 } 630 631 plugins = _plugins; 632 pluginStateMapper.notify(); 633 /** 初始化meta信息 */ 634 PluginMeta.setupMeta(plugins.map(_ => _.name)); 635 } catch (e: any) { 636 ToastAndroid.show( 637 `插件初始化失败:${e?.message ?? e}`, 638 ToastAndroid.LONG, 639 ); 640 errorLog('插件初始化失败', e?.message); 641 throw e; 642 } 643} 644 645// 安装插件 646async function installPlugin(pluginPath: string) { 647 // if (pluginPath.endsWith('.js')) { 648 const funcCode = await readFile(pluginPath, 'utf8'); 649 const plugin = new Plugin(funcCode, pluginPath); 650 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 651 if (_pluginIndex !== -1) { 652 throw new Error('插件已安装'); 653 } 654 if (plugin.hash !== '') { 655 const fn = nanoid(); 656 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 657 await copyFile(pluginPath, _pluginPath); 658 plugin.path = _pluginPath; 659 plugins = plugins.concat(plugin); 660 pluginStateMapper.notify(); 661 return; 662 } 663 throw new Error('插件无法解析'); 664 // } 665 // throw new Error('插件不存在'); 666} 667 668async function installPluginFromUrl(url: string) { 669 try { 670 const funcCode = (await axios.get(url)).data; 671 if (funcCode) { 672 const plugin = new Plugin(funcCode, ''); 673 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 674 if (_pluginIndex !== -1) { 675 // 静默忽略 676 return; 677 } 678 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 679 if (oldVersionPlugin) { 680 if ( 681 compare( 682 oldVersionPlugin.instance.version ?? '', 683 plugin.instance.version ?? '', 684 '>', 685 ) 686 ) { 687 throw new Error('已安装更新版本的插件'); 688 } 689 } 690 691 if (plugin.hash !== '') { 692 const fn = nanoid(); 693 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 694 await writeFile(_pluginPath, funcCode, 'utf8'); 695 plugin.path = _pluginPath; 696 plugins = plugins.concat(plugin); 697 if (oldVersionPlugin) { 698 plugins = plugins.filter( 699 _ => _.hash !== oldVersionPlugin.hash, 700 ); 701 try { 702 await unlink(oldVersionPlugin.path); 703 } catch {} 704 } 705 pluginStateMapper.notify(); 706 return; 707 } 708 throw new Error('插件无法解析!'); 709 } 710 } catch (e: any) { 711 devLog('error', 'URL安装插件失败', e, e?.message); 712 errorLog('URL安装插件失败', e); 713 throw new Error(e?.message ?? ''); 714 } 715} 716 717/** 卸载插件 */ 718async function uninstallPlugin(hash: string) { 719 const targetIndex = plugins.findIndex(_ => _.hash === hash); 720 if (targetIndex !== -1) { 721 try { 722 const pluginName = plugins[targetIndex].name; 723 await unlink(plugins[targetIndex].path); 724 plugins = plugins.filter(_ => _.hash !== hash); 725 pluginStateMapper.notify(); 726 if (plugins.every(_ => _.name !== pluginName)) { 727 await MediaMeta.removePlugin(pluginName); 728 } 729 } catch {} 730 } 731} 732 733async function uninstallAllPlugins() { 734 await Promise.all( 735 plugins.map(async plugin => { 736 try { 737 const pluginName = plugin.name; 738 await unlink(plugin.path); 739 await MediaMeta.removePlugin(pluginName); 740 } catch (e) {} 741 }), 742 ); 743 plugins = []; 744 pluginStateMapper.notify(); 745 746 /** 清除空余文件,异步做就可以了 */ 747 readDir(pathConst.pluginPath) 748 .then(fns => { 749 fns.forEach(fn => { 750 unlink(fn.path).catch(emptyFunction); 751 }); 752 }) 753 .catch(emptyFunction); 754} 755 756async function updatePlugin(plugin: Plugin) { 757 const updateUrl = plugin.instance.srcUrl; 758 if (!updateUrl) { 759 throw new Error('没有更新源'); 760 } 761 try { 762 await installPluginFromUrl(updateUrl); 763 } catch (e: any) { 764 if (e.message === '插件已安装') { 765 throw new Error('当前已是最新版本'); 766 } else { 767 throw e; 768 } 769 } 770} 771 772function getByMedia(mediaItem: ICommon.IMediaBase) { 773 return getByName(mediaItem?.platform); 774} 775 776function getByHash(hash: string) { 777 return hash === localPluginHash 778 ? localFilePlugin 779 : plugins.find(_ => _.hash === hash); 780} 781 782function getByName(name: string) { 783 return name === localPluginPlatform 784 ? localFilePlugin 785 : plugins.find(_ => _.name === name); 786} 787 788function getValidPlugins() { 789 return plugins.filter(_ => _.state === 'enabled'); 790} 791 792function getSearchablePlugins() { 793 return plugins.filter(_ => _.state === 'enabled' && _.instance.search); 794} 795 796function getSortedSearchablePlugins() { 797 return getSearchablePlugins().sort((a, b) => 798 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 799 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 800 0 801 ? -1 802 : 1, 803 ); 804} 805 806function useSortedPlugins() { 807 const _plugins = pluginStateMapper.useMappedState(); 808 const _pluginMetaAll = PluginMeta.usePluginMetaAll(); 809 810 const [sortedPlugins, setSortedPlugins] = useState( 811 [..._plugins].sort((a, b) => 812 (_pluginMetaAll[a.name]?.order ?? Infinity) - 813 (_pluginMetaAll[b.name]?.order ?? Infinity) < 814 0 815 ? -1 816 : 1, 817 ), 818 ); 819 820 useEffect(() => { 821 InteractionManager.runAfterInteractions(() => { 822 setSortedPlugins( 823 [..._plugins].sort((a, b) => 824 (_pluginMetaAll[a.name]?.order ?? Infinity) - 825 (_pluginMetaAll[b.name]?.order ?? Infinity) < 826 0 827 ? -1 828 : 1, 829 ), 830 ); 831 }); 832 }, [_plugins, _pluginMetaAll]); 833 834 return sortedPlugins; 835} 836 837const PluginManager = { 838 setup, 839 installPlugin, 840 installPluginFromUrl, 841 updatePlugin, 842 uninstallPlugin, 843 getByMedia, 844 getByHash, 845 getByName, 846 getValidPlugins, 847 getSearchablePlugins, 848 getSortedSearchablePlugins, 849 usePlugins: pluginStateMapper.useMappedState, 850 useSortedPlugins, 851 uninstallAllPlugins, 852}; 853 854export default PluginManager; 855