import { components, UserComponentMetadata } from '@/components/component' import { UserStyle } from '@/plugins/style' import { PluginMetadata, plugins } from '@/plugins/plugin' import { componentToSettings, isUserComponent } from './helpers' type Property = string | number | symbol /** 表示一个组件的设置 */ export interface ComponentSettings { /** 是否启用此组件 */ enabled: boolean /** 组件选项 */ options: O } /** 脚本总设置 */ export interface Settings { [name: string]: any /** 用户安装的样式 */ userStyles: Record> /** 用户安装的插件 */ userPlugins: Record & { code: string })> /** 用户安装的组件 */ userComponents: Record /** 组件更新的代码 */ update?: string /** 组件设置 */ components: Record /** 插件设置 */ plugins: Record } // 默认设置 let internalSettings: Settings = { userStyles: {}, userPlugins: {}, userComponents: {}, components: {}, plugins: {}, } let settingsLoaded = false type ValueChangeListener = ( value: T, oldValue: T, prop: Property, propPath?: Property[], ) => void const registeredListeners = new Map() const settingsChangedHandler = ( value: any, oldValue: any, prop: Property, propPath: Property[] = [], ) => { if (settingsLoaded) { GM_setValue(prop.toString(), internalSettings[prop.toString()]) const path = propPath.join('.') // 如果父级是普通对象或数组也会通知, 但没有旧值 if (propPath.length > 1) { const parentPath = propPath.slice(0, propPath.length - 1).join('.') const parent = lodash.get(internalSettings, parentPath) const notifyParent = Array.isArray(parent) || lodash.isPlainObject(parent) if (notifyParent) { const handlers = registeredListeners.get(parentPath) handlers?.forEach(h => h(parent, null, prop, propPath)) } } const handlers = registeredListeners.get(path) handlers?.forEach(h => h(value, oldValue, prop, propPath)) } } /** 标识经由 `createProxy` 控制的对象 */ export const isProxy = Symbol('isProxy') /** * 为目标对象创建深层 Proxy * @param targetObj 目标对象 * @param valueChangeListener 对象自身属性变化时的回调函数 */ export const createProxy = (targetObj: any, valueChangeListener?: ValueChangeListener) => { const applyProxy = (obj: any, rootProp?: Property, propPath: Property[] = []) => { for (const [key, value] of Object.entries(obj)) { if (typeof value === 'object' && !(value instanceof RegExp)) { obj[key] = applyProxy(value, rootProp || key, [...propPath, key]) } } const proxy = new Proxy(obj, { get(o, prop) { if (prop === isProxy) { return true } return o[prop] }, set(o, prop, value) { const oldValue = o[prop] const isImplicitProp = ( !Object.prototype.hasOwnProperty.call(o, prop) && oldValue !== undefined ) if (unsafeWindow.proxyDebug) { console.log({ isImplicitProp, prop, value }) } /** * 是否对 value 启用深层 Proxy * - 是 Object (或 Array) * - 不能是 RegExp * - 不能是已经启用过的 * - 不能是上游原型链里的 */ const deep = ( typeof value === 'object' && !(value instanceof RegExp) && !(value[isProxy] === true) && !isImplicitProp ) if (deep) { value = applyProxy(value, rootProp || prop, [...propPath, prop]) } o[prop] = value if (!isImplicitProp) { valueChangeListener?.(value, oldValue, rootProp || prop, [...propPath, prop]) } return true }, deleteProperty(o, prop) { const oldValue = o[prop] delete o[prop] valueChangeListener?.(undefined, oldValue, rootProp || prop, [...propPath, prop]) return true }, }) return proxy } return applyProxy(targetObj) } // 载入插件设置 plugins.forEach(plugin => { internalSettings.plugins[plugin.name] = true }) // 载入组件设置 components.forEach(component => { internalSettings.components[component.name] = componentToSettings(component) }) // 读取保存的设置 const readSettings = (obj: any) => { for (const [key, value] of Object.entries(obj)) { let result: any const gmValue = GM_getValue(key, value) if (typeof gmValue === 'object') { result = lodash.defaultsDeep(gmValue, value) } else { result = gmValue } obj[key] = result } return obj } /** 默认设置 */ export const defaultSettings = lodash.cloneDeep(internalSettings) internalSettings = createProxy(readSettings(internalSettings), settingsChangedHandler) for (const [key, value] of Object.entries(internalSettings)) { GM_setValue(key, value) } /** * 添加对设置里某项的监听 * @param path 设置项的属性路径 * @param listener 监听函数 * @param initCall 添加监听后, 是否已当前设置值立即触发一次监听函数 */ export const addSettingsChangeListener = ( path: string, listener: ValueChangeListener, initCall = false, ) => { const handlers = registeredListeners.get(path) if (handlers) { handlers.push(listener) } else { registeredListeners.set(path, [listener]) } if (initCall) { const value = lodash.get(internalSettings, path) listener(value, value, '', []) } } /** * 移除对设置项的某个监听函数 * @param path 设置项的属性路径 * @param listener 监听函数 */ export const removeSettingsChangeListener = (path: string, listener: ValueChangeListener) => { const handlers = registeredListeners.get(path) if (!handlers) { return } const index = handlers.indexOf(listener) if (index === -1) { return } handlers.splice(index, 1) } const componentPath = (path: string) => { const [name, optionName] = path.split('.') if (!isUserComponent(name)) { if (optionName === undefined) { return `components.${name}.enabled` } return `components.${name}.options.${optionName}` } // user components if (optionName === undefined) { return `userComponents.${name}.settings.enabled` } return `userComponents.${name}.settings.options.${optionName}` } /** * 添加对组件设置的监听 * @param path 组件名称, 或后面跟上`.`代表组件选项; 例如`darkMode`或`defaultPlayerMode.autoLightOff` * @param listener 监听函数 * @param initCall 添加监听后, 是否已当前设置值立即触发一次监听函数 */ export const addComponentListener = ( path: string, listener: ValueChangeListener, initCall = false, ) => { addSettingsChangeListener(componentPath(path), listener, initCall) } /** * 移除对组件设置的监听 * @param path 组件设置路径 * @param listener 监听函数 */ export const removeComponentListener = (path: string, listener: ValueChangeListener) => { removeSettingsChangeListener(componentPath(path), listener) } settingsLoaded = true /** 脚本当前的设置 */ export const settings: Settings = internalSettings export * from './helpers'