Add ability to edit/delete multiple sets/plans

This commit is contained in:
Brandon Presley 2022-12-13 22:54:37 +13:00
parent c3b14e901d
commit 2e347deb53
10 changed files with 259 additions and 95 deletions

View File

@ -6,13 +6,20 @@ export default function ConfirmDialog({
onOk,
show,
setShow,
onCancel,
}: {
title: string
children: JSX.Element | JSX.Element[] | string
onOk: () => void
show: boolean
setShow: (show: boolean) => void
onCancel?: () => void
}) {
const cancel = () => {
setShow(false)
onCancel && onCancel()
}
return (
<Portal>
<Dialog visible={show} onDismiss={() => setShow(false)}>
@ -22,7 +29,7 @@ export default function ConfirmDialog({
</Dialog.Content>
<Dialog.Actions>
<Button onPress={onOk}>OK</Button>
<Button onPress={() => setShow(false)}>Cancel</Button>
<Button onPress={cancel}>Cancel</Button>
</Dialog.Actions>
</Dialog>
</Portal>

View File

@ -5,7 +5,13 @@ import {DrawerParamList} from './drawer-param-list'
import DrawerMenu from './DrawerMenu'
import useDark from './use-dark'
export default function DrawerHeader({name}: {name: keyof DrawerParamList}) {
export default function DrawerHeader({
name,
ids,
}: {
name: keyof DrawerParamList
ids: number[]
}) {
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>()
const dark = useDark()
@ -17,7 +23,7 @@ export default function DrawerHeader({name}: {name: keyof DrawerParamList}) {
onPress={navigation.openDrawer}
/>
<Appbar.Content title={name} />
<DrawerMenu name={name} />
<DrawerMenu name={name} ids={ids} />
</Appbar.Header>
)
}

View File

@ -4,23 +4,34 @@ import {IconButton, Menu} from 'react-native-paper'
import ConfirmDialog from './ConfirmDialog'
import {planRepo, setRepo} from './db'
import {DrawerParamList} from './drawer-param-list'
import {toast} from './toast'
import {HomePageParams} from './home-page-params'
import useDark from './use-dark'
export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
export default function DrawerMenu({
name,
ids,
}: {
name: keyof DrawerParamList
ids: number[]
}) {
const [showMenu, setShowMenu] = useState(false)
const [showRemove, setShowRemove] = useState(false)
const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
const {navigate} = useNavigation<NavigationProp<HomePageParams>>()
const dark = useDark()
const remove = useCallback(async () => {
setShowMenu(false)
setShowRemove(false)
if (name === 'Home') await setRepo.delete({})
else if (name === 'Plans') await planRepo.delete({})
toast('All data has been deleted.')
if (name === 'Home') await setRepo.delete(ids.length > 0 ? ids : {})
else if (name === 'Plans') await planRepo.delete(ids.length > 0 ? ids : {})
reset({index: 0, routes: [{name}]})
}, [reset, name])
}, [reset, name, ids])
const edit = useCallback(() => {
navigate('EditSets', {ids})
setShowMenu(false)
}, [ids, navigate])
if (name === 'Home' || name === 'Plans')
return (
@ -39,12 +50,22 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
onPress={() => setShowRemove(true)}
title="Delete"
/>
{ids.length > 0 && name === 'Home' && (
<Menu.Item icon="edit" title="Edit" onPress={edit} />
)}
<ConfirmDialog
title="Delete all data"
show={showRemove}
setShow={setShowRemove}
onOk={remove}>
This irreversibly deletes all data from the app. Are you sure?
onOk={remove}
onCancel={() => setShowMenu(false)}>
{ids.length === 0 ? (
<>This irreversibly deletes all data from the app. Are you sure?</>
) : (
<>This will delete {ids.length} records. Are you sure?</>
)}
</ConfirmDialog>
</Menu>
)

151
EditSets.tsx Normal file
View File

@ -0,0 +1,151 @@
import {
RouteProp,
useFocusEffect,
useNavigation,
useRoute,
} from '@react-navigation/native'
import {useCallback, useRef, useState} from 'react'
import {TextInput, View} from 'react-native'
import DocumentPicker from 'react-native-document-picker'
import {Button, Card, TouchableRipple} from 'react-native-paper'
import ConfirmDialog from './ConfirmDialog'
import {MARGIN, PADDING} from './constants'
import {setRepo, settingsRepo} from './db'
import GymSet from './gym-set'
import {HomePageParams} from './home-page-params'
import MassiveInput from './MassiveInput'
import Settings from './settings'
import StackHeader from './StackHeader'
export default function EditSets() {
const {params} = useRoute<RouteProp<HomePageParams, 'EditSets'>>()
const {ids} = params
const navigation = useNavigation()
const [settings, setSettings] = useState<Settings>({} as Settings)
const [name, setName] = useState('')
const [reps, setReps] = useState('')
const [weight, setWeight] = useState('')
const [newImage, setNewImage] = useState('')
const [unit, setUnit] = useState('')
const [showRemove, setShowRemove] = useState(false)
const weightRef = useRef<TextInput>(null)
const repsRef = useRef<TextInput>(null)
const unitRef = useRef<TextInput>(null)
const [selection, setSelection] = useState({
start: 0,
end: 1,
})
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({where: {}}).then(setSettings)
}, []),
)
const handleSubmit = async () => {
console.log(`${EditSets.name}.handleSubmit:`, {uri: newImage, name})
const update: Partial<GymSet> = {}
if (name) update.name = name
if (reps) update.reps = Number(reps)
if (weight) update.weight = Number(weight)
if (unit) update.unit = unit
if (newImage) update.image = newImage
await setRepo.update(ids, update)
navigation.goBack()
}
const changeImage = useCallback(async () => {
const {fileCopyUri} = await DocumentPicker.pickSingle({
type: DocumentPicker.types.images,
copyTo: 'documentDirectory',
})
if (fileCopyUri) setNewImage(fileCopyUri)
}, [])
const handleRemove = useCallback(async () => {
setNewImage('')
setShowRemove(false)
}, [])
return (
<>
<StackHeader title={`Edit ${ids.length} sets`} />
<View style={{padding: PADDING, flex: 1}}>
<MassiveInput
label="Name"
value={name}
onChangeText={setName}
autoCorrect={false}
autoFocus={!name}
onSubmitEditing={() => repsRef.current?.focus()}
/>
<MassiveInput
label="Reps"
keyboardType="numeric"
value={reps}
onChangeText={setReps}
onSubmitEditing={() => weightRef.current?.focus()}
selection={selection}
onSelectionChange={e => setSelection(e.nativeEvent.selection)}
autoFocus={!!name}
innerRef={repsRef}
/>
<MassiveInput
label="Weight"
keyboardType="numeric"
value={weight}
onChangeText={setWeight}
onSubmitEditing={handleSubmit}
innerRef={weightRef}
/>
{settings.showUnit && (
<MassiveInput
autoCapitalize="none"
label="Unit"
value={unit}
onChangeText={setUnit}
innerRef={unitRef}
/>
)}
{settings.images && newImage && (
<TouchableRipple
style={{marginBottom: MARGIN}}
onPress={changeImage}
onLongPress={() => setShowRemove(true)}>
<Card.Cover source={{uri: newImage}} />
</TouchableRipple>
)}
<ConfirmDialog
title="Remove image"
onOk={handleRemove}
show={showRemove}
setShow={setShowRemove}>
Are you sure you want to remove the image?
</ConfirmDialog>
{settings.images && !newImage && (
<Button
style={{marginBottom: MARGIN}}
onPress={changeImage}
icon="add-photo-alternate">
Image
</Button>
)}
</View>
<Button
mode="contained"
icon="save"
style={{margin: MARGIN}}
onPress={handleSubmit}>
Save
</Button>
</>
)
}

View File

@ -1,5 +1,6 @@
import {createStackNavigator} from '@react-navigation/stack'
import EditSet from './EditSet'
import EditSets from './EditSets'
import {HomePageParams} from './home-page-params'
import SetList from './SetList'
@ -11,6 +12,7 @@ export default function HomePage() {
screenOptions={{headerShown: false, animationEnabled: false}}>
<Stack.Screen name="Sets" component={SetList} />
<Stack.Screen name="EditSet" component={EditSet} />
<Stack.Screen name="EditSets" component={EditSets} />
</Stack.Navigator>
)
}

View File

@ -4,25 +4,26 @@ import {
useNavigation,
} from '@react-navigation/native'
import {useCallback, useMemo, useState} from 'react'
import {GestureResponderEvent, Text} from 'react-native'
import {Divider, List, Menu} from 'react-native-paper'
import {Text} from 'react-native'
import {List} from 'react-native-paper'
import {getBestSet} from './best.service'
import {planRepo} from './db'
import {defaultSet} from './gym-set'
import {Plan} from './plan'
import {PlanPageParams} from './plan-page-params'
import {DAYS} from './time'
import useDark from './use-dark'
export default function PlanItem({
item,
onRemove,
setIds,
ids,
}: {
item: Plan
onRemove: () => void
ids: number[]
setIds: (value: number[]) => void
}) {
const [show, setShow] = useState(false)
const [anchor, setAnchor] = useState({x: 0, y: 0})
const [today, setToday] = useState<string>()
const dark = useDark()
const days = useMemo(() => item.days.split(','), [item.days])
const navigation = useNavigation<NavigationProp<PlanPageParams>>()
@ -33,34 +34,22 @@ export default function PlanItem({
}, []),
)
const remove = useCallback(async () => {
if (typeof item.id === 'number') await planRepo.delete(item.id)
setShow(false)
onRemove()
}, [setShow, item.id, onRemove])
const start = useCallback(async () => {
console.log(`${PlanItem.name}.start:`, {item})
setShow(false)
const workout = item.workouts.split(',')[0]
let first = await getBestSet(workout)
if (!first) first = {...defaultSet, name: workout}
delete first.id
navigation.navigate('StartPlan', {plan: item, first})
}, [item, navigation])
if (ids.length === 0)
return navigation.navigate('StartPlan', {plan: item, first})
const removing = ids.find(id => id === item.id)
if (removing) setIds(ids.filter(id => id !== item.id))
else setIds([...ids, item.id])
}, [ids, setIds, item, navigation])
const longPress = useCallback(
(e: GestureResponderEvent) => {
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
setShow(true)
},
[setAnchor, setShow],
)
const edit = useCallback(() => {
setShow(false)
navigation.navigate('EditPlan', {plan: item})
}, [navigation, item])
const longPress = useCallback(() => {
if (ids.length > 0) return
setIds([item.id])
}, [ids.length, item.id, setIds])
const title = useMemo(
() =>
@ -84,12 +73,11 @@ export default function PlanItem({
[item.workouts],
)
const copy = useCallback(() => {
const plan: Plan = {...item}
delete plan.id
setShow(false)
navigation.navigate('EditPlan', {plan})
}, [navigation, item])
const backgroundColor = useMemo(() => {
if (!ids.includes(item.id)) return
if (dark) return '#c2c2c2'
return '#c2c2c2'
}, [dark, ids, item.id])
return (
<List.Item
@ -97,14 +85,7 @@ export default function PlanItem({
title={title}
description={description}
onLongPress={longPress}
right={() => (
<Menu anchor={anchor} visible={show} onDismiss={() => setShow(false)}>
<Menu.Item icon="edit" onPress={edit} title="Edit" />
<Menu.Item icon="content-copy" onPress={copy} title="Copy" />
<Divider />
<Menu.Item icon="delete" onPress={remove} title="Delete" />
</Menu>
)}
style={{backgroundColor}}
/>
)
}

View File

@ -17,6 +17,7 @@ import PlanItem from './PlanItem'
export default function PlanList() {
const [term, setTerm] = useState('')
const [plans, setPlans] = useState<Plan[]>()
const [ids, setIds] = useState<number[]>([])
const navigation = useNavigation<NavigationProp<PlanPageParams>>()
const refresh = useCallback(async (value: string) => {
@ -43,9 +44,9 @@ export default function PlanList() {
const renderItem = useCallback(
({item}: {item: Plan}) => (
<PlanItem item={item} key={item.id} onRemove={() => refresh(term)} />
<PlanItem ids={ids} setIds={setIds} item={item} key={item.id} />
),
[refresh, term],
[ids],
)
const onAdd = () =>
@ -53,7 +54,7 @@ export default function PlanList() {
return (
<>
<DrawerHeader name="Plans" />
<DrawerHeader name="Plans" ids={ids} />
<Page onAdd={onAdd} term={term} search={search}>
{plans?.length === 0 ? (
<List.Item

View File

@ -1,57 +1,54 @@
import {NavigationProp, useNavigation} from '@react-navigation/native'
import {useCallback, useState} from 'react'
import {GestureResponderEvent, Image} from 'react-native'
import {Divider, List, Menu, Text} from 'react-native-paper'
import {setRepo} from './db'
import {format} from 'date-fns'
import {useCallback, useMemo} from 'react'
import {Image} from 'react-native'
import {List, Text} from 'react-native-paper'
import GymSet from './gym-set'
import {HomePageParams} from './home-page-params'
import Settings from './settings'
import useDark from './use-dark'
import {format} from 'date-fns'
export default function SetItem({
item,
onRemove,
settings,
ids,
setIds,
}: {
item: GymSet
onRemove: () => void
settings: Settings
ids: number[]
setIds: (value: number[]) => void
}) {
const [showMenu, setShowMenu] = useState(false)
const [anchor, setAnchor] = useState({x: 0, y: 0})
const dark = useDark()
const navigation = useNavigation<NavigationProp<HomePageParams>>()
const remove = useCallback(async () => {
console.log(`${SetItem.name}.remove:`, {id: item.id})
if (typeof item.id === 'number') await setRepo.delete(item.id)
setShowMenu(false)
onRemove()
}, [setShowMenu, onRemove, item.id])
const longPress = useCallback(() => {
if (ids.length > 0) return
setIds([item.id])
}, [ids.length, item.id, setIds])
const copy = useCallback(() => {
const set: GymSet = {...item}
delete set.id
setShowMenu(false)
navigation.navigate('EditSet', {set})
}, [navigation, item])
const press = useCallback(() => {
if (ids.length === 0) return navigation.navigate('EditSet', {set: item})
const removing = ids.find(id => id === item.id)
if (removing) setIds(ids.filter(id => id !== item.id))
else setIds([...ids, item.id])
}, [ids, item, navigation, setIds])
const longPress = useCallback(
(e: GestureResponderEvent) => {
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
setShowMenu(true)
},
[setShowMenu, setAnchor],
)
const backgroundColor = useMemo(() => {
if (!ids.includes(item.id)) return
if (dark) return '#c2c2c2'
return '#c2c2c2'
}, [dark, ids, item.id])
return (
<>
<List.Item
onPress={() => navigation.navigate('EditSet', {set: item})}
onPress={press}
title={item.name}
description={`${item.reps} x ${item.weight}${item.unit || 'kg'}`}
onLongPress={longPress}
style={{backgroundColor}}
left={() =>
settings.images &&
item.image && (
@ -69,14 +66,6 @@ export default function SetItem({
{format(new Date(item.created), settings.date || 'P')}
</Text>
)}
<Menu
anchor={anchor}
visible={showMenu}
onDismiss={() => setShowMenu(false)}>
<Menu.Item icon="content-copy" onPress={copy} title="Copy" />
<Divider />
<Menu.Item icon="delete" onPress={remove} title="Delete" />
</Menu>
</>
)}
/>

View File

@ -24,6 +24,7 @@ export default function SetList() {
const [term, setTerm] = useState('')
const [end, setEnd] = useState(false)
const [settings, setSettings] = useState<Settings>()
const [ids, setIds] = useState<number[]>([])
const navigation = useNavigation<NavigationProp<HomePageParams>>()
const refresh = useCallback(async (value: string) => {
@ -58,9 +59,11 @@ export default function SetList() {
item={item}
key={item.id}
onRemove={() => refresh(term)}
ids={ids}
setIds={setIds}
/>
),
[refresh, term, settings],
[refresh, term, settings, ids],
)
const next = useCallback(async () => {
@ -101,7 +104,7 @@ export default function SetList() {
return (
<>
<DrawerHeader name="Home" />
<DrawerHeader name="Home" ids={ids} />
<Page onAdd={onAdd} term={term} search={search}>
{sets?.length === 0 ? (
<List.Item

View File

@ -5,4 +5,7 @@ export type HomePageParams = {
EditSet: {
set: GymSet
}
EditSets: {
ids: number[]
}
}