mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-07-07 18:45:09 +00:00
Add custom themes
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
"@angular/router": "^18.0.6",
|
"@angular/router": "^18.0.6",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"bottleneck": "^2.19.5",
|
"bottleneck": "^2.19.5",
|
||||||
|
"culori": "^4.0.1",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"electron-unhandled": "^5.0.0",
|
"electron-unhandled": "^5.0.0",
|
||||||
"electron-updater": "^6.2.1",
|
"electron-updater": "^6.2.1",
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"@angular/cli": "^18.0.7",
|
"@angular/cli": "^18.0.7",
|
||||||
"@angular/compiler-cli": "^18.0.6",
|
"@angular/compiler-cli": "^18.0.6",
|
||||||
"@angular/language-service": "^18.0.6",
|
"@angular/language-service": "^18.0.6",
|
||||||
|
"@types/culori": "^2.1.1",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/lodash": "^4.17.6",
|
"@types/lodash": "^4.17.6",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
|
|||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
|||||||
bottleneck:
|
bottleneck:
|
||||||
specifier: ^2.19.5
|
specifier: ^2.19.5
|
||||||
version: 2.19.5
|
version: 2.19.5
|
||||||
|
culori:
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.11
|
specifier: ^1.11.11
|
||||||
version: 1.11.11
|
version: 1.11.11
|
||||||
@@ -105,6 +108,9 @@ importers:
|
|||||||
'@angular/language-service':
|
'@angular/language-service':
|
||||||
specifier: ^18.0.6
|
specifier: ^18.0.6
|
||||||
version: 18.0.6
|
version: 18.0.6
|
||||||
|
'@types/culori':
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
'@types/fs-extra':
|
'@types/fs-extra':
|
||||||
specifier: ^11.0.4
|
specifier: ^11.0.4
|
||||||
version: 11.0.4
|
version: 11.0.4
|
||||||
@@ -1173,6 +1179,9 @@ packages:
|
|||||||
'@types/cacheable-request@6.0.3':
|
'@types/cacheable-request@6.0.3':
|
||||||
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
||||||
|
|
||||||
|
'@types/culori@2.1.1':
|
||||||
|
resolution: {integrity: sha512-NzLYD0vNHLxTdPp8+RlvGbR2NfOZkwxcYGFwxNtm+WH2NuUNV8785zv1h0sulFQ5aFQ9n/jNDUuJeo3Bh7+oFA==}
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
@@ -1824,6 +1833,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==}
|
resolution: {integrity: sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
culori@4.0.1:
|
||||||
|
resolution: {integrity: sha512-LSnjA6HuIUOlkfKVbzi2OlToZE8OjFi667JWN9qNymXVXzGDmvuP60SSgC+e92sd7B7158f7Fy3Mb6rXS5EDPw==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
daisyui@4.12.10:
|
daisyui@4.12.10:
|
||||||
resolution: {integrity: sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==}
|
resolution: {integrity: sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==}
|
||||||
engines: {node: '>=16.9.0'}
|
engines: {node: '>=16.9.0'}
|
||||||
@@ -5161,6 +5174,8 @@ snapshots:
|
|||||||
'@types/node': 18.16.0
|
'@types/node': 18.16.0
|
||||||
'@types/responselike': 1.0.3
|
'@types/responselike': 1.0.3
|
||||||
|
|
||||||
|
'@types/culori@2.1.1': {}
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 0.7.34
|
'@types/ms': 0.7.34
|
||||||
@@ -5946,6 +5961,8 @@ snapshots:
|
|||||||
|
|
||||||
culori@3.3.0: {}
|
culori@3.3.0: {}
|
||||||
|
|
||||||
|
culori@4.0.1: {}
|
||||||
|
|
||||||
daisyui@4.12.10(postcss@8.4.39):
|
daisyui@4.12.10(postcss@8.4.39):
|
||||||
dependencies:
|
dependencies:
|
||||||
css-selector-tokenizer: 0.8.0
|
css-selector-tokenizer: 0.8.0
|
||||||
|
|||||||
@@ -17,38 +17,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="form-control w-full max-w-xs">
|
<div class="form-control w-full max-w-xs">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Appearance</span>
|
<span class="label-text">Appearance</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
<div class="dropdown dropdown-hover">
|
|
||||||
<label tabindex="0" class="btn btn-neutral">{{ capitalize(settingsService.theme) }}</label>
|
|
||||||
<ul tabindex="0" class="dropdown-content z-[2] menu p-2 shadow bg-neutral text-neutral-content rounded-box w-36">
|
|
||||||
<li>
|
|
||||||
<h2 class="menu-title text-neutral-content text-opacity-50">Dark</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a (click)="setTheme('business')">Business</a></li>
|
|
||||||
<li><a (click)="setTheme('dark')">Dark</a></li>
|
|
||||||
<li><a (click)="setTheme('dim')">Dim</a></li>
|
|
||||||
<li><a (click)="setTheme('night')">Night</a></li>
|
|
||||||
<li><a (click)="setTheme('sunset')">Sunset</a></li>
|
|
||||||
<li><a (click)="setTheme('synthwave')">Synthwave</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h2 class="menu-title text-neutral-content text-opacity-50">Light</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a (click)="setTheme('aqua')">Aqua</a></li>
|
|
||||||
<li><a (click)="setTheme('emerald')">Emerald</a></li>
|
|
||||||
<li><a (click)="setTheme('lemonade')">Lemonade</a></li>
|
|
||||||
<li><a (click)="setTheme('nord')">Nord</a></li>
|
|
||||||
<li><a (click)="setTheme('valentine')">Valentine</a></li>
|
|
||||||
<li><a (click)="setTheme('winter')">Winter</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="join join-vertical">
|
<div class="join join-vertical">
|
||||||
<div class="tooltip tooltip-right" data-tip="Zoom In (ctrl +)">
|
<div class="tooltip tooltip-right" data-tip="Zoom In (ctrl +)">
|
||||||
<button class="join-item btn btn-square btn-neutral btn-xs" (click)="settingsService.zoomIn()">
|
<button class="join-item btn btn-square btn-neutral btn-xs" (click)="settingsService.zoomIn()">
|
||||||
@@ -61,8 +34,40 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (!settingsService.customTheme) {
|
||||||
|
<div class="dropdown dropdown-hover">
|
||||||
|
<label tabindex="0" class="btn btn-neutral">{{ capitalize(settingsService.theme) }}</label>
|
||||||
|
<ul tabindex="0" class="dropdown-content z-[2] menu p-2 shadow bg-neutral text-neutral-content rounded-box w-36">
|
||||||
|
<li>
|
||||||
|
<h2 class="menu-title text-neutral-content text-opacity-50">Dark</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a (click)="setTheme('business')">Business</a></li>
|
||||||
|
<li><a (click)="setTheme('dark')">Dark</a></li>
|
||||||
|
<li><a (click)="setTheme('dim')">Dim</a></li>
|
||||||
|
<li><a (click)="setTheme('night')">Night</a></li>
|
||||||
|
<li><a (click)="setTheme('sunset')">Sunset</a></li>
|
||||||
|
<li><a (click)="setTheme('synthwave')">Synthwave</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h2 class="menu-title text-neutral-content text-opacity-50">Light</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a (click)="setTheme('aqua')">Aqua</a></li>
|
||||||
|
<li><a (click)="setTheme('emerald')">Emerald</a></li>
|
||||||
|
<li><a (click)="setTheme('lemonade')">Lemonade</a></li>
|
||||||
|
<li><a (click)="setTheme('nord')">Nord</a></li>
|
||||||
|
<li><a (click)="setTheme('valentine')">Valentine</a></li>
|
||||||
|
<li><a (click)="setTheme('winter')">Winter</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button (click)="getCustomTheme()" class="btn btn-primary">Use custom theme</button>
|
||||||
|
} @else {
|
||||||
|
<button (click)="clearCustomTheme()" class="btn btn-neutral">Clear custom theme</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
|
|||||||
@@ -76,6 +76,31 @@ export class SettingsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCustomTheme() {
|
||||||
|
const result = await window.electron.invoke.showOpenDialog({
|
||||||
|
title: 'Choose custom theme',
|
||||||
|
defaultPath: this.settingsService.libraryDirectory || '',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [
|
||||||
|
{ name: "Themes", extensions: ["json"] },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.canceled === false) {
|
||||||
|
const newThemeColors = await window.electron.invoke.getThemeColors(result.filePaths[0])
|
||||||
|
|
||||||
|
if (newThemeColors) {
|
||||||
|
this.settingsService.customTheme = newThemeColors
|
||||||
|
} else {
|
||||||
|
alert(`ERROR: ${result.filePaths[0]} was not a valid JSON file.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCustomTheme() {
|
||||||
|
this.settingsService.customTheme = null
|
||||||
|
}
|
||||||
|
|
||||||
openLibraryDirectory() {
|
openLibraryDirectory() {
|
||||||
if (this.settingsService.libraryDirectory) {
|
if (this.settingsService.libraryDirectory) {
|
||||||
window.electron.emit.showFolder(this.settingsService.libraryDirectory)
|
window.electron.emit.showFolder(this.settingsService.libraryDirectory)
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { Inject, Injectable } from '@angular/core'
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Difficulty, Instrument } from 'scan-chart'
|
import { Difficulty, Instrument } from 'scan-chart'
|
||||||
|
|
||||||
|
import { ThemeColors } from '../../../../src-shared/interfaces/theme.interface.js'
|
||||||
import { Settings, themes } from '../../../../src-shared/Settings.js'
|
import { Settings, themes } from '../../../../src-shared/Settings.js'
|
||||||
|
import { colorNames, convertColorFormat } from '../../../../src-shared/UtilFunctions.js'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -19,18 +21,20 @@ export class SettingsService {
|
|||||||
|
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
this.settings = await window.electron.invoke.getSettings()
|
this.settings = await window.electron.invoke.getSettings()
|
||||||
if (!themes.includes(this.settings.theme)) {
|
if (this.settings.customTheme) {
|
||||||
|
setThemeColors(this.settings.customTheme)
|
||||||
|
} else if (!themes.includes(this.settings.theme)) {
|
||||||
this.changeTheme('dark')
|
this.changeTheme('dark')
|
||||||
} else {
|
} else {
|
||||||
this.changeTheme(this.settings.theme)
|
this.changeTheme(this.settings.theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
private saveSettings() {
|
||||||
window.electron.emit.setSettings(this.settings)
|
window.electron.emit.setSettings(this.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
changeTheme(theme: typeof themes[number]) {
|
private changeTheme(theme: typeof themes[number]) {
|
||||||
this.document.documentElement.setAttribute('data-theme', theme)
|
this.document.documentElement.setAttribute('data-theme', theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +54,6 @@ export class SettingsService {
|
|||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Individual getters/setters
|
|
||||||
get libraryDirectory() {
|
get libraryDirectory() {
|
||||||
return this.settings.libraryPath
|
return this.settings.libraryPath
|
||||||
}
|
}
|
||||||
@@ -70,9 +73,27 @@ export class SettingsService {
|
|||||||
get theme() {
|
get theme() {
|
||||||
return this.settings.theme
|
return this.settings.theme
|
||||||
}
|
}
|
||||||
set theme(newValue: typeof themes[number]) {
|
set theme(value: typeof themes[number]) {
|
||||||
this.settings.theme = newValue
|
this.settings.theme = value
|
||||||
this.changeTheme(newValue)
|
if (!this.settings.customTheme) {
|
||||||
|
this.changeTheme(value)
|
||||||
|
}
|
||||||
|
this.saveSettings()
|
||||||
|
}
|
||||||
|
get customTheme() {
|
||||||
|
return this.settings.customTheme
|
||||||
|
}
|
||||||
|
set customTheme(value: ThemeColors | null) {
|
||||||
|
if (value) {
|
||||||
|
const failed = setThemeColors(value)
|
||||||
|
if (failed) { return }
|
||||||
|
} else {
|
||||||
|
for (const themeColor in colorNames) {
|
||||||
|
document.documentElement.style.removeProperty(colorNames[themeColor])
|
||||||
|
}
|
||||||
|
this.changeTheme(this.settings.theme)
|
||||||
|
}
|
||||||
|
this.settings.customTheme = value
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,3 +129,16 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setThemeColors(themeColors: ThemeColors) {
|
||||||
|
try {
|
||||||
|
const result = convertColorFormat(themeColors)
|
||||||
|
for (const cssKey in result) {
|
||||||
|
document.documentElement.style.setProperty(cssKey, result[cssKey])
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} catch (err) {
|
||||||
|
alert(`ERROR: the provided color theme is improperly formatted.`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { IpcInvokeHandlers, IpcToMainEmitHandlers } from '../src-shared/interfac
|
|||||||
import { download } from './ipc/DownloadHandler.ipc.js'
|
import { download } from './ipc/DownloadHandler.ipc.js'
|
||||||
import { getSettings, setSettings } from './ipc/SettingsHandler.ipc.js'
|
import { getSettings, setSettings } from './ipc/SettingsHandler.ipc.js'
|
||||||
import { downloadUpdate, getCurrentVersion, getUpdateAvailable, quitAndInstall, retryUpdate } from './ipc/UpdateHandler.ipc.js'
|
import { downloadUpdate, getCurrentVersion, getUpdateAvailable, quitAndInstall, retryUpdate } from './ipc/UpdateHandler.ipc.js'
|
||||||
import { getPlatform, isMaximized, maximize, minimize, openUrl, quit, restore, showFile, showFolder, showOpenDialog, toggleDevTools } from './ipc/UtilHandlers.ipc.js'
|
import { getPlatform, getThemeColors, isMaximized, maximize, minimize, openUrl, quit, restore, showFile, showFolder, showOpenDialog, toggleDevTools } from './ipc/UtilHandlers.ipc.js'
|
||||||
|
|
||||||
export function getIpcInvokeHandlers(): IpcInvokeHandlers {
|
export function getIpcInvokeHandlers(): IpcInvokeHandlers {
|
||||||
return {
|
return {
|
||||||
@@ -12,6 +12,7 @@ export function getIpcInvokeHandlers(): IpcInvokeHandlers {
|
|||||||
getUpdateAvailable,
|
getUpdateAvailable,
|
||||||
isMaximized,
|
isMaximized,
|
||||||
showOpenDialog,
|
showOpenDialog,
|
||||||
|
getThemeColors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { app, dialog, OpenDialogOptions, shell } from 'electron'
|
import { app, dialog, OpenDialogOptions, shell } from 'electron'
|
||||||
|
import fsExtra from 'fs-extra'
|
||||||
|
|
||||||
|
import { ThemeColors } from '../../src-shared/interfaces/theme.interface.js'
|
||||||
import { mainWindow } from '../main.js'
|
import { mainWindow } from '../main.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,3 +50,11 @@ export function showFile(filePath: string) {
|
|||||||
export async function getPlatform() {
|
export async function getPlatform() {
|
||||||
return process.platform
|
return process.platform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getThemeColors(path: string) {
|
||||||
|
try {
|
||||||
|
return await fsExtra.readJson(path) as ThemeColors
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const electronApi: ContextBridgeApi = {
|
|||||||
getUpdateAvailable: getInvoker('getUpdateAvailable'),
|
getUpdateAvailable: getInvoker('getUpdateAvailable'),
|
||||||
isMaximized: getInvoker('isMaximized'),
|
isMaximized: getInvoker('isMaximized'),
|
||||||
showOpenDialog: getInvoker('showOpenDialog'),
|
showOpenDialog: getInvoker('showOpenDialog'),
|
||||||
|
getThemeColors: getInvoker('getThemeColors'),
|
||||||
},
|
},
|
||||||
emit: {
|
emit: {
|
||||||
download: getEmitter('download'),
|
download: getEmitter('download'),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Difficulty, Instrument } from 'scan-chart'
|
import { Difficulty, Instrument } from 'scan-chart'
|
||||||
|
|
||||||
|
import { ThemeColors } from './interfaces/theme.interface'
|
||||||
|
|
||||||
export const themes = [
|
export const themes = [
|
||||||
'business',
|
'business',
|
||||||
'dark',
|
'dark',
|
||||||
@@ -23,6 +25,7 @@ export const themes = [
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
downloadVideos: boolean // If background videos should be downloaded
|
downloadVideos: boolean // If background videos should be downloaded
|
||||||
theme: typeof themes[number] // The name of the currently enabled UI theme
|
theme: typeof themes[number] // The name of the currently enabled UI theme
|
||||||
|
customTheme: ThemeColors | null // The colors of a custom theme
|
||||||
libraryPath: string | undefined // The path to the user's library
|
libraryPath: string | undefined // The path to the user's library
|
||||||
isSng: boolean // If the chart should be downloaded as a .sng file or as a chart folder
|
isSng: boolean // If the chart should be downloaded as a .sng file or as a chart folder
|
||||||
isCompactTable: boolean // If the search result table should have reduced padding
|
isCompactTable: boolean // If the search result table should have reduced padding
|
||||||
@@ -37,6 +40,7 @@ export interface Settings {
|
|||||||
export const defaultSettings: Settings = {
|
export const defaultSettings: Settings = {
|
||||||
downloadVideos: true,
|
downloadVideos: true,
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
|
customTheme: null,
|
||||||
libraryPath: undefined,
|
libraryPath: undefined,
|
||||||
isSng: false,
|
isSng: false,
|
||||||
isCompactTable: false,
|
isCompactTable: false,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { interpolate as culoriInterpolate, oklch, wcagContrast } from 'culori'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Difficulty, Instrument } from 'scan-chart'
|
import { Difficulty, Instrument } from 'scan-chart'
|
||||||
|
|
||||||
import { ChartData } from './interfaces/search.interface'
|
import { ChartData } from './interfaces/search.interface'
|
||||||
|
import { ThemeColors } from './interfaces/theme.interface'
|
||||||
|
|
||||||
// WARNING: do not import anything related to Electron; the code will not compile correctly.
|
// WARNING: do not import anything related to Electron; the code will not compile correctly.
|
||||||
|
|
||||||
@@ -166,3 +168,179 @@ export function hasIssues(chart: Pick<ChartData, 'metadataIssues' | 'folderIssue
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
export const colorNames = {
|
||||||
|
"primary": "--p",
|
||||||
|
"primary-content": "--pc",
|
||||||
|
|
||||||
|
"secondary": "--s",
|
||||||
|
"secondary-content": "--sc",
|
||||||
|
|
||||||
|
"accent": "--a",
|
||||||
|
"accent-content": "--ac",
|
||||||
|
|
||||||
|
"neutral": "--n",
|
||||||
|
"neutral-content": "--nc",
|
||||||
|
|
||||||
|
"base-100": "--b1",
|
||||||
|
"base-200": "--b2",
|
||||||
|
"base-300": "--b3",
|
||||||
|
"base-content": "--bc",
|
||||||
|
|
||||||
|
"info": "--in",
|
||||||
|
"info-content": "--inc",
|
||||||
|
|
||||||
|
"success": "--su",
|
||||||
|
"success-content": "--suc",
|
||||||
|
|
||||||
|
"warning": "--wa",
|
||||||
|
"warning-content": "--wac",
|
||||||
|
|
||||||
|
"error": "--er",
|
||||||
|
"error-content": "--erc",
|
||||||
|
} as { [colorName: string]: string }
|
||||||
|
const defaultVariables = {
|
||||||
|
"--rounded-box": "1rem",
|
||||||
|
"--rounded-btn": "0.5rem",
|
||||||
|
"--rounded-badge": "1.9rem",
|
||||||
|
"--animation-btn": "0.25s",
|
||||||
|
"--animation-input": ".2s",
|
||||||
|
"--btn-focus-scale": "0.95",
|
||||||
|
"--border-btn": "1px",
|
||||||
|
"--tab-border": "1px",
|
||||||
|
"--tab-radius": "0.5rem",
|
||||||
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
export function convertColorFormat(input: ThemeColors) {
|
||||||
|
if (typeof input !== "object" || input === null) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultObj: { [cssKey: string]: string } = {}
|
||||||
|
|
||||||
|
for (const [rule, value] of Object.entries(input)) {
|
||||||
|
if (Object.hasOwn(colorNames, rule)) {
|
||||||
|
const colorObj = oklch(value)
|
||||||
|
resultObj[colorNames[rule]] = colorObjToString(colorObj!)
|
||||||
|
} else {
|
||||||
|
resultObj[rule] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto generate base colors
|
||||||
|
if (!Object.hasOwn(input, "base-100")) {
|
||||||
|
resultObj["--b1"] = "100% 0 0"
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "base-200")) {
|
||||||
|
resultObj["--b2"] = generateDarkenColorFrom(input["base-100"], 0.07)
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "base-300")) {
|
||||||
|
if (Object.hasOwn(input, "base-200")) {
|
||||||
|
resultObj["--b3"] = generateDarkenColorFrom(input["base-200"], 0.07)
|
||||||
|
} else {
|
||||||
|
resultObj["--b3"] = generateDarkenColorFrom(input["base-100"], 0.14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto generate state colors
|
||||||
|
if (!Object.hasOwn(input, "info")) {
|
||||||
|
resultObj["--in"] = "72.06% 0.191 231.6"
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "success")) {
|
||||||
|
resultObj["--su"] = "64.8% 0.150 160"
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "warning")) {
|
||||||
|
resultObj["--wa"] = "84.71% 0.199 83.87"
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "error")) {
|
||||||
|
resultObj["--er"] = "71.76% 0.221 22.18"
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto generate content colors
|
||||||
|
if (!Object.hasOwn(input, "base-content")) {
|
||||||
|
resultObj["--bc"] = generateForegroundColorFrom(input["base-100"], 0.8)
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "primary-content")) {
|
||||||
|
resultObj["--pc"] = generateForegroundColorFrom(input.primary, 0.8)
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "secondary-content")) {
|
||||||
|
resultObj["--sc"] = generateForegroundColorFrom(input.secondary, 0.8)
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "accent-content")) {
|
||||||
|
resultObj["--ac"] = generateForegroundColorFrom(input.accent, 0.8)
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "neutral-content")) {
|
||||||
|
resultObj["--nc"] = generateForegroundColorFrom(input.neutral, 0.8)
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "info-content")) {
|
||||||
|
if (Object.hasOwn(input, "info")) {
|
||||||
|
resultObj["--inc"] = generateForegroundColorFrom(input.info, 0.8)
|
||||||
|
} else {
|
||||||
|
resultObj["--inc"] = "0% 0 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "success-content")) {
|
||||||
|
if (Object.hasOwn(input, "success")) {
|
||||||
|
resultObj["--suc"] = generateForegroundColorFrom(input.success, 0.8)
|
||||||
|
} else {
|
||||||
|
resultObj["--suc"] = "0% 0 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "warning-content")) {
|
||||||
|
if (Object.hasOwn(input, "warning")) {
|
||||||
|
resultObj["--wac"] = generateForegroundColorFrom(input.warning, 0.8)
|
||||||
|
} else {
|
||||||
|
resultObj["--wac"] = "0% 0 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(input, "error-content")) {
|
||||||
|
if (Object.hasOwn(input, "error")) {
|
||||||
|
resultObj["--erc"] = generateForegroundColorFrom(input.error, 0.8)
|
||||||
|
} else {
|
||||||
|
resultObj["--erc"] = "0% 0 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add css variables if not exist
|
||||||
|
for (const item of Object.entries(defaultVariables)) {
|
||||||
|
const [variable, value] = item
|
||||||
|
if (!Object.hasOwn(input, variable)) {
|
||||||
|
resultObj[variable] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add other custom styles
|
||||||
|
if (!Object.hasOwn(colorNames, rule)) {
|
||||||
|
resultObj[rule] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultObj
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateForegroundColorFrom(input: string, percentage = 0.8) {
|
||||||
|
const result = culoriInterpolate([input, isDark(input) ? "white" : "black"], "oklch")(percentage)
|
||||||
|
return colorObjToString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDarkenColorFrom(input: string, percentage = 0.07) {
|
||||||
|
const result = culoriInterpolate([input, "black"], "oklch")(percentage)
|
||||||
|
return colorObjToString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorObjToString(input: { l: number; c: number; h?: number }) {
|
||||||
|
const { l, c, h } = input
|
||||||
|
return `${Number.parseFloat((cutNumber(l) * 100).toFixed(6))}% ${cutNumber(c)} ${cutNumber(h ?? 0)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function cutNumber(number: number) {
|
||||||
|
if (number) {
|
||||||
|
return +number.toFixed(6)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDark(color: string) {
|
||||||
|
return wcagContrast(color, "black") < wcagContrast(color, "white")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { UpdateInfo } from 'electron-updater'
|
|||||||
|
|
||||||
import { Settings } from '../Settings.js'
|
import { Settings } from '../Settings.js'
|
||||||
import { Download, DownloadProgress } from './download.interface.js'
|
import { Download, DownloadProgress } from './download.interface.js'
|
||||||
|
import { ThemeColors } from './theme.interface.js'
|
||||||
import { UpdateProgress } from './update.interface.js'
|
import { UpdateProgress } from './update.interface.js'
|
||||||
|
|
||||||
export interface ContextBridgeApi {
|
export interface ContextBridgeApi {
|
||||||
@@ -45,6 +46,10 @@ export interface IpcInvokeEvents {
|
|||||||
input: OpenDialogOptions
|
input: OpenDialogOptions
|
||||||
output: OpenDialogReturnValue
|
output: OpenDialogReturnValue
|
||||||
}
|
}
|
||||||
|
getThemeColors: {
|
||||||
|
input: string
|
||||||
|
output: ThemeColors | null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IpcInvokeHandlers = {
|
export type IpcInvokeHandlers = {
|
||||||
|
|||||||
28
src-shared/interfaces/theme.interface.ts
Normal file
28
src-shared/interfaces/theme.interface.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
export interface ThemeColors {
|
||||||
|
'primary': string
|
||||||
|
'primary-focus': string
|
||||||
|
'primary-content': string
|
||||||
|
'secondary': string
|
||||||
|
'secondary-focus': string
|
||||||
|
'secondary-content': string
|
||||||
|
'accent': string
|
||||||
|
'accent-focus': string
|
||||||
|
'accent-content': string
|
||||||
|
'neutral': string
|
||||||
|
'neutral-focus': string
|
||||||
|
'neutral-content': string
|
||||||
|
'base-100': string
|
||||||
|
'base-200': string
|
||||||
|
'base-300': string
|
||||||
|
'base-content': string
|
||||||
|
'info': string
|
||||||
|
'info-content': string
|
||||||
|
'success': string
|
||||||
|
'success-content': string
|
||||||
|
'warning': string
|
||||||
|
'warning-content': string
|
||||||
|
'error': string
|
||||||
|
'error-content': string
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user