mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-06-02 22:31:49 +08:00
✨ Feature(custom): support theme system and add a new theme hub for piclist
This commit is contained in:
81
src/main/apis/app/theme/index.ts
Normal file
81
src/main/apis/app/theme/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user