mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-03 15:32:10 +08:00
新增自定义源,但需要注意的是,移动端自定义源的环境与PC端不同,某些API不可用
This commit is contained in:
parent
1021bda5d5
commit
e612a169de
@ -188,6 +188,9 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// The version of react-native is set by the React Native Gradle Plugin
|
// The version of react-native is set by the React Native Gradle Plugin
|
||||||
implementation("com.facebook.react:react-android")
|
implementation("com.facebook.react:react-android")
|
||||||
|
// implementation "androidx.javascriptengine:javascriptengine:1.0.0-alpha07"
|
||||||
|
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||||
|
implementation group: 'wang.harlon.quickjs', name: 'wrapper-android', version: '1.0.0'
|
||||||
|
|
||||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||||
|
507
android/app/src/main/assets/script/user-api-preload.js
Normal file
507
android/app/src/main/assets/script/user-api-preload.js
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
|
||||||
|
globalThis.lx_setup = (key, id, name, description, rawScript) => {
|
||||||
|
delete globalThis.setup
|
||||||
|
const _nativeCall = globalThis.__lx_native_call__
|
||||||
|
delete globalThis.__lx_native_call__
|
||||||
|
const set_timeout = globalThis.__lx_native_call__set_timeout
|
||||||
|
delete globalThis.__lx_native_call__set_timeout
|
||||||
|
const utils_str2b64 = globalThis.__lx_native_call__utils_str2b64
|
||||||
|
delete globalThis.__lx_native_call__utils_str2b64
|
||||||
|
const utils_b642buf = globalThis.__lx_native_call__utils_b642buf
|
||||||
|
delete globalThis.__lx_native_call__utils_b642buf
|
||||||
|
const utils_str2md5 = globalThis.__lx_native_call__utils_str2md5
|
||||||
|
delete globalThis.__lx_native_call__utils_str2md5
|
||||||
|
const utils_aes_encrypt = globalThis.__lx_native_call__utils_aes_encrypt
|
||||||
|
delete globalThis.__lx_native_call__utils_aes_encrypt
|
||||||
|
const utils_rsa_encrypt = globalThis.__lx_native_call__utils_rsa_encrypt
|
||||||
|
delete globalThis.__lx_native_call__utils_rsa_encrypt
|
||||||
|
const KEY_PREFIX = {
|
||||||
|
publicKeyStart: '-----BEGIN PUBLIC KEY-----',
|
||||||
|
publicKeyEnd: '-----END PUBLIC KEY-----',
|
||||||
|
privateKeyStart: '-----BEGIN PRIVATE KEY-----',
|
||||||
|
privateKeyEnd: '-----END PRIVATE KEY-----',
|
||||||
|
}
|
||||||
|
const RSA_PADDING = {
|
||||||
|
OAEPWithSHA1AndMGF1Padding: 'RSA/ECB/OAEPWithSHA1AndMGF1Padding',
|
||||||
|
NoPadding: 'RSA/ECB/NoPadding',
|
||||||
|
}
|
||||||
|
const AES_MODE = {
|
||||||
|
CBC_128_PKCS7Padding: 'AES/CBC/PKCS7Padding',
|
||||||
|
ECB_128_NoPadding: 'AES',
|
||||||
|
}
|
||||||
|
const nativeCall = (action, data) => {
|
||||||
|
data = JSON.stringify(data)
|
||||||
|
// console.log('nativeCall', action, data)
|
||||||
|
_nativeCall(key, action, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const callbacks = new Map()
|
||||||
|
let timeoutId = 0
|
||||||
|
const setTimeout = (callback, timeout = 0, ...params) => {
|
||||||
|
if (typeof timeout !== 'number' || timeout < 0) throw new Error('timeout required number')
|
||||||
|
if (timeoutId > 90000000000) throw new Error('max timeout')
|
||||||
|
const id = timeoutId++
|
||||||
|
callbacks.set(id, {
|
||||||
|
callback,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
set_timeout(id, parseInt(timeout))
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
const clearTimeout = (id) => {
|
||||||
|
const tagret = callbacks.get(id)
|
||||||
|
if (!tagret) return
|
||||||
|
callbacks.delete(id)
|
||||||
|
}
|
||||||
|
const handleSetTimeout = (id) => {
|
||||||
|
const tagret = callbacks.get(id)
|
||||||
|
if (!tagret) return
|
||||||
|
callbacks.delete(id)
|
||||||
|
tagret.callback(...tagret.params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将字节数组解码为字符串(UTF-8)
|
||||||
|
function bytesToString(bytes) {
|
||||||
|
let result = ''
|
||||||
|
let i = 0
|
||||||
|
while (i < bytes.length) {
|
||||||
|
const byte = bytes[i]
|
||||||
|
if (byte < 128) {
|
||||||
|
result += String.fromCharCode(byte)
|
||||||
|
i++
|
||||||
|
} else if (byte >= 192 && byte < 224) {
|
||||||
|
result += String.fromCharCode(((byte & 31) << 6) | (bytes[i + 1] & 63))
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
result += String.fromCharCode(((byte & 15) << 12) | ((bytes[i + 1] & 63) << 6) | (bytes[i + 2] & 63))
|
||||||
|
i += 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
// 将字符串编码为字节数组(UTF-8)
|
||||||
|
function stringToBytes(inputString) {
|
||||||
|
const bytes = []
|
||||||
|
for (let i = 0; i < inputString.length; i++) {
|
||||||
|
const charCode = inputString.charCodeAt(i)
|
||||||
|
if (charCode < 128) {
|
||||||
|
bytes.push(charCode)
|
||||||
|
} else if (charCode < 2048) {
|
||||||
|
bytes.push((charCode >> 6) | 192)
|
||||||
|
bytes.push((charCode & 63) | 128)
|
||||||
|
} else {
|
||||||
|
bytes.push((charCode >> 12) | 224)
|
||||||
|
bytes.push(((charCode >> 6) & 63) | 128)
|
||||||
|
bytes.push((charCode & 63) | 128)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
const NATIVE_EVENTS_NAMES = {
|
||||||
|
init: 'init',
|
||||||
|
showUpdateAlert: 'showUpdateAlert',
|
||||||
|
request: 'request',
|
||||||
|
cancelRequest: 'cancelRequest',
|
||||||
|
response: 'response',
|
||||||
|
'utils.crypto.aesEncrypt': 'utils.crypto.aesEncrypt',
|
||||||
|
'utils.crypto.rsaEncrypt': 'utils.crypto.rsaEncrypt',
|
||||||
|
'utils.crypto.randomBytes': 'utils.crypto.randomBytes',
|
||||||
|
'utils.crypto.md5': 'utils.crypto.md5',
|
||||||
|
'utils.buffer.from': 'utils.buffer.from',
|
||||||
|
'utils.buffer.bufToString': 'utils.buffer.bufToString',
|
||||||
|
'utils.zlib.inflate': 'utils.zlib.inflate',
|
||||||
|
'utils.zlib.deflate': 'utils.zlib.deflate',
|
||||||
|
}
|
||||||
|
const EVENT_NAMES = Object.freeze({
|
||||||
|
request: 'request',
|
||||||
|
inited: 'inited',
|
||||||
|
updateAlert: 'updateAlert',
|
||||||
|
})
|
||||||
|
const eventNames = Object.values(EVENT_NAMES)
|
||||||
|
const events = {
|
||||||
|
request: null,
|
||||||
|
}
|
||||||
|
const allSources = ['kw', 'kg', 'tx', 'wy', 'mg']
|
||||||
|
const supportQualitys = {
|
||||||
|
kw: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
kg: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
tx: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
wy: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
mg: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
}
|
||||||
|
const supportActions = {
|
||||||
|
kw: ['musicUrl'],
|
||||||
|
kg: ['musicUrl'],
|
||||||
|
tx: ['musicUrl'],
|
||||||
|
wy: ['musicUrl'],
|
||||||
|
mg: ['musicUrl'],
|
||||||
|
xm: ['musicUrl'],
|
||||||
|
}
|
||||||
|
const requestQueue = new Map()
|
||||||
|
let isInitedApi = false
|
||||||
|
let isShowedUpdateAlert = false
|
||||||
|
|
||||||
|
const sendNativeRequest = (url, options, callback) => {
|
||||||
|
const requestKey = Math.random().toString()
|
||||||
|
const requestInfo = {
|
||||||
|
aborted: false,
|
||||||
|
abort: () => {
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.cancelRequest, requestKey)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
requestQueue.set(requestKey, {
|
||||||
|
callback,
|
||||||
|
// timeout: setTimeout(() => {
|
||||||
|
// const req = requestQueue.get(requestKey)
|
||||||
|
// if (req) req.timeout = null
|
||||||
|
// nativeCall(NATIVE_EVENTS_NAMES.cancelRequest, requestKey)
|
||||||
|
// }, 30000),
|
||||||
|
requestInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.request, { requestKey, url, options })
|
||||||
|
return requestInfo
|
||||||
|
}
|
||||||
|
const handleNativeResponse = ({ requestKey, error, response }) => {
|
||||||
|
const targetRequest = requestQueue.get(requestKey)
|
||||||
|
if (!targetRequest) return
|
||||||
|
requestQueue.delete(requestKey)
|
||||||
|
targetRequest.requestInfo.aborted = true
|
||||||
|
// if (targetRequest.timeout) clearTimeout(targetRequest.timeout)
|
||||||
|
if (error == null) targetRequest.callback(null, response)
|
||||||
|
else targetRequest.callback(new Error(error), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRequest = ({ requestKey, data }) => {
|
||||||
|
// console.log(data)
|
||||||
|
if (!events.request) return nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: 'Request event is not defined' })
|
||||||
|
try {
|
||||||
|
events.request.call(globalThis.lx, { source: data.source, action: data.action, info: data.info }).then(response => {
|
||||||
|
let result
|
||||||
|
switch (data.action) {
|
||||||
|
case 'musicUrl':
|
||||||
|
result = {
|
||||||
|
source: data.source,
|
||||||
|
action: data.action,
|
||||||
|
data: {
|
||||||
|
type: data.info.type,
|
||||||
|
url: response,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: true, result })
|
||||||
|
}).catch(err => {
|
||||||
|
// console.log('handleRequest err', err)
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: err.message })
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('handleRequest call err', err)
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsCall = (action, data) => {
|
||||||
|
// console.log('jsCall', action, data)
|
||||||
|
switch (action) {
|
||||||
|
case '__set_timeout__':
|
||||||
|
handleSetTimeout(data)
|
||||||
|
break
|
||||||
|
case 'request':
|
||||||
|
handleRequest(data)
|
||||||
|
break
|
||||||
|
case 'response':
|
||||||
|
handleNativeResponse(data)
|
||||||
|
break
|
||||||
|
case NATIVE_EVENTS_NAMES['utils.buffer.from']:
|
||||||
|
break
|
||||||
|
case NATIVE_EVENTS_NAMES['utils.buffer.bufToString']:
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return 'Unknown action: ' + action
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, '__lx_native__', {
|
||||||
|
enumerable: false,
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
value: (_key, action, data) => {
|
||||||
|
if (key != _key) return 'Invalid key'
|
||||||
|
return jsCall(action, JSON.parse(data))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} info {
|
||||||
|
* openDevTools: false,
|
||||||
|
* status: true,
|
||||||
|
* message: 'xxx',
|
||||||
|
* sources: {
|
||||||
|
* kw: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
* kg: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
* tx: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
* wy: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
* mg: ['128k', '320k', 'flac', 'flac24bit'],
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const handleInit = (info) => {
|
||||||
|
if (!info) {
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: null, status: false, errorMessage: 'Init failed' })
|
||||||
|
// sendMessage(NATIVE_EVENTS_NAMES.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!info.status) {
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: null, status: false, errorMessage: 'Init failed' })
|
||||||
|
// sendMessage(NATIVE_EVENTS_NAMES.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sourceInfo = {
|
||||||
|
sources: {},
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const source of allSources) {
|
||||||
|
const userSource = info.sources[source]
|
||||||
|
if (!userSource || userSource.type !== 'music') continue
|
||||||
|
const qualitys = supportQualitys[source]
|
||||||
|
const actions = supportActions[source]
|
||||||
|
sourceInfo.sources[source] = {
|
||||||
|
type: 'music',
|
||||||
|
actions: actions.filter(a => userSource.actions.includes(a)),
|
||||||
|
qualitys: qualitys.filter(q => userSource.qualitys.includes(q)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// console.log(error)
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: null, status: false, errorMessage: error.message })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: sourceInfo, status: true })
|
||||||
|
}
|
||||||
|
const handleShowUpdateAlert = (data, resolve, reject) => {
|
||||||
|
if (!data || typeof data != 'object') return reject(new Error('parameter format error.'))
|
||||||
|
if (!data.log || typeof data.log != 'string') return reject(new Error('log is required.'))
|
||||||
|
if (data.updateUrl && !/^https?:\/\/[^\s$.?#].[^\s]*$/.test(data.updateUrl) && data.updateUrl.length > 1024) delete data.updateUrl
|
||||||
|
if (data.log.length > 1024) data.log = data.log.substring(0, 1024) + '...'
|
||||||
|
nativeCall(NATIVE_EVENTS_NAMES.showUpdateAlert, { log: data.log, updateUrl: data.updateUrl, name })
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataToB64 = (data) => {
|
||||||
|
if (typeof data === 'string') return utils_str2b64(data)
|
||||||
|
else if (Array.isArray(data) || ArrayBuffer.isView(data)) return utils.buffer.bufToString(data, 'base64')
|
||||||
|
throw new Error('data type error: ' + typeof data + ' raw data: ' + data)
|
||||||
|
}
|
||||||
|
const utils = Object.freeze({
|
||||||
|
crypto: {
|
||||||
|
aesEncrypt(buffer, mode, key, iv) {
|
||||||
|
// console.log('aesEncrypt', buffer, mode, key, iv)
|
||||||
|
switch (mode) {
|
||||||
|
case 'aes-128-cbc':
|
||||||
|
return utils.buffer.from(utils_aes_encrypt(dataToB64(buffer), dataToB64(key), dataToB64(iv), AES_MODE.CBC_128_PKCS7Padding), 'base64')
|
||||||
|
case 'aes-128-ecb':
|
||||||
|
return utils.buffer.from(utils_aes_encrypt(dataToB64(buffer), dataToB64(key), '', AES_MODE.ECB_128_NoPadding), 'base64')
|
||||||
|
default:
|
||||||
|
throw new Error('Binary encoding is not supported for input strings')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rsaEncrypt(buffer, key) {
|
||||||
|
// console.log('rsaEncrypt', buffer, key)
|
||||||
|
if (typeof key !== 'string') throw new Error('Invalid RSA key')
|
||||||
|
key = key.replace(KEY_PREFIX.publicKeyStart, '')
|
||||||
|
.replace(KEY_PREFIX.publicKeyEnd, '')
|
||||||
|
return utils.buffer.from(utils_rsa_encrypt(dataToB64(buffer), key, RSA_PADDING.NoPadding), 'base64')
|
||||||
|
},
|
||||||
|
randomBytes(size) {
|
||||||
|
const byteArray = new Uint8Array(size)
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
byteArray[i] = Math.floor(Math.random() * 256) // 随机生成一个字节的值(0-255)
|
||||||
|
}
|
||||||
|
return byteArray
|
||||||
|
},
|
||||||
|
md5(str) {
|
||||||
|
if (typeof str !== 'string') throw new Error('param required a string')
|
||||||
|
const md5 = utils_str2md5(str)
|
||||||
|
// console.log('md5', str, md5)
|
||||||
|
return md5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buffer: {
|
||||||
|
from(input, encoding) {
|
||||||
|
// console.log('buffer.from', input, encoding)
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
switch (encoding) {
|
||||||
|
case 'binary':
|
||||||
|
throw new Error('Binary encoding is not supported for input strings')
|
||||||
|
case 'base64':
|
||||||
|
return new Uint8Array(JSON.parse(utils_b642buf(input)))
|
||||||
|
case 'hex':
|
||||||
|
return new Uint8Array(input.match(/.{1,2}/g).map(byte => parseInt(byte, 16)))
|
||||||
|
default:
|
||||||
|
return new Uint8Array(stringToBytes(input))
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(input)) {
|
||||||
|
return new Uint8Array(input)
|
||||||
|
} else {
|
||||||
|
throw new Error('Unsupported input type: ' + input + ' encoding: ' + encoding)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bufToString(buf, format) {
|
||||||
|
// console.log('buffer.bufToString', buf, format)
|
||||||
|
if (Array.isArray(buf) || ArrayBuffer.isView(buf)) {
|
||||||
|
switch (format) {
|
||||||
|
case 'binary':
|
||||||
|
// return new TextDecoder('latin1').decode(new Uint8Array(buf))
|
||||||
|
return buf
|
||||||
|
case 'hex':
|
||||||
|
return new Uint8Array(buf).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
|
||||||
|
case 'base64':
|
||||||
|
return utils_str2b64(bytesToString(Array.from(buf)))
|
||||||
|
case 'utf8':
|
||||||
|
case 'utf-8':
|
||||||
|
default:
|
||||||
|
return bytesToString(Array.from(buf))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Input is not a valid buffer: ' + buf + ' format: ' + format)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// zlib: {
|
||||||
|
// inflate(buf) {
|
||||||
|
// return new Promise((resolve, reject) => {
|
||||||
|
// zlib.inflate(buf, (err, data) => {
|
||||||
|
// if (err) reject(new Error(err.message))
|
||||||
|
// else resolve(data)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// deflate(data) {
|
||||||
|
// return new Promise((resolve, reject) => {
|
||||||
|
// zlib.deflate(data, (err, buf) => {
|
||||||
|
// if (err) reject(new Error(err.message))
|
||||||
|
// else resolve(buf)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
})
|
||||||
|
Object.defineProperty(globalThis, 'lx', {
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
value: Object.freeze({
|
||||||
|
EVENT_NAMES,
|
||||||
|
request(url, { method = 'get', timeout, headers, body, form, formData, binary }, callback) {
|
||||||
|
let options = { headers, binary: binary === true }
|
||||||
|
// let data
|
||||||
|
// if (body) {
|
||||||
|
// data = body
|
||||||
|
// } else if (form) {
|
||||||
|
// data = form
|
||||||
|
// // data.content_type = 'application/x-www-form-urlencoded'
|
||||||
|
// options.json = false
|
||||||
|
// } else if (formData) {
|
||||||
|
// data = formData
|
||||||
|
// // data.content_type = 'multipart/form-data'
|
||||||
|
// options.json = false
|
||||||
|
// }
|
||||||
|
if (timeout && typeof timeout == 'number') options.timeout = Math.min(options.timeout, 60_000)
|
||||||
|
|
||||||
|
let request = sendNativeRequest(url, { method, body, form, formData, ...options }, (err, resp) => {
|
||||||
|
callback(err, {
|
||||||
|
statusCode: resp.statusCode,
|
||||||
|
statusMessage: resp.statusMessage,
|
||||||
|
headers: resp.headers,
|
||||||
|
// bytes: resp.bytes,
|
||||||
|
// raw: resp.raw,
|
||||||
|
body: resp.body,
|
||||||
|
}, resp.body)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!request.aborted) request.abort()
|
||||||
|
request = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
send(eventName, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName))
|
||||||
|
switch (eventName) {
|
||||||
|
case EVENT_NAMES.inited:
|
||||||
|
if (isInitedApi) return reject(new Error('Script is inited'))
|
||||||
|
isInitedApi = true
|
||||||
|
handleInit(data)
|
||||||
|
resolve()
|
||||||
|
break
|
||||||
|
case EVENT_NAMES.updateAlert:
|
||||||
|
if (isShowedUpdateAlert) return reject(new Error('The update alert can only be called once.'))
|
||||||
|
isShowedUpdateAlert = true
|
||||||
|
handleShowUpdateAlert(data, resolve, reject)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
reject(new Error('Unknown event name: ' + eventName))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
on(eventName, handler) {
|
||||||
|
if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
|
||||||
|
switch (eventName) {
|
||||||
|
case EVENT_NAMES.request:
|
||||||
|
events.request = handler
|
||||||
|
break
|
||||||
|
default: return Promise.reject(new Error('The event is not supported: ' + eventName))
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
utils,
|
||||||
|
currentScriptInfo: Object.freeze({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
}),
|
||||||
|
version: '1.0.0',
|
||||||
|
env: 'mobile',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'setTimeout', {
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
value: setTimeout,
|
||||||
|
})
|
||||||
|
Object.defineProperty(globalThis, 'clearTimeout', {
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
value: clearTimeout,
|
||||||
|
})
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
value: globalThis,
|
||||||
|
})
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
value: Object.freeze({
|
||||||
|
getElementsByTagName(name) {
|
||||||
|
if (name == 'script') {
|
||||||
|
return [
|
||||||
|
Object.freeze({
|
||||||
|
innerText: rawScript,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Preload finished.')
|
||||||
|
}
|
@ -15,6 +15,7 @@ import cn.toside.music.mobile.cache.CachePackage;
|
|||||||
import cn.toside.music.mobile.crypto.CryptoPackage;
|
import cn.toside.music.mobile.crypto.CryptoPackage;
|
||||||
import cn.toside.music.mobile.gzip.GzipPackage;
|
import cn.toside.music.mobile.gzip.GzipPackage;
|
||||||
import cn.toside.music.mobile.lyric.LyricPackage;
|
import cn.toside.music.mobile.lyric.LyricPackage;
|
||||||
|
import cn.toside.music.mobile.userApi.UserApiPackage;
|
||||||
import cn.toside.music.mobile.utils.UtilsPackage;
|
import cn.toside.music.mobile.utils.UtilsPackage;
|
||||||
|
|
||||||
public class MainApplication extends NavigationApplication {
|
public class MainApplication extends NavigationApplication {
|
||||||
@ -37,6 +38,7 @@ public class MainApplication extends NavigationApplication {
|
|||||||
packages.add(new LyricPackage());
|
packages.add(new LyricPackage());
|
||||||
packages.add(new UtilsPackage());
|
packages.add(new UtilsPackage());
|
||||||
packages.add(new CryptoPackage());
|
packages.add(new CryptoPackage());
|
||||||
|
packages.add(new UserApiPackage());
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.whl.quickjs.wrapper.QuickJSContext;
|
||||||
|
|
||||||
|
public class Console implements QuickJSContext.Console {
|
||||||
|
private final Handler eventHandler;
|
||||||
|
|
||||||
|
Console(Handler eventHandler) {
|
||||||
|
this.eventHandler = eventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendLog(String type, String log) {
|
||||||
|
Message message = this.eventHandler.obtainMessage();
|
||||||
|
message.what = 1001;
|
||||||
|
message.obj = new Object[]{type, log};
|
||||||
|
Log.d("UserApi Log", "[" + type + "]" + log);
|
||||||
|
this.eventHandler.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void log(String info) {
|
||||||
|
sendLog("log", info);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void info(String info) {
|
||||||
|
sendLog("info", info);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void warn(String info) {
|
||||||
|
sendLog("warn", info);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void error(String info) {
|
||||||
|
sendLog("error", info);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
public class HandlerWhat {
|
||||||
|
public static final int ACTION = 1000;
|
||||||
|
public static final int INIT = 99;
|
||||||
|
public static final int INIT_FAILED = 500;
|
||||||
|
public static final int INIT_SUCCESS = 200;
|
||||||
|
public static final int LOG = 1001;
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
|
||||||
|
public class JavaScriptThread extends HandlerThread {
|
||||||
|
private Handler handler;
|
||||||
|
private QuickJS javaScriptExecutor;
|
||||||
|
private final ReactApplicationContext reactContext;
|
||||||
|
private final Bundle scriptInfo;
|
||||||
|
|
||||||
|
JavaScriptThread(ReactApplicationContext context, Bundle info) {
|
||||||
|
super("JavaScriptThread");
|
||||||
|
this.reactContext = context;
|
||||||
|
this.scriptInfo = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepareHandler(final Handler mainHandler) {
|
||||||
|
start();
|
||||||
|
Log.d("UserApi [thread]", "running 2");
|
||||||
|
this.handler = new Handler(getLooper()) {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(@NonNull Message message) {
|
||||||
|
if (javaScriptExecutor == null) {
|
||||||
|
javaScriptExecutor = new QuickJS(reactContext, mainHandler);
|
||||||
|
Log.d("UserApi [thread]", "javaScript executor created");
|
||||||
|
String result = javaScriptExecutor.loadScript(scriptInfo);
|
||||||
|
if ("".equals(result)) {
|
||||||
|
Log.d("UserApi [thread]", "script loaded");
|
||||||
|
mainHandler.sendEmptyMessage(HandlerWhat.INIT_SUCCESS);
|
||||||
|
} else {
|
||||||
|
Log.w("UserApi [thread]", "script load failed: " + result);
|
||||||
|
Handler handler = mainHandler;
|
||||||
|
handler.sendMessage(handler.obtainMessage(HandlerWhat.INIT_FAILED, result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (message.what == HandlerWhat.INIT) return;
|
||||||
|
if (message.what == HandlerWhat.ACTION) {
|
||||||
|
Object[] data = (Object[]) message.obj;
|
||||||
|
Log.d("UserApi [handler]", "handler action: " + data[0]);
|
||||||
|
javaScriptExecutor.callJS((String) data[0], data[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.w("UserApi [handler]", "Unknown message what: " + message.what);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Handler getHandler() {
|
||||||
|
return this.handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopThread() {
|
||||||
|
quit();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class JsHandler extends Handler {
|
||||||
|
private final UtilsEvent utilsEvent;
|
||||||
|
|
||||||
|
JsHandler(Looper looper, UtilsEvent utilsEvent) {
|
||||||
|
super(looper);
|
||||||
|
this.utilsEvent = utilsEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendInitFailedEvent(String errorMessage) {
|
||||||
|
WritableMap params = Arguments.createMap();
|
||||||
|
params.putString("action", "init");
|
||||||
|
params.putString("data", "{ \"info\": null, \"status\": false, \"errorMessage\": \"Create JavaScript Env Failed\" }");
|
||||||
|
UtilsEvent utilsEvent = this.utilsEvent;
|
||||||
|
Objects.requireNonNull(utilsEvent);
|
||||||
|
utilsEvent.sendEvent("api-action", params);
|
||||||
|
sendLogEvent(new Object[]{"error", errorMessage});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendLogEvent(Object[] data) {
|
||||||
|
WritableMap params = Arguments.createMap();
|
||||||
|
params.putString("action", "log");
|
||||||
|
params.putString("type", (String) data[0]);
|
||||||
|
params.putString("log", (String) data[1]);
|
||||||
|
UtilsEvent utilsEvent = this.utilsEvent;
|
||||||
|
Objects.requireNonNull(utilsEvent);
|
||||||
|
utilsEvent.sendEvent("api-action", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendActionEvent(String action, String data) {
|
||||||
|
WritableMap params = Arguments.createMap();
|
||||||
|
params.putString("action", action);
|
||||||
|
params.putString("data", data);
|
||||||
|
UtilsEvent utilsEvent = this.utilsEvent;
|
||||||
|
Objects.requireNonNull(utilsEvent);
|
||||||
|
utilsEvent.sendEvent("api-action", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case HandlerWhat.INIT_SUCCESS: break;
|
||||||
|
case HandlerWhat.INIT_FAILED:
|
||||||
|
sendInitFailedEvent((String) msg.obj);
|
||||||
|
break;
|
||||||
|
case HandlerWhat.ACTION:
|
||||||
|
Object[] action = (Object[]) msg.obj;
|
||||||
|
sendActionEvent((String) action[0], (String) action[1]);
|
||||||
|
Log.d("UserApi [api call]", "action: " + action[0]);
|
||||||
|
break;
|
||||||
|
case HandlerWhat.LOG:
|
||||||
|
sendLogEvent((Object[]) msg.obj);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.w("UserApi [api call]", "Unknown message what: " + msg.what);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
import cn.toside.music.mobile.crypto.AES;
|
||||||
|
import cn.toside.music.mobile.crypto.RSA;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
|
||||||
|
import com.whl.quickjs.android.QuickJSLoader;
|
||||||
|
import com.whl.quickjs.wrapper.JSCallFunction;
|
||||||
|
import com.whl.quickjs.wrapper.QuickJSContext;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.UUID;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
|
public class QuickJS {
|
||||||
|
private final Handler eventHandler;
|
||||||
|
private String key;
|
||||||
|
private final ReactApplicationContext reactContext;
|
||||||
|
private boolean isInited = false;
|
||||||
|
private QuickJSContext jsContext = null;
|
||||||
|
final Handler timeoutHandler = new Handler();
|
||||||
|
|
||||||
|
public QuickJS(ReactApplicationContext context, Handler eventHandler) {
|
||||||
|
this.reactContext = context;
|
||||||
|
this.eventHandler = eventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
if (this.isInited) return;
|
||||||
|
QuickJSLoader.init();
|
||||||
|
this.key = UUID.randomUUID().toString();
|
||||||
|
this.isInited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPreloadScript() {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = this.reactContext.getAssets().open("script/user-api-preload.js");
|
||||||
|
byte[] buffer = new byte[inputStream.available()];
|
||||||
|
inputStream.read(buffer);
|
||||||
|
inputStream.close();
|
||||||
|
return new String(buffer, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createEnvObj(QuickJSContext jsContext) {
|
||||||
|
jsContext.getGlobalObject().setProperty("__lx_native_call__", args -> {
|
||||||
|
if (this.key.equals(args[0])) {
|
||||||
|
callNative((String) args[1], (String) args[2]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
jsContext.getGlobalObject().setProperty("__lx_native_call__utils_str2b64", args -> {
|
||||||
|
try {
|
||||||
|
return new String(Base64.encode(((String) args[0]).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UserApi [utils]", "utils_str2b64 error: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jsContext.getGlobalObject().setProperty("__lx_native_call__utils_b642buf", args -> {
|
||||||
|
try {
|
||||||
|
byte[] byteArray = Base64.decode(((String) args[0]).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
|
||||||
|
StringBuilder jsonArrayString = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < byteArray.length; i++) {
|
||||||
|
jsonArrayString.append((int) byteArray[i]);
|
||||||
|
if (i < byteArray.length - 1) {
|
||||||
|
jsonArrayString.append(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonArrayString.append("]");
|
||||||
|
return jsonArrayString.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UserApi [utils]", "utils_b642buf error: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jsContext.getGlobalObject().setProperty("__lx_native_call__utils_str2md5", args -> {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] inputBytes = ((String) args[0]).getBytes();
|
||||||
|
byte[] md5Bytes = md.digest(inputBytes);
|
||||||
|
StringBuilder md5String = new StringBuilder();
|
||||||
|
for (byte b : md5Bytes) {
|
||||||
|
md5String.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return md5String.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
Log.e("UserApi [utils]", "utils_str2md5 error: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jsContext.getGlobalObject().setProperty("__lx_native_call__utils_aes_encrypt", args -> {
|
||||||
|
try {
|
||||||
|
return AES.encrypt((String) args[0], (String) args[1], (String) args[2], (String) args[3]);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UserApi [utils]", "utils_aes_encrypt error: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jsContext.getGlobalObject().setProperty("__lx_native_call__utils_rsa_encrypt", args -> {
|
||||||
|
try {
|
||||||
|
return RSA.encryptRSAToString((String) args[0], (String) args[1], (String) args[2]);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UserApi [utils]", "utils_rsa_encrypt error: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jsContext.getGlobalObject().setProperty("__lx_native_call__set_timeout", args -> {
|
||||||
|
this.timeoutHandler.postDelayed(() -> {
|
||||||
|
callJS("__set_timeout__", args[0]);
|
||||||
|
}, (int) args[1]);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean createJSEnv(String id, String name, String desc, String rawScript) {
|
||||||
|
init();
|
||||||
|
QuickJSContext quickJSContext = this.jsContext;
|
||||||
|
if (quickJSContext != null) quickJSContext.destroy();
|
||||||
|
QuickJSContext create = QuickJSContext.create();
|
||||||
|
this.jsContext = create;
|
||||||
|
create.setConsole(new Console(this.eventHandler));
|
||||||
|
String preloadScript = getPreloadScript();
|
||||||
|
if (preloadScript == null) return false;
|
||||||
|
createEnvObj(this.jsContext);
|
||||||
|
this.jsContext.evaluate(preloadScript);
|
||||||
|
this.jsContext.getGlobalObject().getJSFunction("lx_setup").call(this.key, id, name, desc, rawScript);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void callNative(String action, String data) {
|
||||||
|
Message message = this.eventHandler.obtainMessage();
|
||||||
|
message.what = HandlerWhat.ACTION;
|
||||||
|
message.obj = new Object[]{action, data};
|
||||||
|
Log.d("UserApi [script call]", "script call action: " + action + " data: " + data);
|
||||||
|
this.eventHandler.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String loadScript(Bundle scriptInfo) {
|
||||||
|
Log.d("UserApi", "UserApi Thread id: " + Thread.currentThread().getId());
|
||||||
|
String script = scriptInfo.getString("script", "");
|
||||||
|
if (createJSEnv(scriptInfo.getString("id", ""),
|
||||||
|
scriptInfo.getString("name", "Unknown"),
|
||||||
|
scriptInfo.getString("description", ""), script)) {
|
||||||
|
try {
|
||||||
|
this.jsContext.evaluate(script);
|
||||||
|
return "";
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UserApi", "load script error: " + e.getMessage());
|
||||||
|
return e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "create JavaScript Env failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object callJS(String action, Object... args) {
|
||||||
|
Object[] params;
|
||||||
|
if (args == null) {
|
||||||
|
params = new Object[]{this.key, action};
|
||||||
|
} else {
|
||||||
|
Object[] params2 = new Object[args.length + 2];
|
||||||
|
params2[0] = this.key;
|
||||||
|
params2[1] = action;
|
||||||
|
System.arraycopy(args, 0, params2, 2, args.length);
|
||||||
|
params = params2;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return this.jsContext.getGlobalObject().getJSFunction("__lx_native__").call(params);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Message message = eventHandler.obtainMessage();
|
||||||
|
message.what = HandlerWhat.LOG;
|
||||||
|
message.obj = new Object[]{"error", "Call script error: " + e.getMessage()};
|
||||||
|
eventHandler.sendMessage(message);
|
||||||
|
Log.e("UserApi", "Call script error: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
import java.lang.Thread;
|
||||||
|
|
||||||
|
public class UserApiModule extends ReactContextBaseJavaModule {
|
||||||
|
private JavaScriptThread javaScriptThread;
|
||||||
|
private final ReactApplicationContext reactContext;
|
||||||
|
private UtilsEvent utilsEvent;
|
||||||
|
|
||||||
|
UserApiModule(ReactApplicationContext reactContext) {
|
||||||
|
super(reactContext);
|
||||||
|
this.javaScriptThread = null;
|
||||||
|
this.utilsEvent = null;
|
||||||
|
this.reactContext = reactContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "UserApiModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void loadScript(ReadableMap data) {
|
||||||
|
if (this.utilsEvent == null) this.utilsEvent = new UtilsEvent(this.reactContext);
|
||||||
|
if (this.javaScriptThread != null) destroy();
|
||||||
|
Bundle info = Arguments.toBundle(data);
|
||||||
|
this.javaScriptThread = new JavaScriptThread(this.reactContext, info);
|
||||||
|
final JsHandler jsHandler = new JsHandler(this.reactContext.getMainLooper(), this.utilsEvent);
|
||||||
|
this.javaScriptThread.prepareHandler(jsHandler);
|
||||||
|
this.javaScriptThread.getHandler().sendEmptyMessage(HandlerWhat.INIT);
|
||||||
|
this.javaScriptThread.setUncaughtExceptionHandler((thread, ex) -> {
|
||||||
|
Message message = jsHandler.obtainMessage();
|
||||||
|
message.what = HandlerWhat.LOG;
|
||||||
|
message.obj = new Object[]{"error", "Uncaught exception in JavaScriptThread: " + ex.getMessage()};
|
||||||
|
jsHandler.sendMessage(message);
|
||||||
|
Log.e("JavaScriptThread", "Uncaught exception in JavaScriptThread: " + ex.getMessage());
|
||||||
|
});
|
||||||
|
Log.d("UserApi", "Module Thread id: " + Thread.currentThread().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public boolean sendAction(String action, String info) {
|
||||||
|
JavaScriptThread javaScriptThread = this.javaScriptThread;
|
||||||
|
if (javaScriptThread == null) return false;
|
||||||
|
Message message = javaScriptThread.getHandler().obtainMessage();
|
||||||
|
message.what = HandlerWhat.ACTION;
|
||||||
|
message.obj = new Object[]{action, info};
|
||||||
|
this.javaScriptThread.getHandler().sendMessage(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void destroy() {
|
||||||
|
JavaScriptThread javaScriptThread = this.javaScriptThread;
|
||||||
|
if (javaScriptThread == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
javaScriptThread.stopThread();
|
||||||
|
this.javaScriptThread = null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage;
|
||||||
|
import com.facebook.react.bridge.NativeModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UserApiPackage implements ReactPackage {
|
||||||
|
@Override
|
||||||
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||||
|
return Arrays.asList(new UserApiModule(reactContext));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package cn.toside.music.mobile.userApi;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||||
|
|
||||||
|
public class UtilsEvent {
|
||||||
|
final String API_ACTION = "api-action";
|
||||||
|
private final ReactApplicationContext reactContext;
|
||||||
|
|
||||||
|
UtilsEvent(ReactApplicationContext reactContext) {
|
||||||
|
this.reactContext = reactContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendEvent(String eventName, @Nullable WritableMap params) {
|
||||||
|
reactContext
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit(eventName, params);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
|
### 新增
|
||||||
|
|
||||||
|
- 新增自定义源,但需要注意的是,移动端自定义源的环境与PC端不同,某些API不可用
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
- 添加是否忽略电池优化检查,用于提醒用户添加白名单,确保APP后台播放稳定性
|
- 添加是否忽略电池优化检查,用于提醒用户添加白名单,确保APP后台播放稳定性
|
||||||
|
@ -25,6 +25,8 @@ export interface Props {
|
|||||||
*/
|
*/
|
||||||
onPress?: (e: GestureResponderEvent) => void
|
onPress?: (e: GestureResponderEvent) => void
|
||||||
|
|
||||||
|
size?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom color for checkbox.
|
* Custom color for checkbox.
|
||||||
*/
|
*/
|
||||||
@ -45,6 +47,7 @@ const PADDING = scaleSizeW(4)
|
|||||||
const Checkbox = ({
|
const Checkbox = ({
|
||||||
status,
|
status,
|
||||||
disabled,
|
disabled,
|
||||||
|
size = 1,
|
||||||
onPress,
|
onPress,
|
||||||
tintColors,
|
tintColors,
|
||||||
...rest
|
...rest
|
||||||
@ -86,12 +89,12 @@ const Checkbox = ({
|
|||||||
accessibilityRole="checkbox"
|
accessibilityRole="checkbox"
|
||||||
accessibilityState={{ disabled, checked }}
|
accessibilityState={{ disabled, checked }}
|
||||||
accessibilityLiveRegion="polite"
|
accessibilityLiveRegion="polite"
|
||||||
style={{ ...styles.container, padding: PADDING }}
|
style={{ ...styles.container, padding: PADDING, marginLeft: -PADDING }}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
allowFontScaling={false}
|
allowFontScaling={false}
|
||||||
name="checkbox-blank-outline"
|
name="checkbox-blank-outline"
|
||||||
size={24}
|
size={24 * size}
|
||||||
color={tintColors.false}
|
color={tintColors.false}
|
||||||
/>
|
/>
|
||||||
<View style={[StyleSheet.absoluteFill, styles.fillContainer]}>
|
<View style={[StyleSheet.absoluteFill, styles.fillContainer]}>
|
||||||
@ -99,7 +102,7 @@ const Checkbox = ({
|
|||||||
<Icon
|
<Icon
|
||||||
allowFontScaling={false}
|
allowFontScaling={false}
|
||||||
name={icon}
|
name={icon}
|
||||||
size={24}
|
size={24 * size}
|
||||||
color={tintColors.true}
|
color={tintColors.true}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
@ -15,6 +15,7 @@ export interface CheckBoxProps {
|
|||||||
onChange: (check: boolean) => void
|
onChange: (check: boolean) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
need?: boolean
|
need?: boolean
|
||||||
|
size?: number
|
||||||
marginRight?: number
|
marginRight?: number
|
||||||
marginBottom?: number
|
marginBottom?: number
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ export interface CheckBoxProps {
|
|||||||
helpDesc?: string
|
helpDesc?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ check, label, children, onChange, helpTitle, helpDesc, disabled = false, need = false, marginRight = 0, marginBottom = 0 }: CheckBoxProps) => {
|
export default ({ check, label, children, onChange, helpTitle, helpDesc, disabled = false, need = false, marginRight = 0, marginBottom = 0, size = 1 }: CheckBoxProps) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [isDisabled, setDisabled] = useState(false)
|
const [isDisabled, setDisabled] = useState(false)
|
||||||
const tintColors = {
|
const tintColors = {
|
||||||
@ -65,10 +66,10 @@ export default ({ check, label, children, onChange, helpTitle, helpDesc, disable
|
|||||||
}
|
}
|
||||||
return (helpTitle ?? helpDesc) ? (
|
return (helpTitle ?? helpDesc) ? (
|
||||||
<TouchableOpacity style={styles.helpBtn} onPress={handleShowHelp}>
|
<TouchableOpacity style={styles.helpBtn} onPress={handleShowHelp}>
|
||||||
<Icon name="help" />
|
<Icon size={15 * size} name="help" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : null
|
) : null
|
||||||
}, [helpTitle, helpDesc])
|
}, [helpTitle, helpDesc, size])
|
||||||
|
|
||||||
|
|
||||||
const contentStyle = { ...styles.content, marginBottom: scaleSizeH(marginBottom) }
|
const contentStyle = { ...styles.content, marginBottom: scaleSizeH(marginBottom) }
|
||||||
@ -78,16 +79,16 @@ export default ({ check, label, children, onChange, helpTitle, helpDesc, disable
|
|||||||
disabled
|
disabled
|
||||||
? (
|
? (
|
||||||
<View style={contentStyle}>
|
<View style={contentStyle}>
|
||||||
<CheckBox status={check ? 'checked' : 'unchecked'} disabled={true} tintColors={disabledTintColors} />
|
<CheckBox status={check ? 'checked' : 'unchecked'} disabled={true} tintColors={disabledTintColors} size={size} />
|
||||||
<View style={labelStyle}>{label ? <Text style={styles.name} color={theme['c-500']}>{label}</Text> : children}</View>
|
<View style={labelStyle}>{label ? <Text style={styles.name} color={theme['c-500']} size={15 * size}>{label}</Text> : children}</View>
|
||||||
{helpComponent}
|
{helpComponent}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<View style={contentStyle}>
|
<View style={contentStyle}>
|
||||||
<CheckBox status={check ? 'checked' : 'unchecked'} disabled={isDisabled} onPress={handleLabelPress} tintColors={tintColors} />
|
<CheckBox status={check ? 'checked' : 'unchecked'} disabled={isDisabled} onPress={handleLabelPress} tintColors={tintColors} size={size} />
|
||||||
<TouchableOpacity style={labelStyle} activeOpacity={0.3} onPress={handleLabelPress}>
|
<TouchableOpacity style={labelStyle} activeOpacity={0.3} onPress={handleLabelPress}>
|
||||||
{label ? <Text style={styles.name}>{label}</Text> : children}
|
{label ? <Text style={styles.name} size={15 * size}>{label}</Text> : children}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{helpComponent}
|
{helpComponent}
|
||||||
</View>
|
</View>
|
||||||
|
@ -73,6 +73,8 @@ export const storageDataPrefix = {
|
|||||||
cheatTip: '@cheat_tip',
|
cheatTip: '@cheat_tip',
|
||||||
|
|
||||||
dislikeList: '@dislike_list',
|
dislikeList: '@dislike_list',
|
||||||
|
|
||||||
|
userApi: '@user_api__',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// v0.x.x 版本的 data keys
|
// v0.x.x 版本的 data keys
|
||||||
@ -105,6 +107,7 @@ export const NAV_MENUS = [
|
|||||||
export type NAV_ID_Type = typeof NAV_MENUS[number]['id']
|
export type NAV_ID_Type = typeof NAV_MENUS[number]['id']
|
||||||
|
|
||||||
export const LXM_FILE_EXT_RXP = /\.(json|lxmc)$/
|
export const LXM_FILE_EXT_RXP = /\.(json|lxmc)$/
|
||||||
|
export const USER_API_SOURCE_FILE_EXT_RXP = /\.(js)$/
|
||||||
|
|
||||||
export const MUSIC_TOGGLE_MODE = {
|
export const MUSIC_TOGGLE_MODE = {
|
||||||
listLoop: 'listLoop', // 列表循环
|
listLoop: 'listLoop', // 列表循环
|
||||||
|
@ -45,12 +45,14 @@ global.lx = {
|
|||||||
// syncKeyInfo: {},
|
// syncKeyInfo: {},
|
||||||
|
|
||||||
isEnableSyncLog: false,
|
isEnableSyncLog: false,
|
||||||
|
isEnableUserApiLog: false,
|
||||||
|
|
||||||
playerTrackId: '',
|
playerTrackId: '',
|
||||||
|
|
||||||
gettingUrlId: '',
|
gettingUrlId: '',
|
||||||
|
|
||||||
qualityList: {},
|
qualityList: {},
|
||||||
|
apis: {},
|
||||||
|
|
||||||
jumpMyListPosition: false,
|
jumpMyListPosition: false,
|
||||||
|
|
||||||
|
@ -3,26 +3,22 @@ import musicSdk from '@/utils/musicSdk'
|
|||||||
// import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'
|
// import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'
|
||||||
import { updateSetting } from './common'
|
import { updateSetting } from './common'
|
||||||
import settingState from '@/store/setting/state'
|
import settingState from '@/store/setting/state'
|
||||||
|
import { destroyUserApi, setUserApi } from './userApi'
|
||||||
|
import apiSourceInfo from '@/utils/musicSdk/api-source-info'
|
||||||
|
|
||||||
|
|
||||||
export const setUserApi = (apiId: string) => {
|
export const setApiSource = (apiId: string) => {
|
||||||
if (/^user_api/.test(apiId)) {
|
if (/^user_api/.test(apiId)) {
|
||||||
// qualityList.value = {}
|
setUserApi(apiId).catch(err => {
|
||||||
// userApi.status = false
|
console.log(err)
|
||||||
// userApi.message = 'initing'
|
let api = apiSourceInfo.find(api => !api.disabled)
|
||||||
|
if (!api) return
|
||||||
// await setUserApiAction(apiId).then(() => {
|
if (api.id != settingState.setting['common.apiSource']) setApiSource(api.id)
|
||||||
// apiSource.value = apiId
|
})
|
||||||
// }).catch(err => {
|
|
||||||
// console.log(err)
|
|
||||||
// let api = apiSourceInfo.find(api => !api.disabled)
|
|
||||||
// if (!api) return
|
|
||||||
// apiSource.value = api.id
|
|
||||||
// if (api.id != appSetting['common.apiSource']) setApiSource(api.id)
|
|
||||||
// })
|
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
global.lx.qualityList = musicSdk.supportQuality[apiId] ?? {}
|
global.lx.qualityList = musicSdk.supportQuality[apiId] ?? {}
|
||||||
|
destroyUserApi()
|
||||||
// apiSource.value = apiId
|
// apiSource.value = apiId
|
||||||
// void setUserApiAction(apiId)
|
// void setUserApiAction(apiId)
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,12 @@ import { initSetting, showPactModal } from '@/core/common'
|
|||||||
import registerPlaybackService from '@/plugins/player/service'
|
import registerPlaybackService from '@/plugins/player/service'
|
||||||
import initTheme from './theme'
|
import initTheme from './theme'
|
||||||
import initI18n from './i18n'
|
import initI18n from './i18n'
|
||||||
|
import initUserApi from './userApi'
|
||||||
import initPlayer from './player'
|
import initPlayer from './player'
|
||||||
import dataInit from './dataInit'
|
import dataInit from './dataInit'
|
||||||
import initSync from './sync'
|
import initSync from './sync'
|
||||||
// import syncSetting from './syncSetting'
|
// import syncSetting from './syncSetting'
|
||||||
import { setUserApi } from '@/core/apiSource'
|
import { setApiSource } from '@/core/apiSource'
|
||||||
import commonActions from '@/store/common/action'
|
import commonActions from '@/store/common/action'
|
||||||
import settingState from '@/store/setting/state'
|
import settingState from '@/store/setting/state'
|
||||||
import { checkUpdate } from '@/core/version'
|
import { checkUpdate } from '@/core/version'
|
||||||
@ -42,7 +43,10 @@ export default async() => {
|
|||||||
await initI18n(setting)
|
await initI18n(setting)
|
||||||
bootLog('I18n inited.')
|
bootLog('I18n inited.')
|
||||||
|
|
||||||
setUserApi(setting['common.apiSource'])
|
await initUserApi(setting)
|
||||||
|
bootLog('User Api inited.')
|
||||||
|
|
||||||
|
setApiSource(setting['common.apiSource'])
|
||||||
bootLog('Api inited.')
|
bootLog('Api inited.')
|
||||||
|
|
||||||
registerPlaybackService()
|
registerPlaybackService()
|
||||||
|
172
src/core/init/userApi.ts
Normal file
172
src/core/init/userApi.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { type InitParams, onScriptAction, sendAction, type ResponseParams, type UpdateInfoParams } from '@/utils/nativeModules/userApi'
|
||||||
|
import { log, setUserApiList, setUserApiStatus } from '../userApi'
|
||||||
|
import settingState from '@/store/setting/state'
|
||||||
|
import BackgroundTimer from 'react-native-background-timer'
|
||||||
|
import { httpFetch } from '@/utils/request'
|
||||||
|
import { getUserApiList } from '@/utils/data'
|
||||||
|
|
||||||
|
|
||||||
|
export default async(setting: LX.AppSetting) => {
|
||||||
|
const requestQueue = new Map<string, { resolve: (value: ResponseParams['result']) => void, reject: (error: Error) => void, timeout: number }>()
|
||||||
|
|
||||||
|
const cancelRequest = (requestKey: string, message: string) => {
|
||||||
|
const target = requestQueue.get(requestKey)
|
||||||
|
if (!target) return
|
||||||
|
requestQueue.delete(requestKey)
|
||||||
|
BackgroundTimer.clearTimeout(target.timeout)
|
||||||
|
target.reject(new Error(message))
|
||||||
|
}
|
||||||
|
const sendUserApiRequest = async(data: LX.UserApi.UserApiRequestParams) => new Promise<ResponseParams['result']>((resolve, reject) => {
|
||||||
|
requestQueue.set(data.requestKey, {
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
timeout: BackgroundTimer.setTimeout(() => {
|
||||||
|
const target = requestQueue.get(data.requestKey)
|
||||||
|
if (!target) return
|
||||||
|
requestQueue.delete(data.requestKey)
|
||||||
|
target.reject(new Error('request timeout'))
|
||||||
|
}, 30_000),
|
||||||
|
})
|
||||||
|
sendAction('request', data)
|
||||||
|
})
|
||||||
|
const handleUserApiResponse = ({ status, result, requestKey, errorMessage }: ResponseParams) => {
|
||||||
|
const target = requestQueue.get(requestKey)
|
||||||
|
if (!target) return
|
||||||
|
requestQueue.delete(requestKey)
|
||||||
|
BackgroundTimer.clearTimeout(target.timeout)
|
||||||
|
if (status) target.resolve(result)
|
||||||
|
else target.reject(new Error(errorMessage ?? 'failed'))
|
||||||
|
}
|
||||||
|
const handleStateChange = ({ status, errorMessage, info, id }: InitParams) => {
|
||||||
|
// console.log(status, message, info)
|
||||||
|
setUserApiStatus(status, errorMessage)
|
||||||
|
if (!status || !info?.sources || id !== settingState.setting['common.apiSource']) return
|
||||||
|
|
||||||
|
let apis: any = {}
|
||||||
|
let qualitys: LX.QualityList = {}
|
||||||
|
for (const [source, { actions, type, qualitys: sourceQualitys }] of Object.entries(info.sources)) {
|
||||||
|
if (type != 'music') continue
|
||||||
|
apis[source as LX.Source] = {}
|
||||||
|
for (const action of actions) {
|
||||||
|
switch (action) {
|
||||||
|
case 'musicUrl':
|
||||||
|
apis[source].getMusicUrl = (songInfo: LX.Music.MusicInfo, type: LX.Quality) => {
|
||||||
|
const requestKey = `request__${Math.random().toString().substring(2)}`
|
||||||
|
return {
|
||||||
|
canceleFn() {
|
||||||
|
// userApiRequestCancel(requestKey)
|
||||||
|
},
|
||||||
|
promise: sendUserApiRequest({
|
||||||
|
requestKey,
|
||||||
|
data: {
|
||||||
|
source,
|
||||||
|
action: 'musicUrl',
|
||||||
|
info: {
|
||||||
|
type,
|
||||||
|
musicInfo: songInfo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||||
|
}).then(res => {
|
||||||
|
// console.log(res)
|
||||||
|
if (!/^https?:/.test(res.data.url)) return Promise.reject(new Error('Get url failed'))
|
||||||
|
return { type, url: res.data.url }
|
||||||
|
}).catch(async err => {
|
||||||
|
console.log(err.message)
|
||||||
|
return Promise.reject(err)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qualitys[source as LX.Source] = sourceQualitys
|
||||||
|
}
|
||||||
|
global.lx.qualityList = qualitys
|
||||||
|
global.lx.apis = apis
|
||||||
|
global.state_event.apiSourceUpdated(settingState.setting['common.apiSource'])
|
||||||
|
}
|
||||||
|
const showUpdateAlert = ({ name, log, updateUrl }: UpdateInfoParams) => {
|
||||||
|
// if (updateUrl) {
|
||||||
|
// void dialog({
|
||||||
|
// message: `${t('user_api__update_alert', { name })}\n${log}`,
|
||||||
|
// selection: true,
|
||||||
|
// showCancel: true,
|
||||||
|
// confirmButtonText: t('user_api__update_alert_open_url'),
|
||||||
|
// cancelButtonText: t('close'),
|
||||||
|
// }).then(confirm => {
|
||||||
|
// if (!confirm) return
|
||||||
|
// window.setTimeout(() => {
|
||||||
|
// void openUrl(updateUrl)
|
||||||
|
// }, 300)
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// void dialog({
|
||||||
|
// message: `${t('user_api__update_alert', { name })}\n${log}`,
|
||||||
|
// selection: true,
|
||||||
|
// confirmButtonText: t('ok'),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
onScriptAction((event) => {
|
||||||
|
// console.log('script actuon: ', event)
|
||||||
|
switch (event.action) {
|
||||||
|
case 'init':
|
||||||
|
handleStateChange(event.data)
|
||||||
|
break
|
||||||
|
case 'response':
|
||||||
|
handleUserApiResponse(event.data)
|
||||||
|
break
|
||||||
|
case 'request':
|
||||||
|
httpFetch(event.data.url, {
|
||||||
|
...event.data.options,
|
||||||
|
credentials: 'omit',
|
||||||
|
cache: 'default',
|
||||||
|
}).promise.then(response => {
|
||||||
|
// console.log(response)
|
||||||
|
sendAction('response', {
|
||||||
|
error: null,
|
||||||
|
requestKey: event.data.requestKey,
|
||||||
|
response,
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
sendAction('response', {
|
||||||
|
error: err.message,
|
||||||
|
requestKey: event.data.requestKey,
|
||||||
|
response: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'cancelRequest':
|
||||||
|
cancelRequest(event.data, 'request canceled')
|
||||||
|
break
|
||||||
|
case 'showUpdateAlert':
|
||||||
|
showUpdateAlert(event.data)
|
||||||
|
break
|
||||||
|
case 'log':
|
||||||
|
switch ((event as unknown as { type: keyof typeof log }).type) {
|
||||||
|
case 'log':
|
||||||
|
case 'info':
|
||||||
|
log.info((event as unknown as { log: string }).log)
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
log.error((event as unknown as { log: string }).log)
|
||||||
|
break
|
||||||
|
case 'warn':
|
||||||
|
log.warn((event as unknown as { log: string }).log)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setUserApiList(await getUserApiList())
|
||||||
|
}
|
69
src/core/userApi.ts
Normal file
69
src/core/userApi.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { action, state } from '@/store/userApi'
|
||||||
|
import { addUserApi, getUserApiScript, removeUserApi as removeUserApiFromStore, setUserApiAllowShowUpdateAlert as setUserApiAllowShowUpdateAlertFromStore } from '@/utils/data'
|
||||||
|
import { destroy, loadScript } from '@/utils/nativeModules/userApi'
|
||||||
|
import { log as writeLog } from '@/utils/log'
|
||||||
|
|
||||||
|
|
||||||
|
export const setUserApi = async(apiId: string) => {
|
||||||
|
global.lx.qualityList = {}
|
||||||
|
setUserApiStatus(false, 'initing')
|
||||||
|
|
||||||
|
const target = state.list.find(api => api.id === apiId)
|
||||||
|
if (!target) throw new Error('api not found')
|
||||||
|
const script = await getUserApiScript(target.id)
|
||||||
|
loadScript({ ...target, script })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const destroyUserApi = () => {
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const setUserApiStatus: typeof action['setStatus'] = (status, message) => {
|
||||||
|
action.setStatus(status, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setUserApiList: typeof action['setUserApiList'] = (list) => {
|
||||||
|
action.setUserApiList(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importUserApi = async(script: string) => {
|
||||||
|
const info = await addUserApi(script)
|
||||||
|
action.addUserApi(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeUserApi = async(ids: string[]) => {
|
||||||
|
console.log(ids)
|
||||||
|
const list = await removeUserApiFromStore(ids)
|
||||||
|
console.log(list)
|
||||||
|
action.setUserApiList(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setUserApiAllowShowUpdateAlert = async(id: string, enable: boolean) => {
|
||||||
|
await setUserApiAllowShowUpdateAlertFromStore(id, enable)
|
||||||
|
action.setUserApiAllowShowUpdateAlert(id, enable)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const log = {
|
||||||
|
r_info(...params: any[]) {
|
||||||
|
writeLog.info(...params)
|
||||||
|
},
|
||||||
|
r_warn(...params: any[]) {
|
||||||
|
writeLog.warn(...params)
|
||||||
|
},
|
||||||
|
r_error(...params: any[]) {
|
||||||
|
writeLog.error(...params)
|
||||||
|
},
|
||||||
|
log(...params: any[]) {
|
||||||
|
if (global.lx.isEnableUserApiLog) writeLog.info(...params)
|
||||||
|
},
|
||||||
|
info(...params: any[]) {
|
||||||
|
if (global.lx.isEnableUserApiLog) writeLog.info(...params)
|
||||||
|
},
|
||||||
|
warn(...params: any[]) {
|
||||||
|
if (global.lx.isEnableUserApiLog) writeLog.warn(...params)
|
||||||
|
},
|
||||||
|
error(...params: any[]) {
|
||||||
|
if (global.lx.isEnableUserApiLog) writeLog.error(...params)
|
||||||
|
},
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
"change_position_music_multi_title": "Adjust the position of the selected {num} song to",
|
"change_position_music_multi_title": "Adjust the position of the selected {num} song to",
|
||||||
"change_position_music_title": "Adjust the position of {name} to",
|
"change_position_music_title": "Adjust the position of {name} to",
|
||||||
"change_position_tip": "Please enter a new position",
|
"change_position_tip": "Please enter a new position",
|
||||||
|
"close": "Close",
|
||||||
"collect": "Collect",
|
"collect": "Collect",
|
||||||
"collect_songlist": "Collection Songlist",
|
"collect_songlist": "Collection Songlist",
|
||||||
"collect_success": "Collection success",
|
"collect_success": "Collection success",
|
||||||
@ -206,9 +207,13 @@
|
|||||||
"setting_basic_show_exit_btn": "Show exit app button",
|
"setting_basic_show_exit_btn": "Show exit app button",
|
||||||
"setting_basic_source": "Music source",
|
"setting_basic_source": "Music source",
|
||||||
"setting_basic_source_direct": "Direct API",
|
"setting_basic_source_direct": "Direct API",
|
||||||
|
"setting_basic_source_status_failed": "initialization failed",
|
||||||
|
"setting_basic_source_status_initing": "Initializing",
|
||||||
|
"setting_basic_source_status_success": "Initialization successful",
|
||||||
"setting_basic_source_temp": "Temporary API (some features not available; workaround if Test API unavailable)",
|
"setting_basic_source_temp": "Temporary API (some features not available; workaround if Test API unavailable)",
|
||||||
"setting_basic_source_test": "Test API (Available for most software features)",
|
"setting_basic_source_test": "Test API (Available for most software features)",
|
||||||
"setting_basic_source_title": "Choose a music source",
|
"setting_basic_source_title": "Choose a music source",
|
||||||
|
"setting_basic_source_user_api_btn": "Custom source management",
|
||||||
"setting_basic_sourcename": "Source name",
|
"setting_basic_sourcename": "Source name",
|
||||||
"setting_basic_sourcename_alias": "Aliases",
|
"setting_basic_sourcename_alias": "Aliases",
|
||||||
"setting_basic_sourcename_real": "Original",
|
"setting_basic_sourcename_real": "Original",
|
||||||
@ -260,6 +265,7 @@
|
|||||||
"setting_other_log_sync_log": "Record synchronization log",
|
"setting_other_log_sync_log": "Record synchronization log",
|
||||||
"setting_other_log_tip_clean_success": "Log cleaning completed",
|
"setting_other_log_tip_clean_success": "Log cleaning completed",
|
||||||
"setting_other_log_tip_null": "The log is empty~",
|
"setting_other_log_tip_null": "The log is empty~",
|
||||||
|
"setting_other_log_user_api_log": "Logging custom source logs",
|
||||||
"setting_play_audio_offload": "Enable audio offload",
|
"setting_play_audio_offload": "Enable audio offload",
|
||||||
"setting_play_audio_offload_tip": "Enabling audio offloading can save power consumption, but on some devices, all songs may prompt \"Audio loading error\" or \"The whole song cannot be played completely\". This is caused by a bug in the current system.\nFor People who encounter this problem can turn off this option and restart the application completely to try again.",
|
"setting_play_audio_offload_tip": "Enabling audio offloading can save power consumption, but on some devices, all songs may prompt \"Audio loading error\" or \"The whole song cannot be played completely\". This is caused by a bug in the current system.\nFor People who encounter this problem can turn off this option and restart the application completely to try again.",
|
||||||
"setting_play_auto_clean_played_list": "Automatically clear the played list",
|
"setting_play_auto_clean_played_list": "Automatically clear the played list",
|
||||||
@ -382,6 +388,14 @@
|
|||||||
"timeout_exit_tip_on": "Stop playing after {time}",
|
"timeout_exit_tip_on": "Stop playing after {time}",
|
||||||
"toggle_source_failed": "Failed to change the source, please try to manually search for the song in other sources to play",
|
"toggle_source_failed": "Failed to change the source, please try to manually search for the song in other sources to play",
|
||||||
"toggle_source_try": "Try switching to another source...",
|
"toggle_source_try": "Try switching to another source...",
|
||||||
|
"user_api_allow_show_update_alert": "Allow update popups to be displayed",
|
||||||
|
"user_api_btn_import": "Import",
|
||||||
|
"user_api_import_desc": "Select custom source file",
|
||||||
|
"user_api_import_failed_tip": "Import failed",
|
||||||
|
"user_api_note": "Tip: Although we have isolated the running environment of the script as much as possible, importing scripts containing malicious behaviors may still affect your system, so please import with caution.",
|
||||||
|
"user_api_readme": "Source writing instructions: ",
|
||||||
|
"user_api_remove_tip": "Do you really want to remove {name}?",
|
||||||
|
"user_api_title": "Custom source management",
|
||||||
"version_btn_close": "Close",
|
"version_btn_close": "Close",
|
||||||
"version_btn_downloading": "I am trying to download...{total}/{current} ({progress}%)",
|
"version_btn_downloading": "I am trying to download...{total}/{current} ({progress}%)",
|
||||||
"version_btn_failed": "Retry",
|
"version_btn_failed": "Retry",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"change_position_music_multi_title": "将已选的 {num} 首歌曲的位置调整到",
|
"change_position_music_multi_title": "将已选的 {num} 首歌曲的位置调整到",
|
||||||
"change_position_music_title": "将 {name} 的位置调整到",
|
"change_position_music_title": "将 {name} 的位置调整到",
|
||||||
"change_position_tip": "请输入新的位置",
|
"change_position_tip": "请输入新的位置",
|
||||||
|
"close": "关闭",
|
||||||
"collect": "收藏",
|
"collect": "收藏",
|
||||||
"collect_songlist": "收藏歌单",
|
"collect_songlist": "收藏歌单",
|
||||||
"collect_success": "收藏成功",
|
"collect_success": "收藏成功",
|
||||||
@ -206,9 +207,13 @@
|
|||||||
"setting_basic_show_exit_btn": "显示退出应用按钮",
|
"setting_basic_show_exit_btn": "显示退出应用按钮",
|
||||||
"setting_basic_source": "音乐来源",
|
"setting_basic_source": "音乐来源",
|
||||||
"setting_basic_source_direct": "试听接口(这是最后的选择...)",
|
"setting_basic_source_direct": "试听接口(这是最后的选择...)",
|
||||||
|
"setting_basic_source_status_failed": "初始化失败",
|
||||||
|
"setting_basic_source_status_initing": "初始化中",
|
||||||
|
"setting_basic_source_status_success": "初始化成功",
|
||||||
"setting_basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)",
|
"setting_basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)",
|
||||||
"setting_basic_source_test": "测试接口(几乎软件的所有功能都可用)",
|
"setting_basic_source_test": "测试接口(几乎软件的所有功能都可用)",
|
||||||
"setting_basic_source_title": "选择音乐来源",
|
"setting_basic_source_title": "选择音乐来源",
|
||||||
|
"setting_basic_source_user_api_btn": "自定义源管理",
|
||||||
"setting_basic_sourcename": "音源名字",
|
"setting_basic_sourcename": "音源名字",
|
||||||
"setting_basic_sourcename_alias": "别名",
|
"setting_basic_sourcename_alias": "别名",
|
||||||
"setting_basic_sourcename_real": "原名",
|
"setting_basic_sourcename_real": "原名",
|
||||||
@ -260,6 +265,7 @@
|
|||||||
"setting_other_log_sync_log": "记录同步日志",
|
"setting_other_log_sync_log": "记录同步日志",
|
||||||
"setting_other_log_tip_clean_success": "日志清理完成",
|
"setting_other_log_tip_clean_success": "日志清理完成",
|
||||||
"setting_other_log_tip_null": "日志是空的哦~",
|
"setting_other_log_tip_null": "日志是空的哦~",
|
||||||
|
"setting_other_log_user_api_log": "记录自定义源日志",
|
||||||
"setting_play_audio_offload": "启用音频卸载",
|
"setting_play_audio_offload": "启用音频卸载",
|
||||||
"setting_play_audio_offload_tip": "启用音频卸载可以节省耗电量,但在某些设备上可能会出现所有歌曲都提示 「音频加载出错」 或者 「无法完整播放整首歌」 的问题,这是由于当前系统的bug导致的。\n对于遇到这个问题的人可以关闭此选项后完全重启应用再试。",
|
"setting_play_audio_offload_tip": "启用音频卸载可以节省耗电量,但在某些设备上可能会出现所有歌曲都提示 「音频加载出错」 或者 「无法完整播放整首歌」 的问题,这是由于当前系统的bug导致的。\n对于遇到这个问题的人可以关闭此选项后完全重启应用再试。",
|
||||||
"setting_play_auto_clean_played_list": "自动清空已播放列表",
|
"setting_play_auto_clean_played_list": "自动清空已播放列表",
|
||||||
@ -382,6 +388,14 @@
|
|||||||
"timeout_exit_tip_on": "{time} 后停止播放",
|
"timeout_exit_tip_on": "{time} 后停止播放",
|
||||||
"toggle_source_failed": "换源失败,请尝试手动在其他源搜索该歌曲播放",
|
"toggle_source_failed": "换源失败,请尝试手动在其他源搜索该歌曲播放",
|
||||||
"toggle_source_try": "尝试切换到其他源...",
|
"toggle_source_try": "尝试切换到其他源...",
|
||||||
|
"user_api_allow_show_update_alert": "允许显示更新弹窗",
|
||||||
|
"user_api_btn_import": "导入",
|
||||||
|
"user_api_import_desc": "选择自定义源文件",
|
||||||
|
"user_api_import_failed_tip": "导入失败",
|
||||||
|
"user_api_note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",
|
||||||
|
"user_api_readme": "源编写说明:",
|
||||||
|
"user_api_remove_tip": "你真的要移除 {name} 吗?",
|
||||||
|
"user_api_title": "自定义源管理",
|
||||||
"version_btn_close": "关闭",
|
"version_btn_close": "关闭",
|
||||||
"version_btn_downloading": "正在努力下载中...{total}/{current} ({progress}%)",
|
"version_btn_downloading": "正在努力下载中...{total}/{current} ({progress}%)",
|
||||||
"version_btn_failed": "重试",
|
"version_btn_failed": "重试",
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { memo, useCallback, useMemo } from 'react'
|
import { memo, useCallback, useMemo, useRef } from 'react'
|
||||||
|
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
|
|
||||||
import SubTitle from '../../components/SubTitle'
|
import SubTitle from '../../components/SubTitle'
|
||||||
import CheckBox from '@/components/common/CheckBox'
|
import CheckBox from '@/components/common/CheckBox'
|
||||||
import { createStyle } from '@/utils/tools'
|
import { createStyle } from '@/utils/tools'
|
||||||
import { setUserApi } from '@/core/apiSource'
|
import { setApiSource } from '@/core/apiSource'
|
||||||
import { useI18n } from '@/lang'
|
import { useI18n } from '@/lang'
|
||||||
import apiSourceInfo from '@/utils/musicSdk/api-source-info'
|
import apiSourceInfo from '@/utils/musicSdk/api-source-info'
|
||||||
import { useSettingValue } from '@/store/setting/hook'
|
import { useSettingValue } from '@/store/setting/hook'
|
||||||
|
import { useStatus, useUserApiList } from '@/store/userApi'
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import UserApiEditModal, { type UserApiEditModalType } from './UserApiEditModal'
|
||||||
|
// import { importUserApi, removeUserApi } from '@/core/userApi'
|
||||||
|
|
||||||
const apiSourceList = apiSourceInfo.map(api => ({
|
const apiSourceList = apiSourceInfo.map(api => ({
|
||||||
id: api.id,
|
id: api.id,
|
||||||
@ -40,8 +44,35 @@ export default memo(() => {
|
|||||||
id: s.id,
|
id: s.id,
|
||||||
})), [t])
|
})), [t])
|
||||||
const setApiSourceId = useCallback((id: string) => {
|
const setApiSourceId = useCallback((id: string) => {
|
||||||
setUserApi(id)
|
setApiSource(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
const userApiListRaw = useUserApiList()
|
||||||
|
const apiStatus = useStatus()
|
||||||
|
const apiSourceSetting = useSettingValue('common.apiSource')
|
||||||
|
const userApiList = useMemo(() => {
|
||||||
|
const getApiStatus = () => {
|
||||||
|
let status
|
||||||
|
if (apiStatus.status) status = t('setting_basic_source_status_success')
|
||||||
|
else if (apiStatus.message == 'initing') status = t('setting_basic_source_status_initing')
|
||||||
|
else status = t('setting_basic_source_status_failed')
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
return userApiListRaw.map(api => {
|
||||||
|
return {
|
||||||
|
id: api.id,
|
||||||
|
name: `${api.name}${api.description ? `(${api.description})` : ''}${api.id == apiSourceSetting ? `[${getApiStatus()}]` : ''}`,
|
||||||
|
// status: apiStatus.status,
|
||||||
|
// message: apiStatus.message,
|
||||||
|
// disabled: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [userApiListRaw, apiStatus, apiSourceSetting, t])
|
||||||
|
|
||||||
|
const modalRef = useRef<UserApiEditModalType>(null)
|
||||||
|
const handleShow = () => {
|
||||||
|
modalRef.current?.show()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubTitle title={t('setting_basic_source')}>
|
<SubTitle title={t('setting_basic_source')}>
|
||||||
@ -49,7 +80,14 @@ export default memo(() => {
|
|||||||
{
|
{
|
||||||
list.map(({ id, name }) => <Item name={name} id={id} key={id} change={setApiSourceId} />)
|
list.map(({ id, name }) => <Item name={name} id={id} key={id} change={setApiSourceId} />)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
userApiList.map(({ id, name }) => <Item name={name} id={id} key={id} change={setApiSourceId} />)
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.btn}>
|
||||||
|
<Button onPress={handleShow}>{t('setting_basic_source_user_api_btn')}</Button>
|
||||||
|
</View>
|
||||||
|
<UserApiEditModal ref={modalRef} />
|
||||||
</SubTitle>
|
</SubTitle>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -61,4 +99,8 @@ const styles = createStyle({
|
|||||||
// flexDirection: 'row',
|
// flexDirection: 'row',
|
||||||
// flexWrap: 'wrap',
|
// flexWrap: 'wrap',
|
||||||
},
|
},
|
||||||
|
btn: {
|
||||||
|
marginTop: 10,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import Text from '@/components/common/Text'
|
||||||
|
import { View, TouchableOpacity, ScrollView } from 'react-native'
|
||||||
|
import { confirmDialog, createStyle } from '@/utils/tools'
|
||||||
|
import { useTheme } from '@/store/theme/hook'
|
||||||
|
import { useI18n } from '@/lang'
|
||||||
|
import { useUserApiList } from '@/store/userApi'
|
||||||
|
import { useSettingValue } from '@/store/setting/hook'
|
||||||
|
import { removeUserApi, setUserApiAllowShowUpdateAlert } from '@/core/userApi'
|
||||||
|
import { BorderRadius } from '@/theme'
|
||||||
|
import CheckBox from '@/components/common/CheckBox'
|
||||||
|
import { Icon } from '@/components/common/Icon'
|
||||||
|
|
||||||
|
|
||||||
|
const ListItem = ({ item, activeId, onRemove, onChangeAllowShowUpdateAlert }: {
|
||||||
|
item: LX.UserApi.UserApiInfo
|
||||||
|
activeId: string
|
||||||
|
onRemove: (id: string, name: string) => void
|
||||||
|
onChangeAllowShowUpdateAlert: (id: string, enabled: boolean) => void
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const t = useI18n()
|
||||||
|
const changeAllowShowUpdateAlert = (check: boolean) => {
|
||||||
|
onChangeAllowShowUpdateAlert(item.id, check)
|
||||||
|
}
|
||||||
|
const handleRemove = () => {
|
||||||
|
onRemove(item.id, item.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ ...styles.listItem, backgroundColor: activeId == item.id ? theme['c-primary-background-active'] : 'transparent' }}>
|
||||||
|
<View>
|
||||||
|
<Text size={13}>{item.name}</Text>
|
||||||
|
<Text size={12} color={theme['c-font-label']}>{item.description}</Text>
|
||||||
|
<CheckBox check={item.allowShowUpdateAlert} label={t('user_api_allow_show_update_alert')} onChange={changeAllowShowUpdateAlert} size={0.8} />
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<TouchableOpacity style={styles.btn} onPress={handleRemove}>
|
||||||
|
<Icon name="close" color={theme['c-button-font']} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserApiEditModalProps {
|
||||||
|
onSave: (rules: string) => void
|
||||||
|
// onSourceChange: SourceSelectorProps['onSourceChange']
|
||||||
|
}
|
||||||
|
export interface UserApiEditModalType {
|
||||||
|
show: (rules: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const userApiList = useUserApiList()
|
||||||
|
const apiSource = useSettingValue('common.apiSource')
|
||||||
|
|
||||||
|
const handleRemove = useCallback(async(id: string, name: string) => {
|
||||||
|
const confirm = await confirmDialog({
|
||||||
|
message: global.i18n.t('user_api_remove_tip', { name }),
|
||||||
|
cancelButtonText: global.i18n.t('cancel_button_text_2'),
|
||||||
|
confirmButtonText: global.i18n.t('confirm_button_text'),
|
||||||
|
bgClose: false,
|
||||||
|
})
|
||||||
|
if (!confirm) return
|
||||||
|
void removeUserApi([id])
|
||||||
|
}, [])
|
||||||
|
const handleChangeAllowShowUpdateAlert = useCallback((id: string, enabled: boolean) => {
|
||||||
|
void setUserApiAllowShowUpdateAlert(id, enabled)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.scrollView} keyboardShouldPersistTaps={'always'}>
|
||||||
|
<View onStartShouldSetResponder={() => true}>
|
||||||
|
{
|
||||||
|
userApiList.map((item) => {
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
activeId={apiSource}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
onChangeAllowShowUpdateAlert={handleChangeAllowShowUpdateAlert}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const styles = createStyle({
|
||||||
|
scrollView: {
|
||||||
|
flexGrow: 0,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
paddingBottom: 15,
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
listItem: {
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: BorderRadius.normal,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
// btns: {
|
||||||
|
// padding: 5,
|
||||||
|
// },
|
||||||
|
btn: {
|
||||||
|
padding: 10,
|
||||||
|
// backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
|||||||
|
import ChoosePath, { type ChoosePathType } from '@/components/common/ChoosePath'
|
||||||
|
import { USER_API_SOURCE_FILE_EXT_RXP } from '@/config/constant'
|
||||||
|
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||||
|
import { handleImport } from './action'
|
||||||
|
|
||||||
|
export interface SelectInfo {
|
||||||
|
// listInfo: LX.List.MyListInfo
|
||||||
|
// selectedList: LX.Music.MusicInfo[]
|
||||||
|
// index: number
|
||||||
|
// listId: string
|
||||||
|
// single: boolean
|
||||||
|
action: 'import'
|
||||||
|
}
|
||||||
|
const initSelectInfo = {}
|
||||||
|
|
||||||
|
// export interface ScriptImportExportProps {
|
||||||
|
// // onRename: (listInfo: LX.List.UserListInfo) => void
|
||||||
|
// // onImport: (index: number) => void
|
||||||
|
// // onExport: (listInfo: LX.List.MyListInfo) => void
|
||||||
|
// // onSync: (listInfo: LX.List.UserListInfo) => void
|
||||||
|
// // onRemove: (listInfo: LX.List.MyListInfo) => void
|
||||||
|
// }
|
||||||
|
export interface ScriptImportExportType {
|
||||||
|
import: () => void
|
||||||
|
// export: (listInfo: LX.List.MyListInfo, index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef<ScriptImportExportType, {}>((props, ref) => {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const choosePathRef = useRef<ChoosePathType>(null)
|
||||||
|
const selectInfoRef = useRef<SelectInfo>((initSelectInfo as SelectInfo))
|
||||||
|
// console.log('render import export')
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
import() {
|
||||||
|
selectInfoRef.current = {
|
||||||
|
action: 'import',
|
||||||
|
}
|
||||||
|
if (visible) {
|
||||||
|
choosePathRef.current?.show({
|
||||||
|
title: global.i18n.t('user_api_import_desc'),
|
||||||
|
dirOnly: false,
|
||||||
|
filter: USER_API_SOURCE_FILE_EXT_RXP,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setVisible(true)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
choosePathRef.current?.show({
|
||||||
|
title: global.i18n.t('user_api_import_desc'),
|
||||||
|
dirOnly: false,
|
||||||
|
filter: USER_API_SOURCE_FILE_EXT_RXP,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// export(listInfo, index) {
|
||||||
|
// selectInfoRef.current = {
|
||||||
|
// action: 'export',
|
||||||
|
// listInfo,
|
||||||
|
// index,
|
||||||
|
// }
|
||||||
|
// if (visible) {
|
||||||
|
// choosePathRef.current?.show({
|
||||||
|
// title: global.i18n.t('list_export_part_desc'),
|
||||||
|
// dirOnly: true,
|
||||||
|
// filter: LXM_FILE_EXT_RXP,
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// setVisible(true)
|
||||||
|
// requestAnimationFrame(() => {
|
||||||
|
// choosePathRef.current?.show({
|
||||||
|
// title: global.i18n.t('list_export_part_desc'),
|
||||||
|
// dirOnly: true,
|
||||||
|
// filter: LXM_FILE_EXT_RXP,
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
const onConfirmPath = (path: string) => {
|
||||||
|
switch (selectInfoRef.current.action) {
|
||||||
|
case 'import':
|
||||||
|
handleImport(path)
|
||||||
|
break
|
||||||
|
// case 'export':
|
||||||
|
// handleExport(selectInfoRef.current.listInfo, path)
|
||||||
|
// break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
visible
|
||||||
|
? <ChoosePath ref={choosePathRef} onConfirm={onConfirmPath} />
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
})
|
@ -0,0 +1,17 @@
|
|||||||
|
import { importUserApi } from '@/core/userApi'
|
||||||
|
import { log } from '@/utils/log'
|
||||||
|
import { readFile } from '@/utils/nativeModules/utils'
|
||||||
|
import { toast } from '@/utils/tools'
|
||||||
|
|
||||||
|
|
||||||
|
export const handleImport = (path: string) => {
|
||||||
|
// toast(global.i18n.t('setting_backup_part_import_list_tip_unzip'))
|
||||||
|
void readFile(path).then(async script => {
|
||||||
|
if (script == null) throw new Error('Read file failed')
|
||||||
|
await importUserApi(script)
|
||||||
|
}).catch((error: any) => {
|
||||||
|
log.error(error.stack)
|
||||||
|
toast(global.i18n.t('user_api_import_failed_tip'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
|||||||
|
import { useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
||||||
|
import Text from '@/components/common/Text'
|
||||||
|
import { View, TouchableOpacity } from 'react-native'
|
||||||
|
import { createStyle, openUrl } from '@/utils/tools'
|
||||||
|
import { useTheme } from '@/store/theme/hook'
|
||||||
|
import { useI18n } from '@/lang'
|
||||||
|
import Dialog, { type DialogType } from '@/components/common/Dialog'
|
||||||
|
import Button from '@/components/common/Button'
|
||||||
|
import List from './List'
|
||||||
|
import ScriptImportExport, { type ScriptImportExportType } from './ScriptImportExport'
|
||||||
|
|
||||||
|
// interface UrlInputType {
|
||||||
|
// setText: (text: string) => void
|
||||||
|
// getText: () => string
|
||||||
|
// focus: () => void
|
||||||
|
// }
|
||||||
|
// const UrlInput = forwardRef<UrlInputType, {}>((props, ref) => {
|
||||||
|
// const theme = useTheme()
|
||||||
|
// const t = useI18n()
|
||||||
|
// const [text, setText] = useState('')
|
||||||
|
// const inputRef = useRef<InputType>(null)
|
||||||
|
// const [height, setHeight] = useState(100)
|
||||||
|
|
||||||
|
// useImperativeHandle(ref, () => ({
|
||||||
|
// getText() {
|
||||||
|
// return text.trim()
|
||||||
|
// },
|
||||||
|
// setText(text) {
|
||||||
|
// setText(text)
|
||||||
|
// },
|
||||||
|
// focus() {
|
||||||
|
// inputRef.current?.focus()
|
||||||
|
// },
|
||||||
|
// }))
|
||||||
|
|
||||||
|
// const handleLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
|
||||||
|
// setHeight(nativeEvent.layout.height)
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <View style={styles.inputContent} onLayout={handleLayout}>
|
||||||
|
// <Input
|
||||||
|
// ref={inputRef}
|
||||||
|
// value={text}
|
||||||
|
// onChangeText={setText}
|
||||||
|
// textAlignVertical="top"
|
||||||
|
// placeholder={t('setting_dislike_list_input_tip')}
|
||||||
|
// size={12}
|
||||||
|
// style={{ ...styles.input, height, backgroundColor: theme['c-primary-input-background'] }}
|
||||||
|
// />
|
||||||
|
// </View>
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
|
||||||
|
// export interface UserApiEditModalProps {
|
||||||
|
// onSave: (rules: string) => void
|
||||||
|
// // onSourceChange: SourceSelectorProps['onSourceChange']
|
||||||
|
// }
|
||||||
|
export interface UserApiEditModalType {
|
||||||
|
show: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef<UserApiEditModalType, {}>((props, ref) => {
|
||||||
|
const dialogRef = useRef<DialogType>(null)
|
||||||
|
const scriptImportExportRef = useRef<ScriptImportExportType>(null)
|
||||||
|
// const sourceSelectorRef = useRef<SourceSelectorType>(null)
|
||||||
|
// const inputRef = useRef<UrlInputType>(null)
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const theme = useTheme()
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const handleShow = () => {
|
||||||
|
dialogRef.current?.setVisible(true)
|
||||||
|
// requestAnimationFrame(() => {
|
||||||
|
// inputRef.current?.setText('')
|
||||||
|
// sourceSelectorRef.current?.setSource(source)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// inputRef.current?.focus()
|
||||||
|
// }, 300)
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
show() {
|
||||||
|
if (visible) handleShow()
|
||||||
|
else {
|
||||||
|
setVisible(true)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
handleShow()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
dialogRef.current?.setVisible(false)
|
||||||
|
}
|
||||||
|
const handleImport = () => {
|
||||||
|
scriptImportExportRef.current?.import()
|
||||||
|
}
|
||||||
|
const openFAQPage = () => {
|
||||||
|
void openUrl('https://lyswhut.github.io/lx-music-doc/mobile/custom-source')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
visible
|
||||||
|
? (
|
||||||
|
<Dialog ref={dialogRef} bgHide={false}>
|
||||||
|
<View style={styles.content}>
|
||||||
|
{/* <UrlInput ref={inputRef} /> */}
|
||||||
|
<Text style={styles.title}>{t('user_api_title')}</Text>
|
||||||
|
<List />
|
||||||
|
<View style={styles.tips}>
|
||||||
|
<Text style={styles.tipsText} size={12}>
|
||||||
|
{t('user_api_readme')}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity onPress={openFAQPage}>
|
||||||
|
<Text style={{ ...styles.tipsText, textDecorationLine: 'underline' }} size={12} color={theme['c-primary-font']}>FAQ</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.tipsText} size={12}>{t('user_api_note')}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.btns}>
|
||||||
|
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>
|
||||||
|
<Text size={14} color={theme['c-button-font']}>{t('close')}</Text>
|
||||||
|
</Button>
|
||||||
|
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleImport}>
|
||||||
|
<Text size={14} color={theme['c-button-font']}>{t('user_api_btn_import')}</Text>
|
||||||
|
</Button>
|
||||||
|
<ScriptImportExport ref={scriptImportExportRef} />
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const styles = createStyle({
|
||||||
|
content: {
|
||||||
|
// flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
paddingHorizontal: 15,
|
||||||
|
paddingTop: 15,
|
||||||
|
paddingBottom: 10,
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginBottom: 15,
|
||||||
|
textAlign: 'center',
|
||||||
|
// backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
},
|
||||||
|
tips: {
|
||||||
|
marginTop: 15,
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
tipsText: {
|
||||||
|
marginTop: 8,
|
||||||
|
textAlignVertical: 'bottom',
|
||||||
|
// lineHeight: 18,
|
||||||
|
// backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
},
|
||||||
|
btns: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingBottom: 15,
|
||||||
|
paddingLeft: 15,
|
||||||
|
// paddingRight: 15,
|
||||||
|
},
|
||||||
|
btn: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingBottom: 8,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 4,
|
||||||
|
marginRight: 15,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -17,6 +17,7 @@ export default memo(() => {
|
|||||||
const [logText, setLogText] = useState('')
|
const [logText, setLogText] = useState('')
|
||||||
const isUnmountedRef = useRef(true)
|
const isUnmountedRef = useRef(true)
|
||||||
const [isEnableSyncErrorLog, setIsEnableSyncErrorLog] = useState(global.lx.isEnableSyncLog)
|
const [isEnableSyncErrorLog, setIsEnableSyncErrorLog] = useState(global.lx.isEnableSyncLog)
|
||||||
|
const [isEnableUserApiLog, setIsEnableUserApiLog] = useState(global.lx.isEnableUserApiLog)
|
||||||
|
|
||||||
const getErrorLog = () => {
|
const getErrorLog = () => {
|
||||||
void getLogs().then(log => {
|
void getLogs().then(log => {
|
||||||
@ -45,6 +46,11 @@ export default memo(() => {
|
|||||||
global.lx.isEnableSyncLog = enable
|
global.lx.isEnableSyncLog = enable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSetEnableUserApiLog = (enable: boolean) => {
|
||||||
|
setIsEnableUserApiLog(enable)
|
||||||
|
global.lx.isEnableUserApiLog = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isUnmountedRef.current = false
|
isUnmountedRef.current = false
|
||||||
@ -58,6 +64,7 @@ export default memo(() => {
|
|||||||
<SubTitle title={t('setting_other_log')}>
|
<SubTitle title={t('setting_other_log')}>
|
||||||
<View style={styles.checkBox}>
|
<View style={styles.checkBox}>
|
||||||
<CheckBoxItem check={isEnableSyncErrorLog} label={t('setting_other_log_sync_log')} onChange={handleSetEnableSyncErrorLog} />
|
<CheckBoxItem check={isEnableSyncErrorLog} label={t('setting_other_log_sync_log')} onChange={handleSetEnableSyncErrorLog} />
|
||||||
|
<CheckBoxItem check={isEnableUserApiLog} label={t('setting_other_log_user_api_log')} onChange={handleSetEnableUserApiLog} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.btn}>
|
<View style={styles.btn}>
|
||||||
<Button onPress={openLogModal}>{t('setting_other_log_btn_show')}</Button>
|
<Button onPress={openLogModal}>{t('setting_other_log_btn_show')}</Button>
|
||||||
|
31
src/store/userApi/action.ts
Normal file
31
src/store/userApi/action.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { state } from './state'
|
||||||
|
import { event } from './event'
|
||||||
|
|
||||||
|
export const setStatus = (status: LX.UserApi.UserApiStatus['status'], message: LX.UserApi.UserApiStatus['message']) => {
|
||||||
|
state.status.status = status
|
||||||
|
state.status.message = message
|
||||||
|
|
||||||
|
event.status_changed({ status, message })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const setUserApiList = (list: LX.UserApi.UserApiInfo[]) => {
|
||||||
|
state.list = list
|
||||||
|
|
||||||
|
event.list_changed([...list])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addUserApi = (info: LX.UserApi.UserApiInfo) => {
|
||||||
|
state.list.push(info)
|
||||||
|
|
||||||
|
event.list_changed([...state.list])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const setUserApiAllowShowUpdateAlert = (id: string, enable: boolean) => {
|
||||||
|
const targetIndex = state.list.findIndex(api => api.id == id)
|
||||||
|
if (targetIndex < 0) return
|
||||||
|
state.list.splice(targetIndex, 1, { ...state.list[targetIndex], allowShowUpdateAlert: enable })
|
||||||
|
|
||||||
|
event.list_changed([...state.list])
|
||||||
|
}
|
26
src/store/userApi/event.ts
Normal file
26
src/store/userApi/event.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import Event from '@/event/Event'
|
||||||
|
|
||||||
|
|
||||||
|
class UserApiEvent extends Event {
|
||||||
|
status_changed(status: { status: boolean, message?: string }) {
|
||||||
|
this.emit('status_changed', status)
|
||||||
|
}
|
||||||
|
|
||||||
|
list_changed(list: LX.UserApi.UserApiInfo[]) {
|
||||||
|
this.emit('list_changed', list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type EventMethods = Omit<EventType, keyof Event>
|
||||||
|
|
||||||
|
|
||||||
|
declare class EventType extends UserApiEvent {
|
||||||
|
on<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): any
|
||||||
|
off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): any
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserApiEventTypes = Omit<EventType, keyof Omit<Event, 'on' | 'off'>>
|
||||||
|
|
||||||
|
|
||||||
|
export const event: UserApiEventTypes = new UserApiEvent()
|
29
src/store/userApi/hook.ts
Normal file
29
src/store/userApi/hook.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { state } from './state'
|
||||||
|
import { event } from './event'
|
||||||
|
|
||||||
|
export const useStatus = () => {
|
||||||
|
const [value, update] = useState(state.status)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
event.on('status_changed', update)
|
||||||
|
return () => {
|
||||||
|
event.off('status_changed', update)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserApiList = () => {
|
||||||
|
const [value, update] = useState(state.list)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
event.on('list_changed', update)
|
||||||
|
return () => {
|
||||||
|
event.off('list_changed', update)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
4
src/store/userApi/index.ts
Normal file
4
src/store/userApi/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export * as action from './action'
|
||||||
|
export * from './state'
|
||||||
|
export * from './hook'
|
22
src/store/userApi/state.ts
Normal file
22
src/store/userApi/state.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
interface InitState {
|
||||||
|
list: LX.UserApi.UserApiInfo[]
|
||||||
|
status: {
|
||||||
|
status: boolean
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
apis: Partial<LX.UserApi.UserApiSources>
|
||||||
|
}
|
||||||
|
const state: InitState = {
|
||||||
|
list: [],
|
||||||
|
status: {
|
||||||
|
status: false,
|
||||||
|
message: 'initing',
|
||||||
|
},
|
||||||
|
apis: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
state,
|
||||||
|
}
|
2
src/types/app.d.ts
vendored
2
src/types/app.d.ts
vendored
@ -31,9 +31,11 @@ interface GlobalData {
|
|||||||
isScreenKeepAwake: boolean
|
isScreenKeepAwake: boolean
|
||||||
isPlayedStop: boolean
|
isPlayedStop: boolean
|
||||||
isEnableSyncLog: boolean
|
isEnableSyncLog: boolean
|
||||||
|
isEnableUserApiLog: boolean
|
||||||
playerTrackId: string
|
playerTrackId: string
|
||||||
|
|
||||||
qualityList: LX.QualityList
|
qualityList: LX.QualityList
|
||||||
|
apis: Partial<LX.UserApi.UserApiSources>
|
||||||
|
|
||||||
jumpMyListPosition: boolean
|
jumpMyListPosition: boolean
|
||||||
|
|
||||||
|
60
src/types/user_api.d.ts
vendored
Normal file
60
src/types/user_api.d.ts
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
declare namespace LX {
|
||||||
|
namespace UserApi {
|
||||||
|
type UserApiSourceInfoType = 'music'
|
||||||
|
type UserApiSourceInfoActions = 'musicUrl'
|
||||||
|
|
||||||
|
interface UserApiSourceInfo {
|
||||||
|
name: string
|
||||||
|
type: UserApiSourceInfoType
|
||||||
|
actions: UserApiSourceInfoActions[]
|
||||||
|
qualitys: LX.Quality[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserApiSources = Record<LX.Source, UserApiSourceInfo>
|
||||||
|
|
||||||
|
|
||||||
|
interface UserApiInfo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
// script: string
|
||||||
|
allowShowUpdateAlert: boolean
|
||||||
|
sources?: UserApiSources
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserApiStatus {
|
||||||
|
status: boolean
|
||||||
|
message?: string
|
||||||
|
apiInfo?: UserApiInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserApiUpdateInfo {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
log: string
|
||||||
|
updateUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserApiRequestParams {
|
||||||
|
requestKey: string
|
||||||
|
data: any
|
||||||
|
}
|
||||||
|
interface UserApiRequestParams {
|
||||||
|
requestKey: string
|
||||||
|
data: any
|
||||||
|
}
|
||||||
|
type UserApiRequestCancelParams = string
|
||||||
|
type UserApiSetApiParams = string
|
||||||
|
|
||||||
|
interface UserApiSetAllowUpdateAlertParams {
|
||||||
|
id: string
|
||||||
|
enable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportUserApi {
|
||||||
|
apiInfo: UserApiInfo
|
||||||
|
apiList: UserApiInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ const syncHostPrefix = storageDataPrefix.syncHost
|
|||||||
const syncHostHistoryPrefix = storageDataPrefix.syncHostHistory
|
const syncHostHistoryPrefix = storageDataPrefix.syncHostHistory
|
||||||
const listPrefix = storageDataPrefix.list
|
const listPrefix = storageDataPrefix.list
|
||||||
const dislikeListPrefix = storageDataPrefix.dislikeList
|
const dislikeListPrefix = storageDataPrefix.dislikeList
|
||||||
|
const userApiPrefix = storageDataPrefix.userApi
|
||||||
|
|
||||||
// const defaultListKey = listPrefix + 'default'
|
// const defaultListKey = listPrefix + 'default'
|
||||||
// const loveListKey = listPrefix + 'love'
|
// const loveListKey = listPrefix + 'love'
|
||||||
@ -460,3 +461,54 @@ export const removeSyncHostHistory = async(index: number) => {
|
|||||||
await saveData(syncHostHistoryPrefix, syncHostHistory)
|
await saveData(syncHostHistoryPrefix, syncHostHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let userApis: LX.UserApi.UserApiInfo[] = []
|
||||||
|
export const getUserApiList = async(): Promise<LX.UserApi.UserApiInfo[]> => {
|
||||||
|
userApis = await getData<LX.UserApi.UserApiInfo[]>(userApiPrefix) ?? []
|
||||||
|
return [...userApis]
|
||||||
|
}
|
||||||
|
export const getUserApiScript = async(id: string): Promise<string> => {
|
||||||
|
const script = await getData<string>(`${userApiPrefix}${id}`) ?? ''
|
||||||
|
return script
|
||||||
|
}
|
||||||
|
export const addUserApi = async(script: string): Promise<LX.UserApi.UserApiInfo> => {
|
||||||
|
let scriptInfo = script.split(/\r?\n/)
|
||||||
|
let name = scriptInfo[1] || ''
|
||||||
|
let description = scriptInfo[2] || ''
|
||||||
|
name = name.startsWith(' * @name ') ? name.replace(' * @name ', '').trim() : `user_api_${new Date().toLocaleString()}`
|
||||||
|
if (name.length > 24) name = name.substring(0, 24) + '...'
|
||||||
|
description = description.startsWith(' * @description ') ? description.replace(' * @description ', '').trim() : ''
|
||||||
|
if (description.length > 36) description = description.substring(0, 36) + '...'
|
||||||
|
const apiInfo = {
|
||||||
|
id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
// script,
|
||||||
|
allowShowUpdateAlert: true,
|
||||||
|
}
|
||||||
|
userApis.push(apiInfo)
|
||||||
|
await saveDataMultiple([
|
||||||
|
[userApiPrefix, userApis],
|
||||||
|
[`${userApiPrefix}${apiInfo.id}`, script],
|
||||||
|
])
|
||||||
|
return apiInfo
|
||||||
|
}
|
||||||
|
export const removeUserApi = async(ids: string[]) => {
|
||||||
|
if (!userApis) return []
|
||||||
|
const _ids: string[] = []
|
||||||
|
for (let index = userApis.length - 1; index > -1; index--) {
|
||||||
|
if (ids.includes(userApis[index].id)) {
|
||||||
|
_ids.push(`${userApiPrefix}${userApis[index].id}`)
|
||||||
|
userApis.splice(index, 1)
|
||||||
|
ids.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveData(userApiPrefix, userApis)
|
||||||
|
if (_ids.length) await removeDataMultiple(_ids)
|
||||||
|
return [...userApis]
|
||||||
|
}
|
||||||
|
export const setUserApiAllowShowUpdateAlert = async(id: string, enable: boolean) => {
|
||||||
|
const targetApi = userApis?.find(api => api.id == id)
|
||||||
|
if (!targetApi) return
|
||||||
|
targetApi.allowShowUpdateAlert = enable
|
||||||
|
await saveData(userApiPrefix, userApis)
|
||||||
|
}
|
||||||
|
@ -47,6 +47,7 @@ for (const api of apiSourceInfo) {
|
|||||||
const getAPI = source => apiList[`${settingState.setting['common.apiSource']}_api_${source}`]
|
const getAPI = source => apiList[`${settingState.setting['common.apiSource']}_api_${source}`]
|
||||||
|
|
||||||
const apis = source => {
|
const apis = source => {
|
||||||
|
if (/^user_api/.test(settingState.setting['common.apiSource'])) return global.lx.apis[source]
|
||||||
const api = getAPI(source)
|
const api = getAPI(source)
|
||||||
if (api) return api
|
if (api) return api
|
||||||
throw new Error('Api is not found')
|
throw new Error('Api is not found')
|
||||||
|
89
src/utils/nativeModules/userApi.ts
Normal file
89
src/utils/nativeModules/userApi.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { NativeEventEmitter, NativeModules } from 'react-native'
|
||||||
|
|
||||||
|
const { UserApiModule } = NativeModules
|
||||||
|
|
||||||
|
export const loadScript = (info: LX.UserApi.UserApiInfo & { script: string }) => {
|
||||||
|
UserApiModule.loadScript({
|
||||||
|
id: info.id,
|
||||||
|
name: info.name,
|
||||||
|
description: info.description,
|
||||||
|
script: info.script,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendResponseParams {
|
||||||
|
requestKey: string
|
||||||
|
error: string | null
|
||||||
|
response: {
|
||||||
|
statusCode: number
|
||||||
|
statusMessage: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: any
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
export interface SendActions {
|
||||||
|
request: LX.UserApi.UserApiRequestParams
|
||||||
|
response: SendResponseParams
|
||||||
|
}
|
||||||
|
export const sendAction = <T extends keyof SendActions>(action: T, data: SendActions[T]) => {
|
||||||
|
UserApiModule.sendAction(action, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const clearAppCache = CacheModule.clearAppCache as () => Promise<void>
|
||||||
|
|
||||||
|
export interface InitParams {
|
||||||
|
id: string
|
||||||
|
status: boolean
|
||||||
|
errorMessage: string
|
||||||
|
info: LX.UserApi.UserApiInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseParams {
|
||||||
|
status: boolean
|
||||||
|
errorMessage?: string
|
||||||
|
requestKey: string
|
||||||
|
result: any
|
||||||
|
}
|
||||||
|
export interface UpdateInfoParams {
|
||||||
|
name: string
|
||||||
|
log: string
|
||||||
|
updateUrl: string
|
||||||
|
}
|
||||||
|
export interface RequestParams {
|
||||||
|
requestKey: string
|
||||||
|
url: string
|
||||||
|
options: {
|
||||||
|
method: string
|
||||||
|
data: any
|
||||||
|
timeout: number
|
||||||
|
headers: any
|
||||||
|
binary: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type CancelRequestParams = string
|
||||||
|
|
||||||
|
export interface Actions {
|
||||||
|
init: InitParams
|
||||||
|
request: RequestParams
|
||||||
|
cancelRequest: CancelRequestParams
|
||||||
|
response: ResponseParams
|
||||||
|
showUpdateAlert: UpdateInfoParams
|
||||||
|
log: string
|
||||||
|
}
|
||||||
|
export type ActionsEvent = { [K in keyof Actions]: { action: K, data: Actions[K] } }[keyof Actions]
|
||||||
|
|
||||||
|
export const onScriptAction = (callback: (event: ActionsEvent) => void): () => void => {
|
||||||
|
const eventEmitter = new NativeEventEmitter(UserApiModule)
|
||||||
|
const eventListener = eventEmitter.addListener('api-action', event => {
|
||||||
|
if (event.data) event.data = JSON.parse(event.data)
|
||||||
|
callback(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventListener.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const destroy = () => {
|
||||||
|
UserApiModule.destroy()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user