Feature(custom): support theme system and add a new theme hub for piclist

This commit is contained in:
Kuingsmile
2026-01-14 23:00:07 +08:00
parent d98f955a76
commit 50a59a124a
55 changed files with 1440 additions and 1621 deletions

View File

@@ -0,0 +1,81 @@
import path from 'node:path'
import { themesDir } from '@core/datastore/dirs'
import * as fsWalk from '@nodelib/fs.walk'
import AdmZip from 'adm-zip'
import windowManager from 'apis/app/window/windowManager'
import axios from 'axios'
import fs from 'fs-extra'
import { IWindowList } from '~/utils/enum'
let insertedCSSKeyMain: string | undefined
export async function resolveThemes(): Promise<{ key: string; label: string }[]> {
const files = fsWalk.walkSync(themesDir(), {
followSymbolicLinks: true,
fs,
stats: true,
throwErrorOnBrokenSymbolicLink: false,
})
const result: string[] = []
files.forEach(item => {
if (item.stats?.isFile()) {
result.push(item.path.replace(themesDir() + '/', ''))
}
})
const themes = await Promise.all(
result
.filter(file => file.endsWith('.css'))
.map(async file => {
const css = (await fs.readFile(file, 'utf-8')) || ''
let name = file
if (css.startsWith('/*')) {
name = css.split('\n')[0].replace('/*', '').replace('*/', '').trim() || file
}
return { key: file, label: name }
}),
)
if (themes.find(theme => theme.key === 'default.css')) {
return themes
} else {
return [{ key: 'default.css', label: '默认' }, ...themes]
}
}
export async function fetchThemes(): Promise<void> {
const zipUrl = 'https://github.com/Kuingsmile/piclist-themeHub/releases/download/latest/themes.zip'
const zipData = await axios.get(zipUrl, {
responseType: 'arraybuffer',
headers: { 'Content-Type': 'application/octet-stream' },
})
const zip = new AdmZip(zipData.data as Buffer)
zip.extractAllTo(themesDir(), true)
}
export async function importThemes(files: string[]): Promise<void> {
for (const file of files) {
if (fs.existsSync(file))
await fs.copyFile(file, path.join(themesDir(), `${new Date().getTime().toString(16)}-${path.basename(file)}`))
}
}
export async function readTheme(theme: string): Promise<string> {
if (!fs.existsSync(path.join(themesDir(), theme))) return ''
const result = await fs.readFile(path.join(themesDir(), theme), 'utf-8')
return result
}
export async function applyTheme(theme: string): Promise<void> {
theme = path.basename(theme)
console.log('Applying theme:', theme)
const css = await readTheme(theme)
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
try {
await windowManager.get(IWindowList.SETTING_WINDOW)?.webContents.removeInsertedCSS(insertedCSSKeyMain || '')
insertedCSSKeyMain = await windowManager.get(IWindowList.SETTING_WINDOW)?.webContents.insertCSS(css)
} catch (e) {
console.error(e)
}
}
}

View File

@@ -12,6 +12,7 @@ import { configPaths } from '~/utils/configPaths'
import { IWindowList } from '~/utils/enum'
import logo from '../../../../../resources/logo.png?asset&asarUnpack'
import { applyTheme } from '../theme'
const windowList = new Map<string, IWindowListItem>()
@@ -205,6 +206,10 @@ windowList.set(IWindowList.SETTING_WINDOW, {
})
}
})
window.on('ready-to-show', () => {
const customTheme = picgo.getConfig<string>(configPaths.settings.theme) || 'default.css'
applyTheme(customTheme)
})
bus.emit(CREATE_APP_MENU)
windowManager.create(IWindowList.MINI_WINDOW)
},

View File

@@ -1,8 +1,9 @@
import picgo from '@core/picgo'
import { app, shell } from 'electron'
import { app, nativeTheme, shell } from 'electron'
import { applyTheme, fetchThemes, importThemes, resolveThemes } from '~/apis/app/theme'
import { i18nManager } from '~/i18n'
import { IRPCActionType } from '~/utils/enum'
import { IRPCActionType, IRPCType } from '~/utils/enum'
export default [
{
@@ -32,4 +33,51 @@ export default [
picgo.i18n.setLanguage(lang)
},
},
{
action: IRPCActionType.GET_SYSTEM_THEME,
handler: async () => {
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
},
type: IRPCType.INVOKE,
},
{
action: IRPCActionType.SET_SYSTEM_THEME,
handler: async (_: IIPCEvent, args: [theme: 'light' | 'dark' | 'system']) => {
nativeTheme.themeSource = args[0]
},
},
{
action: IRPCActionType.APPLY_THEME,
handler: async (_: IIPCEvent, args: [theme: string]) => {
applyTheme(args[0])
},
},
{
action: IRPCActionType.THEME_RESOLVE_THEMES,
handler: async () => {
return await resolveThemes()
},
type: IRPCType.INVOKE,
},
{
action: IRPCActionType.THEME_FETCH_THEMES,
handler: async () => {
await fetchThemes()
},
type: IRPCType.INVOKE,
},
{
action: IRPCActionType.THEME_IMPORT_THEMES,
handler: async (_: IIPCEvent, args: [files: string[]]) => {
await importThemes(args[0])
},
type: IRPCType.INVOKE,
},
{
action: IRPCActionType.THEME_APPLY_THEME,
handler: async (_: IIPCEvent, args: [theme: string]) => {
await applyTheme(args[0])
},
type: IRPCType.INVOKE,
},
]

View File

@@ -11,7 +11,7 @@ import { createTray, setDockMenu } from 'apis/app/system'
import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
import windowManager from 'apis/app/window/windowManager'
import axios from 'axios'
import { app, globalShortcut, Notification, protocol, screen } from 'electron'
import { app, globalShortcut, nativeTheme, Notification, protocol, screen } from 'electron'
import updater from 'electron-updater'
import fs from 'fs-extra'
@@ -180,6 +180,12 @@ class LifeCycle {
windowManager.create(IWindowList.TRAY_WINDOW)
windowManager.create(IWindowList.SETTING_WINDOW)
const isAutoListenClipboard = picgo.getConfig<boolean>(configPaths.settings.isAutoListenClipboard) || false
const systemTheme = picgo.getConfig<'light' | 'dark' | 'system' | undefined>(configPaths.settings.systemTheme)
if (systemTheme) {
nativeTheme.themeSource = systemTheme
} else {
nativeTheme.themeSource = 'system'
}
const ClipboardWatcher = clipboardPoll
if (isAutoListenClipboard) {
picgo.saveConfig({ [configPaths.settings.isListeningClipboard]: true })
@@ -362,7 +368,6 @@ class LifeCycle {
}
async launchApp() {
console.log('launchApp called', app.getPath('exe'))
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()

View File

@@ -2,7 +2,8 @@ import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { appConfigPath } from '@core/datastore/dirs'
import { appConfigPath, themesDir } from '@core/datastore/dirs'
import * as fsWalk from '@nodelib/fs.walk'
import fs from 'fs-extra'
import yaml from 'yaml'
@@ -17,6 +18,7 @@ function beforeOpen() {
resolveMacWorkFlow()
}
resolveClipboardImageGenerator()
resolveCss()
resolveOtherI18nFiles()
}
@@ -86,16 +88,50 @@ function resolveClipboardImageGenerator() {
diffFilesAndUpdate(item.origin, item.dest)
})
}
}
function getClipboardFiles() {
const files = ['linux.sh', 'mac.applescript', 'windows.ps1', 'windows10.ps1', 'wsl.sh']
function getClipboardFiles() {
const files = ['linux.sh', 'mac.applescript', 'windows.ps1', 'windows10.ps1', 'wsl.sh']
return files.map(item => {
return {
origin: path.join(dirname, '../../resources', item).replace('app.asar', 'app.asar.unpacked'),
dest: path.join(CONFIG_DIR, item),
return files.map(item => {
return {
origin: path.join(dirname, '../../resources', item).replace('app.asar', 'app.asar.unpacked'),
dest: path.join(CONFIG_DIR, item),
}
})
}
function getFileInCssDir() {
const cssDir = path.join(dirname, '../../resources/theme').replace('app.asar', 'app.asar.unpacked')
const res = fsWalk.walkSync(cssDir, {
followSymbolicLinks: true,
fs,
stats: true,
throwErrorOnBrokenSymbolicLink: false,
})
const result: string[] = []
res.forEach(item => {
if (item.stats?.isFile()) {
result.push(item.path)
}
})
return result
}
function resolveCss() {
try {
fs.ensureDirSync(themesDir())
const css = getFileInCssDir()
css.forEach(item => {
const dest = path.join(themesDir(), path.basename(item))
if (!fs.pathExistsSync(dest)) {
fs.copyFileSync(item, dest)
} else {
diffFilesAndUpdate(item, dest)
}
})
} catch (e) {
console.error(e)
}
}

View File

@@ -30,6 +30,8 @@ export interface IConfigStruct {
}
settings: {
shortKey: Record<string, IShortKeyConfig>
systemTheme: 'light' | 'dark' | 'auto'
customTheme: string
logLevel: string[]
logPath: string
logFileSizeLimit: number
@@ -85,6 +87,7 @@ export interface IConfigStruct {
galleryPicBedFilter: string[]
enableSecondUploader?: boolean
lastSyncTime?: number
theme: string
}
needReload: boolean
picgoPlugins: IPicGoPlugins
@@ -120,6 +123,8 @@ export const configPaths = {
_path: 'settings.shortKey',
'picgo:upload': 'settings.shortKey[picgo:upload]',
},
systemTheme: 'settings.systemTheme',
customTheme: 'settings.customTheme',
logLevel: 'settings.logLevel',
logPath: 'settings.logPath',
logFileSizeLimit: 'settings.logFileSizeLimit',
@@ -175,6 +180,7 @@ export const configPaths = {
galleryPicBedFilter: 'settings.galleryPicBedFilter',
enableSecondUploader: 'settings.enableSecondUploader',
lastSyncTime: 'settings.lastSyncTime',
theme: 'settings.theme',
},
needReload: 'needReload',
picgoPlugins: 'picgoPlugins',

View File

@@ -70,6 +70,13 @@ export const IRPCType = {
export const IRPCActionType = {
// system rpc
GET_SYSTEM_THEME: 'GET_SYSTEM_THEME',
SET_SYSTEM_THEME: 'SET_SYSTEM_THEME',
APPLY_THEME: 'APPLY_THEME',
THEME_RESOLVE_THEMES: 'THEME_RESOLVE_THEMES',
THEME_FETCH_THEMES: 'THEME_FETCH_THEMES',
THEME_IMPORT_THEMES: 'THEME_IMPORT_THEMES',
THEME_APPLY_THEME: 'THEME_APPLY_THEME',
RELOAD_APP: 'RELOAD_APP',
OPEN_URL: 'OPEN_URL',
OPEN_FILE: 'OPEN_FILE',