mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-05 10:38:56 +08:00
422 lines
12 KiB
TypeScript
422 lines
12 KiB
TypeScript
import { Platform, NativeModules, ToastAndroid, BackHandler, Linking, Dimensions, Alert, Appearance, PermissionsAndroid, AppState, StyleSheet, type ScaledSize } from 'react-native'
|
|
// import ExtraDimensions from 'react-native-extra-dimensions-android'
|
|
import Clipboard from '@react-native-clipboard/clipboard'
|
|
import { storageDataPrefix } from '@/config/constant'
|
|
import { gzip, ungzip } from '@/utils/nativeModules/gzip'
|
|
import { readFile, writeFile, temporaryDirectoryPath, unlink } from '@/utils/fs'
|
|
import { isNotificationsEnabled, openNotificationPermissionActivity, shareText } from '@/utils/nativeModules/utils'
|
|
import musicSdk from '@/utils/musicSdk'
|
|
import { getData, removeData, saveData } from '@/plugins/storage'
|
|
import BackgroundTimer from 'react-native-background-timer'
|
|
import { scaleSizeH, scaleSizeW, setSpText } from './pixelRatio'
|
|
|
|
|
|
// https://stackoverflow.com/a/47349998
|
|
let deviceLanguage = Platform.OS === 'ios'
|
|
? NativeModules.SettingsManager.settings.AppleLocale ||
|
|
NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
|
|
: NativeModules.I18nManager.localeIdentifier
|
|
deviceLanguage = typeof deviceLanguage === 'string' ? deviceLanguage.substring(0, 5).toLocaleLowerCase() : ''
|
|
|
|
export const isAndroid = Platform.OS === 'android'
|
|
// @ts-expect-error
|
|
export const osVer = Platform.constants.Release as string
|
|
|
|
export const isActive = () => AppState.currentState == 'active'
|
|
|
|
|
|
// fix https://github.com/facebook/react-native/issues/4934
|
|
export const getWindowSise = (windowDimensions?: ReturnType<(typeof Dimensions)['get']>) => {
|
|
windowDimensions ??= Dimensions.get('window')
|
|
// if (Platform.OS === 'ios') return windowDimensions
|
|
return windowDimensions
|
|
// const windowSize = {
|
|
// width: ExtraDimensions.getRealWindowWidth(),
|
|
// height: ExtraDimensions.getRealWindowHeight(),
|
|
// }
|
|
// if (
|
|
// (windowDimensions.height > windowDimensions.width && windowSize.height < windowSize.width) ||
|
|
// (windowDimensions.width > windowDimensions.height && windowSize.width < windowSize.height)
|
|
// ) {
|
|
// windowSize.height = windowSize.width
|
|
// }
|
|
// windowSize.width = windowDimensions.width
|
|
|
|
// if (ExtraDimensions.isSoftMenuBarEnabled()) {
|
|
// windowSize.height -= ExtraDimensions.getSoftMenuBarHeight()
|
|
// }
|
|
// return windowSize
|
|
}
|
|
|
|
export const checkStoragePermissions = async() => PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE)
|
|
|
|
export const requestStoragePermission = async() => {
|
|
const isGranted = await checkStoragePermissions()
|
|
if (isGranted) return isGranted
|
|
|
|
try {
|
|
const granted = await PermissionsAndroid.requestMultiple(
|
|
[
|
|
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
|
|
],
|
|
// {
|
|
// title: '存储读写权限申请',
|
|
// message:
|
|
// '洛雪音乐助手需要使用存储读写权限才能下载歌曲.',
|
|
// buttonNeutral: '一会再问我',
|
|
// buttonNegative: '取消',
|
|
// buttonPositive: '确定',
|
|
// },
|
|
)
|
|
// console.log(granted)
|
|
// console.log(Object.values(granted).every(r => r === PermissionsAndroid.RESULTS.GRANTED))
|
|
// console.log(PermissionsAndroid.RESULTS)
|
|
const granteds = Object.values(granted)
|
|
return granteds.every(r => r === PermissionsAndroid.RESULTS.GRANTED)
|
|
? true
|
|
: granteds.includes(PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN)
|
|
? null
|
|
: false
|
|
// if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
|
// console.log('You can use the storage')
|
|
// } else {
|
|
// console.log('Storage permission denied')
|
|
// }
|
|
} catch (err: any) {
|
|
// console.warn(err)
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 显示toast
|
|
* @param message 消息
|
|
* @param duration 时长
|
|
* @param position 位置
|
|
*/
|
|
export const toast = (message: string, duration: 'long' | 'short' = 'short', position: 'top' | 'center' | 'bottom' = 'bottom') => {
|
|
let _duration
|
|
switch (duration) {
|
|
case 'long':
|
|
_duration = ToastAndroid.LONG
|
|
break
|
|
case 'short':
|
|
default:
|
|
_duration = ToastAndroid.SHORT
|
|
break
|
|
}
|
|
let _position
|
|
switch (position) {
|
|
case 'top':
|
|
_position = ToastAndroid.TOP
|
|
break
|
|
case 'center':
|
|
_position = ToastAndroid.CENTER
|
|
break
|
|
case 'bottom':
|
|
default:
|
|
_position = ToastAndroid.BOTTOM
|
|
break
|
|
}
|
|
ToastAndroid.showWithGravity(message, _duration, _position)
|
|
}
|
|
|
|
export const openUrl = async(url: string): Promise<void> => Linking.canOpenURL(url).then(async() => Linking.openURL(url))
|
|
|
|
export const assertApiSupport = (source: LX.Source): boolean => {
|
|
return source == 'local' || global.lx.qualityList[source] != null
|
|
}
|
|
|
|
// const handleRemoveDataMultiple = async keys => {
|
|
// await removeDataMultiple(keys.splice(0, 500))
|
|
// if (keys.length) return handleRemoveDataMultiple(keys)
|
|
// }
|
|
|
|
export const exitApp = BackHandler.exitApp
|
|
|
|
export const handleSaveFile = async(path: string, data: any) => {
|
|
// if (!path.endsWith('.json')) path += '.json'
|
|
// const buffer = gzip(data)
|
|
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
|
|
await writeFile(tempFilePath, JSON.stringify(data), 'utf8')
|
|
await gzip(tempFilePath, path)
|
|
await unlink(tempFilePath)
|
|
}
|
|
export const handleReadFile = async<T = unknown>(path: string): Promise<T> => {
|
|
let isJSON = path.endsWith('.json')
|
|
let data
|
|
if (isJSON) {
|
|
data = await readFile(path, 'utf8')
|
|
} else {
|
|
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
|
|
await ungzip(path, tempFilePath)
|
|
data = await readFile(tempFilePath, 'utf8')
|
|
await unlink(tempFilePath)
|
|
}
|
|
return JSON.parse(data)
|
|
}
|
|
|
|
export const confirmDialog = async({
|
|
message = '',
|
|
cancelButtonText = global.i18n.t('dialog_cancel'),
|
|
confirmButtonText = global.i18n.t('dialog_confirm'),
|
|
bgClose = true,
|
|
}) => {
|
|
return new Promise(resolve => {
|
|
Alert.alert('', message, [
|
|
{
|
|
text: cancelButtonText,
|
|
onPress() {
|
|
resolve(false)
|
|
},
|
|
},
|
|
{
|
|
text: confirmButtonText,
|
|
onPress() {
|
|
resolve(true)
|
|
},
|
|
},
|
|
], {
|
|
cancelable: bgClose,
|
|
onDismiss() {
|
|
resolve(false)
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
export const clipboardWriteText = (str: string) => {
|
|
Clipboard.setString(str)
|
|
}
|
|
|
|
|
|
export const checkNotificationPermission = async() => {
|
|
const isHide = await getData(storageDataPrefix.notificationTipEnable)
|
|
if (isHide != null) return
|
|
const enabled = await isNotificationsEnabled()
|
|
if (enabled) return
|
|
Alert.alert(
|
|
global.i18n.t('notifications_check_title'),
|
|
global.i18n.t('notifications_check_tip'),
|
|
[
|
|
{
|
|
text: global.i18n.t('never_show'),
|
|
onPress: () => {
|
|
void saveData(storageDataPrefix.notificationTipEnable, '1')
|
|
toast(global.i18n.t('disagree_tip'))
|
|
},
|
|
},
|
|
{
|
|
text: global.i18n.t('disagree'),
|
|
onPress: () => {
|
|
toast(global.i18n.t('disagree_tip'))
|
|
},
|
|
},
|
|
{
|
|
text: global.i18n.t('agree_go'),
|
|
onPress: () => {
|
|
void openNotificationPermissionActivity()
|
|
},
|
|
},
|
|
],
|
|
)
|
|
}
|
|
export const resetNotificationPermissionCheck = async() => {
|
|
return removeData(storageDataPrefix.notificationTipEnable)
|
|
}
|
|
|
|
export const shareMusic = (shareType: LX.ShareType, downloadFileName: LX.AppSetting['download.fileName'], musicInfo: LX.Music.MusicInfo) => {
|
|
const name = musicInfo.name
|
|
const singer = musicInfo.singer
|
|
const detailUrl = musicInfo.source == 'local' ? '' : musicSdk[musicInfo.source]?.getMusicDetailPageUrl(musicInfo) ?? ''
|
|
const musicTitle = downloadFileName.replace('歌名', name).replace('歌手', singer)
|
|
switch (shareType) {
|
|
case 'system':
|
|
void shareText(global.i18n.t('share_card_title_music', { name }), global.i18n.t('share_title_music'), `${musicTitle.replace(/\s/g, '')}${detailUrl ? '\n' + detailUrl : ''}`)
|
|
break
|
|
case 'clipboard':
|
|
clipboardWriteText(`${musicTitle}${detailUrl ? '\n' + detailUrl : ''}`)
|
|
toast(global.i18n.t('copy_name_tip'))
|
|
break
|
|
}
|
|
}
|
|
|
|
export const onDimensionChange = (handler: (info: { window: ScaledSize, screen: ScaledSize }) => void) => {
|
|
return Dimensions.addEventListener('change', handler)
|
|
}
|
|
|
|
|
|
export const getAppearance = () => {
|
|
return Appearance.getColorScheme() ?? 'light'
|
|
}
|
|
|
|
export const onAppearanceChange = (callback: (colorScheme: Parameters<Parameters<typeof Appearance['addChangeListener']>[0]>[0]['colorScheme']) => void) => {
|
|
return Appearance.addChangeListener(({ colorScheme }) => {
|
|
callback(colorScheme)
|
|
})
|
|
}
|
|
|
|
let isSupportedAutoTheme: boolean | null = null
|
|
export const getIsSupportedAutoTheme = () => {
|
|
if (isSupportedAutoTheme == null) {
|
|
const osVerNum = parseInt(osVer)
|
|
isSupportedAutoTheme = isAndroid
|
|
? osVerNum >= 10
|
|
: osVerNum >= 13
|
|
}
|
|
return isSupportedAutoTheme
|
|
}
|
|
|
|
|
|
export const showImportTip = (type: string) => {
|
|
let message
|
|
switch (type) {
|
|
case 'defautlList':
|
|
case 'playList':
|
|
case 'playList_v2':
|
|
message = global.i18n.t('list_import_tip__playlist')
|
|
break
|
|
case 'setting':
|
|
case 'setting_v2':
|
|
message = global.i18n.t('list_import_tip__setting')
|
|
break
|
|
case 'allData':
|
|
case 'allData_v2':
|
|
message = global.i18n.t('list_import_tip__alldata')
|
|
break
|
|
case 'playListPart':
|
|
case 'playListPart_v2':
|
|
message = global.i18n.t('list_import_tip__playlist_part')
|
|
break
|
|
|
|
default:
|
|
message = global.i18n.t('list_import_tip__unknown')
|
|
break
|
|
}
|
|
Alert.alert(
|
|
global.i18n.t('list_import_tip__failed'),
|
|
message,
|
|
[
|
|
{
|
|
text: global.i18n.t('ok'),
|
|
},
|
|
],
|
|
)
|
|
}
|
|
|
|
|
|
/**
|
|
* 生成节流函数
|
|
* @param fn 回调
|
|
* @param delay 延迟
|
|
* @returns
|
|
*/
|
|
export function throttleBackgroundTimer<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
|
|
let timer: number | null = null
|
|
let _args: Args
|
|
return (...args: Args) => {
|
|
_args = args
|
|
if (timer) return
|
|
timer = BackgroundTimer.setTimeout(() => {
|
|
timer = null
|
|
void fn(..._args)
|
|
}, delay)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成防抖函数
|
|
* @param fn 回调
|
|
* @param delay 延迟
|
|
* @returns
|
|
*/
|
|
export function debounceBackgroundTimer<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
|
|
let timer: number | null = null
|
|
let _args: Args
|
|
return (...args: Args) => {
|
|
_args = args
|
|
if (timer) BackgroundTimer.clearTimeout(timer)
|
|
timer = BackgroundTimer.setTimeout(() => {
|
|
timer = null
|
|
void fn(..._args)
|
|
}, delay)
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
type Styles = StyleSheet.NamedStyles<Record<string, {}>>
|
|
type Style = Styles[keyof Styles]
|
|
const trasformeProps: Array<keyof Style> = [
|
|
// @ts-expect-error
|
|
'fontSize',
|
|
// @ts-expect-error
|
|
'lineHeight',
|
|
// 'margin',
|
|
// 'marginLeft',
|
|
// 'marginRight',
|
|
// 'marginTop',
|
|
// 'marginBottom',
|
|
// 'padding',
|
|
// 'paddingLeft',
|
|
// 'paddingRight',
|
|
// 'paddingTop',
|
|
// 'paddingBottom',
|
|
'left',
|
|
'right',
|
|
'top',
|
|
'bottom',
|
|
]
|
|
export const trasformeStyle = <T extends Style>(styles: T): T => {
|
|
const newStyle: T = { ...styles }
|
|
|
|
for (const [p, v] of Object.entries(newStyle) as Array<[keyof Style, Style[keyof Style]]>) {
|
|
if (typeof v != 'number') continue
|
|
switch (p) {
|
|
case 'height':
|
|
case 'minHeight':
|
|
case 'marginTop':
|
|
case 'marginBottom':
|
|
case 'paddingTop':
|
|
case 'paddingBottom':
|
|
newStyle[p] = scaleSizeH(v)
|
|
break
|
|
case 'width':
|
|
case 'minWidth':
|
|
case 'marginLeft':
|
|
case 'marginRight':
|
|
case 'paddingLeft':
|
|
case 'paddingRight':
|
|
newStyle[p] = scaleSizeW(v)
|
|
break
|
|
case 'padding':
|
|
newStyle.paddingRight = newStyle.paddingLeft = scaleSizeW(v)
|
|
newStyle.paddingBottom = newStyle.paddingTop = scaleSizeH(v)
|
|
break
|
|
case 'margin':
|
|
newStyle.marginRight = newStyle.marginLeft = scaleSizeW(v)
|
|
newStyle.marginBottom = newStyle.marginTop = scaleSizeH(v)
|
|
break
|
|
default:
|
|
// @ts-expect-error
|
|
if (trasformeProps.includes(p)) newStyle[p] = setSpText(v)
|
|
break
|
|
}
|
|
}
|
|
return newStyle
|
|
}
|
|
|
|
export const createStyle = <T extends StyleSheet.NamedStyles<T>>(styles: T | StyleSheet.NamedStyles<T>): T => {
|
|
const newStyle: Record<string, Style> = { ...styles }
|
|
for (const [n, s] of Object.entries(newStyle)) {
|
|
newStyle[n] = trasformeStyle(s)
|
|
}
|
|
return StyleSheet.create(newStyle as StyleSheet.NamedStyles<T>)
|
|
}
|
|
|
|
|
|
export {
|
|
deviceLanguage,
|
|
}
|