Merge branch 'master' into intermediary-tables
This commit is contained in:
commit
e6542798ea
1
App.tsx
1
App.tsx
|
@ -58,6 +58,7 @@ const App = () => {
|
||||||
console.log(`${App.name}.useEffect:`, {gotSettings: settings})
|
console.log(`${App.name}.useEffect:`, {gotSettings: settings})
|
||||||
setTheme(settings.theme)
|
setTheme(settings.theme)
|
||||||
if (settings.lightColor) setLightColor(settings.lightColor)
|
if (settings.lightColor) setLightColor(settings.lightColor)
|
||||||
|
if (settings.darkColor) setDarkColor(settings.darkColor)
|
||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
}
|
}
|
||||||
init()
|
init()
|
||||||
|
|
124
DrawerMenu.tsx
124
DrawerMenu.tsx
|
@ -1,21 +1,11 @@
|
||||||
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||||
import {useCallback, useState} from 'react'
|
import {useCallback, useState} from 'react'
|
||||||
import DocumentPicker from 'react-native-document-picker'
|
import {IconButton, Menu} from 'react-native-paper'
|
||||||
import {FileSystem} from 'react-native-file-access'
|
|
||||||
import {Divider, IconButton, Menu} from 'react-native-paper'
|
|
||||||
import ConfirmDialog from './ConfirmDialog'
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
import {AppDataSource} from './data-source'
|
import {planRepo, setRepo} from './db'
|
||||||
import {planRepo} from './db'
|
|
||||||
import {DrawerParamList} from './drawer-param-list'
|
import {DrawerParamList} from './drawer-param-list'
|
||||||
import GymSet from './gym-set'
|
|
||||||
import {Plan} from './plan'
|
|
||||||
import {toast} from './toast'
|
import {toast} from './toast'
|
||||||
import useDark from './use-dark'
|
import useDark from './use-dark'
|
||||||
import {write} from './write'
|
|
||||||
|
|
||||||
const setFields = 'id,name,reps,weight,created,unit,hidden,sets,minutes,seconds'
|
|
||||||
const planFields = 'id,days,workouts'
|
|
||||||
const setRepo = AppDataSource.manager.getRepository(GymSet)
|
|
||||||
|
|
||||||
export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
|
export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
|
||||||
const [showMenu, setShowMenu] = useState(false)
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
|
@ -23,113 +13,6 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
|
||||||
const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
|
const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
|
||||||
const dark = useDark()
|
const dark = useDark()
|
||||||
|
|
||||||
const exportSets = useCallback(async () => {
|
|
||||||
const sets = await setRepo.find({})
|
|
||||||
const data = [setFields]
|
|
||||||
.concat(
|
|
||||||
sets.map(set =>
|
|
||||||
setFields
|
|
||||||
.split(',')
|
|
||||||
.map(fieldString => {
|
|
||||||
const field = fieldString as keyof GymSet
|
|
||||||
if (field === 'unit') return set[field] || 'kg'
|
|
||||||
return set[field]
|
|
||||||
})
|
|
||||||
.join(','),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.join('\n')
|
|
||||||
console.log(`${DrawerMenu.name}.exportSets`, {length: sets.length})
|
|
||||||
await write('sets.csv', data)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const exportPlans = useCallback(async () => {
|
|
||||||
const plans = await planRepo.find({})
|
|
||||||
const data = [planFields]
|
|
||||||
.concat(plans.map(set => `"${set.id}","${set.days}","${set.workouts}"`))
|
|
||||||
.join('\n')
|
|
||||||
console.log(`${DrawerMenu.name}.exportPlans`, {length: plans.length})
|
|
||||||
await write('plans.csv', data)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const download = useCallback(async () => {
|
|
||||||
setShowMenu(false)
|
|
||||||
if (name === 'Home') exportSets()
|
|
||||||
else if (name === 'Plans') exportPlans()
|
|
||||||
}, [name, exportSets, exportPlans])
|
|
||||||
|
|
||||||
const uploadSets = useCallback(async () => {
|
|
||||||
const result = await DocumentPicker.pickSingle()
|
|
||||||
const file = await FileSystem.readFile(result.uri)
|
|
||||||
console.log(`${DrawerMenu.name}.uploadSets:`, file.length)
|
|
||||||
const lines = file.split('\n')
|
|
||||||
if (!setFields.includes(lines[0])) return toast('Invalid csv.')
|
|
||||||
const values = lines
|
|
||||||
.slice(1)
|
|
||||||
.filter(line => line)
|
|
||||||
.map(line => {
|
|
||||||
let [
|
|
||||||
,
|
|
||||||
setName,
|
|
||||||
reps,
|
|
||||||
weight,
|
|
||||||
created,
|
|
||||||
unit,
|
|
||||||
hidden,
|
|
||||||
sets,
|
|
||||||
minutes,
|
|
||||||
seconds,
|
|
||||||
] = line.split(',')
|
|
||||||
const set: GymSet = {
|
|
||||||
name: setName,
|
|
||||||
reps: +reps,
|
|
||||||
weight: +weight,
|
|
||||||
created,
|
|
||||||
unit: unit ?? 'kg',
|
|
||||||
hidden: !!Number(hidden),
|
|
||||||
sets: +sets,
|
|
||||||
minutes: +minutes,
|
|
||||||
seconds: +seconds,
|
|
||||||
image: '',
|
|
||||||
}
|
|
||||||
return set
|
|
||||||
})
|
|
||||||
await setRepo.insert(values)
|
|
||||||
toast('Data imported.')
|
|
||||||
reset({index: 0, routes: [{name}]})
|
|
||||||
}, [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.')
|
|
||||||
const values = file
|
|
||||||
.split('\n')
|
|
||||||
.slice(1)
|
|
||||||
.filter(line => line)
|
|
||||||
.map(set => {
|
|
||||||
const [, days, workouts] = set
|
|
||||||
.split('","')
|
|
||||||
.map(cell => cell.replace(/"/g, ''))
|
|
||||||
const plan: Plan = {
|
|
||||||
days,
|
|
||||||
workouts,
|
|
||||||
}
|
|
||||||
return plan
|
|
||||||
})
|
|
||||||
await planRepo.insert(values)
|
|
||||||
toast('Data imported.')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const upload = useCallback(async () => {
|
|
||||||
setShowMenu(false)
|
|
||||||
if (name === 'Home') await uploadSets()
|
|
||||||
else if (name === 'Plans') await uploadPlans()
|
|
||||||
reset({index: 0, routes: [{name}]})
|
|
||||||
}, [name, uploadPlans, uploadSets, reset])
|
|
||||||
|
|
||||||
const remove = useCallback(async () => {
|
const remove = useCallback(async () => {
|
||||||
setShowMenu(false)
|
setShowMenu(false)
|
||||||
setShowRemove(false)
|
setShowRemove(false)
|
||||||
|
@ -151,9 +34,6 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
|
||||||
icon="more-vert"
|
icon="more-vert"
|
||||||
/>
|
/>
|
||||||
}>
|
}>
|
||||||
<Menu.Item icon="arrow-downward" onPress={download} title="Download" />
|
|
||||||
<Menu.Item icon="arrow-upward" onPress={upload} title="Upload" />
|
|
||||||
<Divider />
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon="delete"
|
icon="delete"
|
||||||
onPress={() => setShowRemove(true)}
|
onPress={() => setShowRemove(true)}
|
||||||
|
|
16
EditSet.tsx
16
EditSet.tsx
|
@ -99,18 +99,6 @@ export default function EditSet() {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleName = useCallback((value: string) => {
|
|
||||||
setName(value.replace(/,|'/g, ''))
|
|
||||||
if (value.match(/,|'/))
|
|
||||||
toast('Commas and single quotes would break CSV exports')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleUnit = useCallback((value: string) => {
|
|
||||||
setUnit(value.replace(/,|'/g, ''))
|
|
||||||
if (value.match(/,|'/))
|
|
||||||
toast('Commas and single quotes would break CSV exports')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const changeImage = useCallback(async () => {
|
const changeImage = useCallback(async () => {
|
||||||
const {fileCopyUri} = await DocumentPicker.pickSingle({
|
const {fileCopyUri} = await DocumentPicker.pickSingle({
|
||||||
type: DocumentPicker.types.images,
|
type: DocumentPicker.types.images,
|
||||||
|
@ -133,7 +121,7 @@ export default function EditSet() {
|
||||||
<MassiveInput
|
<MassiveInput
|
||||||
label="Name"
|
label="Name"
|
||||||
value={name}
|
value={name}
|
||||||
onChangeText={handleName}
|
onChangeText={setName}
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoFocus={!name}
|
autoFocus={!name}
|
||||||
onSubmitEditing={() => repsRef.current?.focus()}
|
onSubmitEditing={() => repsRef.current?.focus()}
|
||||||
|
@ -165,7 +153,7 @@ export default function EditSet() {
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
label="Unit"
|
label="Unit"
|
||||||
value={unit}
|
value={unit}
|
||||||
onChangeText={handleUnit}
|
onChangeText={setUnit}
|
||||||
innerRef={unitRef}
|
innerRef={unitRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {defaultSet} from './gym-set'
|
||||||
import MassiveInput from './MassiveInput'
|
import MassiveInput from './MassiveInput'
|
||||||
import Settings from './settings'
|
import Settings from './settings'
|
||||||
import StackHeader from './StackHeader'
|
import StackHeader from './StackHeader'
|
||||||
import {toast} from './toast'
|
|
||||||
import {WorkoutsPageParams} from './WorkoutsPage'
|
import {WorkoutsPageParams} from './WorkoutsPage'
|
||||||
|
|
||||||
export default function EditWorkout() {
|
export default function EditWorkout() {
|
||||||
|
@ -101,18 +100,6 @@ export default function EditWorkout() {
|
||||||
setShowRemove(false)
|
setShowRemove(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleName = (value: string) => {
|
|
||||||
setName(value.replace(/,|'/g, ''))
|
|
||||||
if (value.match(/,|'/))
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitName = () => {
|
const submitName = () => {
|
||||||
if (settings.steps) stepsRef.current?.focus()
|
if (settings.steps) stepsRef.current?.focus()
|
||||||
else setsRef.current?.focus()
|
else setsRef.current?.focus()
|
||||||
|
@ -127,7 +114,7 @@ export default function EditWorkout() {
|
||||||
autoFocus
|
autoFocus
|
||||||
label="Name"
|
label="Name"
|
||||||
value={name}
|
value={name}
|
||||||
onChangeText={handleName}
|
onChangeText={setName}
|
||||||
onSubmitEditing={submitName}
|
onSubmitEditing={submitName}
|
||||||
/>
|
/>
|
||||||
{settings?.steps && (
|
{settings?.steps && (
|
||||||
|
@ -135,7 +122,7 @@ export default function EditWorkout() {
|
||||||
innerRef={stepsRef}
|
innerRef={stepsRef}
|
||||||
selectTextOnFocus={false}
|
selectTextOnFocus={false}
|
||||||
value={steps}
|
value={steps}
|
||||||
onChangeText={handleSteps}
|
onChangeText={setSteps}
|
||||||
label="Steps"
|
label="Steps"
|
||||||
multiline
|
multiline
|
||||||
onSubmitEditing={() => setsRef.current?.focus()}
|
onSubmitEditing={() => setsRef.current?.focus()}
|
||||||
|
|
|
@ -66,7 +66,7 @@ export default function SetItem({
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
color: dark ? '#909090ff' : '#717171ff',
|
color: dark ? '#909090ff' : '#717171ff',
|
||||||
}}>
|
}}>
|
||||||
{format(new Date(item.created), settings.date)}
|
{format(new Date(item.created), settings.date || 'P')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Menu
|
<Menu
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {
|
||||||
|
NavigationProp,
|
||||||
|
useFocusEffect,
|
||||||
|
useNavigation,
|
||||||
|
} from '@react-navigation/native'
|
||||||
import {format} from 'date-fns'
|
import {format} from 'date-fns'
|
||||||
import {useCallback, useMemo, useState} from 'react'
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
|
@ -9,9 +13,13 @@ import {
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import DocumentPicker from 'react-native-document-picker'
|
import DocumentPicker from 'react-native-document-picker'
|
||||||
|
import {Dirs, FileSystem} from 'react-native-file-access'
|
||||||
import {Button, Subheading} from 'react-native-paper'
|
import {Button, Subheading} from 'react-native-paper'
|
||||||
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
import {ITEM_PADDING, MARGIN} from './constants'
|
import {ITEM_PADDING, MARGIN} from './constants'
|
||||||
|
import {AppDataSource} from './data-source'
|
||||||
import {settingsRepo} from './db'
|
import {settingsRepo} from './db'
|
||||||
|
import {DrawerParamList} from './drawer-param-list'
|
||||||
import DrawerHeader from './DrawerHeader'
|
import DrawerHeader from './DrawerHeader'
|
||||||
import Input from './input'
|
import Input from './input'
|
||||||
import {darkOptions, lightOptions, themeOptions} from './options'
|
import {darkOptions, lightOptions, themeOptions} from './options'
|
||||||
|
@ -39,6 +47,8 @@ export default function SettingsPage() {
|
||||||
const [showDate, setShowDate] = useState(false)
|
const [showDate, setShowDate] = useState(false)
|
||||||
const [noSound, setNoSound] = useState(false)
|
const [noSound, setNoSound] = useState(false)
|
||||||
const [formatOptions, setFormatOptions] = useState<string[]>(defaultFormats)
|
const [formatOptions, setFormatOptions] = useState<string[]>(defaultFormats)
|
||||||
|
const [importing, setImporting] = useState(false)
|
||||||
|
const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
|
@ -237,6 +247,21 @@ export default function SettingsPage() {
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const confirmImport = useCallback(async () => {
|
||||||
|
setImporting(false)
|
||||||
|
await AppDataSource.destroy()
|
||||||
|
const result = await DocumentPicker.pickSingle()
|
||||||
|
await FileSystem.cp(result.uri, Dirs.DatabaseDir + '/massive.db')
|
||||||
|
await AppDataSource.initialize()
|
||||||
|
reset({index: 0, routes: [{name: 'Settings'}]})
|
||||||
|
}, [reset])
|
||||||
|
|
||||||
|
const exportDatabase = useCallback(async () => {
|
||||||
|
const path = Dirs.DatabaseDir + '/massive.db'
|
||||||
|
await FileSystem.cpExternal(path, 'massive.db', 'downloads')
|
||||||
|
toast('Database exported. Check downloads.')
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Settings" />
|
<DrawerHeader name="Settings" />
|
||||||
|
@ -258,7 +283,28 @@ export default function SettingsPage() {
|
||||||
<Button onPress={changeSound}>{soundString || 'Default'}</Button>
|
<Button onPress={changeSound}>{soundString || 'Default'}</Button>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
{'export database'.includes(term.toLowerCase()) && (
|
||||||
|
<Button style={{alignSelf: 'flex-start'}} onPress={exportDatabase}>
|
||||||
|
Export database
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{'import database'.includes(term.toLowerCase()) && (
|
||||||
|
<Button
|
||||||
|
style={{alignSelf: 'flex-start'}}
|
||||||
|
onPress={() => setImporting(true)}>
|
||||||
|
Import database
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Are you sure?"
|
||||||
|
onOk={confirmImport}
|
||||||
|
setShow={setImporting}
|
||||||
|
show={importing}>
|
||||||
|
Importing a database overwrites your current data. This action cannot be
|
||||||
|
reversed!
|
||||||
|
</ConfirmDialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,12 +111,6 @@ export default function StartPlan() {
|
||||||
NativeModules.AlarmModule.timer(...args)
|
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')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader title={params.plan.days.replace(/,/g, ', ')} />
|
<StackHeader title={params.plan.days.replace(/,/g, ', ')} />
|
||||||
|
@ -146,7 +140,7 @@ export default function StartPlan() {
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
label="Unit"
|
label="Unit"
|
||||||
value={unit}
|
value={unit}
|
||||||
onChangeText={handleUnit}
|
onChangeText={setUnit}
|
||||||
innerRef={unitRef}
|
innerRef={unitRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -41,8 +41,8 @@ android {
|
||||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 36113
|
versionCode 36115
|
||||||
versionName "1.87"
|
versionName "1.89"
|
||||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||||
|
|
||||||
if (isNewArchitectureEnabled()) {
|
if (isNewArchitectureEnabled()) {
|
||||||
|
|
|
@ -156,6 +156,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
||||||
val manager = getManager()
|
val manager = getManager()
|
||||||
manager.notify(NOTIFICATION_ID_DONE, builder.build())
|
manager.notify(NOTIFICATION_ID_DONE, builder.build())
|
||||||
manager.cancel(NOTIFICATION_ID_PENDING)
|
manager.cancel(NOTIFICATION_ID_PENDING)
|
||||||
|
Log.d("AlarmModule", "Finished: vibrate=$vibrate,sound=$sound,noSound=$noSound")
|
||||||
val alarmIntent = Intent(context, AlarmService::class.java).apply {
|
val alarmIntent = Intent(context, AlarmService::class.java).apply {
|
||||||
putExtra("vibrate", vibrate)
|
putExtra("vibrate", vibrate)
|
||||||
putExtra("sound", sound)
|
putExtra("sound", sound)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "massive",
|
"name": "massive",
|
||||||
"version": "1.87",
|
"version": "1.89",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user