diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png index 0f3a4799e2d23fdbd4687a4f69ca7e8782e39191..dc4e3df218ad765fa0fa95e761b317e924e49755 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-5-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-5-chromium-linux.png index a42d198be8565c99ece0d0a69d63af4e9ea7f0eb..743dfab446179342230d037db28decc215420a12 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-5-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-6-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-6-chromium-linux.png index 62c2010fe89f3e470e7f9600b6314c75003b7b95..b11bd064aac544187aed626877a278f9ca3198e7 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-6-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-6-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png index e3724cb79825609ad01def74a2ce084ea9d6b3df..994e5cd4cdc012bdc4c708fd6d4c64a52d32504f 100644 Binary files a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png index e64c9a6a143ee7290b6dad5562de98ac72f7c3b8..e93644663c19e7b693f38d1c6db645f6f7c528ee 100644 Binary files a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png index 2986a14ddad72eedbe797c7d46ebf95cdb21d959..5a326d4d6841e16d169bcfb0c6e82a6b6293e71c 100644 Binary files a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/settings/Themes.tsx b/packages/desktop-client/src/components/settings/Themes.tsx index 44e713ae53f3b6c8e0decfcb2d50ba12cde55840..10fff9480f041ef4c9b9016831cd6d4014841ac7 100644 --- a/packages/desktop-client/src/components/settings/Themes.tsx +++ b/packages/desktop-client/src/components/settings/Themes.tsx @@ -1,31 +1,91 @@ -import React from 'react'; +import React, { type ReactNode } from 'react'; -import { type Theme } from 'loot-core/types/prefs'; +import { type DarkTheme, type Theme } from 'loot-core/types/prefs'; -import { themeOptions, useTheme, theme as themeStyle } from '../../style'; +import { + themeOptions, + useTheme, + theme as themeStyle, + usePreferredDarkTheme, + darkThemeOptions, +} from '../../style'; +import { tokens } from '../../tokens'; import { Select } from '../common/Select'; import { Text } from '../common/Text'; +import { View } from '../common/View'; +import { useSidebar } from '../sidebar/SidebarProvider'; import { Setting } from './UI'; +function Column({ title, children }: { title: string; children: ReactNode }) { + return ( + <View + style={{ + alignItems: 'flex-start', + flexGrow: 1, + gap: '0.5em', + width: '100%', + }} + > + <Text style={{ fontWeight: 500 }}>{title}</Text> + <View style={{ alignItems: 'flex-start', gap: '1em' }}>{children}</View> + </View> + ); +} + export function ThemeSettings() { + const sidebar = useSidebar(); const [theme, switchTheme] = useTheme(); + const [darkTheme, switchDarkTheme] = usePreferredDarkTheme(); return ( <Setting primaryAction={ - <Select<Theme> - onChange={value => { - switchTheme(value); - }} - value={theme} - options={themeOptions} - buttonStyle={{ - ':hover': { - backgroundColor: themeStyle.buttonNormalBackgroundHover, + <View + style={{ + flexDirection: 'column', + gap: '1em', + width: '100%', + [`@media (min-width: ${ + sidebar.floating + ? tokens.breakpoint_small + : tokens.breakpoint_medium + })`]: { + flexDirection: 'row', }, }} - /> + > + <Column title="Theme"> + <Select<Theme> + onChange={value => { + switchTheme(value); + }} + value={theme} + options={themeOptions} + buttonStyle={{ + ':hover': { + backgroundColor: themeStyle.buttonNormalBackgroundHover, + }, + }} + /> + </Column> + {theme === 'auto' && ( + <Column title="Dark theme"> + <Select<DarkTheme> + onChange={value => { + switchDarkTheme(value); + }} + value={darkTheme} + options={darkThemeOptions} + buttonStyle={{ + ':hover': { + backgroundColor: themeStyle.buttonNormalBackgroundHover, + }, + }} + /> + </Column> + )} + </View> } > <Text> diff --git a/packages/desktop-client/src/style/theme.tsx b/packages/desktop-client/src/style/theme.tsx index 96a7e95950945c542dbdabbc269ee19321a5a99a..09e17c481738741da57a52a7f98184d0f0e9da93 100644 --- a/packages/desktop-client/src/style/theme.tsx +++ b/packages/desktop-client/src/style/theme.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { isNonProductionEnvironment } from 'loot-core/src/shared/environment'; -import type { Theme } from 'loot-core/src/types/prefs'; +import type { DarkTheme, Theme } from 'loot-core/src/types/prefs'; import { useGlobalPref } from '../hooks/useGlobalPref'; @@ -25,13 +25,25 @@ export const themeOptions = Object.entries(themes).map( ([key, { name }]) => [key, name] as [Theme, string], ); +export const darkThemeOptions = Object.entries({ + dark: themes.dark, + midnight: themes.midnight, +}).map(([key, { name }]) => [key, name] as [DarkTheme, string]); + export function useTheme() { const [theme = 'light', setThemePref] = useGlobalPref('theme'); return [theme, setThemePref] as const; } +export function usePreferredDarkTheme() { + const [darkTheme = 'dark', setDarkTheme] = + useGlobalPref('preferredDarkTheme'); + return [darkTheme, setDarkTheme] as const; +} + export function ThemeStyle() { const [theme] = useTheme(); + const [darkThemePreference] = usePreferredDarkTheme(); const [themeColors, setThemeColors] = useState< | typeof lightTheme | typeof darkTheme @@ -42,9 +54,11 @@ export function ThemeStyle() { useEffect(() => { if (theme === 'auto') { + const darkTheme = themes[darkThemePreference]; + function darkThemeMediaQueryListener(event: MediaQueryListEvent) { if (event.matches) { - setThemeColors(themes['dark'].colors); + setThemeColors(darkTheme.colors); } else { setThemeColors(themes['light'].colors); } @@ -59,7 +73,7 @@ export function ThemeStyle() { ); if (darkThemeMediaQuery.matches) { - setThemeColors(themes['dark'].colors); + setThemeColors(darkTheme.colors); } else { setThemeColors(themes['light'].colors); } @@ -73,7 +87,7 @@ export function ThemeStyle() { } else { setThemeColors(themes[theme].colors); } - }, [theme]); + }, [theme, darkThemePreference]); if (!themeColors) return null; diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 0a84bdbaf96b4fcd1604b0ad0848c8eefb401591..c8dcd9f687455d51d2107a5536e58ce21d254734 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -1256,6 +1256,12 @@ handlers['save-global-prefs'] = async function (prefs) { if ('theme' in prefs) { await asyncStorage.setItem('theme', prefs.theme); } + if ('preferredDarkTheme' in prefs) { + await asyncStorage.setItem( + 'preferred-dark-theme', + prefs.preferredDarkTheme, + ); + } if ('serverSelfSignedCert' in prefs) { await asyncStorage.setItem( 'server-self-signed-cert', @@ -1272,12 +1278,14 @@ handlers['load-global-prefs'] = async function () { [, documentDir], [, encryptKey], [, theme], + [, preferredDarkTheme], ] = await asyncStorage.multiGet([ 'floating-sidebar', 'max-months', 'document-dir', 'encrypt-key', 'theme', + 'preferred-dark-theme', ]); return { floatingSidebar: floatingSidebar === 'true' ? true : false, @@ -1292,6 +1300,10 @@ handlers['load-global-prefs'] = async function () { theme === 'midnight' ? theme : 'auto', + preferredDarkTheme: + preferredDarkTheme === 'dark' || preferredDarkTheme === 'midnight' + ? preferredDarkTheme + : 'dark', }; }; diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index a9dd5235cdb192fee3d2d23cb8d928017ce837b3..839dd625b5258ae45350aa2178dbd2787af7e736 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -83,11 +83,13 @@ export type LocalPrefs = SyncedPrefs & }>; export type Theme = 'light' | 'dark' | 'auto' | 'midnight' | 'development'; +export type DarkTheme = 'dark' | 'midnight'; export type GlobalPrefs = Partial<{ floatingSidebar: boolean; maxMonths: number; keyId?: string; theme: Theme; + preferredDarkTheme: DarkTheme; documentDir: string; // Electron only serverSelfSignedCert: string; // Electron only }>; diff --git a/upcoming-release-notes/3325.md b/upcoming-release-notes/3325.md new file mode 100644 index 0000000000000000000000000000000000000000..a926a72420868c1d072083b5c5c1afc971952b25 --- /dev/null +++ b/upcoming-release-notes/3325.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [tim-smart] +--- + +Add setting to set preferred dark theme for "System default" mode