const PATH = 'data/third/custom-theme'
const THEME_FILE = PATH + '/themes.json'
const VariableList = [
// 文字颜色
'--color-light',
'--color-dark',
// 背景颜色
'--bg-color-light',
'--bg-color-dark',
// 滚动条颜色
'--scrollbar-track-bg-light',
'--scrollbar-thumb-bg-light',
'--scrollbar-track-bg-dark',
'--scrollbar-thumb-bg-dark',
// 普通按钮颜色
'--btn-normal-color-light',
'--btn-normal-bg-light',
'--btn-normal-hover-color-light',
'--btn-normal-hover-border-color-light',
'--btn-normal-active-color-light',
'--btn-normal-active-border-color-light',
'--btn-normal-color-dark',
'--btn-normal-bg-dark',
'--btn-normal-hover-color-dark',
'--btn-normal-hover-border-color-dark',
'--btn-normal-active-color-dark',
'--btn-normal-active-border-color-dark',
// 主要按钮颜色
'--btn-primary-color-light',
'--btn-primary-bg-light',
'--btn-primary-hover-bg-light',
'--btn-primary-active-bg-light',
'--btn-primary-color-dark',
'--btn-primary-bg-dark',
'--btn-primary-hover-bg-dark',
'--btn-primary-active-bg-dark',
// 链接按钮颜色
'--btn-link-color-light',
'--btn-link-bg-light',
'--btn-link-hover-color-light',
'--btn-link-active-color-light',
'--btn-link-color-dark',
'--btn-link-bg-dark',
'--btn-link-hover-color-dark',
'--btn-link-active-color-dark',
// 文本按钮颜色
'--btn-text-color-light',
'--btn-text-bg-light',
'--btn-text-hover-bg-light',
'--btn-text-active-bg-light',
'--btn-text-color-dark',
'--btn-text-bg-dark',
'--btn-text-hoer-color-dark',
'--btn-text-hover-bg-dark',
'--btn-text-active-color-dark',
'--btn-text-active-bg-dark',
// 单选
'--radio-normal-color-light',
'--radio-normal-bg-light',
'--radio-normal-hover-color-light',
'--radio-primary-color-light',
'--radio-primary-bg-light',
'--radio-primary-hover-bg-light',
'--radio-primary-active-bg-light',
'--radio-normal-color-dark',
'--radio-normal-bg-dark',
'--radio-normal-hover-color-dark',
'--radio-primary-color-dark',
'--radio-primary-bg-dark',
'--radio-primary-hover-bg-dark',
'--radio-primary-active-bg-dark',
// 卡片
'--card-color-light',
'--card-bg-light',
'--card-hover-bg-light',
'--card-active-bg-light',
'--card-color-dark',
'--card-bg-dark',
'--card-hover-bg-dark',
'--card-active-bg-dark',
// 进度条
'--progress-bg-light',
'--progress-inner-bg-light',
'--progress-bg-dark',
'--progress-inner-bg-dark',
// 下拉
'--dropdown-bg-light',
'--dropdown-bg-dark',
// 模态框
'--modal-bg-light',
'--modal-mask-bg-light',
'--modal-bg-dark',
'--modal-mask-bg-dark',
// 开关
'--switch-on-bg-light',
'--switch-on-dot-bg-light',
'--switch-on-bg-dark',
'--switch-on-dot-bg-dark',
'--switch-off-bg-light',
'--switch-off-dot-bg-light',
'--switch-off-bg-dark',
'--switch-off-dot-bg-dark',
// 输入框
'--input-color-light',
'--input-bg-light',
'--input-color-dark',
'--input-bg-dark',
// 颜色选择器
'--color-picker-bg-light',
'--color-picker-bg-dark',
// 分割线
'--divider-color-light',
'--divider-color-dark',
// 选择框
'--select-color-light',
'--select-bg-light',
'--select-option-bg-light',
'--select-color-dark',
'--select-bg-dark',
'--select-option-bg-dark',
// 提示
'--toast-bg-light',
'--toast-bg-dark',
// 菜单
'--menu-bg-light',
'--menu-item-hover-light',
'--menu-bg-dark',
'--menu-item-hover-dark',
// 表格
'--table-tr-odd-bg-light',
'--table-tr-even-bg-light',
'--table-tr-odd-hover-bg-light',
'--table-tr-even-hover-bg-light',
'--table-tr-odd-bg-dark',
'--table-tr-even-bg-dark',
'--table-tr-odd-hover-bg-dark',
'--table-tr-even-hover-bg-dark',
// 延迟颜色
'--level-0-color',
'--level-1-color',
'--level-2-color',
'--level-3-color',
'--level-4-color'
]
const BackgroundList = [
['#00000000', 'none'],
['#FFDEE9', 'linear-gradient(0deg, #FFDEE9 0%, #B5FFFC 100%)'],
['#4158D0', 'linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%)'],
['#0093E9', 'linear-gradient(160deg, #0093E9 0%, #80D0C7 100%)'],
['#8EC5FC', 'linear-gradient(62deg, #8EC5FC 0%, #E0C3FC 100%)'],
['#D9AFD9', 'linear-gradient(0deg, #D9AFD9 0%, #97D9E1 100%)'],
['#FFFFFF', 'linear-gradient(180deg, #FFFFFF 0%, #6284FF 50%, #FF0000 100%)'],
['#00DBDE', 'linear-gradient(90deg, #00DBDE 0%, #FC00FF 100%)'],
['#FBAB7E', 'linear-gradient(62deg, #FBAB7E 0%, #F7CE68 100%)'],
['#85FFBD', 'linear-gradient(45deg, #85FFBD 0%, #FFFB7D 100%)'],
['#8BC6EC', 'linear-gradient(135deg, #8BC6EC 0%, #9599E2 100%)'],
['#08AEEA', 'linear-gradient(0deg, #08AEEA 0%, #2AF598 100%)'],
['#52ACFF', 'linear-gradient(180deg, #52ACFF 25%, #FFE32C 100%)'],
['#FFE53B', 'linear-gradient(147deg, #FFE53B 0%, #FF2525 74%)'],
['#21D4FD', 'linear-gradient(19deg, #21D4FD 0%, #B721FF 100%)'],
['#3EECAC', 'linear-gradient(19deg, #3EECAC 0%, #EE74E1 100%)'],
['#FA8BFF', 'linear-gradient(45deg, #FA8BFF 0%, #2BD2FF 52%, #2BFF88 90%)'],
['#FF9A8B', 'linear-gradient(90deg, #FF9A8B 0%, #FF6A88 55%, #FF99AC 100%)'],
['#FBDA61', 'linear-gradient(45deg, #FBDA61 0%, #FF5ACD 100%)'],
['#F4D03F', 'linear-gradient(132deg, #F4D03F 0%, #16A085 100%)'],
['#A9C9FF', 'linear-gradient(180deg, #A9C9FF 0%, #FFBBEC 100%)'],
['#74EBD5', 'linear-gradient(90deg, #74EBD5 0%, #9FACE6 100%)'],
['#FAACA8', 'linear-gradient(19deg, #FAACA8 0%, #DDD6F3 100%)'],
['#FAD961', 'linear-gradient(90deg, #FAD961 0%, #F76B1C 100%)'],
['#FEE140', 'linear-gradient(90deg, #FEE140 0%, #FA709A 100%)'],
['#FF3CAC', 'linear-gradient(225deg, #FF3CAC 0%, #784BA0 50%, #2B86C5 100%)']
]
const onInstall = async () => {
await Reset(false)
await onReady()
Plugins.message.success('安装完成')
}
const onUninstall = async () => {
await Plugins.confirm('提示', '卸载后,主题文件将被删除!')
await Plugins.RemoveFile(PATH)
Clear()
}
const onReady = async () => {
const config = JSON.parse(await Plugins.ReadFile(THEME_FILE))
await setVariable(config)
await setBackground(config)
await setCustomCSS
}
const onRun = async () => {
const config = JSON.parse(await Plugins.ReadFile(THEME_FILE))
await setVariable(config)
await setBackground(config)
await setCustomCSS()
Plugins.message.success('主题已生效')
}
/* 触发器 配置插件时 */
const onConfigure = async (config, old) => {
setCustomCSS(config.CustomCSS)
}
/**
* 插件钩子 - 右键:选取背景
*/
const Select = async () => {
const config = JSON.parse(await Plugins.ReadFile(THEME_FILE))
const backgroundColor = document.body.style.backgroundColor
const backgroundImage = document.body.style.backgroundImage
try {
const { index } = await Plugins.picker.single(
'请选择背景',
BackgroundList.map((theme, index) => {
return {
label: '背景' + (index + 1),
value: { theme, index },
background: theme[1],
onSelect: ({ value }) => {
document.body.style.backgroundColor = value.theme[0]
document.body.style.backgroundImage = value.theme[1]
}
}
}),
[]
)
config.backgroundIndex = index
await Plugins.WriteFile(THEME_FILE, JSON.stringify(config, null, 2))
await setBackground(config)
} catch (error) {
document.body.style.backgroundColor = backgroundColor
document.body.style.backgroundImage = backgroundImage
}
}
/**
* 插件钩子 - 右键:清除主题
*/
const Clear = () => {
VariableList.forEach((property) => {
document.body.style.removeProperty(property)
})
document.body.style.backgroundColor = ''
document.body.style.backgroundImage = ''
clearCustomCSS()
}
/**
* 插件钩子 - 右键:重置配置
*/
const Reset = async (isReset = true) => {
isReset && (await Plugins.confirm('提示', '主题文件将被替换为默认!'))
const config = { variable: {}, backgroundIndex: 0 }
const theme = getComputedStyle(document.documentElement)
VariableList.forEach((property) => {
config.variable[property] = theme.getPropertyValue(property)
})
await Plugins.WriteFile(THEME_FILE, JSON.stringify(config, null, 2))
isReset && Plugins.message.success('重置成功')
isReset && onReady()
}
/*
* 插件钩子 - 右键:自定义图标
*/
const CustomIcon = async () => {
const iconMap = {
huorong: {
title: '火绒图标',
icons: [
'https://github.com/clash-verge-rev/icon-hub/raw/main/huorong/common.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/raw/main/huorong/sysproxy.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/raw/main/huorong/tun.ico?raw=true'
]
},
vergeRevColorful: {
title: 'ClashVergeRev 彩色',
icons: [
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-cat/common.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/blob/main/official-cat/sysproxy.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-cat/tun.ico?raw=true'
]
},
vergeRevLight: {
title: 'ClashVergeRev 亮色',
icons: [
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-white/common.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-white/sysproxy.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-white/tun.ico?raw=true'
]
},
vergeRevDark: {
title: 'ClashVergeRev 暗色',
icons: [
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-black/common.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-black/sysproxy.ico?raw=true',
'https://github.com/clash-verge-rev/icon-hub/raw/main/official-black/tun.ico?raw=true'
]
},
[Plugins.APP_TITLE]: {
title: Plugins.APP_TITLE,
icons: ['/favicon.ico']
}
}
const renderImgs = (imgs) => imgs.map((img) => `
`).join('')
const renderAction = (id) => `选中`
let iconId
const handlerId = Plugins.sampleID()
window[handlerId] = (id) => {
iconId = id
Plugins.message.success('已记住你的选择,点击确定生效。', 1000)
}
const items = Object.entries(iconMap).reduce((p, c) => {
return [...p, `| ${renderImgs(c[1].icons)} | ${c[1].title} | ${renderAction(c[0])} |`]
}, [])
try {
await Plugins.confirm('要替换成什么图标呢?', `|预览|名称|操作|\n| - | - | - |\n${items.join('\n')}\n`, { type: 'markdown' })
if (iconMap[iconId]) {
if (iconId === Plugins.APP_TITLE) {
await Plugins.RemoveFile('data/.cache/icons')
} else {
const { destroy } = Plugins.message.info('正在下载图标...', 9999)
const [normal, proxy, tun] = iconMap[iconId].icons
await Promise.all([
Plugins.Download(normal, 'data/.cache/icons/tray_normal_dark.ico').then(() => {
Plugins.CopyFile('data/.cache/icons/tray_normal_dark.ico', 'data/.cache/icons/tray_normal_light.ico')
}),
Plugins.Download(proxy, 'data/.cache/icons/tray_proxy_dark.ico').then(() => {
Plugins.CopyFile('data/.cache/icons/tray_proxy_dark.ico', 'data/.cache/icons/tray_proxy_light.ico')
}),
Plugins.Download(tun, 'data/.cache/icons/tray_tun_dark.ico').then(() => {
Plugins.CopyFile('data/.cache/icons/tray_tun_dark.ico', 'data/.cache/icons/tray_tun_light.ico')
})
])
destroy()
}
if (await Plugins.confirm('替换成功', '是否立即重启客户端?').catch(() => 0)) {
await Plugins.RestartApp()
}
}
} finally {
delete window[handlerId]
}
}
/**
* 设置自定义CSS颜色
*/
const setVariable = async (config) => {
Object.entries(config.variable).forEach(([property, value]) => {
document.body.style.setProperty(property, value)
})
}
/**
* 自定义CSS
*/
const setCustomCSS = async (css = Plugin.CustomCSS) => {
clearCustomCSS()
if (css) {
const style = document.createElement('style')
style.id = Plugin.id + '_custom_css'
style.type = 'text/css'
style.rel = 'stylesheet'
style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
}
}
const clearCustomCSS = () => {
const dom = document.getElementById(Plugin.id + '_custom_css')
dom && dom.remove()
}
/**
* 设置背景
*/
const setBackground = async (config) => {
if (Plugin.BgImagePath) {
const isFromNetwork = Plugin.BgImagePath.startsWith('http') || Plugin.BgImagePath.startsWith('//')
if (isFromNetwork) {
document.body.style.backgroundImage = `url(${Plugin.BgImagePath})`
document.body.style.backgroundSize = '100% 100%'
return
}
const base64 = await Plugins.ignoredError(Plugins.ReadFile, Plugin.BgImagePath, { Mode: 'Binary' })
if (!base64) {
console.log(`[${Plugin.name}]`, '读取背景图片失败,跳过。')
return
}
const suffix = Plugin.BgImagePath.split('.').pop() || 'jpg'
document.body.style.backgroundImage = `url(data:image/${suffix};base64,${base64})`
return
}
const [color, gradientImage] = BackgroundList[config.backgroundIndex]
document.body.style.backgroundColor = color
document.body.style.backgroundImage = gradientImage
}