feat(theme): add system theme support

close #13
This commit is contained in:
Elysia
2026-02-13 14:49:41 +08:00
parent 1d808b035a
commit 7aeb3d5dd4
4 changed files with 87 additions and 22 deletions

View File

@@ -1,4 +1,12 @@
import { app, BrowserWindow, ipcMain, Menu, shell, Tray } from "electron";
import {
app,
BrowserWindow,
ipcMain,
Menu,
shell,
Tray,
nativeTheme,
} from "electron";
import { fileURLToPath } from "node:url";
import path from "node:path";
import os from "node:os";
@@ -116,6 +124,10 @@ ipcMain.on("renderer-ready", (event, args) => {
logger.info(`isLoaded set to: ${isLoaded.value}`);
});
ipcMain.on("set-theme-source", (event, theme: "system" | "light" | "dark") => {
nativeTheme.themeSource = theme;
});
app.whenReady().then(() => {
createWindow();
handleCommandLine(process.argv);

View File

@@ -9,7 +9,7 @@
:categories="categories"
:active-category="activeCategory"
:category-counts="categoryCounts"
:is-dark-theme="isDarkTheme"
:theme-mode="themeMode"
@toggle-theme="toggleTheme"
@select-category="selectCategory"
/>
@@ -153,7 +153,15 @@ const axiosInstance = axios.create({
});
// 响应式状态
const isDarkTheme = ref(false);
const themeMode = ref<"light" | "dark" | "auto">("auto");
const systemIsDark = ref(
window.matchMedia("(prefers-color-scheme: dark)").matches,
);
const isDarkTheme = computed(() => {
if (themeMode.value === "auto") return systemIsDark.value;
return themeMode.value === "dark";
});
const categories: Ref<Record<string, string>> = ref({});
const apps: Ref<App[]> = ref([]);
const activeCategory = ref("all");
@@ -218,18 +226,39 @@ const hasSelectedUpgrades = computed(() => {
});
// 方法
const syncThemePreference = (enabled: boolean) => {
document.documentElement.classList.toggle("dark", enabled);
const syncThemePreference = () => {
document.documentElement.classList.toggle("dark", isDarkTheme.value);
};
const initTheme = () => {
const savedTheme = localStorage.getItem("theme");
isDarkTheme.value = savedTheme === "dark";
syncThemePreference(isDarkTheme.value);
if (
savedTheme === "dark" ||
savedTheme === "light" ||
savedTheme === "auto"
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
themeMode.value = savedTheme as any;
} else {
themeMode.value = "auto";
}
window.ipcRenderer.send(
"set-theme-source",
themeMode.value === "auto" ? "system" : themeMode.value,
);
syncThemePreference();
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
systemIsDark.value = e.matches;
});
};
const toggleTheme = () => {
isDarkTheme.value = !isDarkTheme.value;
if (themeMode.value === "auto") themeMode.value = "light";
else if (themeMode.value === "light") themeMode.value = "dark";
else themeMode.value = "auto";
};
const selectCategory = (category: string) => {
@@ -736,8 +765,15 @@ onMounted(async () => {
});
// 观察器
watch(isDarkTheme, (newVal) => {
localStorage.setItem("theme", newVal ? "dark" : "light");
syncThemePreference(newVal);
watch(themeMode, (newVal) => {
localStorage.setItem("theme", newVal);
window.ipcRenderer.send(
"set-theme-source",
newVal === "auto" ? "system" : newVal,
);
});
watch(isDarkTheme, () => {
syncThemePreference();
});
</script>

View File

@@ -17,7 +17,7 @@
</div>
</div>
<ThemeToggle :is-dark="isDarkTheme" @toggle="toggleTheme" />
<ThemeToggle :theme-mode="themeMode" @toggle="toggleTheme" />
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted pr-2">
<button
@@ -72,7 +72,7 @@ defineProps<{
categories: Record<string, any>;
activeCategory: string;
categoryCounts: Record<string, number>;
isDarkTheme: boolean;
themeMode: "light" | "dark" | "auto";
}>();
const emit = defineEmits<{

View File

@@ -2,15 +2,12 @@
<button
type="button"
class="flex items-center justify-between rounded-2xl border border-slate-200/80 bg-white/70 px-4 py-3 text-sm font-medium text-slate-600 shadow-sm transition hover:border-brand/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-300"
:aria-pressed="isDark"
:aria-pressed="themeMode === 'dark'"
@click="toggle"
>
<span class="flex items-center gap-2">
<i
class="fas"
:class="isDark ? 'fa-moon text-amber-200' : 'fa-sun text-amber-400'"
></i>
<span>{{ isDark ? "深色主题" : "浅色主题" }}</span>
<i class="fas" :class="iconClass"></i>
<span>{{ label }}</span>
</span>
<span
class="relative inline-flex h-6 w-12 items-center rounded-full bg-slate-300/80 transition dark:bg-slate-700"
@@ -18,7 +15,7 @@
<span
:class="[
'inline-block h-4 w-4 rounded-full bg-white shadow transition',
isDark ? 'translate-x-6' : 'translate-x-1',
togglePosition,
]"
></span>
</span>
@@ -26,8 +23,10 @@
</template>
<script setup lang="ts">
defineProps<{
isDark: boolean;
import { computed } from "vue";
const props = defineProps<{
themeMode: "light" | "dark" | "auto";
}>();
const emit = defineEmits<{
@@ -37,4 +36,22 @@ const emit = defineEmits<{
const toggle = () => {
emit("toggle");
};
const label = computed(() => {
if (props.themeMode === "auto") return "跟随系统";
if (props.themeMode === "dark") return "深色主题";
return "浅色主题";
});
const iconClass = computed(() => {
if (props.themeMode === "auto") return "fa-adjust text-slate-500";
if (props.themeMode === "dark") return "fa-moon text-amber-200";
return "fa-sun text-amber-400";
});
const togglePosition = computed(() => {
if (props.themeMode === "auto") return "translate-x-4";
if (props.themeMode === "dark") return "translate-x-7";
return "translate-x-1";
});
</script>