feat: add system theme mode

This commit is contained in:
t8y2
2026-05-13 10:48:34 +08:00
parent 7ce2cf2163
commit b0a808bc14
8 changed files with 159 additions and 27 deletions
+3 -2
View File
@@ -49,7 +49,7 @@ const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const savedSqlStore = useSavedSqlStore();
const { message: toastMessage, visible: toastVisible, toast } = useToast();
const { isDark, applyTheme, toggleTheme } = useTheme();
const { isDark, themeMode, applyTheme, setThemeMode } = useTheme();
const {
checkingUpdates,
updateInfo,
@@ -537,6 +537,7 @@ onUnmounted(() => {
<div class="h-screen w-screen flex flex-col bg-background text-foreground overflow-hidden">
<AppToolbar
:is-dark="isDark"
:theme-mode="themeMode"
:show-ai-panel="showAiPanel"
:show-history="showHistory"
:checking-updates="checkingUpdates"
@@ -544,7 +545,7 @@ onUnmounted(() => {
:has-sql-file-connections="hasSqlFileConnections"
@new-connection="showConnectionDialog = true"
@new-query="newQuery"
@toggle-theme="toggleTheme"
@set-theme-mode="setThemeMode"
@toggle-ai="toggleAiPanel"
@toggle-history="showHistory = !showHistory"
@open-github="openGitHub"
+41 -8
View File
@@ -7,6 +7,8 @@ import {
Globe,
Moon,
Sun,
Monitor,
Check,
History,
Bot,
ArrowLeftRight,
@@ -27,6 +29,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import WindowControls from "@/components/layout/WindowControls.vue";
import { useWindowControls } from "@/composables/useWindowControls";
import { currentLocale, setLocale, type Locale } from "@/i18n";
import type { AppThemeMode } from "@/lib/appTheme";
const localeOptions: { value: Locale; flag: string; label: string }[] = [
{ value: "en", flag: "🇺🇸", label: "English" },
@@ -36,6 +39,7 @@ const localeOptions: { value: Locale; flag: string; label: string }[] = [
defineProps<{
isDark: boolean;
themeMode: AppThemeMode;
showAiPanel: boolean;
showHistory: boolean;
checkingUpdates: boolean;
@@ -46,7 +50,7 @@ defineProps<{
const emit = defineEmits<{
"new-connection": [];
"new-query": [];
"toggle-theme": [];
"set-theme-mode": [mode: AppThemeMode];
"toggle-ai": [];
"toggle-history": [];
"open-github": [];
@@ -178,15 +182,44 @@ function onToolbarDblClick(e: MouseEvent) {
<TooltipContent>AI</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="emit('toggle-theme')">
<Moon v-if="!isDark" class="h-4 w-4" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon" class="h-8 w-8" :title="t('toolbar.theme')">
<Monitor v-if="themeMode === 'system'" class="h-4 w-4" />
<Moon v-else-if="isDark" class="h-4 w-4" />
<Sun v-else class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ isDark ? "Light" : "Dark" }}</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
class="gap-2"
:class="{ 'bg-accent': themeMode === 'light' }"
@select="emit('set-theme-mode', 'light')"
>
<Sun class="h-4 w-4" />
{{ t("toolbar.themeLight") }}
<Check v-if="themeMode === 'light'" class="ml-auto h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
class="gap-2"
:class="{ 'bg-accent': themeMode === 'dark' }"
@select="emit('set-theme-mode', 'dark')"
>
<Moon class="h-4 w-4" />
{{ t("toolbar.themeDark") }}
<Check v-if="themeMode === 'dark'" class="ml-auto h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
class="gap-2"
:class="{ 'bg-accent': themeMode === 'system' }"
@select="emit('set-theme-mode', 'system')"
>
<Monitor class="h-4 w-4" />
{{ t("toolbar.themeSystem") }}
<Check v-if="themeMode === 'system'" class="ml-auto h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger as-child>
+55 -17
View File
@@ -1,25 +1,63 @@
import { ref } from "vue";
import { computed, ref } from "vue";
import {
APP_THEME_STORAGE_KEY,
getTauriThemeForMode,
normalizeAppThemeMode,
resolveAppThemeAppearance,
type AppThemeMode,
} from "@/lib/appTheme";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import type { Theme } from "@tauri-apps/api/window";
const themeMode = ref<AppThemeMode>(
normalizeAppThemeMode(typeof localStorage === "undefined" ? null : localStorage.getItem(APP_THEME_STORAGE_KEY)),
);
const systemPrefersDark = ref(readSystemPrefersDark());
const isDark = computed(() => resolveAppThemeAppearance(themeMode.value, systemPrefersDark.value) === "dark");
let mediaQuery: MediaQueryList | null = null;
let isListeningForSystemTheme = false;
function readSystemPrefersDark() {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
function setupSystemThemeListener() {
if (isListeningForSystemTheme || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
systemPrefersDark.value = mediaQuery.matches;
const onChange = (event: MediaQueryListEvent) => {
systemPrefersDark.value = event.matches;
if (themeMode.value === "system") applyTheme();
};
mediaQuery.addEventListener("change", onChange);
isListeningForSystemTheme = true;
}
function applyTheme() {
if (typeof document !== "undefined") {
document.documentElement.classList.toggle("dark", isDark.value);
}
if (!isTauriRuntime()) return;
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => {
getCurrentWindow()
.setTheme(getTauriThemeForMode(themeMode.value))
.catch(() => {});
});
}
function setThemeMode(mode: AppThemeMode) {
themeMode.value = mode;
localStorage.setItem(APP_THEME_STORAGE_KEY, mode);
applyTheme();
}
export function useTheme() {
const isDark = ref(localStorage.getItem("dbx-theme") === "dark");
function applyTheme() {
document.documentElement.classList.toggle("dark", isDark.value);
if (!isTauriRuntime()) return;
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => {
getCurrentWindow()
.setTheme(isDark.value ? ("dark" as Theme) : ("light" as Theme))
.catch(() => {});
});
}
setupSystemThemeListener();
function toggleTheme() {
isDark.value = !isDark.value;
localStorage.setItem("dbx-theme", isDark.value ? "dark" : "light");
applyTheme();
setThemeMode(isDark.value ? "light" : "dark");
}
return { isDark, applyTheme, toggleTheme };
return { isDark, themeMode, applyTheme, setThemeMode, toggleTheme };
}
+4
View File
@@ -34,6 +34,10 @@ export default {
formatSqlFailed: "Failed to format SQL",
saveSql: "Save to SQL Library",
openSql: "Open SQL file",
theme: "Theme",
themeLight: "Light",
themeDark: "Dark",
themeSystem: "Follow System",
sqlSaved: "SQL saved",
sqlOpenFailed: "Failed to open file: {message}",
sqlSaveFailed: "Failed to save file: {message}",
+4
View File
@@ -34,6 +34,10 @@ export default {
formatSqlFailed: "Error al formatear el SQL",
saveSql: "Guardar en biblioteca SQL",
openSql: "Abrir archivo SQL",
theme: "Tema",
themeLight: "Claro",
themeDark: "Oscuro",
themeSystem: "Seguir sistema",
sqlSaved: "SQL guardado",
sqlOpenFailed: "Error al abrir el archivo: {message}",
sqlSaveFailed: "Error al guardar el archivo: {message}",
+4
View File
@@ -34,6 +34,10 @@ export default {
formatSqlFailed: "SQL 格式化失败",
saveSql: "保存到 SQL 库",
openSql: "打开 SQL 文件",
theme: "主题",
themeLight: "亮色",
themeDark: "暗色",
themeSystem: "跟随系统",
sqlSaved: "SQL 已保存",
sqlOpenFailed: "打开文件失败:{message}",
sqlSaveFailed: "保存文件失败:{message}",
+20
View File
@@ -0,0 +1,20 @@
import type { Theme } from "@tauri-apps/api/window";
export const APP_THEME_STORAGE_KEY = "dbx-theme";
export type AppThemeMode = "light" | "dark" | "system";
export type AppThemeAppearance = "light" | "dark";
export function normalizeAppThemeMode(value: string | null): AppThemeMode {
if (value === "dark" || value === "light" || value === "system") return value;
return "light";
}
export function resolveAppThemeAppearance(mode: AppThemeMode, systemPrefersDark: boolean): AppThemeAppearance {
if (mode === "system") return systemPrefersDark ? "dark" : "light";
return mode;
}
export function getTauriThemeForMode(mode: AppThemeMode): Theme | null {
return mode === "system" ? null : mode;
}