Fix system theme pair integration

This commit is contained in:
Yuri Karamian
2026-08-06 15:12:43 +02:00
parent aeddcc6e58
commit 1950c4cc3c
8 changed files with 73 additions and 52 deletions
+36 -3
View File
@@ -332,7 +332,11 @@ pub fn set_theme(state: State<'_, AppState>, theme: String) -> Result<(), String
} }
#[tauri::command] #[tauri::command]
pub fn set_system_themes(state: State<'_, AppState>, light: String, dark: String) -> Result<(), String> { pub fn set_system_themes(
state: State<'_, AppState>,
light: String,
dark: String,
) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.system_light_theme = light; config.system_light_theme = light;
config.system_dark_theme = dark; config.system_dark_theme = dark;
@@ -363,12 +367,41 @@ pub fn save_custom_theme(state: State<'_, AppState>, theme: crate::types::Custom
#[tauri::command] #[tauri::command]
pub fn delete_custom_theme(state: State<'_, AppState>, id: String) -> Result<(), String> { pub fn delete_custom_theme(state: State<'_, AppState>, id: String) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
clear_custom_theme_references(&mut config, &id);
save_app_config(&config)?;
Ok(())
}
fn clear_custom_theme_references(config: &mut AppConfig, id: &str) {
config.custom_themes.retain(|t| t.id != id); config.custom_themes.retain(|t| t.id != id);
if config.theme == id { if config.theme == id {
config.theme = "system".to_string(); config.theme = "system".to_string();
} }
save_app_config(&config)?; if config.system_light_theme == id {
Ok(()) config.system_light_theme = "light".to_string();
}
if config.system_dark_theme == id {
config.system_dark_theme = "dark".to_string();
}
}
#[cfg(test)]
mod custom_theme_reference_tests {
use super::*;
#[test]
fn deleting_custom_theme_resets_system_pair_references() {
let mut config = AppConfig::default();
config.theme = "custom-work".to_string();
config.system_light_theme = "custom-work".to_string();
config.system_dark_theme = "custom-work".to_string();
clear_custom_theme_references(&mut config, "custom-work");
assert_eq!(config.theme, "system");
assert_eq!(config.system_light_theme, "light");
assert_eq!(config.system_dark_theme, "dark");
}
} }
#[tauri::command] #[tauri::command]
+14 -1
View File
@@ -505,7 +505,7 @@ pub struct TaskItem {
#[cfg(test)] #[cfg(test)]
mod startup_view_tests { mod startup_view_tests {
use super::StartupView; use super::{AppConfig, StartupView};
#[test] #[test]
fn serializes_supported_startup_views() { fn serializes_supported_startup_views() {
@@ -526,4 +526,17 @@ mod startup_view_tests {
StartupView::All StartupView::All
); );
} }
#[test]
fn existing_configs_default_system_theme_pair() {
let mut value = serde_json::to_value(AppConfig::default()).unwrap();
let object = value.as_object_mut().unwrap();
object.remove("system_light_theme");
object.remove("system_dark_theme");
let config: AppConfig = serde_json::from_value(value).unwrap();
assert_eq!(config.system_light_theme, "light");
assert_eq!(config.system_dark_theme, "dark");
}
} }
+3 -22
View File
@@ -22,6 +22,7 @@
showCommandPalette, showCommandPalette,
theme, theme,
resolvedTheme, resolvedTheme,
customThemes,
focusMode, focusMode,
readOnly, readOnly,
activeNote, activeNote,
@@ -508,7 +509,8 @@
createAndFocusNote(); createAndFocusNote();
return; return;
case 'toggle-theme': { case 'toggle-theme': {
const isDark = darkThemes.includes($resolvedTheme); const customTheme = $customThemes.find(theme => theme.id === $resolvedTheme);
const isDark = darkThemes.includes($resolvedTheme) || (customTheme?.is_dark ?? false);
const next = isDark ? 'light' : 'dark'; const next = isDark ? 'light' : 'dark';
$theme = next; $theme = next;
setTheme(next); setTheme(next);
@@ -566,23 +568,6 @@
} }
} }
function applyTheme(t: string) {
const namedThemes = ['solarized-light', 'solarized-dark', 'catppuccin', 'nord', 'tokyo-night', 'github-light', 'github-dark', 'dracula', 'blueberry', 'forest-green', 'gruvbox', 'midnight-tide', 'cherry-blossom', 'synthwave', 'ember', 'moonlit', 'light-coffee', 'dark-coffee', 'cotton-candy', 'crimson', 'cloud', 'peach', 'material-dark', 'material-light', 'monokai', 'rose-pine', 'everforest', 'horizon', 'cyberpunk', 'black', 'one-dark'];
const root = document.documentElement;
root.classList.remove('dark');
root.removeAttribute('data-theme');
if (namedThemes.includes(t)) {
root.setAttribute('data-theme', t);
if (darkThemes.includes(t)) root.classList.add('dark');
} else if (t === 'dark') {
root.classList.add('dark');
}
}
$effect(() => {
applyTheme($theme);
});
$effect(() => { $effect(() => {
$collapsedNotebooks; $collapsedNotebooks;
persistState(); persistState();
@@ -664,10 +649,6 @@
prefetchPromise = readNote(lastNotePath).catch(() => null); prefetchPromise = readNote(lastNotePath).catch(() => null);
} }
// One-off apply to avoid a flash before the layout's reactive effect runs; OS appearance
// changes are picked up by resolvedTheme, so no media-query listener is needed here.
applyTheme($resolvedTheme);
// Run sidebar and note list refresh in parallel // Run sidebar and note list refresh in parallel
await Promise.all([sidebar?.refresh(), noteList?.refresh()]); await Promise.all([sidebar?.refresh(), noteList?.refresh()]);
+1 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { showCommandPalette, showSearch, theme, resolvedTheme, sourceMode, viewMode, activeNotebook, activeTag } from '$lib/stores/app'; import { showCommandPalette, showSearch, theme, sourceMode, viewMode, activeNotebook, activeTag } from '$lib/stores/app';
import { setTheme, reindex } from '$lib/api'; import { setTheme, reindex } from '$lib/api';
import { darkThemes } from '$lib/platform'; import { darkThemes } from '$lib/platform';
@@ -89,7 +89,6 @@
action: () => { action: () => {
$theme = 'system'; $theme = 'system';
setTheme('system'); setTheme('system');
applyTheme($resolvedTheme);
$showCommandPalette = false; $showCommandPalette = false;
} }
}, },
+1 -18
View File
@@ -8,8 +8,7 @@
activeNotePath, activeNotePath,
editorDirty, editorDirty,
readOnly, readOnly,
sourceMode, sourceMode
resolvedTheme
} from '$lib/stores/app'; } from '$lib/stores/app';
import { readNote } from '$lib/api'; import { readNote } from '$lib/api';
import { keybindings, matchAction } from '$lib/keybindings'; import { keybindings, matchAction } from '$lib/keybindings';
@@ -17,8 +16,6 @@
let { notePath }: { notePath: string } = $props(); let { notePath }: { notePath: string } = $props();
import { darkThemes } from '$lib/platform';
const appWindow = getCurrentWindow(); const appWindow = getCurrentWindow();
const isMac = navigator.platform.startsWith('Mac'); const isMac = navigator.platform.startsWith('Mac');
let editor = $state<Editor>(null!); let editor = $state<Editor>(null!);
@@ -36,20 +33,6 @@
return () => { unlisten.then(fn => fn()); }; return () => { unlisten.then(fn => fn()); };
}); });
$effect(() => {
const t = $resolvedTheme || 'light';
const namedThemes = ['solarized-light', 'solarized-dark', 'catppuccin', 'nord', 'tokyo-night', 'github-light', 'github-dark', 'dracula', 'blueberry', 'forest-green', 'gruvbox', 'midnight-tide', 'cherry-blossom', 'synthwave', 'ember', 'moonlit', 'light-coffee', 'dark-coffee', 'cotton-candy', 'crimson', 'cloud', 'peach', 'material-dark', 'material-light', 'monokai', 'rose-pine', 'everforest', 'horizon', 'cyberpunk', 'black', 'one-dark'];
const root = document.documentElement;
root.classList.remove('dark');
root.removeAttribute('data-theme');
if (namedThemes.includes(t)) {
root.setAttribute('data-theme', t);
if (darkThemes.includes(t)) root.classList.add('dark');
} else if (t === 'dark') {
root.classList.add('dark');
}
});
let lastMouseDown = 0; let lastMouseDown = 0;
const RESIZE_EDGE = 6; const RESIZE_EDGE = 6;
+10 -4
View File
@@ -690,9 +690,15 @@
async function removeCustomTheme(ct: CustomTheme) { async function removeCustomTheme(ct: CustomTheme) {
try { try {
await deleteCustomTheme(ct.id); await deleteCustomTheme(ct.id);
if (systemLightTheme === ct.id) systemLightTheme = 'light';
if (systemDarkTheme === ct.id) systemDarkTheme = 'dark';
if ($appConfig) { if ($appConfig) {
$appConfig.custom_themes = $appConfig.custom_themes.filter(t => t.id !== ct.id); $appConfig = {
$appConfig = { ...$appConfig }; ...$appConfig,
custom_themes: $appConfig.custom_themes.filter(t => t.id !== ct.id),
system_light_theme: systemLightTheme,
system_dark_theme: systemDarkTheme,
};
} }
if ($theme === ct.id) { if ($theme === ct.id) {
$theme = 'system'; $theme = 'system';
@@ -1044,9 +1050,9 @@
} }
}); });
// Re-apply when theme changes // Re-apply when the concrete theme changes, including an OS appearance switch.
$effect(() => { $effect(() => {
const _ = $theme; const _ = $resolvedTheme;
const preset = accentPresets.find(p => p.name === activeAccent); const preset = accentPresets.find(p => p.name === activeAccent);
if (preset) { if (preset) {
applyAccent(preset); applyAccent(preset);
+6 -1
View File
@@ -102,8 +102,13 @@ export const resolvedTheme = derived(
[theme, appConfig, systemPrefersDark], [theme, appConfig, systemPrefersDark],
([$theme, $config, $prefersDark]): string => { ([$theme, $config, $prefersDark]): string => {
if ($theme !== "system") return $theme; if ($theme !== "system") return $theme;
const fallback = $prefersDark ? "dark" : "light";
const paired = $prefersDark ? $config?.system_dark_theme : $config?.system_light_theme; const paired = $prefersDark ? $config?.system_dark_theme : $config?.system_light_theme;
return paired || ($prefersDark ? "dark" : "light"); if (!paired) return fallback;
if (paired.startsWith("custom-") && !$config?.custom_themes.some((item) => item.id === paired)) {
return fallback;
}
return paired;
}, },
); );
+2 -1
View File
@@ -197,7 +197,8 @@
} }
// Apply saved accent // Apply saved accent
if (config.accent_color) { if (config.accent_color) {
const isDark = darkThemes.includes(themeValue); const customTheme = config.custom_themes.find(theme => theme.id === themeValue);
const isDark = darkThemes.includes(themeValue) || (customTheme?.is_dark ?? false);
let color: string | null = null; let color: string | null = null;
if (config.accent_color.startsWith('#')) { if (config.accent_color.startsWith('#')) {
color = config.accent_color; color = config.accent_color;