Add ability to set app theme

Closes #79
This commit is contained in:
Brandon Presley 2022-10-04 14:35:56 +13:00
parent 3164347158
commit eb53d58991
12 changed files with 151 additions and 79 deletions

49
App.tsx
View File

@ -3,7 +3,7 @@ import {
DefaultTheme as NavigationDefaultTheme,
NavigationContainer,
} from '@react-navigation/native';
import React, {useState} from 'react';
import React, {useEffect, useMemo, useState} from 'react';
import {useColorScheme} from 'react-native';
import {
DarkTheme as PaperDarkTheme,
@ -12,8 +12,12 @@ import {
} from 'react-native-paper';
import Ionicon from 'react-native-vector-icons/MaterialIcons';
import {lightColors} from './colors';
import {runMigrations} from './db';
import MassiveSnack from './MassiveSnack';
import Routes from './Routes';
import Settings from './settings';
import {getSettings} from './settings.service';
import {SettingsContext} from './use-settings';
export const CombinedDefaultTheme = {
...NavigationDefaultTheme,
@ -40,21 +44,36 @@ export const CustomTheme = React.createContext({
});
const App = () => {
const dark = useColorScheme() === 'dark';
const isDark = useColorScheme() === 'dark';
const [settings, setSettings] = useState<Settings>();
const [color, setColor] = useState(
dark
isDark
? CombinedDarkTheme.colors.primary.toUpperCase()
: CombinedDefaultTheme.colors.primary.toUpperCase(),
);
const theme = dark
? {
...CombinedDarkTheme,
colors: {...CombinedDarkTheme.colors, primary: color},
}
: {
...CombinedDefaultTheme,
colors: {...CombinedDefaultTheme.colors, primary: color},
};
useEffect(() => {
runMigrations().then(async () => {
const gotSettings = await getSettings();
setSettings(gotSettings);
if (gotSettings.color) setColor(gotSettings.color);
});
}, [setColor]);
const theme = useMemo(() => {
const darkTheme = {
...CombinedDarkTheme,
colors: {...CombinedDarkTheme.colors, primary: color},
};
const lightTheme = {
...CombinedDefaultTheme,
colors: {...CombinedDefaultTheme.colors, primary: color},
};
let value = isDark ? darkTheme : lightTheme;
if (settings?.theme === 'dark') value = darkTheme;
else if (settings?.theme === 'light') value = lightTheme;
return value;
}, [color, isDark, settings]);
return (
<CustomTheme.Provider value={{color, setColor}}>
@ -63,7 +82,11 @@ const App = () => {
settings={{icon: props => <Ionicon {...props} />}}>
<NavigationContainer theme={theme}>
<MassiveSnack>
<Routes />
{settings && (
<SettingsContext.Provider value={{settings, setSettings}}>
<Routes />
</SettingsContext.Provider>
)}
</MassiveSnack>
</NavigationContainer>
</Provider>

View File

@ -1,15 +1,15 @@
import React from 'react';
import {useColorScheme} from 'react-native';
import {TextInput} from 'react-native-paper';
import {CombinedDefaultTheme} from './App';
import {MARGIN} from './constants';
import useDark from './use-dark';
export default function MassiveInput(
props: Partial<React.ComponentProps<typeof TextInput>> & {
innerRef?: React.Ref<any>;
},
) {
const dark = useColorScheme() === 'dark';
const dark = useDark();
return (
<TextInput

View File

@ -1,7 +1,6 @@
import React, {useContext, useState} from 'react';
import {useColorScheme} from 'react-native';
import {Snackbar} from 'react-native-paper';
import {CombinedDarkTheme, CustomTheme} from './App';
import {CustomTheme} from './App';
export const SnackbarContext = React.createContext<{
toast: (value: string, timeout: number) => void;
@ -10,11 +9,10 @@ export const SnackbarContext = React.createContext<{
export default function MassiveSnack({
children,
}: {
children: JSX.Element[] | JSX.Element;
children?: JSX.Element[] | JSX.Element;
}) {
const [snackbar, setSnackbar] = useState('');
const [timeoutId, setTimeoutId] = useState(0);
const dark = useColorScheme() === 'dark';
const {color} = useContext(CustomTheme);
const toast = (value: string, timeout: number) => {
@ -35,7 +33,7 @@ export default function MassiveSnack({
action={{
label: 'Close',
onPress: () => setSnackbar(''),
color: dark ? CombinedDarkTheme.colors.background : color,
color,
}}>
{snackbar}
</Snackbar>

View File

@ -1,36 +1,19 @@
import {createDrawerNavigator} from '@react-navigation/drawer';
import React, {useContext, useEffect, useState} from 'react';
import {useColorScheme} from 'react-native';
import React from 'react';
import {IconButton} from 'react-native-paper';
import {CustomTheme} from './App';
import BestPage from './BestPage';
import {runMigrations} from './db';
import {DrawerParamList} from './drawer-param-list';
import HomePage from './HomePage';
import PlanPage from './PlanPage';
import Route from './route';
import Settings from './settings';
import {getSettings} from './settings.service';
import SettingsPage from './SettingsPage';
import {SettingsContext} from './use-settings';
import useDark from './use-dark';
import WorkoutsPage from './WorkoutsPage';
const Drawer = createDrawerNavigator<DrawerParamList>();
export default function Routes() {
const [settings, setSettings] = useState<Settings>();
const dark = useColorScheme() === 'dark';
const {setColor} = useContext(CustomTheme);
useEffect(() => {
runMigrations().then(async () => {
const gotSettings = await getSettings();
setSettings(gotSettings);
if (gotSettings.color) setColor(gotSettings.color);
});
}, [setColor]);
if (!settings) return null;
const dark = useDark();
const routes: Route[] = [
{name: 'Home', component: HomePage, icon: 'home'},
@ -41,23 +24,21 @@ export default function Routes() {
];
return (
<SettingsContext.Provider value={{settings, setSettings}}>
<Drawer.Navigator
screenOptions={{
headerTintColor: dark ? 'white' : 'black',
swipeEdgeWidth: 1000,
}}>
{routes.map(route => (
<Drawer.Screen
key={route.name}
name={route.name}
component={route.component}
options={{
drawerIcon: () => <IconButton icon={route.icon} />,
}}
/>
))}
</Drawer.Navigator>
</SettingsContext.Provider>
<Drawer.Navigator
screenOptions={{
headerTintColor: dark ? 'white' : 'black',
swipeEdgeWidth: 1000,
}}>
{routes.map(route => (
<Drawer.Screen
key={route.name}
name={route.name}
component={route.component}
options={{
drawerIcon: () => <IconButton icon={route.icon} />,
}}
/>
))}
</Drawer.Navigator>
);
}

View File

@ -30,6 +30,7 @@ export default function SettingsPage() {
const [workouts, setWorkouts] = useState(!!settings.workouts);
const [steps, setSteps] = useState(!!settings.steps);
const [date, setDate] = useState(settings.date || '%Y-%m-%d %H:%M');
const [theme, setTheme] = useState(settings.theme || 'system');
const [showDate, setShowDate] = useState(!!settings.showDate);
const {color, setColor} = useContext(CustomTheme);
const {toast} = useContext(SnackbarContext);
@ -54,6 +55,7 @@ export default function SettingsPage() {
steps: +steps,
date,
showDate: +showDate,
theme,
});
getSettings().then(setSettings);
}, [
@ -70,6 +72,7 @@ export default function SettingsPage() {
setSettings,
date,
showDate,
theme,
]);
const changeAlarmEnabled = useCallback(
@ -182,9 +185,36 @@ export default function SettingsPage() {
{input.name}
</Switch>
))}
{'theme'.includes(search.toLowerCase()) && (
<Picker
style={{color}}
dropdownIconColor={color}
selectedValue={theme}
onValueChange={value => setTheme(value)}>
<Picker.Item value="system" label="Follow system theme" />
<Picker.Item value="dark" label="Dark theme" />
<Picker.Item value="light" label="Light theme" />
</Picker>
)}
{'color'.includes(search.toLowerCase()) && (
<Picker
style={{color, marginTop: -10}}
dropdownIconColor={color}
selectedValue={color}
onValueChange={value => setColor(value)}>
{lightColors.concat(darkColors).map(colorOption => (
<Picker.Item
key={colorOption.hex}
value={colorOption.hex}
label="Primary color"
color={colorOption.hex}
/>
))}
</Picker>
)}
{'new set'.includes(search.toLowerCase()) && (
<Picker
style={{color, marginTop: 0}}
style={{color, marginTop: -10}}
dropdownIconColor={color}
selectedValue={newSet}
onValueChange={value => setNewSet(value)}>
@ -193,22 +223,6 @@ export default function SettingsPage() {
<Picker.Item value="empty" label="New sets are empty" />
</Picker>
)}
{'theme'.includes(search.toLowerCase()) && (
<Picker
style={{color, marginTop: -10}}
dropdownIconColor={color}
selectedValue={color}
onValueChange={value => setColor(value)}>
{darkColors.concat(lightColors).map(colorOption => (
<Picker.Item
key={colorOption.hex}
value={colorOption.hex}
label={`${colorOption.name} theme`}
color={colorOption.hex}
/>
))}
</Picker>
)}
{'date format'.includes(search.toLowerCase()) && (
<Picker
style={{color, marginTop: -10}}

View File

@ -1,8 +1,10 @@
import React, {useContext} from 'react';
import React, {useContext, useMemo} from 'react';
import {Pressable} from 'react-native';
import {Switch as PaperSwitch, Text} from 'react-native-paper';
import {CustomTheme} from './App';
import {CombinedDarkTheme, CombinedDefaultTheme, CustomTheme} from './App';
import {colorShade} from './colors';
import {MARGIN} from './constants';
import useDark from './use-dark';
export default function Switch({
value,
@ -16,6 +18,19 @@ export default function Switch({
children: string;
}) {
const {color} = useContext(CustomTheme);
const dark = useDark();
const track = useMemo(() => {
if (dark)
return {
false: CombinedDarkTheme.colors.placeholder,
true: colorShade(color, -40),
};
return {
false: CombinedDefaultTheme.colors.placeholder,
true: colorShade(color, -40),
};
}, [dark, color]);
return (
<Pressable
@ -26,6 +41,7 @@ export default function Switch({
alignItems: 'center',
}}>
<PaperSwitch
trackColor={track}
color={color}
style={{marginRight: MARGIN}}
value={value}

View File

@ -6,7 +6,7 @@ import {
useRoute,
} from '@react-navigation/native';
import React, {useCallback, useEffect, useState} from 'react';
import {useColorScheme, View} from 'react-native';
import {View} from 'react-native';
import {FileSystem} from 'react-native-file-access';
import {IconButton} from 'react-native-paper';
import Share from 'react-native-share';
@ -19,11 +19,12 @@ import {Metrics} from './metrics';
import {Periods} from './periods';
import Set from './set';
import {formatMonth} from './time';
import useDark from './use-dark';
import Volume from './volume';
export default function ViewBest() {
const {params} = useRoute<RouteProp<BestPageParams, 'ViewBest'>>();
const dark = useColorScheme() === 'dark';
const dark = useDark();
const [weights, setWeights] = useState<Set[]>([]);
const [volumes, setVolumes] = useState<Volume[]>([]);
const [metric, setMetric] = useState(Metrics.Weight);

View File

@ -11,3 +11,26 @@ export const darkColors = [
{hex: '#000000', name: 'Black'},
{hex: '#CD5C5C', name: 'Red'},
];
export const colorShade = (color: any, amount: number) => {
color = color.replace(/^#/, '');
if (color.length === 3)
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
let [r, g, b] = color.match(/.{2}/g);
[r, g, b] = [
parseInt(r, 16) + amount,
parseInt(g, 16) + amount,
parseInt(b, 16) + amount,
];
r = Math.max(Math.min(255, r), 0).toString(16);
g = Math.max(Math.min(255, g), 0).toString(16);
b = Math.max(Math.min(255, b), 0).toString(16);
const rr = (r.length < 2 ? '0' : '') + r;
const gg = (g.length < 2 ? '0' : '') + g;
const bb = (b.length < 2 ? '0' : '') + b;
return `#${rr}${gg}${bb}`;
};

3
db.ts
View File

@ -112,6 +112,9 @@ const migrations = [
`
ALTER TABLE settings ADD COLUMN showDate BOOLEAN DEFAULT 0
`,
`
ALTER TABLE settings ADD COLUMN theme TEXT
`,
];
export let db: SQLiteDatabase;

View File

@ -26,3 +26,4 @@ git commit --no-verify --message "Set versionCode=$versionCode"
git tag "$versionCode"
git push origin HEAD & git push --tags
cd ..
./install.sh

View File

@ -12,4 +12,5 @@ export default interface Settings {
steps?: number;
date?: string;
showDate: number;
theme?: 'system' | 'dark' | 'light';
}

11
use-dark.ts Normal file
View File

@ -0,0 +1,11 @@
import {useColorScheme} from 'react-native';
import {useSettings} from './use-settings';
export default function useDark() {
const dark = useColorScheme() === 'dark';
const {settings} = useSettings();
if (settings.theme === 'dark') return true;
if (settings.theme === 'light') return false;
return dark;
}