From 6add72563e435b8564b0a2436d37a2027d78cd52 Mon Sep 17 00:00:00 2001 From: Anderson Shindy Oki Date: Thu, 30 Jul 2026 19:36:23 +0900 Subject: [PATCH] Persist sidebar theme switch (#3476) --- frontend/src/App/Navbar.tsx | 83 ++++++++++++++++++++++++++----- frontend/src/apis/hooks/system.ts | 11 ++-- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/frontend/src/App/Navbar.tsx b/frontend/src/App/Navbar.tsx index fb849c5b8..4843057e3 100644 --- a/frontend/src/App/Navbar.tsx +++ b/frontend/src/App/Navbar.tsx @@ -14,19 +14,23 @@ import { Collapse, Divider, Group, + MantineColorScheme, Stack, Text, - useComputedColorScheme, useMantineColorScheme, } from "@mantine/core"; import { + faCircleHalfStroke, faHeart, faMoon, faSun, IconDefinition, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQueryClient } from "@tanstack/react-query"; import clsx from "clsx"; +import { useSettingsMutation, useSystemSettings } from "@/apis/hooks"; +import { QueryKeys } from "@/apis/queries/keys"; import { Action } from "@/components"; import { useNavbar } from "@/contexts/Navbar"; import { useRouteItems } from "@/Router"; @@ -96,14 +100,74 @@ function useIsActive(parent: string, route: RouteObject) { ); } +const themeCycle: { + scheme: MantineColorScheme; + icon: IconDefinition; + label: string; + color: string; +}[] = [ + { scheme: "auto", icon: faCircleHalfStroke, label: "Auto", color: "brand" }, + { scheme: "light", icon: faSun, label: "Light", color: "warning" }, + { scheme: "dark", icon: faMoon, label: "Dark", color: "info" }, +]; + +const ThemeSwitcher: FunctionComponent = () => { + const { setColorScheme } = useMantineColorScheme(); + + const client = useQueryClient(); + const settings = useSystemSettings(); + const { mutate } = useSettingsMutation({ silent: true }); + + const current = (settings.data?.general.theme ?? + "auto") as MantineColorScheme; + + const index = Math.max( + 0, + themeCycle.findIndex((t) => t.scheme === current), + ); + const active = themeCycle[index]; + + const cycle = () => { + const next = themeCycle[(index + 1) % themeCycle.length]; + + // Apply immediately for instant feedback. + setColorScheme(next.scheme); + + // Optimistically update the cached settings so everything reading them + // (this button, ThemeLoader, Settings/UI) reflects the change instantly. + const queryKey = [QueryKeys.System, QueryKeys.Settings]; + const previous = client.getQueryData(queryKey); + + client.setQueryData(queryKey, (old) => + old ? { ...old, general: { ...old.general, theme: next.scheme } } : old, + ); + + // Persist through the same settings system as Settings/UI, in the + // background; roll back if the save fails. + mutate( + { "settings-general-theme": next.scheme }, + { + onError: () => { + client.setQueryData(queryKey, previous); + setColorScheme(current); + }, + }, + ); + }; + + return ( + + ); +}; + const AppNavbar: FunctionComponent = () => { const [selection, select] = useState(null); - const { toggleColorScheme } = useMantineColorScheme(); - const computedColorScheme = useComputedColorScheme("light"); - - const dark = computedColorScheme === "dark"; - const routes = useRouteItems(); const { pathname } = useLocation(); @@ -131,12 +195,7 @@ const AppNavbar: FunctionComponent = () => { - toggleColorScheme()} - icon={dark ? faSun : faMoon} - > + ) => @@ -99,9 +100,11 @@ export function useSettingsMutation() { queryKey: [QueryKeys.Plex, "libraries"], }); - showNotification( - notification.info("Settings saved", "Your changes have been saved"), - ); + if (!silent) { + showNotification( + notification.info("Settings saved", "Your changes have been saved"), + ); + } }, onError: () => {