add option to select dark theme for "System default" (#3325)
* add option to select dark theme for "System default" * add release notes * fix release notes * update visual tests
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 78 KiB |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
2
packages/loot-core/src/types/prefs.d.ts
vendored
@@ -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
|
||||
}>;
|
||||
|
||||
6
upcoming-release-notes/3325.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [tim-smart]
|
||||
---
|
||||
|
||||
Add setting to set preferred dark theme for "System default" mode
|
||||