From a69bfd62a63acc9066ac5e86464bbe1d23fe53f9 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Sat, 24 Dec 2022 18:19:35 +1300 Subject: [PATCH] Use react-hook-forms on SettingsPage This greatly reduces our lines of code. Also I thought it might improve performance to address #135 but it didn't make any difference. --- EditPlan.tsx | 4 +- SettingsPage.tsx | 287 ++++++++++++++++++----------------------------- Switch.tsx | 6 +- constants.ts | 8 ++ input.ts | 3 +- package.json | 1 + yarn.lock | 10 ++ 7 files changed, 132 insertions(+), 187 deletions(-) diff --git a/EditPlan.tsx b/EditPlan.tsx index c857a85..ec8bcd3 100644 --- a/EditPlan.tsx +++ b/EditPlan.tsx @@ -79,7 +79,7 @@ export default function EditPlan() { {DAYS.map(day => ( toggleDay(value, day)} + onChange={value => toggleDay(value, day)} onPress={() => toggleDay(!days.includes(day), day)} value={days.includes(day)}> {day} @@ -94,7 +94,7 @@ export default function EditPlan() { names.map(name => ( toggleWorkout(value, name)} + onChange={value => toggleWorkout(value, name)} value={workouts.includes(name)} onPress={() => toggleWorkout(!workouts.includes(name), name)}> {name} diff --git a/SettingsPage.tsx b/SettingsPage.tsx index 8f2d0de..d576acd 100644 --- a/SettingsPage.tsx +++ b/SettingsPage.tsx @@ -4,21 +4,22 @@ import { useNavigation, } from '@react-navigation/native' import {format} from 'date-fns' -import {useCallback, useMemo, useState} from 'react' +import {useCallback, useEffect, useMemo, useState} from 'react' +import {Controller, useForm} from 'react-hook-form' import {DeviceEventEmitter, NativeModules, Platform, View} from 'react-native' import DocumentPicker from 'react-native-document-picker' import {Dirs, FileSystem} from 'react-native-file-access' import {Button, Subheading} from 'react-native-paper' import ConfirmDialog from './ConfirmDialog' -import {ITEM_PADDING, MARGIN} from './constants' +import {ITEM_PADDING, MARGIN, toSentenceCase} from './constants' import {AppDataSource} from './data-source' import {setRepo, settingsRepo} from './db' import {DrawerParamList} from './drawer-param-list' import DrawerHeader from './DrawerHeader' -import Input from './input' import {darkOptions, lightOptions, themeOptions} from './options' import Page from './Page' import Select from './Select' +import Settings from './settings' import Switch from './Switch' import {toast} from './toast' import {useTheme} from './use-theme' @@ -26,39 +27,35 @@ import {useTheme} from './use-theme' const defaultFormats = ['P', 'Pp', 'ccc p', 'p'] export default function SettingsPage() { - const [ignoring, setIgnoring] = useState(false) + const {control, watch} = useForm({ + defaultValues: async () => settingsRepo.findOne({where: {}}), + }) + const settings = watch() const [term, setTerm] = useState('') - const [vibrate, setVibrate] = useState(false) - const [alarm, setAlarm] = useState(false) const [sound, setSound] = useState('') - const [notify, setNotify] = useState(false) - const [images, setImages] = useState(false) - const [showUnit, setShowUnit] = useState(false) - const [steps, setSteps] = useState(false) - const [date, setDate] = useState('P') - const {theme, setTheme, lightColor, setLightColor, darkColor, setDarkColor} = - useTheme() - const [showDate, setShowDate] = useState(false) - const [noSound, setNoSound] = useState(false) + const {setTheme, setLightColor, setDarkColor} = useTheme() const [formatOptions, setFormatOptions] = useState(defaultFormats) const [importing, setImporting] = useState(false) + const [ignoring, setIgnoring] = useState(false) const {reset} = useNavigation>() - const today = new Date() + + useEffect(() => { + if (Object.keys(settings).length === 0) return + settingsRepo.update({}, settings) + setLightColor(settings.lightColor) + setDarkColor(settings.darkColor) + setTheme(settings.theme) + if (!settings.alarm || ignoring) return + DeviceEventEmitter.emit('toast', { + value: 'Timers will now run after each set', + timeout: 4000, + }) + NativeModules.SettingsModule.ignoreBattery() + setIgnoring(true) + }, [settings, setDarkColor, setLightColor, setTheme, ignoring]) useFocusEffect( useCallback(() => { - settingsRepo.findOne({where: {}}).then(settings => { - setAlarm(settings.alarm) - setVibrate(settings.vibrate) - setSound(settings.sound) - setNotify(settings.notify) - setImages(settings.images) - setShowUnit(settings.showUnit) - setSteps(settings.steps) - setDate(settings.date) - setShowDate(settings.showDate) - setNoSound(settings.noSound) - }) if (Platform.OS !== 'android') return NativeModules.SettingsModule.ignoringBattery(setIgnoring) NativeModules.SettingsModule.is24().then((is24: boolean) => { @@ -69,28 +66,6 @@ export default function SettingsPage() { }, []), ) - const changeAlarmEnabled = useCallback( - (enabled: boolean) => { - if (enabled) - DeviceEventEmitter.emit('toast', { - value: 'Timers will now run after each set', - timeout: 4000, - }) - else toast('Stopped timers running after each set.') - if (enabled && !ignoring) NativeModules.SettingsModule.ignoreBattery() - setAlarm(enabled) - settingsRepo.update({}, {alarm: enabled}) - }, - [ignoring], - ) - - const changeVibrate = useCallback((enabled: boolean) => { - if (enabled) toast('When a timer completes, vibrate your phone.') - else toast('Stop vibrating at the end of timers.') - setVibrate(enabled) - settingsRepo.update({}, {vibrate: enabled}) - }, []) - const changeSound = useCallback(async () => { const {fileCopyUri} = await DocumentPicker.pickSingle({ type: 'audio/*', @@ -102,143 +77,91 @@ export default function SettingsPage() { toast('This song will now play after rest timers complete.') }, []) - const changeNotify = useCallback((enabled: boolean) => { - setNotify(enabled) - settingsRepo.update({}, {notify: enabled}) - if (enabled) toast('Show when a set is a new record.') - else toast('Stopped showing notifications for new records.') - }, []) - - const changeImages = useCallback((enabled: boolean) => { - setImages(enabled) - settingsRepo.update({}, {images: enabled}) - if (enabled) toast('Show images for sets.') - else toast('Stopped showing images for sets.') - }, []) - - const changeUnit = useCallback((enabled: boolean) => { - setShowUnit(enabled) - settingsRepo.update({}, {showUnit: enabled}) - if (enabled) toast('Show option to select unit for sets.') - else toast('Hid unit option for sets.') - }, []) - - const changeSteps = useCallback((enabled: boolean) => { - setSteps(enabled) - settingsRepo.update({}, {steps: enabled}) - if (enabled) toast('Show steps for a workout.') - else toast('Stopped showing steps for workouts.') - }, []) - - const changeShowDate = useCallback((enabled: boolean) => { - setShowDate(enabled) - settingsRepo.update({}, {showDate: enabled}) - if (enabled) toast('Show date for sets by default.') - else toast('Stopped showing date for sets by default.') - }, []) - - const changeNoSound = useCallback((enabled: boolean) => { - setNoSound(enabled) - settingsRepo.update({}, {noSound: enabled}) - if (enabled) toast('Disable sound on rest timer alarms.') - else toast('Enabled sound for rest timer alarms.') - }, []) - - const switches: Input[] = [ - {name: 'Rest timers', value: alarm, onChange: changeAlarmEnabled}, - {name: 'Vibrate', value: vibrate, onChange: changeVibrate}, - {name: 'Disable sound', value: noSound, onChange: changeNoSound}, - {name: 'Notifications', value: notify, onChange: changeNotify}, - {name: 'Show images', value: images, onChange: changeImages}, - {name: 'Show unit', value: showUnit, onChange: changeUnit}, - {name: 'Show steps', value: steps, onChange: changeSteps}, - {name: 'Show date', value: showDate, onChange: changeShowDate}, - ].filter(({name}) => name.toLowerCase().includes(term.toLowerCase())) - - const changeTheme = useCallback( - (value: string) => { - settingsRepo.update({}, {theme: value}) - setTheme(value) - }, - [setTheme], - ) - - const changeDate = useCallback((value: string) => { - settingsRepo.update({}, {date: value}) - setDate(value) - }, []) - const soundString = useMemo(() => { if (!sound) return null const split = sound.split('/') return split.pop() }, [sound]) - const changeDarkColor = useCallback( - (value: string) => { - setDarkColor(value) - settingsRepo.update({}, {darkColor: value}) - }, - [setDarkColor], - ) - - const changeLightColor = useCallback( - (value: string) => { - setLightColor(value) - settingsRepo.update({}, {lightColor: value}) - }, - [setLightColor], - ) - const renderSwitch = useCallback( - (item: Input) => ( - item.onChange(!item.value)} - key={item.name} - value={item.value} - onValueChange={item.onChange}> - {item.name} - - ), - [], - ) - - const selects: Input[] = [ - {name: 'Theme', value: theme, onChange: changeTheme, items: themeOptions}, - { - name: 'Dark color', - value: darkColor, - onChange: changeDarkColor, - items: lightOptions, - }, - { - name: 'Light color', - value: lightColor, - onChange: changeLightColor, - items: darkOptions, - }, - { - name: 'Date format', - value: date, - onChange: changeDate, - items: formatOptions.map(option => ({ - label: format(today, option), - value: option, - })), - }, - ].filter(({name}) => name.toLowerCase().includes(term.toLowerCase())) - - const renderSelect = useCallback( - (item: Input) => ( - + )} + /> + ), + [control, getItems], ) const confirmImport = useCallback(async () => { @@ -311,8 +234,12 @@ export default function SettingsPage() { - {switches.map(s => renderSwitch(s))} - {selects.map(s => renderSelect(s))} + {switches + .filter(s => s.toLowerCase().includes(term.toLowerCase())) + .map(s => renderSwitch(s))} + {selects + .filter(s => s.toLowerCase().includes(term.toLowerCase())) + .map(key => renderSelect(key))} {buttons .filter(b => b.name.includes(term.toLowerCase())) .map(b => b.element)} diff --git a/Switch.tsx b/Switch.tsx index 3885ded..f0c1302 100644 --- a/Switch.tsx +++ b/Switch.tsx @@ -4,12 +4,12 @@ import {MARGIN} from './constants' export default function Switch({ value, - onValueChange, + onChange, onPress, children, }: { value?: boolean - onValueChange: (value: boolean) => void + onChange: (value: boolean) => void onPress: () => void children: string }) { @@ -28,7 +28,7 @@ export default function Switch({ color={colors.primary} style={{marginRight: MARGIN}} value={value} - onValueChange={onValueChange} + onValueChange={onChange} /> {children} diff --git a/constants.ts b/constants.ts index 7471ca1..b02141b 100644 --- a/constants.ts +++ b/constants.ts @@ -3,3 +3,11 @@ export const PADDING = 10 export const ITEM_PADDING = 8 export const DARK_RIPPLE = '#444444' export const LIGHT_RIPPLE = '#c2c2c2' + +export const toSentenceCase = (camelCase: string) => { + if (camelCase) { + const result = camelCase.replace(/([A-Z])/g, ' $1') + return result[0].toUpperCase() + result.substring(1).toLowerCase() + } + return '' +} diff --git a/input.ts b/input.ts index ab1e5ec..1c2cc9f 100644 --- a/input.ts +++ b/input.ts @@ -1,8 +1,7 @@ import {Item} from './Select' export default interface Input { + key: keyof T name: string - value?: T - onChange: (value: T) => void items?: Item[] } diff --git a/package.json b/package.json index a98eb71..3ff74a5 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "eslint-plugin-flowtype": "^8.0.3", "jest": "^29.2.2", "react": "^18.2.0", + "react-hook-form": "^7.41.1", "react-native": "^0.70.5", "react-native-document-picker": "^8.1.2", "react-native-file-access": "^2.5.0", diff --git a/yarn.lock b/yarn.lock index 75e7b26..6bc2da7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7306,6 +7306,7 @@ __metadata: jest: ^29.2.2 metro-react-native-babel-preset: ^0.73.3 react: ^18.2.0 + react-hook-form: ^7.41.1 react-native: ^0.70.5 react-native-document-picker: ^8.1.2 react-native-file-access: ^2.5.0 @@ -8762,6 +8763,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.41.1": + version: 7.41.1 + resolution: "react-hook-form@npm:7.41.1" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + checksum: 30f7ea67e29c3527d25f87b9ea8789f6722780759adf0718a0dadc6160227ae763c68ddedf63d8311f0902e4ce27225eff37b93a82eac868fc5e0a0d00dbbd14 + languageName: node + linkType: hard + "react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.2.0": version: 18.2.0 resolution: "react-is@npm:18.2.0"