diff --git a/src/main/resolve/backup.ts b/src/main/resolve/backup.ts index 4fd74e8..074c977 100644 --- a/src/main/resolve/backup.ts +++ b/src/main/resolve/backup.ts @@ -8,7 +8,8 @@ import { overrideConfigPath, overrideDir, profileConfigPath, - profilesDir + profilesDir, + themesDir } from '../utils/dirs' export async function webdavBackup(): Promise { @@ -21,6 +22,7 @@ export async function webdavBackup(): Promise { zip.addLocalFile(controledMihomoConfigPath()) zip.addLocalFile(profileConfigPath()) zip.addLocalFile(overrideConfigPath()) + zip.addLocalFolder(themesDir(), 'themes') zip.addLocalFolder(profilesDir(), 'profiles') zip.addLocalFolder(overrideDir(), 'override') zip.addLocalFolder(overrideDir(), 'substore') diff --git a/src/main/resolve/theme.ts b/src/main/resolve/theme.ts new file mode 100644 index 0000000..7fd1cb7 --- /dev/null +++ b/src/main/resolve/theme.ts @@ -0,0 +1,56 @@ +import { readdir, readFile } from 'fs/promises' +import { themesDir } from '../utils/dirs' +import path from 'path' +import axios from 'axios' +import AdmZip from 'adm-zip' +import { getControledMihomoConfig } from '../config' +import { existsSync } from 'fs' +import { mainWindow } from '..' + +let insertedCSSKey: string | undefined = undefined + +export async function resolveThemes(): Promise<{ key: string; label: string }[]> { + const files = await readdir(themesDir()) + const themes = await Promise.all( + files.map(async (file) => { + const css = (await readFile(path.join(themesDir(), file), 'utf-8')) || '' + let name = file + if (css.startsWith('/*')) { + name = css.split('\n')[0].replace('/*', '').replace('*/', '').trim() || file + } + return { key: file, label: name } + }) + ) + return [{ key: 'default.css', label: '默认' }, ...themes] +} + +export async function fetchThemes(): Promise { + const zipUrl = 'https://github.com/mihomo-party-org/theme-hub/releases/download/latest/themes.zip' + const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig() + const zipData = await axios.get(zipUrl, { + responseType: 'arraybuffer', + headers: { 'Content-Type': 'application/octet-stream' }, + proxy: { + protocol: 'http', + host: '127.0.0.1', + port: mixedPort + } + }) + const zip = new AdmZip(zipData.data as Buffer) + zip.extractAllTo(themesDir(), true) +} + +export async function applyTheme(theme: string): Promise { + if (theme === 'default.css') { + if (insertedCSSKey) { + await mainWindow?.webContents.removeInsertedCSS(insertedCSSKey) + } + return + } + if (!existsSync(path.join(themesDir(), theme))) return + const css = await readFile(path.join(themesDir(), theme), 'utf-8') + if (insertedCSSKey) { + await mainWindow?.webContents.removeInsertedCSS(insertedCSSKey) + } + insertedCSSKey = await mainWindow?.webContents.insertCSS(css) +} diff --git a/src/main/utils/dirs.ts b/src/main/utils/dirs.ts index d4750dd..7b16740 100644 --- a/src/main/utils/dirs.ts +++ b/src/main/utils/dirs.ts @@ -53,6 +53,10 @@ export function resourcesFilesDir(): string { return path.join(resourcesDir(), 'files') } +export function themesDir(): string { + return path.join(dataDir(), 'themes') +} + export function mihomoCoreDir(): string { return path.join(resourcesDir(), 'sidecar') } diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 13ec1ab..2b7077f 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -11,7 +11,8 @@ import { profilePath, profilesDir, resourcesFilesDir, - subStoreDir + subStoreDir, + themesDir } from './dirs' import { defaultConfig, @@ -38,6 +39,9 @@ async function initDirs(): Promise { if (!existsSync(dataDir())) { await mkdir(dataDir()) } + if (!existsSync(themesDir())) { + await mkdir(themesDir()) + } if (!existsSync(profilesDir())) { await mkdir(profilesDir()) } diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 5e72e10..710db23 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -65,13 +65,12 @@ import { getInterfaces } from '../sys/interface' import { copyEnv } from '../resolve/tray' import { registerShortcut } from '../resolve/shortcut' import { mainWindow } from '..' +import { applyTheme, fetchThemes, resolveThemes } from '../resolve/theme' import { subStoreCollections, subStoreSubs } from '../core/subStoreApi' import { logDir } from './dirs' import path from 'path' import v8 from 'v8' -let insertedCSSKey: string | undefined - function ipcErrorWrapper( // eslint-disable-next-line @typescript-eslint/no-explicit-any fn: (...args: any[]) => Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any ): (...args: any[]) => Promise { @@ -203,14 +202,9 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('createHeapSnapshot', () => { v8.writeHeapSnapshot(path.join(logDir(), `${Date.now()}.heapsnapshot`)) }) - ipcMain.handle('insertCSS', (_e, css) => - ipcErrorWrapper(async (css) => { - if (insertedCSSKey) { - await mainWindow?.webContents.removeInsertedCSS(insertedCSSKey) - } - insertedCSSKey = await mainWindow?.webContents.insertCSS(css) - })(css) - ) + ipcMain.handle('resolveThemes', () => ipcErrorWrapper(resolveThemes)()) + ipcMain.handle('fetchThemes', () => ipcErrorWrapper(fetchThemes)()) + ipcMain.handle('applyTheme', (_e, theme) => ipcErrorWrapper(applyTheme)(theme)) ipcMain.handle('copyEnv', ipcErrorWrapper(copyEnv)) ipcMain.handle('alert', (_e, msg) => { dialog.showErrorBox('Mihomo Party', msg) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 5d83827..481003b 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -28,7 +28,7 @@ import MihomoCoreCard from '@renderer/components/sider/mihomo-core-card' import ResourceCard from '@renderer/components/sider/resource-card' import UpdaterButton from '@renderer/components/updater/updater-button' import { useAppConfig } from '@renderer/hooks/use-app-config' -import { insertCSS, setNativeTheme, setTitleBarOverlay } from '@renderer/utils/ipc' +import { applyTheme, setNativeTheme, setTitleBarOverlay } from '@renderer/utils/ipc' import { platform } from '@renderer/utils/init' import { TitleBarOverlayOptions } from 'electron' import SubStoreCard from '@renderer/components/sider/substore-card' @@ -42,8 +42,8 @@ const App: React.FC = () => { const { appConfig, patchAppConfig } = useAppConfig() const { appTheme = 'system', + customTheme, useWindowFrame = false, - injectCSS, siderOrder = [ 'sysproxy', 'tun', @@ -79,15 +79,10 @@ const App: React.FC = () => { } }, []) - useEffect(() => { - if (!injectCSS) return - console.log('injectCSS', injectCSS) - insertCSS(injectCSS) - }, [injectCSS]) - useEffect(() => { setNativeTheme(appTheme) setTheme(appTheme) + if (customTheme) applyTheme(customTheme) if (!useWindowFrame) { const options = { height: 48 } as TitleBarOverlayOptions try { @@ -100,7 +95,7 @@ const App: React.FC = () => { // ignore } } - }, [appTheme, systemTheme]) + }, [appTheme, systemTheme, customTheme]) const onDragEnd = async (event: DragEndEvent): Promise => { const { active, over } = event diff --git a/src/renderer/src/components/settings/css-editor-modal.tsx b/src/renderer/src/components/settings/css-editor-modal.tsx deleted file mode 100644 index e83e3db..0000000 --- a/src/renderer/src/components/settings/css-editor-modal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react' -import { BaseEditor } from '@renderer/components/base/base-editor' -import React, { useState } from 'react' -interface Props { - css: string - onCancel: () => void - onConfirm: (script: string) => void -} -const CSSEditorModal: React.FC = (props) => { - const { css, onCancel, onConfirm } = props - const [currData, setCurrData] = useState(css) - - return ( - - - 编辑 CSS - - setCurrData(value || '')} - /> - - - - - - - - ) -} - -export default CSSEditorModal diff --git a/src/renderer/src/components/settings/general-config.tsx b/src/renderer/src/components/settings/general-config.tsx index 05048a2..b04f448 100644 --- a/src/renderer/src/components/settings/general-config.tsx +++ b/src/renderer/src/components/settings/general-config.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import SettingCard from '../base/base-setting-card' import SettingItem from '../base/base-setting-item' import { Button, Input, Select, SelectItem, Switch, Tab, Tabs, Tooltip } from '@nextui-org/react' @@ -9,19 +9,23 @@ import { copyEnv, disableAutoRun, enableAutoRun, + fetchThemes, relaunchApp, + resolveThemes, restartCore } from '@renderer/utils/ipc' import { useAppConfig } from '@renderer/hooks/use-app-config' import { platform } from '@renderer/utils/init' import { useTheme } from 'next-themes' -import { IoIosHelpCircle } from 'react-icons/io' -import CSSEditorModal from './css-editor-modal' +import { IoIosHelpCircle, IoMdCloudDownload } from 'react-icons/io' const GeneralConfig: React.FC = () => { const { data: enable, mutate: mutateEnable } = useSWR('checkAutoRun', checkAutoRun) const { appConfig, patchAppConfig } = useAppConfig() - const [openCSSEditor, setOpenCSSEditor] = useState(false) + const [customThemes, setCustomThemes] = React.useState([ + { key: 'default.css', label: '默认', content: '' } + ]) + const [fetching, setFetching] = useState(false) const { setTheme } = useTheme() const { silentStart = false, @@ -31,24 +35,20 @@ const GeneralConfig: React.FC = () => { useWindowFrame = false, autoQuitWithoutCore = false, autoQuitWithoutCoreDelay = 60, - injectCSS = DEFAULT_CSS, + customTheme = 'default.css', envType = platform === 'win32' ? 'powershell' : 'bash', autoCheckUpdate, appTheme = 'system' } = appConfig || {} + useEffect(() => { + resolveThemes().then((themes) => { + setCustomThemes(themes) + }) + }) + return ( <> - {openCSSEditor && ( - setOpenCSSEditor(false)} - onConfirm={async (css: string) => { - await patchAppConfig({ injectCSS: css }) - setOpenCSSEditor(false) - }} - /> - )} { }} /> - - - - + { + { + setFetching(true) + try { + await fetchThemes() + setCustomThemes(await resolveThemes()) + } catch (e) { + alert(e) + } finally { + setFetching(false) + } + }} + > + + + } + > + + ) } -const DEFAULT_CSS = `/* 使用 !important 以覆盖默认样式 */ -/* --nextui-xxx 变量只支持hsl色值 */ -/* 若要对所有主题生效,可直接给html元素设置样式 */ - -/* 深色-蓝色 */ -.dark, [data-theme="dark"] { - --nextui-background: 0 0% 0%; - --nextui-foreground-50: 240 5.88% 10%; - --nextui-foreground-100: 240 3.7% 15.88%; - --nextui-foreground-200: 240 5.26% 26.08%; - --nextui-foreground-300: 240 5.2% 33.92%; - --nextui-foreground-400: 240 3.83% 46.08%; - --nextui-foreground-500: 240 5.03% 64.9%; - --nextui-foreground-600: 240 4.88% 83.92%; - --nextui-foreground-700: 240 5.88% 90%; - --nextui-foreground-800: 240 4.76% 95.88%; - --nextui-foreground-900: 0 0% 98.04%; - --nextui-foreground: 210 5.56% 92.94%; - --nextui-focus: 212.01999999999998 100% 46.67%; - --nextui-overlay: 0 0% 0%; - --nextui-divider: 0 0% 100%; - --nextui-divider-opacity: 0.15; - --nextui-content1: 240 5.88% 10%; - --nextui-content1-foreground: 0 0% 98.04%; - --nextui-content2: 240 3.7% 15.88%; - --nextui-content2-foreground: 240 4.76% 95.88%; - --nextui-content3: 240 5.26% 26.08%; - --nextui-content3-foreground: 240 5.88% 90%; - --nextui-content4: 240 5.2% 33.92%; - --nextui-content4-foreground: 240 4.88% 83.92%; - --nextui-default-50: 240 5.88% 10%; - --nextui-default-100: 240 3.7% 15.88%; - --nextui-default-200: 240 5.26% 26.08%; - --nextui-default-300: 240 5.2% 33.92%; - --nextui-default-400: 240 3.83% 46.08%; - --nextui-default-500: 240 5.03% 64.9%; - --nextui-default-600: 240 4.88% 83.92%; - --nextui-default-700: 240 5.88% 90%; - --nextui-default-800: 240 4.76% 95.88%; - --nextui-default-900: 0 0% 98.04%; - --nextui-default-foreground: 0 0% 100%; - --nextui-default: 240 5.26% 26.08%; - --nextui-primary-50: 211.84000000000003 100% 9.61%; - --nextui-primary-100: 211.84000000000003 100% 19.22%; - --nextui-primary-200: 212.24 100% 28.82%; - --nextui-primary-300: 212.14 100% 38.43%; - --nextui-primary-400: 212.01999999999998 100% 46.67%; - --nextui-primary-500: 212.14 92.45% 58.43%; - --nextui-primary-600: 212.24 92.45% 68.82%; - --nextui-primary-700: 211.84000000000003 92.45% 79.22%; - --nextui-primary-800: 211.84000000000003 92.45% 89.61%; - --nextui-primary-900: 212.5 92.31% 94.9%; - --nextui-primary: 212.01999999999998 100% 46.67%; - --nextui-primary-foreground: 0 0% 100%; - --nextui-secondary-50: 270 66.67% 9.41%; - --nextui-secondary-100: 270 66.67% 18.82%; - --nextui-secondary-200: 270 66.67% 28.24%; - --nextui-secondary-300: 270 66.67% 37.65%; - --nextui-secondary-400: 270 66.67% 47.06%; - --nextui-secondary-500: 270 59.26% 57.65%; - --nextui-secondary-600: 270 59.26% 68.24%; - --nextui-secondary-700: 270 59.26% 78.82%; - --nextui-secondary-800: 270 59.26% 89.41%; - --nextui-secondary-900: 270 61.54% 94.9%; - --nextui-secondary-foreground: 0 0% 100%; - --nextui-secondary: 270 59.26% 57.65%; - --nextui-success-50: 145.71000000000004 77.78% 8.82%; - --nextui-success-100: 146.2 79.78% 17.45%; - --nextui-success-200: 145.78999999999996 79.26% 26.47%; - --nextui-success-300: 146.01 79.89% 35.1%; - --nextui-success-400: 145.96000000000004 79.46% 43.92%; - --nextui-success-500: 146.01 62.45% 55.1%; - --nextui-success-600: 145.78999999999996 62.57% 66.47%; - --nextui-success-700: 146.2 61.74% 77.45%; - --nextui-success-800: 145.71000000000004 61.4% 88.82%; - --nextui-success-900: 146.66999999999996 64.29% 94.51%; - --nextui-success-foreground: 0 0% 0%; - --nextui-success: 145.96000000000004 79.46% 43.92%; - --nextui-warning-50: 37.139999999999986 75% 10.98%; - --nextui-warning-100: 37.139999999999986 75% 21.96%; - --nextui-warning-200: 36.95999999999998 73.96% 33.14%; - --nextui-warning-300: 37.00999999999999 74.22% 44.12%; - --nextui-warning-400: 37.02999999999997 91.27% 55.1%; - --nextui-warning-500: 37.00999999999999 91.26% 64.12%; - --nextui-warning-600: 36.95999999999998 91.24% 73.14%; - --nextui-warning-700: 37.139999999999986 91.3% 81.96%; - --nextui-warning-800: 37.139999999999986 91.3% 90.98%; - --nextui-warning-900: 54.55000000000001 91.67% 95.29%; - --nextui-warning-foreground: 0 0% 0%; - --nextui-warning: 37.02999999999997 91.27% 55.1%; - --nextui-danger-50: 340 84.91% 10.39%; - --nextui-danger-100: 339.3299999999999 86.54% 20.39%; - --nextui-danger-200: 339.11 85.99% 30.78%; - --nextui-danger-300: 339 86.54% 40.78%; - --nextui-danger-400: 339.20000000000005 90.36% 51.18%; - --nextui-danger-500: 339 90% 60.78%; - --nextui-danger-600: 339.11 90.6% 70.78%; - --nextui-danger-700: 339.3299999999999 90% 80.39%; - --nextui-danger-800: 340 91.84% 90.39%; - --nextui-danger-900: 339.13 92% 95.1%; - --nextui-danger-foreground: 0 0% 100%; - --nextui-danger: 339.20000000000005 90.36% 51.18%; - --nextui-divider-weight: 1px; - --nextui-disabled-opacity: .5; - --nextui-font-size-tiny: 0.75rem; - --nextui-font-size-small: 0.875rem; - --nextui-font-size-medium: 1rem; - --nextui-font-size-large: 1.125rem; - --nextui-line-height-tiny: 1rem; - --nextui-line-height-small: 1.25rem; - --nextui-line-height-medium: 1.5rem; - --nextui-line-height-large: 1.75rem; - --nextui-radius-small: 8px; - --nextui-radius-medium: 12px; - --nextui-radius-large: 14px; - --nextui-border-width-small: 1px; - --nextui-border-width-medium: 2px; - --nextui-border-width-large: 3px; - --nextui-box-shadow-small: 0px 0px 5px 0px rgb(0 0 0 / 0.05), 0px 2px 10px 0px rgb(0 0 0 / 0.2), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15); - --nextui-box-shadow-medium: 0px 0px 15px 0px rgb(0 0 0 / 0.06), 0px 2px 30px 0px rgb(0 0 0 / 0.22), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15); - --nextui-box-shadow-large: 0px 0px 30px 0px rgb(0 0 0 / 0.07), 0px 30px 60px 0px rgb(0 0 0 / 0.26), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15); - --nextui-hover-opacity: .9; -} -/* 灰色-蓝色 */ -.gray, [data-theme="gray"] { -} -/* 浅色-蓝色 */ -.light, [data-theme="light"] { -} -/* 深色-粉色 */ -.dark-pink, [data-theme="dark-pink"] { -} -/* 灰色-粉色 */ -.gray-pink, [data-theme="gray-pink"] { -} -/* 浅色-粉色 */ -.light-pink, [data-theme="light-pink"] { -} -/* 深色-绿色 */ -.dark-green, [data-theme="dark-green"] { -} -/* 灰色-绿色 */ -.gray-green, [data-theme="gray-green"] { -} -/* 浅色-绿色 */ -.light-green, [data-theme="light-green"] { -} -` export default GeneralConfig diff --git a/src/renderer/src/components/sider/conn-card.tsx b/src/renderer/src/components/sider/conn-card.tsx index 95a6643..cf5f951 100644 --- a/src/renderer/src/components/sider/conn-card.tsx +++ b/src/renderer/src/components/sider/conn-card.tsx @@ -19,7 +19,7 @@ let drawing = false const ConnCard: React.FC = () => { const { theme = 'system', systemTheme = 'dark' } = useTheme() const { appConfig } = useAppConfig() - const { showTraffic, connectionCardStatus = 'col-span-2' } = appConfig || {} + const { showTraffic, connectionCardStatus = 'col-span-2', customTheme } = appConfig || {} const location = useLocation() const match = location.pathname.includes('/connections') @@ -43,7 +43,7 @@ const ConnCard: React.FC = () => { : islight ? window.getComputedStyle(document.documentElement).color : 'rgb(255,255,255)' - }, [theme, systemTheme, match]) + }, [theme, systemTheme, match, customTheme]) const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null useEffect(() => { diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index ce445e1..aaa01e4 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -331,8 +331,16 @@ export async function createHeapSnapshot(): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('createHeapSnapshot')) } -export async function insertCSS(css: string): Promise { - return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('insertCSS', css)) +export async function resolveThemes(): Promise<{ key: string; label: string; content: string }[]> { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('resolveThemes')) +} + +export async function fetchThemes(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('fetchThemes')) +} + +export async function applyTheme(theme: string): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('applyTheme', theme)) } export async function registerShortcut( diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 198a70d..b7f6340 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -223,7 +223,6 @@ interface IAppConfig { substoreCardStatus?: CardStatus sysproxyCardStatus?: CardStatus tunCardStatus?: CardStatus - injectCSS?: string useSubStore: boolean subStoreBackendSyncCron?: string subStoreBackendDownloadCron?: string @@ -238,6 +237,7 @@ interface IAppConfig { proxyInTray: boolean siderOrder: string[] appTheme: AppTheme + customTheme: string autoCheckUpdate: boolean silentStart: boolean autoCloseConnection: boolean