diff --git a/App.tsx b/App.tsx index 72586fd..889c03b 100644 --- a/App.tsx +++ b/App.tsx @@ -4,19 +4,20 @@ import { NavigationContainer, } from '@react-navigation/native' import {useEffect, useMemo, useState} from 'react' -import {useColorScheme} from 'react-native' +import {DeviceEventEmitter, useColorScheme} from 'react-native' import { DarkTheme as PaperDarkTheme, DefaultTheme as PaperDefaultTheme, Provider as PaperProvider, + Snackbar, } from 'react-native-paper' import MaterialIcon from 'react-native-vector-icons/MaterialIcons' import {lightColors} from './colors' import {AppDataSource} from './data-source' import {settingsRepo} from './db' -import MassiveSnack from './MassiveSnack' import Routes from './Routes' import Settings from './settings' +import {TOAST} from './toast' import {defaultSettings, SettingsContext} from './use-settings' export const CombinedDefaultTheme = { @@ -48,6 +49,7 @@ const App = () => { ? CombinedDarkTheme.colors.primary : CombinedDefaultTheme.colors.primary, }) + const [snackbar, setSnackbar] = useState('') useEffect(() => { AppDataSource.initialize().then(async () => { @@ -56,6 +58,10 @@ const App = () => { setSettings(gotSettings) setInitialized(true) }) + DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => { + console.log(`${Routes.name}.toast:`, {value}) + setSnackbar(value) + }) }, []) const theme = useMemo(() => { @@ -87,14 +93,24 @@ const App = () => { theme={theme} settings={{icon: props => }}> - - {initialized && ( - - - - )} - + {initialized && ( + + + + )} + + setSnackbar('')} + visible={!!snackbar} + action={{ + label: 'Close', + onPress: () => setSnackbar(''), + color: theme.colors.primary, + }}> + {snackbar} + ) } diff --git a/DrawerMenu.tsx b/DrawerMenu.tsx index 8a7ca82..0189025 100644 --- a/DrawerMenu.tsx +++ b/DrawerMenu.tsx @@ -8,8 +8,8 @@ import {AppDataSource} from './data-source' import {planRepo} from './db' import {DrawerParamList} from './drawer-param-list' import GymSet from './gym-set' -import {useSnackbar} from './MassiveSnack' import {Plan} from './plan' +import {toast} from './toast' import useDark from './use-dark' import {write} from './write' @@ -20,7 +20,6 @@ const setRepo = AppDataSource.manager.getRepository(GymSet) export default function DrawerMenu({name}: {name: keyof DrawerParamList}) { const [showMenu, setShowMenu] = useState(false) const [showRemove, setShowRemove] = useState(false) - const {toast} = useSnackbar() const {reset} = useNavigation>() const dark = useDark() @@ -65,7 +64,7 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) { console.log(`${DrawerMenu.name}.uploadSets:`, file.length) const lines = file.split('\n') console.log(lines[0]) - if (!setFields.includes(lines[0])) return toast('Invalid csv.', 3000) + if (!setFields.includes(lines[0])) return toast('Invalid csv.') const values = lines .slice(1) .filter(line => line) @@ -92,21 +91,22 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) { sets: +sets, minutes: +minutes, seconds: +seconds, + image: '', } return set }) console.log(`${DrawerMenu.name}.uploadSets:`, {values}) await setRepo.insert(values) - toast('Data imported.', 3000) + toast('Data imported.') reset({index: 0, routes: [{name}]}) - }, [reset, name, toast]) + }, [reset, name]) const uploadPlans = useCallback(async () => { const result = await DocumentPicker.pickSingle() const file = await FileSystem.readFile(result.uri) console.log(`${DrawerMenu.name}.uploadPlans:`, file.length) const lines = file.split('\n') - if (lines[0] != planFields) return toast('Invalid csv.', 3000) + if (lines[0] !== planFields) return toast('Invalid csv.') const values = file .split('\n') .slice(1) @@ -122,8 +122,8 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) { return plan }) await planRepo.insert(values) - toast('Data imported.', 3000) - }, [toast]) + toast('Data imported.') + }, []) const upload = useCallback(async () => { setShowMenu(false) @@ -137,9 +137,9 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) { setShowRemove(false) if (name === 'Home') await setRepo.delete({}) else if (name === 'Plans') await planRepo.delete({}) - toast('All data has been deleted.', 4000) + toast('All data has been deleted.') reset({index: 0, routes: [{name}]}) - }, [reset, name, toast]) + }, [reset, name]) if (name === 'Home' || name === 'Plans') return ( diff --git a/EditSet.tsx b/EditSet.tsx index 13e7000..a3f8f6a 100644 --- a/EditSet.tsx +++ b/EditSet.tsx @@ -2,19 +2,18 @@ import {RouteProp, useNavigation, useRoute} from '@react-navigation/native' import {useCallback} from 'react' import {NativeModules, View} from 'react-native' import {PADDING} from './constants' -import {getNow, setRepo} from './db' +import {setRepo} from './db' import GymSet from './gym-set' import {HomePageParams} from './home-page-params' -import {useSnackbar} from './MassiveSnack' import SetForm from './SetForm' import StackHeader from './StackHeader' +import {toast} from './toast' import {useSettings} from './use-settings' export default function EditSet() { const {params} = useRoute>() const {set} = params const navigation = useNavigation() - const {toast} = useSnackbar() const {settings} = useSettings() const startTimer = useCallback( @@ -35,9 +34,6 @@ export default function EditSet() { const add = useCallback( async (value: GymSet) => { startTimer(value.name) - const [{now}] = await getNow() - value.created = now - value.hidden = false console.log(`${EditSet.name}.add`, {set: value}) const result = await setRepo.save(value) console.log({result}) @@ -46,9 +42,9 @@ export default function EditSet() { value.weight > set.weight || (value.reps > set.reps && value.weight === set.weight) ) - toast("Great work King! That's a new record.", 3000) + toast("Great work King! That's a new record.") }, - [startTimer, set, toast, settings], + [startTimer, set, settings], ) const save = useCallback( diff --git a/EditWorkout.tsx b/EditWorkout.tsx index a435d61..006bfe9 100644 --- a/EditWorkout.tsx +++ b/EditWorkout.tsx @@ -7,8 +7,8 @@ import ConfirmDialog from './ConfirmDialog' import {MARGIN, PADDING} from './constants' import {getNow, planRepo, setRepo} from './db' import MassiveInput from './MassiveInput' -import {useSnackbar} from './MassiveSnack' import StackHeader from './StackHeader' +import {toast} from './toast' import {useSettings} from './use-settings' import {WorkoutsPageParams} from './WorkoutsPage' @@ -26,7 +26,6 @@ export default function EditWorkout() { params.value.seconds?.toString() ?? '30', ) const [sets, setSets] = useState(params.value.sets?.toString() ?? '3') - const {toast} = useSnackbar() const navigation = useNavigation() const setsRef = useRef(null) const stepsRef = useRef(null) @@ -94,13 +93,13 @@ export default function EditWorkout() { const handleName = (value: string) => { setName(value.replace(/,|'/g, '')) if (value.match(/,|'/)) - toast('Commas and single quotes would break CSV exports', 6000) + toast('Commas and single quotes would break CSV exports') } const handleSteps = (value: string) => { setSteps(value.replace(/,|'/g, '')) if (value.match(/,|'/)) - toast('Commas and single quotes would break CSV exports', 6000) + toast('Commas and single quotes would break CSV exports') } const submitName = () => { diff --git a/MassiveSnack.tsx b/MassiveSnack.tsx deleted file mode 100644 index 3613189..0000000 --- a/MassiveSnack.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import {createContext, useContext, useState} from 'react' -import {Snackbar} from 'react-native-paper' -import {CombinedDarkTheme, CombinedDefaultTheme} from './App' -import useDark from './use-dark' - -export const SnackbarContext = createContext<{ - toast: (value: string, timeout: number) => void -}>({toast: () => null}) - -export const useSnackbar = () => { - return useContext(SnackbarContext) -} - -export default function MassiveSnack({ - children, -}: { - children?: JSX.Element[] | JSX.Element -}) { - const [snackbar, setSnackbar] = useState('') - const [timeoutId, setTimeoutId] = useState(0) - const dark = useDark() - - const toast = (value: string, timeout: number) => { - setSnackbar(value) - clearTimeout(timeoutId) - const id = setTimeout(() => setSnackbar(''), timeout) - setTimeoutId(id) - } - - return ( - <> - - {children} - - setSnackbar('')} - visible={!!snackbar} - action={{ - label: 'Close', - onPress: () => setSnackbar(''), - color: dark - ? CombinedDarkTheme.colors.background - : CombinedDefaultTheme.colors.background, - }}> - {snackbar} - - - ) -} diff --git a/Routes.tsx b/Routes.tsx index 23b2060..b93685d 100644 --- a/Routes.tsx +++ b/Routes.tsx @@ -1,6 +1,7 @@ import {createDrawerNavigator} from '@react-navigation/drawer' -import {useMemo} from 'react' -import {IconButton} from 'react-native-paper' +import {useEffect, useMemo, useState} from 'react' +import {DeviceEventEmitter} from 'react-native' +import {IconButton, Snackbar, useTheme} from 'react-native-paper' import BestPage from './BestPage' import {DrawerParamList} from './drawer-param-list' import HomePage from './HomePage' diff --git a/SetForm.tsx b/SetForm.tsx index f07a4a6..0658d78 100644 --- a/SetForm.tsx +++ b/SetForm.tsx @@ -4,10 +4,10 @@ import DocumentPicker from 'react-native-document-picker' import {Button, Card, TouchableRipple} from 'react-native-paper' import ConfirmDialog from './ConfirmDialog' import {MARGIN} from './constants' -import {setRepo} from './db' +import {getNow, setRepo} from './db' import GymSet from './gym-set' import MassiveInput from './MassiveInput' -import {useSnackbar} from './MassiveSnack' +import {toast} from './toast' import {useSettings} from './use-settings' export default function SetForm({ @@ -28,7 +28,6 @@ export default function SetForm({ end: set.reps.toString().length, }) const [removeImage, setRemoveImage] = useState(false) - const {toast} = useSnackbar() const {settings} = useSettings() const weightRef = useRef(null) const repsRef = useRef(null) @@ -42,8 +41,10 @@ export default function SetForm({ image = await setRepo.findOne({where: {name}}).then(s => s?.image) console.log(`${SetForm.name}.handleSubmit:`, {image}) + const [{now}] = await getNow() save({ name, + created: now, reps: Number(reps), weight: Number(weight), id: set.id, @@ -59,13 +60,13 @@ export default function SetForm({ const handleName = (value: string) => { setName(value.replace(/,|'/g, '')) if (value.match(/,|'/)) - toast('Commas and single quotes would break CSV exports', 6000) + toast('Commas and single quotes would break CSV exports') } const handleUnit = (value: string) => { setUnit(value.replace(/,|'/g, '')) if (value.match(/,|'/)) - toast('Commas and single quotes would break CSV exports', 6000) + toast('Commas and single quotes would break CSV exports') } const changeImage = useCallback(async () => { diff --git a/SettingsPage.tsx b/SettingsPage.tsx index 748d846..b4e10eb 100644 --- a/SettingsPage.tsx +++ b/SettingsPage.tsx @@ -1,7 +1,7 @@ import {Picker} from '@react-native-picker/picker' import {useFocusEffect} from '@react-navigation/native' import {useCallback, useEffect, useMemo, useState} from 'react' -import {NativeModules, ScrollView} from 'react-native' +import {DeviceEventEmitter, NativeModules, ScrollView} from 'react-native' import DocumentPicker from 'react-native-document-picker' import {Button} from 'react-native-paper' import {darkColors, lightColors} from './colors' @@ -10,10 +10,10 @@ import {MARGIN} from './constants' import {settingsRepo} from './db' import DrawerHeader from './DrawerHeader' import Input from './input' -import {useSnackbar} from './MassiveSnack' import Page from './Page' import Settings from './settings' import Switch from './Switch' +import {toast} from './toast' import {useSettings} from './use-settings' export default function SettingsPage() { @@ -21,7 +21,6 @@ export default function SettingsPage() { const [ignoring, setIgnoring] = useState(false) const [term, setTerm] = useState('') const {settings, setSettings} = useSettings() - const {toast} = useSnackbar() useEffect(() => { console.log(`${SettingsPage.name}.useEffect:`, {settings}) @@ -43,21 +42,25 @@ export default function SettingsPage() { const changeAlarmEnabled = useCallback( (enabled: boolean) => { - if (enabled) toast('Timers will now run after each set.', 4000) - else toast('Stopped timers running after each set.', 4000) + 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) setBattery(true) update(enabled, 'alarm') }, - [setBattery, ignoring, toast, update], + [setBattery, ignoring, update], ) const changeVibrate = useCallback( (enabled: boolean) => { - if (enabled) toast('When a timer completes, vibrate your phone.', 4000) - else toast('Stop vibrating at the end of timers.', 4000) + if (enabled) toast('When a timer completes, vibrate your phone.') + else toast('Stop vibrating at the end of timers.') update(enabled, 'vibrate') }, - [toast, update], + [update], ) const changeSound = useCallback(async () => { @@ -68,70 +71,70 @@ export default function SettingsPage() { if (!fileCopyUri) return settingsRepo.update({}, {sound: fileCopyUri}) setSettings({...settings, sound: fileCopyUri}) - toast('This song will now play after rest timers complete.', 4000) - }, [toast, setSettings, settings]) + toast('This song will now play after rest timers complete.') + }, [setSettings, settings]) const changeNotify = useCallback( (enabled: boolean) => { update(enabled, 'notify') - if (enabled) toast('Show when a set is a new record.', 4000) - else toast('Stopped showing notifications for new records.', 4000) + if (enabled) toast('Show when a set is a new record.') + else toast('Stopped showing notifications for new records.') }, - [toast, update], + [update], ) const changeImages = useCallback( (enabled: boolean) => { update(enabled, 'images') - if (enabled) toast('Show images for sets.', 4000) - else toast('Stopped showing images for sets.', 4000) + if (enabled) toast('Show images for sets.') + else toast('Stopped showing images for sets.') }, - [toast, update], + [update], ) const changeUnit = useCallback( (enabled: boolean) => { update(enabled, 'showUnit') - if (enabled) toast('Show option to select unit for sets.', 4000) - else toast('Hid unit option for sets.', 4000) + if (enabled) toast('Show option to select unit for sets.') + else toast('Hid unit option for sets.') }, - [toast, update], + [update], ) const changeSteps = useCallback( (enabled: boolean) => { update(enabled, 'steps') - if (enabled) toast('Show steps for a workout.', 4000) - else toast('Stopped showing steps for workouts.', 4000) + if (enabled) toast('Show steps for a workout.') + else toast('Stopped showing steps for workouts.') }, - [toast, update], + [update], ) const changeShowDate = useCallback( (enabled: boolean) => { update(enabled, 'showDate') - if (enabled) toast('Show date for sets by default.', 4000) - else toast('Stopped showing date for sets by default.', 4000) + if (enabled) toast('Show date for sets by default.') + else toast('Stopped showing date for sets by default.') }, - [toast, update], + [update], ) const changeShowSets = useCallback( (enabled: boolean) => { update(enabled, 'showSets') - if (enabled) toast('Show target sets for workouts.', 4000) - else toast('Stopped showing target sets for workouts.', 4000) + if (enabled) toast('Show target sets for workouts.') + else toast('Stopped showing target sets for workouts.') }, - [toast, update], + [update], ) const changeNoSound = useCallback( (enabled: boolean) => { update(enabled, 'noSound') - if (enabled) toast('Disable sound on rest timer alarms.', 4000) - else toast('Enabled sound for rest timer alarms.', 4000) + if (enabled) toast('Disable sound on rest timer alarms.') + else toast('Enabled sound for rest timer alarms.') }, - [toast, update], + [update], ) const switches: Input[] = [ @@ -183,10 +186,13 @@ export default function SettingsPage() { ) }, [term, settings.color, changeTheme, settings.theme]) - const changeColor = useCallback((value: string) => { - setSettings({...settings, color: value}) - settingsRepo.update({}, {color: value}) - }, []) + const changeColor = useCallback( + (value: string) => { + setSettings({...settings, color: value}) + settingsRepo.update({}, {color: value}) + }, + [setSettings, settings], + ) return ( <> diff --git a/StartPlan.tsx b/StartPlan.tsx index 5d8ba35..2d87dac 100644 --- a/StartPlan.tsx +++ b/StartPlan.tsx @@ -10,11 +10,11 @@ import {AppDataSource} from './data-source' import {getNow, setRepo} from './db' import GymSet from './gym-set' import MassiveInput from './MassiveInput' -import {useSnackbar} from './MassiveSnack' import {PlanPageParams} from './plan-page-params' import SetForm from './SetForm' import StackHeader from './StackHeader' import StartPlanItem from './StartPlanItem' +import {toast} from './toast' import {useSettings} from './use-settings' export default function StartPlan() { @@ -23,7 +23,6 @@ export default function StartPlan() { const [reps, setReps] = useState('') const [weight, setWeight] = useState('') const [unit, setUnit] = useState('kg') - const {toast} = useSnackbar() const [minutes, setMinutes] = useState(3) const [seconds, setSeconds] = useState(30) const [best, setBest] = useState() @@ -109,9 +108,9 @@ export default function StartPlan() { settings.notify && (+weight > best.weight || (+reps > best.reps && +weight === best.weight)) ) - toast("Great work King! That's a new record.", 5000) - else if (settings.alarm) toast('Resting...', 3000) - else toast('Added set', 3000) + toast("Great work King! That's a new record.") + else if (settings.alarm) toast('Resting...') + else toast('Added set') if (!settings.alarm) return const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000 const {vibrate, sound, noSound} = settings @@ -119,14 +118,11 @@ export default function StartPlan() { NativeModules.AlarmModule.timer(...args) } - const handleUnit = useCallback( - (value: string) => { - setUnit(value.replace(/,|'/g, '')) - if (value.match(/,|'/)) - toast('Commas and single quotes would break CSV exports', 6000) - }, - [toast], - ) + const handleUnit = useCallback((value: string) => { + setUnit(value.replace(/,|'/g, '')) + if (value.match(/,|'/)) + toast('Commas and single quotes would break CSV exports') + }, []) return ( <> diff --git a/toast.ts b/toast.ts new file mode 100644 index 0000000..8227283 --- /dev/null +++ b/toast.ts @@ -0,0 +1,7 @@ +import {DeviceEventEmitter} from 'react-native' + +export const TOAST = 'toast' + +export function toast(value: string) { + DeviceEventEmitter.emit(TOAST, {value}) +}