Add ability to customize primary color

Closes #40
This commit is contained in:
Brandon Presley 2022-09-24 13:57:51 +12:00
parent 512c0d0467
commit e516cdfdc0
10 changed files with 96 additions and 58 deletions

1
.fdignore Normal file
View File

@ -0,0 +1 @@
*.png

42
App.tsx
View File

@ -4,7 +4,7 @@ import {
DefaultTheme as NavigationDefaultTheme, DefaultTheme as NavigationDefaultTheme,
NavigationContainer, NavigationContainer,
} from '@react-navigation/native'; } from '@react-navigation/native';
import React from 'react'; import React, {useState} from 'react';
import {useColorScheme} from 'react-native'; import {useColorScheme} from 'react-native';
import { import {
DarkTheme as PaperDarkTheme, DarkTheme as PaperDarkTheme,
@ -44,20 +44,40 @@ export const CombinedDarkTheme = {
}, },
}; };
export const CustomTheme = React.createContext({
color: '',
setColor: (_value: string) => {},
});
const App = () => { const App = () => {
const dark = useColorScheme() === 'dark'; const dark = useColorScheme() === 'dark';
const [color, setColor] = useState(
dark
? CombinedDarkTheme.colors.primary
: CombinedDefaultTheme.colors.primary,
);
const theme = dark
? {
...CombinedDarkTheme,
colors: {...CombinedDarkTheme.colors, primary: color},
}
: {
...CombinedDefaultTheme,
colors: {...CombinedDefaultTheme.colors, primary: color},
};
return ( return (
<Provider <CustomTheme.Provider value={{color, setColor}}>
theme={dark ? CombinedDarkTheme : CombinedDefaultTheme} <Provider
settings={{icon: props => <Ionicon {...props} />}}> theme={theme}
<NavigationContainer settings={{icon: props => <Ionicon {...props} />}}>
theme={dark ? CombinedDarkTheme : CombinedDefaultTheme}> <NavigationContainer theme={theme}>
<MassiveSnack> <MassiveSnack>
<Routes /> <Routes />
</MassiveSnack> </MassiveSnack>
</NavigationContainer> </NavigationContainer>
</Provider> </Provider>
</CustomTheme.Provider>
); );
}; };

View File

@ -1,8 +1,8 @@
import * as shape from 'd3-shape'; import * as shape from 'd3-shape';
import React from 'react'; import React, {useContext} from 'react';
import {useColorScheme, View} from 'react-native'; import {View} from 'react-native';
import {Grid, LineChart, XAxis, YAxis} from 'react-native-svg-charts'; import {Grid, LineChart, XAxis, YAxis} from 'react-native-svg-charts';
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'; import {CustomTheme} from './App';
import {MARGIN, PADDING} from './constants'; import {MARGIN, PADDING} from './constants';
import Set from './set'; import Set from './set';
@ -17,7 +17,7 @@ export default function Chart({
xFormat: (value: any, index: number) => string; xFormat: (value: any, index: number) => string;
yFormat: (value: any) => string; yFormat: (value: any) => string;
}) { }) {
const dark = useColorScheme() === 'dark'; const {color} = useContext(CustomTheme);
const axesSvg = {fontSize: 10, fill: 'grey'}; const axesSvg = {fontSize: 10, fill: 'grey'};
const verticalContentInset = {top: 10, bottom: 10}; const verticalContentInset = {top: 10, bottom: 10};
const xAxisHeight = 30; const xAxisHeight = 30;
@ -39,9 +39,7 @@ export default function Chart({
contentInset={verticalContentInset} contentInset={verticalContentInset}
curve={shape.curveBasis} curve={shape.curveBasis}
svg={{ svg={{
stroke: dark stroke: color,
? CombinedDarkTheme.colors.primary
: CombinedDefaultTheme.colors.primary,
}}> }}>
<Grid /> <Grid />
</LineChart> </LineChart>

View File

@ -1,12 +1,13 @@
import React from 'react'; import React, {useContext} from 'react';
import {useColorScheme} from 'react-native'; import {useColorScheme} from 'react-native';
import {FAB} from 'react-native-paper'; import {FAB} from 'react-native-paper';
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'; import {CombinedDarkTheme, CustomTheme} from './App';
export default function MassiveFab( export default function MassiveFab(
props: Partial<React.ComponentProps<typeof FAB>>, props: Partial<React.ComponentProps<typeof FAB>>,
) { ) {
const dark = useColorScheme() === 'dark'; const dark = useColorScheme() === 'dark';
const {color} = useContext(CustomTheme);
return ( return (
<FAB <FAB
@ -17,9 +18,7 @@ export default function MassiveFab(
position: 'absolute', position: 'absolute',
right: 10, right: 10,
bottom: 60, bottom: 60,
backgroundColor: dark backgroundColor: color,
? CombinedDarkTheme.colors.primary
: CombinedDefaultTheme.colors.primary,
}} }}
/> />
); );

View File

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

View File

@ -1,23 +1,12 @@
import React from 'react'; import React, {useContext} from 'react';
import {useColorScheme} from 'react-native';
import {Switch} from 'react-native-paper'; import {Switch} from 'react-native-paper';
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'; import {CustomTheme} from './App';
import {MARGIN} from './constants'; import {MARGIN} from './constants';
export default function MassiveSwitch( export default function MassiveSwitch(
props: Partial<React.ComponentProps<typeof Switch>>, props: Partial<React.ComponentProps<typeof Switch>>,
) { ) {
const dark = useColorScheme() === 'dark'; const {color} = useContext(CustomTheme);
return ( return <Switch color={color} style={{marginRight: MARGIN}} {...props} />;
<Switch
color={
dark
? CombinedDarkTheme.colors.primary
: CombinedDefaultTheme.colors.primary
}
style={{marginRight: MARGIN}}
{...props}
/>
);
} }

View File

@ -1,12 +1,12 @@
import React, {useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {useColorScheme} from 'react-native'; import {useColorScheme} from 'react-native';
import {IconButton} from 'react-native-paper'; import {IconButton} from 'react-native-paper';
import {Drawer, DrawerParamList} from './App'; import {CustomTheme, Drawer, DrawerParamList} from './App';
import BestPage from './BestPage'; import BestPage from './BestPage';
import {runMigrations} from './db'; import {runMigrations} from './db';
import HomePage from './HomePage'; import HomePage from './HomePage';
import PlanPage from './PlanPage'; import PlanPage from './PlanPage';
import {getSettings} from './settings.service'; import {getSettings, settings} from './settings.service';
import SettingsPage from './SettingsPage'; import SettingsPage from './SettingsPage';
import WorkoutsPage from './WorkoutsPage'; import WorkoutsPage from './WorkoutsPage';
@ -19,12 +19,16 @@ interface Route {
export default function Routes() { export default function Routes() {
const [migrated, setMigrated] = useState(false); const [migrated, setMigrated] = useState(false);
const dark = useColorScheme() === 'dark'; const dark = useColorScheme() === 'dark';
const {setColor} = useContext(CustomTheme);
useEffect(() => { useEffect(() => {
runMigrations() runMigrations()
.then(getSettings) .then(getSettings)
.then(() => setMigrated(true)); .then(() => setMigrated(true))
}, []); .then(() => {
if (settings.color) setColor(settings.color);
});
}, [setColor]);
if (!migrated) return null; if (!migrated) return null;

View File

@ -1,8 +1,10 @@
import {Picker} from '@react-native-picker/picker';
import {useFocusEffect} from '@react-navigation/native'; import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useContext, useEffect, useState} from 'react'; import React, {useCallback, useContext, useEffect, useState} from 'react';
import {NativeModules, ScrollView} from 'react-native'; import {NativeModules, ScrollView, StyleSheet} from 'react-native';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import {Button, Text} from 'react-native-paper'; import {Button, Text} from 'react-native-paper';
import {CustomTheme} from './App';
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog';
import {MARGIN} from './constants'; import {MARGIN} from './constants';
import {SnackbarContext} from './MassiveSnack'; import {SnackbarContext} from './MassiveSnack';
@ -27,6 +29,7 @@ export default function SettingsPage() {
const [ignoring, setIgnoring] = useState(false); const [ignoring, setIgnoring] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [showUnit, setShowUnit] = useState(true); const [showUnit, setShowUnit] = useState(true);
const {color, setColor} = useContext(CustomTheme);
const {toast} = useContext(SnackbarContext); const {toast} = useContext(SnackbarContext);
useFocusEffect( useFocusEffect(
@ -35,7 +38,7 @@ export default function SettingsPage() {
setAlarm(!!settings.alarm); setAlarm(!!settings.alarm);
setPredict(!!settings.predict); setPredict(!!settings.predict);
setVibrate(!!settings.vibrate); setVibrate(!!settings.vibrate);
setSound(settings.sound); setSound(settings.sound ?? '');
setNotify(!!settings.notify); setNotify(!!settings.notify);
setImages(!!settings.images); setImages(!!settings.images);
NativeModules.AlarmModule.ignoringBattery(setIgnoring); NativeModules.AlarmModule.ignoringBattery(setIgnoring);
@ -51,9 +54,10 @@ export default function SettingsPage() {
notify: +notify, notify: +notify,
images: +images, images: +images,
showUnit: +showUnit, showUnit: +showUnit,
color,
}); });
getSettings(); getSettings();
}, [vibrate, alarm, predict, sound, notify, images, showUnit]); }, [vibrate, alarm, predict, sound, notify, images, showUnit, color]);
const changeAlarmEnabled = useCallback( const changeAlarmEnabled = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
@ -131,16 +135,16 @@ export default function SettingsPage() {
return ( return (
<Page search={search} setSearch={setSearch}> <Page search={search} setSearch={setSearch}>
<ScrollView style={{margin: MARGIN}}> <ScrollView style={{marginTop: MARGIN}}>
{switches {switches
.filter(input => .filter(input =>
input.name.toLowerCase().includes(search.toLowerCase()), input.name.toLowerCase().includes(search.toLowerCase()),
) )
.map(input => ( .map(input => (
<React.Fragment key={input.name}> <React.Fragment key={input.name}>
<Text style={{marginBottom: MARGIN}}>{input.name}</Text> <Text style={styles.item}>{input.name}</Text>
<MassiveSwitch <MassiveSwitch
style={{alignSelf: 'flex-start', marginBottom: MARGIN}} style={styles.item}
value={input.value} value={input.value}
onValueChange={input.onChange} onValueChange={input.onChange}
/> />
@ -154,6 +158,18 @@ export default function SettingsPage() {
: null} : null}
</Button> </Button>
)} )}
{'color'.includes(search.toLowerCase()) && (
<Picker
style={{color}}
dropdownIconColor={color}
selectedValue={color}
onValueChange={value => setColor(value)}>
<Picker.Item value="#B3E5fC" label="Cyan theme" color="#B3E5fC" />
<Picker.Item value="#8156a7" label="Purple theme" color="#8156a7" />
<Picker.Item value="#007AFF" label="Blue theme" color="#007AFF" />
<Picker.Item value="#ffc0cb" label="Pink theme" color="#ffc0cb" />
</Picker>
)}
</ScrollView> </ScrollView>
<ConfirmDialog <ConfirmDialog
title="Battery optimizations" title="Battery optimizations"
@ -168,3 +184,11 @@ export default function SettingsPage() {
</Page> </Page>
); );
} }
const styles = StyleSheet.create({
item: {
alignSelf: 'flex-start',
marginBottom: MARGIN,
marginLeft: MARGIN,
},
});

3
db.ts
View File

@ -88,6 +88,9 @@ const migrations = [
` `
DROP TABLE workouts DROP TABLE workouts
`, `,
`
ALTER TABLE settings ADD COLUMN color TEXT NULL
`,
]; ];
export let db: SQLiteDatabase; export let db: SQLiteDatabase;

View File

@ -2,8 +2,9 @@ export default interface Settings {
alarm: number; alarm: number;
vibrate: number; vibrate: number;
predict: number; predict: number;
sound: string; sound?: string;
notify: number; notify?: number;
images: number; images?: number;
showUnit: number; showUnit?: number;
color?: string;
} }