feat: Add System Theme Support, Yin & Yang Theme, and Adaptive Plugin Badge Color

Closes #437
Closes #1374
Closes #1578
This commit is contained in:
Rajarshee Chatterjee
2025-10-21 19:02:08 +05:30
parent 5a6f324638
commit ee7d96daf9
13 changed files with 597 additions and 198 deletions
@@ -0,0 +1,132 @@
import React from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
import { ThemeColors } from '@theme/types';
export interface SegmentedControlOption<T extends string = string> {
value: T;
label: string;
icon?: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
}
export interface SegmentedControlProps<T extends string = string> {
options: SegmentedControlOption<T>[];
value: T;
onChange: (value: T) => void;
theme: ThemeColors;
showCheckIcon?: boolean;
}
export function SegmentedControl<T extends string = string>({
options,
value,
onChange,
theme,
showCheckIcon = true,
}: SegmentedControlProps<T>) {
return (
<View style={styles.container}>
{options.map((option, index) => {
const isSelected = value === option.value;
const isFirst = index === 0;
const isLast = index === options.length - 1;
const buttonStyles = [
styles.segment,
isFirst && styles.segmentFirst,
isLast && styles.segmentLast,
!isFirst && !isLast && styles.segmentMiddle,
{
backgroundColor: isSelected
? theme.secondaryContainer
: 'transparent',
borderColor: theme.outline,
},
];
const textColor = isSelected
? theme.onSecondaryContainer
: theme.onSurface;
return (
<View key={option.value} style={buttonStyles}>
<Pressable
style={styles.segmentPressable}
onPress={() => onChange(option.value)}
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
>
{showCheckIcon && isSelected && (
<MaterialCommunityIcons
name="check"
size={18}
color={textColor}
style={styles.checkIcon}
/>
)}
{option.icon && !isSelected && (
<MaterialCommunityIcons
name={option.icon}
size={18}
color={textColor}
style={styles.icon}
/>
)}
<Text style={[styles.segmentText, { color: textColor }]}>
{option.label}
</Text>
</Pressable>
</View>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
height: 48,
},
segment: {
flex: 1,
borderTopWidth: 1,
borderBottomWidth: 1,
overflow: 'hidden',
},
segmentFirst: {
borderLeftWidth: 1,
borderRightWidth: 1,
borderTopLeftRadius: 24,
borderBottomLeftRadius: 24,
},
segmentMiddle: {
borderRightWidth: 1,
},
segmentLast: {
borderRightWidth: 1,
borderTopRightRadius: 24,
borderBottomRightRadius: 24,
},
segmentPressable: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
},
segmentText: {
fontSize: 14,
fontWeight: '500',
letterSpacing: 0.1,
},
checkIcon: {
marginRight: 8,
},
icon: {
marginRight: 8,
},
});
+5
View File
@@ -0,0 +1,5 @@
export { SegmentedControl } from './SegmentedControl';
export type {
SegmentedControlOption,
SegmentedControlProps,
} from './SegmentedControl';
+1
View File
@@ -15,3 +15,4 @@ export { RadioButton } from './RadioButton/RadioButton';
export { default as ConfirmationDialog } from './ConfirmationDialog/ConfirmationDialog';
export { default as SafeAreaView } from './SafeAreaView/SafeAreaView';
export { default as Modal } from './Modal/Modal';
export { SegmentedControl } from './SegmentedControl';
+93 -35
View File
@@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { Appearance } from 'react-native';
import { useEffect, useMemo, useState } from 'react';
import { Appearance, ColorSchemeName } from 'react-native';
import {
useMMKVBoolean,
useMMKVObject,
useMMKVNumber,
useMMKVString,
} from 'react-native-mmkv';
import { overlay } from 'react-native-paper';
@@ -10,52 +10,110 @@ import Color from 'color';
import { defaultTheme } from '@theme/md3/defaultTheme';
import { ThemeColors } from '@theme/types';
import { darkThemes, lightThemes } from '@theme/md3';
const getElevationColor = (colors: ThemeColors, elevation: number) => {
const getElevationColor = (colors: ThemeColors, elevation: number): string => {
return Color(colors.surface)
.mix(Color(colors.primary), elevation)
.rgb()
.string();
};
const addComputedColors = (colors: ThemeColors): ThemeColors => ({
...colors,
surface2: getElevationColor(colors, 0.08),
overlay3: overlay(3, colors.surface),
rippleColor: Color(colors.primary).alpha(0.12).toString(),
surfaceReader: Color(colors.surface).alpha(0.9).toString(),
});
const applyAmoledBlack = (
colors: ThemeColors,
isEnabled: boolean,
): ThemeColors => {
if (!isEnabled || !colors.isDark) {
return colors;
}
return {
...colors,
background: '#000000',
surface: '#000000',
};
};
const applyCustomAccent = (
colors: ThemeColors,
accentColor?: string,
): ThemeColors => {
if (!accentColor) {
return colors;
}
return {
...colors,
primary: accentColor,
secondary: accentColor,
};
};
const findThemeById = (
themeId: number | undefined,
isDark: boolean,
): ThemeColors => {
const themeList = isDark ? darkThemes : lightThemes;
if (themeId !== undefined) {
const theme = themeList.find(t => t.id === themeId);
if (theme) {
return theme;
}
}
return isDark ? defaultTheme.dark : defaultTheme.light;
};
const getBaseTheme = (
themeMode: string,
themeId: number | undefined,
systemColorScheme: ColorSchemeName,
): ThemeColors => {
if (themeMode === 'system') {
const shouldUseDarkTheme = systemColorScheme === 'dark';
return findThemeById(themeId, shouldUseDarkTheme);
}
const isDark = themeMode === 'dark';
return findThemeById(themeId, isDark);
};
export const useTheme = (): ThemeColors => {
const [appTheme] = useMMKVObject<ThemeColors>('APP_THEME');
const [isAmoledBlack] = useMMKVBoolean('AMOLED_BLACK');
const [themeId] = useMMKVNumber('APP_THEME_ID');
const [themeMode = 'system'] = useMMKVString('THEME_MODE');
const [isAmoledBlack = false] = useMMKVBoolean('AMOLED_BLACK');
const [customAccent] = useMMKVString('CUSTOM_ACCENT_COLOR');
const theme: ThemeColors = useMemo(() => {
const isDeviveColorSchemeDark = Appearance.getColorScheme() === 'dark';
const [systemColorScheme, setSystemColorScheme] = useState<ColorSchemeName>(
Appearance.getColorScheme(),
);
let colors: ThemeColors =
appTheme ||
(isDeviveColorSchemeDark ? defaultTheme.dark : defaultTheme.light);
useEffect(() => {
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
setSystemColorScheme(colorScheme);
});
if (isAmoledBlack && colors.isDark) {
colors = {
...colors,
background: '#000000',
surface: '#000000',
};
}
return () => subscription.remove();
}, []);
if (customAccent) {
colors = {
...colors,
primary: customAccent,
secondary: customAccent,
};
}
const theme = useMemo(() => {
const baseTheme = getBaseTheme(themeMode, themeId, systemColorScheme);
const withAmoled = applyAmoledBlack(baseTheme, isAmoledBlack);
const withAccent = applyCustomAccent(withAmoled, customAccent);
const finalTheme = addComputedColors(withAccent);
colors = {
...colors,
surface2: getElevationColor(colors, 0.08),
overlay3: overlay(3, colors.surface),
rippleColor: Color(colors.primary).alpha(0.12).toString(),
surfaceReader: Color(colors.surface).alpha(0.9).toString(),
};
return colors;
}, [appTheme, isAmoledBlack, customAccent]);
return finalTheme;
}, [themeId, themeMode, systemColorScheme, isAmoledBlack, customAccent]);
return theme;
};
+4
View File
@@ -118,6 +118,10 @@ const BottomNavigator = () => {
headerShown: false,
animation: 'shift',
lazy: true,
tabBarBadgeStyle: {
backgroundColor: theme.error,
color: theme.onError,
},
})}
tabBar={renderBottomBar}
>
+4 -4
View File
@@ -3,7 +3,7 @@ import { Text } from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Image, StyleSheet, View } from 'react-native';
import { Button } from '@components';
import PickThemeStep from './PickThemeStep';
import ThemeSelectionStep from './ThemeSelectionStep';
import { useState } from 'react';
import { MMKVStorage } from '@utils/mmkv/mmkv';
import { getString } from '@strings/translations';
@@ -19,9 +19,9 @@ export default function OnboardingScreen() {
const renderStep = () => {
switch (step) {
case OnboardingStep.PICK_THEME:
return <PickThemeStep />;
return <ThemeSelectionStep />;
default:
return <PickThemeStep />;
return <ThemeSelectionStep />;
}
};
const renderHelptext = () => {
@@ -29,7 +29,7 @@ export default function OnboardingScreen() {
case OnboardingStep.PICK_THEME:
return getString('onboardingScreen.pickATheme');
default:
return <PickThemeStep />;
return getString('onboardingScreen.pickATheme');
}
};
-145
View File
@@ -1,145 +0,0 @@
import React from 'react';
import { View, Text, Pressable, StyleSheet, FlatList } from 'react-native';
import { ThemePicker } from '@components/ThemePicker/ThemePicker';
import { ThemeColors } from '@theme/types';
import { useMMKVObject } from 'react-native-mmkv';
import { useTheme } from '@hooks/persisted';
import { darkThemes, lightThemes } from '@theme/md3';
import { getString } from '@strings/translations';
const ThemeList = ({
theme,
list,
}: {
theme: ThemeColors;
list: ThemeColors[];
}) => {
const [, setTheme] = useMMKVObject('APP_THEME');
return (
<FlatList
contentContainerStyle={styles.flatListContainer}
data={list}
renderItem={({ item }) => (
<ThemePicker
key={item.id}
currentTheme={theme}
theme={item}
onPress={() => {
setTheme(item);
}}
/>
)}
horizontal={false}
numColumns={3}
/>
);
};
const SchemeButton = ({
onPress,
isDarkButton = false,
}: {
onPress: () => void;
isDarkButton?: boolean;
}) => {
const theme = useTheme();
const isSelected =
(theme.isDark && isDarkButton) || (!theme.isDark && !isDarkButton);
return (
<View
style={[
styles.schemeButtonWrapper,
isDarkButton ? styles.darkButton : styles.lightButton,
{
borderColor: theme.onSurface,
backgroundColor: isSelected ? theme.primary : theme.surface,
},
]}
>
<Pressable
android_ripple={{ color: theme.rippleColor, borderless: true }}
style={styles.schemeButton}
onPress={onPress}
>
<Text style={{ color: isSelected ? theme.onPrimary : theme.onSurface }}>
{isDarkButton
? getString('onboardingScreen.dark')
: getString('onboardingScreen.light')}
</Text>
</Pressable>
</View>
);
};
const getInverseTheme = (theme: ThemeColors) => {
let index = (theme.isDark ? darkThemes : lightThemes).findIndex(
item => item.id === theme.id,
);
if (index === -1) {
index = 1;
}
if (theme.isDark) {
return lightThemes[index];
}
return darkThemes[index];
};
export default function PickThemeStep() {
const theme = useTheme();
const [, setTheme] = useMMKVObject('APP_THEME');
return (
<View style={styles.flex}>
<View style={styles.buttonContainer}>
<SchemeButton
onPress={() => {
if (theme.isDark) {
setTheme(getInverseTheme(theme));
}
}}
/>
<SchemeButton
onPress={() => {
if (!theme.isDark) {
setTheme(getInverseTheme(theme));
}
}}
isDarkButton={true}
/>
</View>
<ThemeList theme={theme} list={theme.isDark ? darkThemes : lightThemes} />
</View>
);
}
const styles = StyleSheet.create({
buttonContainer: {
flexDirection: 'row',
justifyContent: 'center',
paddingBottom: 16,
},
darkButton: {
borderBottomRightRadius: 24,
borderRightWidth: 1,
borderTopRightRadius: 24,
},
lightButton: {
borderBottomLeftRadius: 24,
borderLeftWidth: 1,
borderRightWidth: 1,
borderTopLeftRadius: 24,
},
schemeButton: {
paddingHorizontal: 36,
paddingVertical: 8,
},
schemeButtonWrapper: {
borderBottomWidth: 1,
borderTopWidth: 1,
},
flatListContainer: {
margin: 'auto',
paddingHorizontal: 8,
paddingBottom: 8,
},
flex: { flex: 1 },
});
@@ -0,0 +1,191 @@
import React, { useMemo } from 'react';
import { View, Text, Pressable, StyleSheet, ScrollView } from 'react-native';
import {
useMMKVBoolean,
useMMKVNumber,
useMMKVString,
} from 'react-native-mmkv';
import { SegmentedControl } from '@components';
import type { SegmentedControlOption } from '@components/SegmentedControl';
import { ThemePicker } from '@components/ThemePicker/ThemePicker';
import { ThemeColors } from '@theme/types';
import { useTheme } from '@hooks/persisted';
import { darkThemes, lightThemes } from '@theme/md3';
import { getString } from '@strings/translations';
type ThemeMode = 'light' | 'dark' | 'system';
interface AmoledToggleProps {
theme: ThemeColors;
}
const AmoledToggle: React.FC<AmoledToggleProps> = ({ theme }) => {
const [isAmoledBlack = false, setAmoledBlack] =
useMMKVBoolean('AMOLED_BLACK');
if (!theme.isDark) {
return null;
}
return (
<View style={styles.amoledContainer}>
<Text style={[styles.amoledLabel, { color: theme.onSurface }]}>
{getString('appearanceScreen.pureBlackDarkMode')}
</Text>
<Pressable
onPress={() => setAmoledBlack(!isAmoledBlack)}
style={[
styles.toggle,
{
backgroundColor: isAmoledBlack
? theme.primary
: theme.surfaceVariant,
},
]}
>
<View
style={[
styles.toggleThumb,
isAmoledBlack && styles.toggleThumbActive,
{
backgroundColor: isAmoledBlack
? theme.onPrimary
: theme.onSurfaceVariant,
},
]}
/>
</Pressable>
</View>
);
};
export default function ThemeSelectionStep() {
const theme = useTheme();
const [themeMode = 'system', setThemeMode] = useMMKVString('THEME_MODE');
const [, setThemeId] = useMMKVNumber('APP_THEME_ID');
const currentMode = themeMode as ThemeMode;
const availableThemes = useMemo(() => {
return theme.isDark ? darkThemes : lightThemes;
}, [theme.isDark]);
const themeModeOptions: SegmentedControlOption<ThemeMode>[] = useMemo(
() => [
{
value: 'system',
label: getString('onboardingScreen.system'),
},
{
value: 'light',
label: getString('onboardingScreen.light'),
},
{
value: 'dark',
label: getString('onboardingScreen.dark'),
},
],
[],
);
const handleModeChange = (mode: ThemeMode) => {
setThemeMode(mode);
if (mode !== 'system') {
const themes = mode === 'dark' ? darkThemes : lightThemes;
const currentThemeInMode = themes.find(t => t.id === theme.id);
if (!currentThemeInMode) {
setThemeId(themes[0].id);
}
}
};
const handleThemeSelect = (selectedTheme: ThemeColors) => {
setThemeId(selectedTheme.id);
setThemeMode(selectedTheme.isDark ? 'dark' : 'light');
};
return (
<View style={styles.container}>
{/* Segmented Control */}
<View style={styles.segmentedControlContainer}>
<SegmentedControl
options={themeModeOptions}
value={currentMode}
onChange={handleModeChange}
theme={theme}
/>
</View>
{/* Theme List */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.themeScrollContent}
>
{availableThemes.map(item => (
<View key={item.id} style={styles.themeItem}>
<ThemePicker
currentTheme={theme}
theme={item}
onPress={() => handleThemeSelect(item)}
/>
</View>
))}
</ScrollView>
{/* AMOLED Toggle */}
<AmoledToggle theme={theme} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
},
segmentedControlContainer: {
marginBottom: 24,
},
themeScrollContent: {
paddingVertical: 16,
paddingHorizontal: 24,
},
themeItem: {
marginHorizontal: 8,
},
amoledContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 4,
marginTop: 'auto',
},
amoledLabel: {
fontSize: 16,
fontWeight: '400',
},
toggle: {
width: 52,
height: 32,
borderRadius: 16,
padding: 2,
justifyContent: 'center',
},
toggleThumb: {
width: 28,
height: 28,
borderRadius: 14,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
},
toggleThumbActive: {
alignSelf: 'flex-end',
},
});
@@ -1,24 +1,29 @@
import React, { useState } from 'react';
import { ScrollView, Text, StyleSheet } from 'react-native';
import React, { useMemo, useState } from 'react';
import { ScrollView, Text, StyleSheet, View } from 'react-native';
import { ThemePicker } from '@components/ThemePicker/ThemePicker';
import type { SegmentedControlOption } from '@components/SegmentedControl';
import SettingSwitch from './components/SettingSwitch';
import ColorPickerModal from '@components/ColorPickerModal/ColorPickerModal';
import { useAppSettings, useTheme } from '@hooks/persisted';
import {
useMMKVBoolean,
useMMKVObject,
useMMKVNumber,
useMMKVString,
} from 'react-native-mmkv';
import { Appbar, List, SafeAreaView } from '@components';
import { Appbar, List, SafeAreaView, SegmentedControl } from '@components';
import { AppearanceSettingsScreenProps } from '@navigators/types';
import { getString } from '@strings/translations';
import { darkThemes, lightThemes } from '@theme/md3';
import { ThemeColors } from '@theme/types';
type ThemeMode = 'light' | 'dark' | 'system';
const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
const theme = useTheme();
const [, setTheme] = useMMKVObject('APP_THEME');
const [, setThemeId] = useMMKVNumber('APP_THEME_ID');
const [themeMode = 'system', setThemeMode] = useMMKVString('THEME_MODE');
const [isAmoledBlack = false, setAmoledBlack] =
useMMKVBoolean('AMOLED_BLACK');
const [, setCustomAccentColor] = useMMKVString('CUSTOM_ACCENT_COLOR');
@@ -32,6 +37,8 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
setAppSettings,
} = useAppSettings();
const currentMode = themeMode as ThemeMode;
/**
* Accent Color Modal
*/
@@ -39,6 +46,46 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
const themeModeOptions: SegmentedControlOption<ThemeMode>[] = useMemo(
() => [
{
value: 'system',
label: getString('appearanceScreen.themeModeSystem'),
},
{
value: 'light',
label: getString('appearanceScreen.themeModeLight'),
},
{
value: 'dark',
label: getString('appearanceScreen.themeModeDark'),
},
],
[],
);
const handleModeChange = (mode: ThemeMode) => {
setThemeMode(mode);
if (mode !== 'system') {
const themes = mode === 'dark' ? darkThemes : lightThemes;
const currentThemeInMode = themes.find(t => t.id === theme.id);
if (!currentThemeInMode) {
setThemeId(themes[0].id);
}
}
};
const handleThemeSelect = (selectedTheme: ThemeColors) => {
setThemeId(selectedTheme.id);
setCustomAccentColor(undefined);
if (currentMode !== 'system') {
setThemeMode(selectedTheme.isDark ? 'dark' : 'light');
}
};
return (
<SafeAreaView excludeTop>
<Appbar
@@ -54,6 +101,18 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
<List.SubHeader theme={theme}>
{getString('appearanceScreen.appTheme')}
</List.SubHeader>
{/* Theme Mode Selector */}
<View style={styles.segmentedControlContainer}>
<SegmentedControl
options={themeModeOptions}
value={currentMode}
onChange={handleModeChange}
theme={theme}
/>
</View>
{/* Light Themes */}
<Text style={[{ color: theme.onSurface }, styles.themeSectionText]}>
{getString('appearanceScreen.lightTheme')}
</Text>
@@ -68,13 +127,12 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
key={item.id}
currentTheme={theme}
theme={item}
onPress={() => {
setTheme(item);
setCustomAccentColor(undefined);
}}
onPress={() => handleThemeSelect(item)}
/>
))}
</ScrollView>
{/* Dark Themes */}
<Text style={[{ color: theme.onSurface }, styles.themeSectionText]}>
{getString('appearanceScreen.darkTheme')}
</Text>
@@ -89,13 +147,11 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
key={item.id}
currentTheme={theme}
theme={item}
onPress={() => {
setTheme(item);
setCustomAccentColor(undefined);
}}
onPress={() => handleThemeSelect(item)}
/>
))}
</ScrollView>
{theme.isDark ? (
<SettingSwitch
label={getString('appearanceScreen.pureBlackDarkMode')}
@@ -188,4 +244,8 @@ const styles = StyleSheet.create({
paddingVertical: 8,
flexDirection: 'row',
},
segmentedControlContainer: {
paddingHorizontal: 16,
paddingVertical: 12,
},
});
+3
View File
@@ -6,6 +6,7 @@ import { lavenderTheme } from './lavender';
import { strawberryDaiquiriTheme } from './strawberry';
import { takoTheme } from './tako';
import { catppuccinTheme } from './catppuccin';
import { yinyangTheme } from './yinyang';
export const lightThemes = [
defaultTheme.light,
@@ -16,6 +17,7 @@ export const lightThemes = [
strawberryDaiquiriTheme.light,
takoTheme.light,
catppuccinTheme.light,
yinyangTheme.light,
];
export const darkThemes = [
defaultTheme.dark,
@@ -26,4 +28,5 @@ export const darkThemes = [
strawberryDaiquiriTheme.dark,
takoTheme.dark,
catppuccinTheme.dark,
yinyangTheme.dark,
];
+78
View File
@@ -0,0 +1,78 @@
import { getString } from '@strings/translations';
export const yinyangTheme = {
light: {
id: 9,
name: getString('appearanceScreen.theme.yinyang'),
isDark: false,
primary: '#000000',
onPrimary: '#FFFFFF',
primaryContainer: '#000000',
onPrimaryContainer: '#FFFFFF',
secondary: '#000000',
onSecondary: '#FFFFFF',
secondaryContainer: '#DDDDDD',
onSecondaryContainer: '#0C0C0C',
tertiary: '#FFFFFF',
onTertiary: '#000000',
tertiaryContainer: '#D8E2FF',
onTertiaryContainer: '#001947',
error: '#BA1A1A',
onError: '#FFFFFF',
errorContainer: '#FFDAD6',
onErrorContainer: '#410002',
background: '#FDFDFD',
onBackground: '#222222',
surface: '#FDFDFD',
onSurface: '#222222',
surfaceVariant: '#E8E8E8',
onSurfaceVariant: '#515151',
outline: '#838383',
outlineVariant: '#CFCFCF',
shadow: '#000000',
scrim: '#000000',
inverseSurface: '#333333',
inverseOnSurface: '#F4F4F4',
inversePrimary: '#A6A6A6',
surfaceDisabled: 'rgba(34, 34, 34, 0.12)',
onSurfaceDisabled: 'rgba(34, 34, 34, 0.38)',
backdrop: 'rgba(0, 0, 0, 0.4)',
},
dark: {
id: 10,
name: getString('appearanceScreen.theme.yinyang'),
isDark: true,
primary: '#FFFFFF',
onPrimary: '#5A5A5A',
primaryContainer: '#FFFFFF',
onPrimaryContainer: '#000000',
secondary: '#FFFFFF',
onSecondary: '#5A5A5A',
secondaryContainer: '#717171',
onSecondaryContainer: '#E4E4E4',
tertiary: '#000000',
onTertiary: '#FFFFFF',
tertiaryContainer: '#00419E',
onTertiaryContainer: '#D8E2FF',
error: '#FFB4AB',
onError: '#690005',
errorContainer: '#93000A',
onErrorContainer: '#FFB4AB',
background: '#1E1E1E',
onBackground: '#E6E6E6',
surface: '#1E1E1E',
onSurface: '#E6E6E6',
surfaceVariant: '#313131',
onSurfaceVariant: '#D1D1D1',
outline: '#999999',
outlineVariant: '#2A2A2A',
shadow: '#000000',
scrim: '#000000',
inverseSurface: '#E6E6E6',
inverseOnSurface: '#1E1E1E',
inversePrimary: '#CECECE',
surfaceDisabled: 'rgba(230, 230, 230, 0.12)',
onSurfaceDisabled: 'rgba(230, 230, 230, 0.38)',
backdrop: 'rgba(255, 255, 255, 0.4)',
},
};
+7 -1
View File
@@ -46,6 +46,10 @@
"pureBlackDarkMode": "Pure black dark mode",
"showHistoryInTheNav": "Show history in the nav",
"showUpdatesInTheNav": "Show updates in the nav",
"themeMode": "Theme mode",
"themeModeLight": "Light",
"themeModeDark": "Dark",
"themeModeSystem": "System",
"theme": {
"default": "Default",
"lavender": "Lavender",
@@ -56,7 +60,8 @@
"teal": "Teal",
"turquoise": "Turquoise",
"yotsuba": "Yotsuba",
"catppuccin": "Catppuccin"
"catppuccin": "Catppuccin",
"yinyang": "Yin & Yang"
}
},
"backupScreen": {
@@ -510,6 +515,7 @@
"pickATheme": "Pick a theme",
"light": "Light",
"dark": "Dark",
"system": "System",
"complete": "Complete"
},
"notifications": {
+6
View File
@@ -45,6 +45,10 @@ export interface StringMap {
'appearanceScreen.pureBlackDarkMode': 'string';
'appearanceScreen.showHistoryInTheNav': 'string';
'appearanceScreen.showUpdatesInTheNav': 'string';
'appearanceScreen.themeMode': 'string';
'appearanceScreen.themeModeLight': 'string';
'appearanceScreen.themeModeDark': 'string';
'appearanceScreen.themeModeSystem': 'string';
'appearanceScreen.theme.default': 'string';
'appearanceScreen.theme.lavender': 'string';
'appearanceScreen.theme.midnightDusk': 'string';
@@ -55,6 +59,7 @@ export interface StringMap {
'appearanceScreen.theme.turquoise': 'string';
'appearanceScreen.theme.yotsuba': 'string';
'appearanceScreen.theme.catppuccin': 'string';
'appearanceScreen.theme.yinyang': 'string';
'backupScreen.backupName': 'string';
'backupScreen.createBackup': 'string';
'backupScreen.createBackupDesc': 'string';
@@ -415,6 +420,7 @@ export interface StringMap {
'onboardingScreen.pickATheme': 'string';
'onboardingScreen.light': 'string';
'onboardingScreen.dark': 'string';
'onboardingScreen.system': 'string';
'onboardingScreen.complete': 'string';
'notifications.IMPORT_EPUB': 'string';
'notifications.UPDATE_LIBRARY': 'string';