= (p: React.PropsWithChildren
& React.RefAttributes) => React.ReactNode | null
12 |
13 | // type UndefinedOrNever = undefined
14 | type Actions = {
15 | [U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined
16 | }
17 |
18 | type WarpPromiseValue = T extends ((...args: infer P) => Promise)
19 | ? ((...args: P) => Promise)
20 | : T extends ((...args: infer P2) => infer R2)
21 | ? ((...args: P2) => Promise)
22 | : Promise
23 |
24 | type WarpPromiseRecord> = {
25 | [K in keyof T]: WarpPromiseValue
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/delay.ts:
--------------------------------------------------------------------------------
1 | import BackgroundTimer from 'react-native-background-timer';
2 |
3 | export default function (millsecond: number) {
4 | return new Promise(resolve => {
5 | BackgroundTimer.setTimeout(() => {
6 | resolve();
7 | }, millsecond);
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/i18n.ts:
--------------------------------------------------------------------------------
1 | import en from '@/locales/en.json'
2 | import zh from '@/locales/zh.json'
3 | import PersistStatus from '@/store/PersistStatus'
4 | import { getLocales } from 'expo-localization'
5 | import { I18n } from 'i18n-js'
6 | import { GlobalState } from './stateMapper'
7 |
8 | const translations = {
9 | en,
10 | zh,
11 | }
12 |
13 | const i18n = new I18n(translations)
14 |
15 | i18n.enableFallback = true
16 | export const nowLanguage = new GlobalState('zh')
17 |
18 | export const setI18nConfig = () => {
19 | const languageCode = PersistStatus.get('app.language') || getLocales()[0].languageCode || 'zh'
20 | nowLanguage.setValue(languageCode)
21 | i18n.locale = languageCode
22 | }
23 |
24 | export const changeLanguage = (languageCode: string) => {
25 | PersistStatus.set('app.language', languageCode)
26 | i18n.locale = languageCode
27 | nowLanguage.setValue(languageCode)
28 | }
29 |
30 | export default i18n
31 |
--------------------------------------------------------------------------------
/src/utils/mediaIndexMap.ts:
--------------------------------------------------------------------------------
1 | interface IIndexMap {
2 | getIndexMap: () => Record>;
3 | getIndex: (musicItem: ICommon.IMediaBase) => number;
4 | has: (mediaItem: ICommon.IMediaBase) => boolean;
5 | }
6 |
7 | export function createMediaIndexMap(
8 | mediaItems: ICommon.IMediaBase[],
9 | ): IIndexMap {
10 | const indexMap: Record> = {};
11 |
12 | mediaItems.forEach((item, index) => {
13 | // 映射中不存在
14 | if (!indexMap[item.platform]) {
15 | indexMap[item.platform] = {
16 | [item.id]: index,
17 | };
18 | } else {
19 | // 修改映射
20 | indexMap[item.platform][item.id] = index;
21 | }
22 | });
23 |
24 | function getIndexMap() {
25 | return indexMap;
26 | }
27 |
28 | function getIndex(mediaItem: ICommon.IMediaBase) {
29 | if (!mediaItem) {
30 | return -1;
31 | }
32 | return indexMap[mediaItem.platform]?.[mediaItem.id] ?? -1;
33 | }
34 |
35 | function has(mediaItem: ICommon.IMediaBase) {
36 | if (!mediaItem) {
37 | return false;
38 | }
39 |
40 | return indexMap[mediaItem.platform]?.[mediaItem.id] > -1;
41 | }
42 |
43 | return {
44 | getIndexMap,
45 | getIndex,
46 | has,
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/src/utils/mediaItem.ts:
--------------------------------------------------------------------------------
1 | import { internalSerializeKey, sortIndexSymbol, timeStampSymbol } from '@/constants/commonConst'
2 | import MediaMeta from '@/store/mediaExtra'
3 |
4 | /** 获取mediakey */
5 | export function getMediaKey(mediaItem: ICommon.IMediaBase) {
6 | return `${mediaItem.platform}@${mediaItem.id}`
7 | }
8 |
9 | /** 比较两media是否相同 */
10 | export function isSameMediaItem(
11 | a: ICommon.IMediaBase | null | undefined,
12 | b: ICommon.IMediaBase | null | undefined,
13 | ) {
14 | return a && b && a.id == b.id && a.platform === b.platform
15 | }
16 |
17 | /** 查找是否存在 */
18 | export function includesMedia(
19 | a: ICommon.IMediaBase[] | null | undefined,
20 | b: ICommon.IMediaBase | null | undefined,
21 | ) {
22 | if (!a || !b) {
23 | return false
24 | }
25 | return a.findIndex((_) => isSameMediaItem(_, b)) !== -1
26 | }
27 |
28 | /** 获取复位的mediaItem */
29 | // export function resetMediaItem>(
30 | // mediaItem: T,
31 | // platform?: string,
32 | // newObj?: boolean,
33 | // ): T {
34 | // // 本地音乐不做处理
35 | // if (
36 | // mediaItem.platform === localPluginPlatform ||
37 | // platform === localPluginPlatform
38 | // ) {
39 | // return newObj ? {...mediaItem} : mediaItem;
40 | // }
41 | // if (!newObj) {
42 | // mediaItem.platform = platform ?? mediaItem.platform;
43 | // mediaItem[internalSerializeKey] = undefined;
44 | // return mediaItem;
45 | // } else {
46 | // return produce(mediaItem, _ => {
47 | // _.platform = platform ?? mediaItem.platform;
48 | // _[internalSerializeKey] = undefined;
49 | // });
50 | // }
51 | // }
52 |
53 | export function mergeProps(
54 | mediaItem: ICommon.IMediaBase,
55 | props: Record | undefined,
56 | anotherProps?: Record | undefined | null,
57 | ) {
58 | return props
59 | ? {
60 | ...mediaItem,
61 | ...props,
62 | ...(anotherProps ?? {}),
63 | id: mediaItem.id,
64 | platform: mediaItem.platform,
65 | }
66 | : mediaItem
67 | }
68 |
69 | export enum InternalDataType {
70 | LOCALPATH = 'localPath',
71 | }
72 |
73 | export function trimInternalData(mediaItem: ICommon.IMediaBase | null | undefined) {
74 | if (!mediaItem) {
75 | return undefined
76 | }
77 | return {
78 | ...mediaItem,
79 | [internalSerializeKey]: undefined,
80 | }
81 | }
82 |
83 | /** 关联歌词 */
84 | export async function associateLrc(musicItem: ICommon.IMediaBase, linkto: ICommon.IMediaBase) {
85 | if (!musicItem || !linkto) {
86 | throw new Error('')
87 | }
88 |
89 | // 如果相同直接断链
90 | MediaMeta.update(musicItem, {
91 | associatedLrc: isSameMediaItem(musicItem, linkto) ? undefined : linkto,
92 | })
93 | }
94 |
95 | export function sortByTimestampAndIndex(array: any[], newArray = false) {
96 | if (newArray) {
97 | array = [...array]
98 | }
99 | return array.sort((a, b) => {
100 | const ts = a[timeStampSymbol] - b[timeStampSymbol]
101 | if (ts !== 0) {
102 | return ts
103 | }
104 | return a[sortIndexSymbol] - b[sortIndexSymbol]
105 | })
106 | }
107 |
--------------------------------------------------------------------------------
/src/utils/musicInfo/Buffer.js:
--------------------------------------------------------------------------------
1 | class Buffer {
2 | constructor() {
3 | this.cursor = 0;
4 | this.size = 0;
5 | this.data = null;
6 | }
7 |
8 | finished() {
9 | return this.cursor >= this.size;
10 | }
11 |
12 | getByte() {
13 | return this.data[this.cursor++];
14 | }
15 |
16 | move(length) {
17 | let start = this.cursor;
18 | this.cursor = this.cursor + length > this.size ? this.size : this.cursor + length;
19 | let end = this.cursor;
20 | return end - start;
21 | }
22 |
23 | setData(data) {
24 | this.size = data.length;
25 | this.data = data;
26 | this.cursor = 0;
27 | }
28 | }
29 |
30 | export default Buffer;
--------------------------------------------------------------------------------
/src/utils/musicInfo/MusicInfo.d.ts:
--------------------------------------------------------------------------------
1 | declare class MusicInfo {
2 | static getMusicInfoAsync(fileUri: string, options?: MusicInfoOptions): Promise
3 | }
4 |
5 | export declare class MusicInfoOptions {
6 | title?: boolean
7 | artist?: boolean
8 | album?: boolean
9 | genre?: boolean
10 | picture?: boolean
11 | }
12 |
13 | export declare class MusicInfoResponse {
14 | title?: string
15 | artist?: string
16 | album?: string
17 | genre?: string
18 | picture?: Picture
19 | }
20 |
21 | export declare class Picture {
22 | description: string
23 | pictureData: string
24 | }
25 | export default MusicInfo
26 |
--------------------------------------------------------------------------------
/src/utils/musicInfo/MusicInfoResponse.js:
--------------------------------------------------------------------------------
1 | // src/utils/musicInfo/MusicInfoResponse.js
2 |
3 | class MusicInfoResponse {
4 | constructor() {
5 | this.title = '';
6 | this.artist = '';
7 | this.album = '';
8 | this.genre = '';
9 | this.picture = null;
10 | }
11 | }
12 |
13 | export default MusicInfoResponse;
--------------------------------------------------------------------------------
/src/utils/musicInfo/index.js:
--------------------------------------------------------------------------------
1 | import MusicInfo from './MusicInfo';
2 | export default MusicInfo;
--------------------------------------------------------------------------------
/src/utils/rpx.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native'
2 |
3 | const windowWidth = Dimensions.get('window').width
4 | const windowHeight = Dimensions.get('window').height
5 | const minWindowEdge = Math.min(windowHeight, windowWidth)
6 | const maxWindowEdge = Math.max(windowHeight, windowWidth)
7 |
8 | export default function (rpx: number) {
9 | return (rpx / 750) * minWindowEdge
10 | }
11 |
12 | export function vh(pct: number) {
13 | return (pct / 100) * Dimensions.get('window').height
14 | }
15 |
16 | export function vw(pct: number) {
17 | return (pct / 100) * Dimensions.get('window').width
18 | }
19 |
20 | export function vmin(pct: number) {
21 | return (pct / 100) * minWindowEdge
22 | }
23 |
24 | export function vmax(pct: number) {
25 | return (pct / 100) * maxWindowEdge
26 | }
27 |
28 | export function sh(pct: number) {
29 | return (pct / 100) * Dimensions.get('screen').height
30 | }
31 |
32 | export function sw(pct: number) {
33 | return (pct / 100) * Dimensions.get('screen').width
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/safeParse.ts:
--------------------------------------------------------------------------------
1 | export default function (raw?: string) {
2 | try {
3 | if (!raw) {
4 | return null;
5 | }
6 | return JSON.parse(raw) as T;
7 | } catch {
8 | return null;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/stateMapper.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export default class StateMapper {
4 | private getFun: () => T
5 | private cbs: Set = new Set([])
6 | constructor(getFun: () => T) {
7 | this.getFun = getFun
8 | }
9 |
10 | notify = () => {
11 | this.cbs.forEach((_) => _?.())
12 | }
13 |
14 | useMappedState = () => {
15 | const [_state, _setState] = useState(this.getFun)
16 | const updateState = () => {
17 | _setState(this.getFun())
18 | }
19 | useEffect(() => {
20 | this.cbs.add(updateState)
21 | return () => {
22 | this.cbs.delete(updateState)
23 | }
24 | }, [])
25 | return _state
26 | }
27 | }
28 |
29 | type UpdateFunc = (prev: T) => T
30 |
31 | export class GlobalState {
32 | private value: T
33 | private stateMapper: StateMapper
34 |
35 | constructor(initValue: T) {
36 | this.value = initValue
37 | this.stateMapper = new StateMapper(this.getValue)
38 | }
39 |
40 | public getValue = () => {
41 | return this.value
42 | }
43 |
44 | public useValue = () => {
45 | return this.stateMapper.useMappedState()
46 | }
47 |
48 | public setValue = (value: T | UpdateFunc) => {
49 | let newValue: T
50 | if (typeof value === 'function') {
51 | newValue = (value as UpdateFunc)(this.value)
52 | } else {
53 | newValue = value
54 | }
55 |
56 | this.value = newValue
57 | this.stateMapper.notify()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/utils/timeformat.ts:
--------------------------------------------------------------------------------
1 | export default function (time: number) {
2 | time = Math.round(time);
3 | if (time < 60) {
4 | return `00:${time.toFixed(0).padStart(2, '0')}`;
5 | }
6 | const sec = Math.floor(time % 60);
7 | time = Math.floor(time / 60);
8 | const min = time % 60;
9 | time = Math.floor(time / 60);
10 | const formatted = `${min.toString().padStart(2, '0')}:${sec
11 | .toFixed(0)
12 | .padStart(2, '0')}`;
13 | if (time === 0) {
14 | return formatted;
15 | }
16 |
17 | return `${time}:${formatted}`;
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/timingClose.ts:
--------------------------------------------------------------------------------
1 | import myTrackPlayer from '@/helpers/trackPlayerIndex'
2 |
3 | import StateMapper from '@/utils/stateMapper'
4 | import { useEffect, useRef, useState } from 'react'
5 | import BackgroundTimer from 'react-native-background-timer'
6 |
7 | import { logInfo } from '@/helpers/logger'
8 | import { NativeModule, NativeModules } from 'react-native'
9 |
10 | interface INativeUtils extends NativeModule {
11 | exitApp: () => void
12 | checkStoragePermission: () => Promise
13 | requestStoragePermission: () => void
14 | }
15 |
16 | const NativeUtils = NativeModules.NativeUtils
17 | let deadline: number | null = null
18 | const stateMapper = new StateMapper(() => deadline)
19 | // let closeAfterPlayEnd = false;
20 | // const closeAfterPlayEndStateMapper = new StateMapper(() => closeAfterPlayEnd);
21 | let timerId: any
22 |
23 | function setTimingClose(_deadline: number | null) {
24 | deadline = _deadline
25 | stateMapper.notify()
26 | timerId && BackgroundTimer.clearTimeout(timerId)
27 | if (_deadline) {
28 | logInfo('将在', (_deadline - Date.now()) / 1000 / 60, '分钟后暂停播放')
29 | timerId = BackgroundTimer.setTimeout(async () => {
30 | // todo: 播完整首歌再关闭
31 | await myTrackPlayer.pause()
32 | // NativeUtils.exitApp()
33 | // if(closeAfterPlayEnd) {
34 | // myTrackPlayer.addEventListener()
35 | // } else {
36 | // // 立即关闭
37 | // NativeUtils.exitApp();
38 | // }
39 | }, _deadline - Date.now())
40 | } else {
41 | timerId = null
42 | }
43 | }
44 |
45 | function useTimingClose() {
46 | const _deadline = stateMapper.useMappedState()
47 | const [countDown, setCountDown] = useState(deadline ? deadline - Date.now() : null)
48 | const intervalRef = useRef()
49 |
50 | useEffect(() => {
51 | // deadline改变时,更新定时器
52 | // 清除原有的定时器
53 | intervalRef.current && clearInterval(intervalRef.current)
54 | intervalRef.current = null
55 |
56 | // 清空定时
57 | if (!_deadline || _deadline <= Date.now()) {
58 | setCountDown(null)
59 | return
60 | } else {
61 | // 更新倒计时
62 | setCountDown(Math.max(_deadline - Date.now(), 0) / 1000)
63 | intervalRef.current = setInterval(() => {
64 | setCountDown(Math.max(_deadline - Date.now(), 0) / 1000)
65 | }, 1000)
66 | }
67 | }, [_deadline])
68 |
69 | return countDown
70 | }
71 |
72 | export { setTimingClose, useTimingClose }
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": false,
5 | "target": "ES6",
6 | "module": "commonjs",
7 | "moduleResolution": "Node",
8 | "jsx": "react-native",
9 | "lib": ["dom", "esnext"],
10 | "noEmit": true,
11 | "allowSyntheticDefaultImports": true,
12 | "skipLibCheck": true,
13 | "resolveJsonModule": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "@/*": ["./src/*"],
17 | "@/assets/*": ["./assets/*"]
18 | }
19 | },
20 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------