forked from brandon.presley/Massive
Run prettier
Something happened with the deno formatter, I can't remember what! Hahahahahaahahaha
This commit is contained in:
parent
44283fc990
commit
f778426aba
104
App.tsx
104
App.tsx
|
@ -2,21 +2,21 @@ import {
|
|||
DarkTheme as NavigationDarkTheme,
|
||||
DefaultTheme as NavigationDefaultTheme,
|
||||
NavigationContainer,
|
||||
} from '@react-navigation/native'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { DeviceEventEmitter, useColorScheme } from 'react-native'
|
||||
} from "@react-navigation/native";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { DeviceEventEmitter, useColorScheme } from "react-native";
|
||||
import {
|
||||
MD3DarkTheme as PaperDarkTheme,
|
||||
MD3LightTheme as PaperDefaultTheme,
|
||||
Provider as PaperProvider,
|
||||
Snackbar,
|
||||
} from 'react-native-paper'
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
|
||||
import { AppDataSource } from './data-source'
|
||||
import { settingsRepo } from './db'
|
||||
import Routes from './Routes'
|
||||
import { TOAST } from './toast'
|
||||
import { ThemeContext } from './use-theme'
|
||||
} from "react-native-paper";
|
||||
import MaterialIcon from "react-native-vector-icons/MaterialIcons";
|
||||
import { AppDataSource } from "./data-source";
|
||||
import { settingsRepo } from "./db";
|
||||
import Routes from "./Routes";
|
||||
import { TOAST } from "./toast";
|
||||
import { ThemeContext } from "./use-theme";
|
||||
|
||||
export const CombinedDefaultTheme = {
|
||||
...NavigationDefaultTheme,
|
||||
|
@ -25,7 +25,7 @@ export const CombinedDefaultTheme = {
|
|||
...NavigationDefaultTheme.colors,
|
||||
...PaperDefaultTheme.colors,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const CombinedDarkTheme = {
|
||||
...NavigationDarkTheme,
|
||||
|
@ -34,58 +34,58 @@ export const CombinedDarkTheme = {
|
|||
...NavigationDarkTheme.colors,
|
||||
...PaperDarkTheme.colors,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const phoneTheme = useColorScheme()
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [snackbar, setSnackbar] = useState('')
|
||||
const [appTheme, setAppTheme] = useState('system')
|
||||
const phoneTheme = useColorScheme();
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [snackbar, setSnackbar] = useState("");
|
||||
const [appTheme, setAppTheme] = useState("system");
|
||||
|
||||
const [lightColor, setLightColor] = useState<string>(
|
||||
CombinedDefaultTheme.colors.primary,
|
||||
)
|
||||
CombinedDefaultTheme.colors.primary
|
||||
);
|
||||
|
||||
const [darkColor, setDarkColor] = useState<string>(
|
||||
CombinedDarkTheme.colors.primary,
|
||||
)
|
||||
CombinedDarkTheme.colors.primary
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize()
|
||||
const settings = await settingsRepo.findOne({ where: {} })
|
||||
setAppTheme(settings.theme)
|
||||
if (settings.lightColor) setLightColor(settings.lightColor)
|
||||
if (settings.darkColor) setDarkColor(settings.darkColor)
|
||||
setInitialized(true)
|
||||
})()
|
||||
(async () => {
|
||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||
const settings = await settingsRepo.findOne({ where: {} });
|
||||
setAppTheme(settings.theme);
|
||||
if (settings.lightColor) setLightColor(settings.lightColor);
|
||||
if (settings.darkColor) setDarkColor(settings.darkColor);
|
||||
setInitialized(true);
|
||||
})();
|
||||
const description = DeviceEventEmitter.addListener(
|
||||
TOAST,
|
||||
({ value }: { value: string }) => {
|
||||
setSnackbar(value)
|
||||
},
|
||||
)
|
||||
return description.remove
|
||||
}, [])
|
||||
setSnackbar(value);
|
||||
}
|
||||
);
|
||||
return description.remove;
|
||||
}, []);
|
||||
|
||||
const paperTheme = useMemo(() => {
|
||||
const darkTheme = lightColor
|
||||
? {
|
||||
...CombinedDarkTheme,
|
||||
colors: { ...CombinedDarkTheme.colors, primary: darkColor },
|
||||
}
|
||||
: CombinedDarkTheme
|
||||
...CombinedDarkTheme,
|
||||
colors: { ...CombinedDarkTheme.colors, primary: darkColor },
|
||||
}
|
||||
: CombinedDarkTheme;
|
||||
const lightTheme = lightColor
|
||||
? {
|
||||
...CombinedDefaultTheme,
|
||||
colors: { ...CombinedDefaultTheme.colors, primary: lightColor },
|
||||
}
|
||||
: CombinedDefaultTheme
|
||||
let value = phoneTheme === 'dark' ? darkTheme : lightTheme
|
||||
if (appTheme === 'dark') value = darkTheme
|
||||
else if (appTheme === 'light') value = lightTheme
|
||||
return value
|
||||
}, [phoneTheme, appTheme, lightColor, darkColor])
|
||||
...CombinedDefaultTheme,
|
||||
colors: { ...CombinedDefaultTheme.colors, primary: lightColor },
|
||||
}
|
||||
: CombinedDefaultTheme;
|
||||
let value = phoneTheme === "dark" ? darkTheme : lightTheme;
|
||||
if (appTheme === "dark") value = darkTheme;
|
||||
else if (appTheme === "light") value = lightTheme;
|
||||
return value;
|
||||
}, [phoneTheme, appTheme, lightColor, darkColor]);
|
||||
|
||||
return (
|
||||
<PaperProvider
|
||||
|
@ -111,18 +111,18 @@ const App = () => {
|
|||
|
||||
<Snackbar
|
||||
duration={3000}
|
||||
onDismiss={() => setSnackbar('')}
|
||||
onDismiss={() => setSnackbar("")}
|
||||
visible={!!snackbar}
|
||||
action={{
|
||||
label: 'Close',
|
||||
onPress: () => setSnackbar(''),
|
||||
label: "Close",
|
||||
onPress: () => setSnackbar(""),
|
||||
textColor: paperTheme.colors.background,
|
||||
}}
|
||||
>
|
||||
{snackbar}
|
||||
</Snackbar>
|
||||
</PaperProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
|
22
AppFab.tsx
22
AppFab.tsx
|
@ -1,31 +1,31 @@
|
|||
import { ComponentProps, useMemo } from 'react'
|
||||
import { FAB, useTheme } from 'react-native-paper'
|
||||
import { CombinedDarkTheme, CombinedDefaultTheme } from './App'
|
||||
import { lightColors } from './colors'
|
||||
import { ComponentProps, useMemo } from "react";
|
||||
import { FAB, useTheme } from "react-native-paper";
|
||||
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
||||
import { lightColors } from "./colors";
|
||||
|
||||
export default function AppFab(props: Partial<ComponentProps<typeof FAB>>) {
|
||||
const { colors } = useTheme()
|
||||
const { colors } = useTheme();
|
||||
|
||||
const fabColor = useMemo(
|
||||
() =>
|
||||
lightColors.map((color) => color.hex).includes(colors.primary)
|
||||
? CombinedDarkTheme.colors.background
|
||||
: CombinedDefaultTheme.colors.background,
|
||||
[colors.primary],
|
||||
)
|
||||
[colors.primary]
|
||||
);
|
||||
|
||||
return (
|
||||
<FAB
|
||||
icon='add'
|
||||
testID='add'
|
||||
icon="add"
|
||||
testID="add"
|
||||
color={fabColor}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
backgroundColor: colors.primary,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
22
AppInput.tsx
22
AppInput.tsx
|
@ -1,26 +1,26 @@
|
|||
import React, { ComponentProps, Ref } from 'react'
|
||||
import { TextInput } from 'react-native-paper'
|
||||
import { CombinedDefaultTheme } from './App'
|
||||
import { MARGIN } from './constants'
|
||||
import useDark from './use-dark'
|
||||
import React, { ComponentProps, Ref } from "react";
|
||||
import { TextInput } from "react-native-paper";
|
||||
import { CombinedDefaultTheme } from "./App";
|
||||
import { MARGIN } from "./constants";
|
||||
import useDark from "./use-dark";
|
||||
|
||||
function AppInput(
|
||||
props: Partial<ComponentProps<typeof TextInput>> & {
|
||||
innerRef?: Ref<any>
|
||||
},
|
||||
innerRef?: Ref<any>;
|
||||
}
|
||||
) {
|
||||
const dark = useDark()
|
||||
const dark = useDark();
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
selectionColor={dark ? '#2A2A2A' : CombinedDefaultTheme.colors.border}
|
||||
selectionColor={dark ? "#2A2A2A" : CombinedDefaultTheme.colors.border}
|
||||
style={{ marginBottom: MARGIN, minWidth: 100 }}
|
||||
selectTextOnFocus
|
||||
ref={props.innerRef}
|
||||
blurOnSubmit={false}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(AppInput)
|
||||
export default React.memo(AppInput);
|
||||
|
|
38
Chart.tsx
38
Chart.tsx
|
@ -1,11 +1,11 @@
|
|||
import { useTheme } from '@react-navigation/native'
|
||||
import * as shape from 'd3-shape'
|
||||
import { View } from 'react-native'
|
||||
import { Grid, LineChart, XAxis, YAxis } from 'react-native-svg-charts'
|
||||
import { CombinedDarkTheme, CombinedDefaultTheme } from './App'
|
||||
import { MARGIN, PADDING } from './constants'
|
||||
import GymSet from './gym-set'
|
||||
import useDark from './use-dark'
|
||||
import { useTheme } from "@react-navigation/native";
|
||||
import * as shape from "d3-shape";
|
||||
import { View } from "react-native";
|
||||
import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
|
||||
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import GymSet from "./gym-set";
|
||||
import useDark from "./use-dark";
|
||||
|
||||
export default function Chart({
|
||||
yData,
|
||||
|
@ -13,21 +13,21 @@ export default function Chart({
|
|||
xData,
|
||||
yFormat,
|
||||
}: {
|
||||
yData: number[]
|
||||
xData: GymSet[]
|
||||
xFormat: (value: any, index: number) => string
|
||||
yFormat: (value: any) => string
|
||||
yData: number[];
|
||||
xData: GymSet[];
|
||||
xFormat: (value: any, index: number) => string;
|
||||
yFormat: (value: any) => string;
|
||||
}) {
|
||||
const { colors } = useTheme()
|
||||
const dark = useDark()
|
||||
const { colors } = useTheme();
|
||||
const dark = useDark();
|
||||
const axesSvg = {
|
||||
fontSize: 10,
|
||||
fill: dark
|
||||
? CombinedDarkTheme.colors.text
|
||||
: CombinedDefaultTheme.colors.text,
|
||||
}
|
||||
const verticalContentInset = { top: 10, bottom: 10 }
|
||||
const xAxisHeight = 30
|
||||
};
|
||||
const verticalContentInset = { top: 10, bottom: 10 };
|
||||
const xAxisHeight = 30;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -35,7 +35,7 @@ export default function Chart({
|
|||
style={{
|
||||
height: 300,
|
||||
padding: PADDING,
|
||||
flexDirection: 'row',
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<YAxis
|
||||
|
@ -66,5 +66,5 @@ export default function Chart({
|
|||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Dialog, Portal, Text } from 'react-native-paper'
|
||||
import { Button, Dialog, Portal, Text } from "react-native-paper";
|
||||
|
||||
export default function ConfirmDialog({
|
||||
title,
|
||||
|
@ -8,17 +8,17 @@ export default function ConfirmDialog({
|
|||
setShow,
|
||||
onCancel,
|
||||
}: {
|
||||
title: string
|
||||
children: JSX.Element | JSX.Element[] | string
|
||||
onOk: () => void
|
||||
show: boolean
|
||||
setShow: (show: boolean) => void
|
||||
onCancel?: () => void
|
||||
title: string;
|
||||
children: JSX.Element | JSX.Element[] | string;
|
||||
onOk: () => void;
|
||||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
onCancel?: () => void;
|
||||
}) {
|
||||
const cancel = () => {
|
||||
setShow(false)
|
||||
onCancel && onCancel()
|
||||
}
|
||||
setShow(false);
|
||||
onCancel && onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
|
@ -33,5 +33,5 @@ export default function ConfirmDialog({
|
|||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
import { DrawerNavigationProp } from '@react-navigation/drawer'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Appbar, IconButton } from 'react-native-paper'
|
||||
import { DrawerParamList } from './drawer-param-list'
|
||||
import { DrawerNavigationProp } from "@react-navigation/drawer";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { Appbar, IconButton } from "react-native-paper";
|
||||
import { DrawerParamList } from "./drawer-param-list";
|
||||
|
||||
export default function DrawerHeader({
|
||||
name,
|
||||
children,
|
||||
}: {
|
||||
name: string
|
||||
children?: JSX.Element | JSX.Element[]
|
||||
name: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>()
|
||||
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>();
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
<IconButton
|
||||
icon='menu'
|
||||
onPress={navigation.openDrawer}
|
||||
/>
|
||||
<IconButton icon="menu" onPress={navigation.openDrawer} />
|
||||
<Appbar.Content title={name} />
|
||||
{children}
|
||||
</Appbar.Header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
142
EditPlan.tsx
142
EditPlan.tsx
|
@ -3,92 +3,94 @@ import {
|
|||
RouteProp,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ScrollView, StyleSheet, View } from 'react-native'
|
||||
import { Button, IconButton, Text } from 'react-native-paper'
|
||||
import { MARGIN, PADDING } from './constants'
|
||||
import { planRepo, setRepo } from './db'
|
||||
import { defaultSet } from './gym-set'
|
||||
import { PlanPageParams } from './plan-page-params'
|
||||
import StackHeader from './StackHeader'
|
||||
import Switch from './Switch'
|
||||
import { DAYS } from './time'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ScrollView, StyleSheet, View } from "react-native";
|
||||
import { Button, IconButton, Text } from "react-native-paper";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { planRepo, setRepo } from "./db";
|
||||
import { defaultSet } from "./gym-set";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import StackHeader from "./StackHeader";
|
||||
import Switch from "./Switch";
|
||||
import { DAYS } from "./time";
|
||||
|
||||
export default function EditPlan() {
|
||||
const { params } = useRoute<RouteProp<PlanPageParams, 'EditPlan'>>()
|
||||
const { plan } = params
|
||||
const { params } = useRoute<RouteProp<PlanPageParams, "EditPlan">>();
|
||||
const { plan } = params;
|
||||
const [days, setDays] = useState<string[]>(
|
||||
plan.days ? plan.days.split(',') : [],
|
||||
)
|
||||
plan.days ? plan.days.split(",") : []
|
||||
);
|
||||
const [workouts, setWorkouts] = useState<string[]>(
|
||||
plan.workouts ? plan.workouts.split(',') : [],
|
||||
)
|
||||
const [names, setNames] = useState<string[]>([])
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>()
|
||||
plan.workouts ? plan.workouts.split(",") : []
|
||||
);
|
||||
const [names, setNames] = useState<string[]>([]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
useEffect(() => {
|
||||
setRepo
|
||||
.createQueryBuilder()
|
||||
.select('name')
|
||||
.select("name")
|
||||
.distinct(true)
|
||||
.orderBy('name')
|
||||
.orderBy("name")
|
||||
.getRawMany()
|
||||
.then((values) => {
|
||||
console.log(EditPlan.name, { values })
|
||||
setNames(values.map((value) => value.name))
|
||||
})
|
||||
}, [])
|
||||
console.log(EditPlan.name, { values });
|
||||
setNames(values.map((value) => value.name));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const save = useCallback(async () => {
|
||||
console.log(`${EditPlan.name}.save`, { days, workouts, plan })
|
||||
if (!days || !workouts) return
|
||||
const newWorkouts = workouts.filter((workout) => workout).join(',')
|
||||
const newDays = days.filter((day) => day).join(',')
|
||||
await planRepo.save({ days: newDays, workouts: newWorkouts, id: plan.id })
|
||||
}, [days, workouts, plan])
|
||||
console.log(`${EditPlan.name}.save`, { days, workouts, plan });
|
||||
if (!days || !workouts) return;
|
||||
const newWorkouts = workouts.filter((workout) => workout).join(",");
|
||||
const newDays = days.filter((day) => day).join(",");
|
||||
await planRepo.save({ days: newDays, workouts: newWorkouts, id: plan.id });
|
||||
}, [days, workouts, plan]);
|
||||
|
||||
const toggleWorkout = useCallback(
|
||||
(on: boolean, name: string) => {
|
||||
if (on) {
|
||||
setWorkouts([...workouts, name])
|
||||
setWorkouts([...workouts, name]);
|
||||
} else {
|
||||
setWorkouts(workouts.filter((workout) => workout !== name))
|
||||
setWorkouts(workouts.filter((workout) => workout !== name));
|
||||
}
|
||||
},
|
||||
[setWorkouts, workouts],
|
||||
)
|
||||
[setWorkouts, workouts]
|
||||
);
|
||||
|
||||
const toggleDay = useCallback(
|
||||
(on: boolean, day: string) => {
|
||||
if (on) {
|
||||
setDays([...days, day])
|
||||
setDays([...days, day]);
|
||||
} else {
|
||||
setDays(days.filter((d) => d !== day))
|
||||
setDays(days.filter((d) => d !== day));
|
||||
}
|
||||
},
|
||||
[setDays, days],
|
||||
)
|
||||
[setDays, days]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader
|
||||
title={typeof plan.id === 'number' ? 'Edit plan' : 'Add plan'}
|
||||
title={typeof plan.id === "number" ? "Edit plan" : "Add plan"}
|
||||
>
|
||||
{typeof plan.id === 'number' && (
|
||||
{typeof plan.id === "number" && (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await save()
|
||||
const newPlan = await planRepo.findOne({ where: { id: plan.id } })
|
||||
await save();
|
||||
const newPlan = await planRepo.findOne({
|
||||
where: { id: plan.id },
|
||||
});
|
||||
let first = await setRepo.findOne({
|
||||
where: { name: workouts[0] },
|
||||
order: { created: 'desc' },
|
||||
})
|
||||
if (!first) first = { ...defaultSet, name: workouts[0] }
|
||||
delete first.id
|
||||
navigation.navigate('StartPlan', { plan: newPlan, first })
|
||||
order: { created: "desc" },
|
||||
});
|
||||
if (!first) first = { ...defaultSet, name: workouts[0] };
|
||||
delete first.id;
|
||||
navigation.navigate("StartPlan", { plan: newPlan, first });
|
||||
}}
|
||||
icon='play-arrow'
|
||||
icon="play-arrow"
|
||||
/>
|
||||
)}
|
||||
</StackHeader>
|
||||
|
@ -104,39 +106,37 @@ export default function EditPlan() {
|
|||
/>
|
||||
))}
|
||||
<Text style={[styles.title, { marginTop: MARGIN }]}>Workouts</Text>
|
||||
{names.length === 0
|
||||
? (
|
||||
<View>
|
||||
<Text>No workouts found.</Text>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
names.map((name) => (
|
||||
<Switch
|
||||
key={name}
|
||||
onChange={(value) => toggleWorkout(value, name)}
|
||||
value={workouts.includes(name)}
|
||||
title={name}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{names.length === 0 ? (
|
||||
<View>
|
||||
<Text>No workouts found.</Text>
|
||||
</View>
|
||||
) : (
|
||||
names.map((name) => (
|
||||
<Switch
|
||||
key={name}
|
||||
onChange={(value) => toggleWorkout(value, name)}
|
||||
value={workouts.includes(name)}
|
||||
title={name}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
disabled={workouts.length === 0 && days.length === 0}
|
||||
style={styles.button}
|
||||
mode='outlined'
|
||||
icon='save'
|
||||
mode="outlined"
|
||||
icon="save"
|
||||
onPress={async () => {
|
||||
await save()
|
||||
navigation.navigate('PlanList')
|
||||
await save();
|
||||
navigation.navigate("PlanList");
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -145,4 +145,4 @@ const styles = StyleSheet.create({
|
|||
marginBottom: MARGIN,
|
||||
},
|
||||
button: {},
|
||||
})
|
||||
});
|
||||
|
|
124
EditSets.tsx
124
EditSets.tsx
|
@ -3,78 +3,78 @@ import {
|
|||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import { Button, Card, TouchableRipple } from 'react-native-paper'
|
||||
import { In } from 'typeorm'
|
||||
import AppInput from './AppInput'
|
||||
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 Settings from './settings'
|
||||
import StackHeader from './StackHeader'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { Button, Card, TouchableRipple } from "react-native-paper";
|
||||
import { In } from "typeorm";
|
||||
import AppInput from "./AppInput";
|
||||
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 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 [names, setNames] = useState('')
|
||||
const [oldReps, setOldReps] = useState('')
|
||||
const [weights, setWeights] = useState('')
|
||||
const [units, setUnits] = useState('')
|
||||
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 [names, setNames] = useState("");
|
||||
const [oldReps, setOldReps] = useState("");
|
||||
const [weights, setWeights] = useState("");
|
||||
const [units, setUnits] = useState("");
|
||||
|
||||
const [selection, setSelection] = useState({
|
||||
start: 0,
|
||||
end: 1,
|
||||
})
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
setRepo.find({ where: { id: In(ids) } }).then((sets) => {
|
||||
setNames(sets.map((set) => set.name).join(', '))
|
||||
setOldReps(sets.map((set) => set.reps).join(', '))
|
||||
setWeights(sets.map((set) => set.weight).join(', '))
|
||||
setUnits(sets.map((set) => set.unit).join(', '))
|
||||
})
|
||||
}, [ids]),
|
||||
)
|
||||
setNames(sets.map((set) => set.name).join(", "));
|
||||
setOldReps(sets.map((set) => set.reps).join(", "));
|
||||
setWeights(sets.map((set) => set.weight).join(", "));
|
||||
setUnits(sets.map((set) => set.unit).join(", "));
|
||||
});
|
||||
}, [ids])
|
||||
);
|
||||
|
||||
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
|
||||
if (Object.keys(update).length > 0) await setRepo.update(ids, update)
|
||||
navigation.goBack()
|
||||
}
|
||||
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;
|
||||
if (Object.keys(update).length > 0) 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)
|
||||
}, [])
|
||||
copyTo: "documentDirectory",
|
||||
});
|
||||
if (fileCopyUri) setNewImage(fileCopyUri);
|
||||
}, []);
|
||||
|
||||
const handleRemove = useCallback(async () => {
|
||||
setNewImage('')
|
||||
setShowRemove(false)
|
||||
}, [])
|
||||
setNewImage("");
|
||||
setShowRemove(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -91,7 +91,7 @@ export default function EditSets() {
|
|||
|
||||
<AppInput
|
||||
label={`Reps: ${oldReps}`}
|
||||
keyboardType='numeric'
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={setReps}
|
||||
selection={selection}
|
||||
|
@ -101,7 +101,7 @@ export default function EditSets() {
|
|||
|
||||
<AppInput
|
||||
label={`Weights: ${weights}`}
|
||||
keyboardType='numeric'
|
||||
keyboardType="numeric"
|
||||
value={weight}
|
||||
onChangeText={setWeight}
|
||||
onSubmitEditing={handleSubmit}
|
||||
|
@ -109,7 +109,7 @@ export default function EditSets() {
|
|||
|
||||
{settings.showUnit && (
|
||||
<AppInput
|
||||
autoCapitalize='none'
|
||||
autoCapitalize="none"
|
||||
label={`Units: ${units}`}
|
||||
value={unit}
|
||||
onChangeText={setUnit}
|
||||
|
@ -126,7 +126,7 @@ export default function EditSets() {
|
|||
</TouchableRipple>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
title='Remove image'
|
||||
title="Remove image"
|
||||
onOk={handleRemove}
|
||||
show={showRemove}
|
||||
setShow={setShowRemove}
|
||||
|
@ -138,7 +138,7 @@ export default function EditSets() {
|
|||
<Button
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
icon='add-photo-alternate'
|
||||
icon="add-photo-alternate"
|
||||
>
|
||||
Image
|
||||
</Button>
|
||||
|
@ -146,13 +146,13 @@ export default function EditSets() {
|
|||
</View>
|
||||
|
||||
<Button
|
||||
mode='outlined'
|
||||
icon='save'
|
||||
mode="outlined"
|
||||
icon="save"
|
||||
style={{ margin: MARGIN }}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
142
EditWorkout.tsx
142
EditWorkout.tsx
|
@ -3,46 +3,46 @@ import {
|
|||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { ScrollView, TextInput, View } from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import { Button, Card, TouchableRipple } from 'react-native-paper'
|
||||
import AppInput from './AppInput'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import { MARGIN, PADDING } from './constants'
|
||||
import { getNow, planRepo, setRepo, settingsRepo } from './db'
|
||||
import { defaultSet } from './gym-set'
|
||||
import Settings from './settings'
|
||||
import StackHeader from './StackHeader'
|
||||
import { WorkoutsPageParams } from './WorkoutsPage'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { ScrollView, TextInput, View } from "react-native";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { Button, Card, TouchableRipple } from "react-native-paper";
|
||||
import AppInput from "./AppInput";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { getNow, planRepo, setRepo, settingsRepo } from "./db";
|
||||
import { defaultSet } from "./gym-set";
|
||||
import Settings from "./settings";
|
||||
import StackHeader from "./StackHeader";
|
||||
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||
|
||||
export default function EditWorkout() {
|
||||
const { params } = useRoute<RouteProp<WorkoutsPageParams, 'EditWorkout'>>()
|
||||
const [removeImage, setRemoveImage] = useState(false)
|
||||
const [showRemove, setShowRemove] = useState(false)
|
||||
const [name, setName] = useState(params.value.name)
|
||||
const [steps, setSteps] = useState(params.value.steps)
|
||||
const [uri, setUri] = useState(params.value.image)
|
||||
const { params } = useRoute<RouteProp<WorkoutsPageParams, "EditWorkout">>();
|
||||
const [removeImage, setRemoveImage] = useState(false);
|
||||
const [showRemove, setShowRemove] = useState(false);
|
||||
const [name, setName] = useState(params.value.name);
|
||||
const [steps, setSteps] = useState(params.value.steps);
|
||||
const [uri, setUri] = useState(params.value.image);
|
||||
const [minutes, setMinutes] = useState(
|
||||
params.value.minutes?.toString() ?? '3',
|
||||
)
|
||||
params.value.minutes?.toString() ?? "3"
|
||||
);
|
||||
const [seconds, setSeconds] = useState(
|
||||
params.value.seconds?.toString() ?? '30',
|
||||
)
|
||||
const [sets, setSets] = useState(params.value.sets?.toString() ?? '3')
|
||||
const navigation = useNavigation()
|
||||
const setsRef = useRef<TextInput>(null)
|
||||
const stepsRef = useRef<TextInput>(null)
|
||||
const minutesRef = useRef<TextInput>(null)
|
||||
const secondsRef = useRef<TextInput>(null)
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
params.value.seconds?.toString() ?? "30"
|
||||
);
|
||||
const [sets, setSets] = useState(params.value.sets?.toString() ?? "3");
|
||||
const navigation = useNavigation();
|
||||
const setsRef = useRef<TextInput>(null);
|
||||
const stepsRef = useRef<TextInput>(null);
|
||||
const minutesRef = useRef<TextInput>(null);
|
||||
const secondsRef = useRef<TextInput>(null);
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings)
|
||||
}, []),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const update = async () => {
|
||||
await setRepo.update(
|
||||
|
@ -53,20 +53,20 @@ export default function EditWorkout() {
|
|||
minutes: +minutes,
|
||||
seconds: +seconds,
|
||||
steps,
|
||||
image: removeImage ? '' : uri,
|
||||
},
|
||||
)
|
||||
image: removeImage ? "" : uri,
|
||||
}
|
||||
);
|
||||
await planRepo.query(
|
||||
`UPDATE plans
|
||||
SET workouts = REPLACE(workouts, $1, $2)
|
||||
WHERE workouts LIKE $3`,
|
||||
[params.value.name, name, `%${params.value.name}%`],
|
||||
)
|
||||
navigation.goBack()
|
||||
}
|
||||
[params.value.name, name, `%${params.value.name}%`]
|
||||
);
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
const now = await getNow()
|
||||
const now = await getNow();
|
||||
await setRepo.save({
|
||||
...defaultSet,
|
||||
name,
|
||||
|
@ -77,42 +77,42 @@ export default function EditWorkout() {
|
|||
sets: sets ? +sets : 3,
|
||||
steps,
|
||||
created: now,
|
||||
})
|
||||
navigation.goBack()
|
||||
}
|
||||
});
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (params.value.name) return update()
|
||||
return add()
|
||||
}
|
||||
if (params.value.name) return update();
|
||||
return add();
|
||||
};
|
||||
|
||||
const changeImage = useCallback(async () => {
|
||||
const { fileCopyUri } = await DocumentPicker.pickSingle({
|
||||
type: DocumentPicker.types.images,
|
||||
copyTo: 'documentDirectory',
|
||||
})
|
||||
if (fileCopyUri) setUri(fileCopyUri)
|
||||
}, [])
|
||||
copyTo: "documentDirectory",
|
||||
});
|
||||
if (fileCopyUri) setUri(fileCopyUri);
|
||||
}, []);
|
||||
|
||||
const handleRemove = useCallback(async () => {
|
||||
setUri('')
|
||||
setRemoveImage(true)
|
||||
setShowRemove(false)
|
||||
}, [])
|
||||
setUri("");
|
||||
setRemoveImage(true);
|
||||
setShowRemove(false);
|
||||
}, []);
|
||||
|
||||
const submitName = () => {
|
||||
if (settings.steps) stepsRef.current?.focus()
|
||||
else setsRef.current?.focus()
|
||||
}
|
||||
if (settings.steps) stepsRef.current?.focus();
|
||||
else setsRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title={params.value.name ? 'Edit workout' : 'Add workout'} />
|
||||
<StackHeader title={params.value.name ? "Edit workout" : "Add workout"} />
|
||||
<View style={{ padding: PADDING, flex: 1 }}>
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<AppInput
|
||||
autoFocus
|
||||
label='Name'
|
||||
label="Name"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
onSubmitEditing={submitName}
|
||||
|
@ -123,7 +123,7 @@ export default function EditWorkout() {
|
|||
selectTextOnFocus={false}
|
||||
value={steps}
|
||||
onChangeText={setSteps}
|
||||
label='Steps'
|
||||
label="Steps"
|
||||
multiline
|
||||
onSubmitEditing={() => setsRef.current?.focus()}
|
||||
/>
|
||||
|
@ -132,8 +132,8 @@ export default function EditWorkout() {
|
|||
innerRef={setsRef}
|
||||
value={sets}
|
||||
onChangeText={setSets}
|
||||
label='Sets per workout'
|
||||
keyboardType='numeric'
|
||||
label="Sets per workout"
|
||||
keyboardType="numeric"
|
||||
onSubmitEditing={() => minutesRef.current?.focus()}
|
||||
/>
|
||||
{settings?.alarm && (
|
||||
|
@ -143,15 +143,15 @@ export default function EditWorkout() {
|
|||
onSubmitEditing={() => secondsRef.current?.focus()}
|
||||
value={minutes}
|
||||
onChangeText={setMinutes}
|
||||
label='Rest minutes'
|
||||
keyboardType='numeric'
|
||||
label="Rest minutes"
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<AppInput
|
||||
innerRef={secondsRef}
|
||||
value={seconds}
|
||||
onChangeText={setSeconds}
|
||||
label='Rest seconds'
|
||||
keyboardType='numeric'
|
||||
label="Rest seconds"
|
||||
keyboardType="numeric"
|
||||
blurOnSubmit
|
||||
/>
|
||||
</>
|
||||
|
@ -169,17 +169,17 @@ export default function EditWorkout() {
|
|||
<Button
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
icon='add-photo-alternate'
|
||||
icon="add-photo-alternate"
|
||||
>
|
||||
Image
|
||||
</Button>
|
||||
)}
|
||||
</ScrollView>
|
||||
<Button disabled={!name} mode='outlined' icon='save' onPress={save}>
|
||||
<Button disabled={!name} mode="outlined" icon="save" onPress={save}>
|
||||
Save
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
title='Remove image'
|
||||
title="Remove image"
|
||||
onOk={handleRemove}
|
||||
show={showRemove}
|
||||
setShow={setShowRemove}
|
||||
|
@ -188,5 +188,5 @@ export default function EditWorkout() {
|
|||
</ConfirmDialog>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { createStackNavigator } from '@react-navigation/stack'
|
||||
import GraphsList from './GraphsList'
|
||||
import GymSet from './gym-set'
|
||||
import ViewGraph from './ViewGraph'
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import GraphsList from "./GraphsList";
|
||||
import GymSet from "./gym-set";
|
||||
import ViewGraph from "./ViewGraph";
|
||||
|
||||
const Stack = createStackNavigator<GraphsPageParams>()
|
||||
const Stack = createStackNavigator<GraphsPageParams>();
|
||||
export type GraphsPageParams = {
|
||||
GraphsList: {}
|
||||
GraphsList: {};
|
||||
ViewGraph: {
|
||||
best: GymSet
|
||||
}
|
||||
}
|
||||
best: GymSet;
|
||||
};
|
||||
};
|
||||
|
||||
export default function GraphsPage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
||||
>
|
||||
<Stack.Screen name='GraphsList' component={GraphsList} />
|
||||
<Stack.Screen name='ViewGraph' component={ViewGraph} />
|
||||
<Stack.Screen name="GraphsList" component={GraphsList} />
|
||||
<Stack.Screen name="ViewGraph" component={ViewGraph} />
|
||||
</Stack.Navigator>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
20
HomePage.tsx
20
HomePage.tsx
|
@ -1,19 +1,19 @@
|
|||
import { createStackNavigator } from '@react-navigation/stack'
|
||||
import EditSet from './EditSet'
|
||||
import EditSets from './EditSets'
|
||||
import { HomePageParams } from './home-page-params'
|
||||
import SetList from './SetList'
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import EditSet from "./EditSet";
|
||||
import EditSets from "./EditSets";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import SetList from "./SetList";
|
||||
|
||||
const Stack = createStackNavigator<HomePageParams>()
|
||||
const Stack = createStackNavigator<HomePageParams>();
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
||||
>
|
||||
<Stack.Screen name='Sets' component={SetList} />
|
||||
<Stack.Screen name='EditSet' component={EditSet} />
|
||||
<Stack.Screen name='EditSets' component={EditSets} />
|
||||
<Stack.Screen name="Sets" component={SetList} />
|
||||
<Stack.Screen name="EditSet" component={EditSet} />
|
||||
<Stack.Screen name="EditSets" component={EditSets} />
|
||||
</Stack.Navigator>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
89
ListMenu.tsx
89
ListMenu.tsx
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { Divider, IconButton, Menu } from 'react-native-paper'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import { useState } from "react";
|
||||
import { Divider, IconButton, Menu } from "react-native-paper";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
|
||||
export default function ListMenu({
|
||||
onEdit,
|
||||
|
@ -10,88 +10,85 @@ export default function ListMenu({
|
|||
onSelect,
|
||||
ids,
|
||||
}: {
|
||||
onEdit: () => void
|
||||
onCopy: () => void
|
||||
onClear: () => void
|
||||
onDelete: () => void
|
||||
onSelect: () => void
|
||||
ids?: number[]
|
||||
onEdit: () => void;
|
||||
onCopy: () => void;
|
||||
onClear: () => void;
|
||||
onDelete: () => void;
|
||||
onSelect: () => void;
|
||||
ids?: number[];
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const [showRemove, setShowRemove] = useState(false)
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [showRemove, setShowRemove] = useState(false);
|
||||
|
||||
const edit = () => {
|
||||
setShowMenu(false)
|
||||
onEdit()
|
||||
}
|
||||
setShowMenu(false);
|
||||
onEdit();
|
||||
};
|
||||
|
||||
const copy = () => {
|
||||
setShowMenu(false)
|
||||
onCopy()
|
||||
}
|
||||
setShowMenu(false);
|
||||
onCopy();
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
setShowMenu(false)
|
||||
onClear()
|
||||
}
|
||||
setShowMenu(false);
|
||||
onClear();
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
setShowMenu(false)
|
||||
setShowRemove(false)
|
||||
onDelete()
|
||||
}
|
||||
setShowMenu(false);
|
||||
setShowRemove(false);
|
||||
onDelete();
|
||||
};
|
||||
|
||||
const select = () => {
|
||||
onSelect()
|
||||
}
|
||||
onSelect();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
visible={showMenu}
|
||||
onDismiss={() => setShowMenu(false)}
|
||||
anchor={
|
||||
<IconButton
|
||||
onPress={() => setShowMenu(true)}
|
||||
icon='more-vert'
|
||||
/>
|
||||
}
|
||||
anchor={<IconButton onPress={() => setShowMenu(true)} icon="more-vert" />}
|
||||
>
|
||||
<Menu.Item leadingIcon='done-all' title='Select all' onPress={select} />
|
||||
<Menu.Item leadingIcon="done-all" title="Select all" onPress={select} />
|
||||
<Menu.Item
|
||||
leadingIcon='clear'
|
||||
title='Clear'
|
||||
leadingIcon="clear"
|
||||
title="Clear"
|
||||
onPress={clear}
|
||||
disabled={ids?.length === 0}
|
||||
/>
|
||||
<Menu.Item
|
||||
leadingIcon='edit'
|
||||
title='Edit'
|
||||
leadingIcon="edit"
|
||||
title="Edit"
|
||||
onPress={edit}
|
||||
disabled={ids?.length === 0}
|
||||
/>
|
||||
<Menu.Item
|
||||
leadingIcon='content-copy'
|
||||
title='Copy'
|
||||
leadingIcon="content-copy"
|
||||
title="Copy"
|
||||
onPress={copy}
|
||||
disabled={ids?.length === 0}
|
||||
/>
|
||||
<Divider />
|
||||
<Menu.Item
|
||||
leadingIcon='delete'
|
||||
leadingIcon="delete"
|
||||
onPress={() => setShowRemove(true)}
|
||||
title='Delete'
|
||||
title="Delete"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title={ids?.length === 0 ? 'Delete all' : 'Delete selected'}
|
||||
title={ids?.length === 0 ? "Delete all" : "Delete selected"}
|
||||
show={showRemove}
|
||||
setShow={setShowRemove}
|
||||
onOk={remove}
|
||||
onCancel={() => setShowMenu(false)}
|
||||
>
|
||||
{ids?.length === 0
|
||||
? <>This irreversibly deletes records from the app. Are you sure?</>
|
||||
: <>This will delete {ids?.length} record(s). Are you sure?</>}
|
||||
{ids?.length === 0 ? (
|
||||
<>This irreversibly deletes records from the app. Are you sure?</>
|
||||
) : (
|
||||
<>This will delete {ids?.length} record(s). Are you sure?</>
|
||||
)}
|
||||
</ConfirmDialog>
|
||||
</Menu>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
28
Page.tsx
28
Page.tsx
|
@ -1,7 +1,7 @@
|
|||
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
|
||||
import { Searchbar } from 'react-native-paper'
|
||||
import AppFab from './AppFab'
|
||||
import { PADDING } from './constants'
|
||||
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { Searchbar } from "react-native-paper";
|
||||
import AppFab from "./AppFab";
|
||||
import { PADDING } from "./constants";
|
||||
|
||||
export default function Page({
|
||||
onAdd,
|
||||
|
@ -10,25 +10,25 @@ export default function Page({
|
|||
search,
|
||||
style,
|
||||
}: {
|
||||
children: JSX.Element | JSX.Element[]
|
||||
onAdd?: () => void
|
||||
term: string
|
||||
search: (value: string) => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
children: JSX.Element | JSX.Element[];
|
||||
onAdd?: () => void;
|
||||
term: string;
|
||||
search: (value: string) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}) {
|
||||
return (
|
||||
<View style={[styles.view, style]}>
|
||||
<Searchbar
|
||||
placeholder='Search'
|
||||
placeholder="Search"
|
||||
value={term}
|
||||
onChangeText={search}
|
||||
icon='search'
|
||||
clearIcon='clear'
|
||||
icon="search"
|
||||
clearIcon="clear"
|
||||
/>
|
||||
{children}
|
||||
{onAdd && <AppFab onPress={onAdd} />}
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -36,4 +36,4 @@ const styles = StyleSheet.create({
|
|||
padding: PADDING,
|
||||
flexGrow: 1,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
112
PlanItem.tsx
112
PlanItem.tsx
|
@ -2,91 +2,89 @@ import {
|
|||
NavigationProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { Text } from 'react-native'
|
||||
import { List } from 'react-native-paper'
|
||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from './constants'
|
||||
import { setRepo } 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'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Text } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
||||
import { setRepo } 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,
|
||||
setIds,
|
||||
ids,
|
||||
}: {
|
||||
item: Plan
|
||||
ids: number[]
|
||||
setIds: (value: number[]) => void
|
||||
item: Plan;
|
||||
ids: number[];
|
||||
setIds: (value: number[]) => void;
|
||||
}) {
|
||||
const [today, setToday] = useState<string>()
|
||||
const dark = useDark()
|
||||
const days = useMemo(() => item.days.split(','), [item.days])
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>()
|
||||
const [today, setToday] = useState<string>();
|
||||
const dark = useDark();
|
||||
const days = useMemo(() => item.days.split(","), [item.days]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const newToday = DAYS[new Date().getDay()]
|
||||
setToday(newToday)
|
||||
}, []),
|
||||
)
|
||||
const newToday = DAYS[new Date().getDay()];
|
||||
setToday(newToday);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
const workout = item.workouts.split(',')[0]
|
||||
const workout = item.workouts.split(",")[0];
|
||||
let first = await setRepo.findOne({
|
||||
where: { name: workout },
|
||||
order: { created: 'desc' },
|
||||
})
|
||||
if (!first) first = { ...defaultSet, name: workout }
|
||||
delete first.id
|
||||
order: { created: "desc" },
|
||||
});
|
||||
if (!first) first = { ...defaultSet, name: workout };
|
||||
delete first.id;
|
||||
if (ids.length === 0) {
|
||||
return navigation.navigate('StartPlan', { plan: item, first })
|
||||
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 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(() => {
|
||||
if (ids.length > 0) return
|
||||
setIds([item.id])
|
||||
}, [ids.length, item.id, setIds])
|
||||
if (ids.length > 0) return;
|
||||
setIds([item.id]);
|
||||
}, [ids.length, item.id, setIds]);
|
||||
|
||||
const title = useMemo(
|
||||
() =>
|
||||
days.map((day, index) => (
|
||||
<Text key={day}>
|
||||
{day === today
|
||||
? (
|
||||
<Text
|
||||
style={{ fontWeight: 'bold', textDecorationLine: 'underline' }}
|
||||
>
|
||||
{day}
|
||||
</Text>
|
||||
)
|
||||
: (
|
||||
day
|
||||
)}
|
||||
{index === days.length - 1 ? '' : ', '}
|
||||
{day === today ? (
|
||||
<Text
|
||||
style={{ fontWeight: "bold", textDecorationLine: "underline" }}
|
||||
>
|
||||
{day}
|
||||
</Text>
|
||||
) : (
|
||||
day
|
||||
)}
|
||||
{index === days.length - 1 ? "" : ", "}
|
||||
</Text>
|
||||
)),
|
||||
[days, today],
|
||||
)
|
||||
[days, today]
|
||||
);
|
||||
|
||||
const description = useMemo(
|
||||
() => item.workouts.replace(/,/g, ', '),
|
||||
[item.workouts],
|
||||
)
|
||||
() => item.workouts.replace(/,/g, ", "),
|
||||
[item.workouts]
|
||||
);
|
||||
|
||||
const backgroundColor = useMemo(() => {
|
||||
if (!ids.includes(item.id)) return
|
||||
if (dark) return DARK_RIPPLE
|
||||
return LIGHT_RIPPLE
|
||||
}, [dark, ids, item.id])
|
||||
if (!ids.includes(item.id)) return;
|
||||
if (dark) return DARK_RIPPLE;
|
||||
return LIGHT_RIPPLE;
|
||||
}, [dark, ids, item.id]);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
|
@ -96,5 +94,5 @@ export default function PlanItem({
|
|||
onLongPress={longPress}
|
||||
style={{ backgroundColor }}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
122
PlanList.tsx
122
PlanList.tsx
|
@ -2,24 +2,24 @@ import {
|
|||
NavigationProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { FlatList } from 'react-native'
|
||||
import { List } from 'react-native-paper'
|
||||
import { Like } from 'typeorm'
|
||||
import { planRepo } from './db'
|
||||
import DrawerHeader from './DrawerHeader'
|
||||
import ListMenu from './ListMenu'
|
||||
import Page from './Page'
|
||||
import { Plan } from './plan'
|
||||
import { PlanPageParams } from './plan-page-params'
|
||||
import PlanItem from './PlanItem'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { Like } from "typeorm";
|
||||
import { planRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import ListMenu from "./ListMenu";
|
||||
import Page from "./Page";
|
||||
import { Plan } from "./plan";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
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 [term, setTerm] = useState("");
|
||||
const [plans, setPlans] = useState<Plan[]>();
|
||||
const [ids, setIds] = useState<number[]>([]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
const refresh = useCallback(async (value: string) => {
|
||||
planRepo
|
||||
|
@ -29,65 +29,65 @@ export default function PlanList() {
|
|||
{ workouts: Like(`%${value.trim()}%`) },
|
||||
],
|
||||
})
|
||||
.then(setPlans)
|
||||
}, [])
|
||||
.then(setPlans);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refresh(term)
|
||||
}, [refresh, term]),
|
||||
)
|
||||
refresh(term);
|
||||
}, [refresh, term])
|
||||
);
|
||||
|
||||
const search = useCallback(
|
||||
(value: string) => {
|
||||
setTerm(value)
|
||||
refresh(value)
|
||||
setTerm(value);
|
||||
refresh(value);
|
||||
},
|
||||
[refresh],
|
||||
)
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: Plan }) => (
|
||||
<PlanItem ids={ids} setIds={setIds} item={item} key={item.id} />
|
||||
),
|
||||
[ids],
|
||||
)
|
||||
[ids]
|
||||
);
|
||||
|
||||
const onAdd = () =>
|
||||
navigation.navigate('EditPlan', { plan: { days: '', workouts: '' } })
|
||||
navigation.navigate("EditPlan", { plan: { days: "", workouts: "" } });
|
||||
|
||||
const edit = useCallback(async () => {
|
||||
const plan = await planRepo.findOne({ where: { id: ids.pop() } })
|
||||
navigation.navigate('EditPlan', { plan })
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
const plan = await planRepo.findOne({ where: { id: ids.pop() } });
|
||||
navigation.navigate("EditPlan", { plan });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const copy = useCallback(async () => {
|
||||
const plan = await planRepo.findOne({
|
||||
where: { id: ids.pop() },
|
||||
})
|
||||
delete plan.id
|
||||
navigation.navigate('EditPlan', { plan })
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
});
|
||||
delete plan.id;
|
||||
navigation.navigate("EditPlan", { plan });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setIds([])
|
||||
}, [])
|
||||
setIds([]);
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
await planRepo.delete(ids.length > 0 ? ids : {})
|
||||
await refresh(term)
|
||||
setIds([])
|
||||
}, [ids, refresh, term])
|
||||
await planRepo.delete(ids.length > 0 ? ids : {});
|
||||
await refresh(term);
|
||||
setIds([]);
|
||||
}, [ids, refresh, term]);
|
||||
|
||||
const select = useCallback(() => {
|
||||
setIds(plans.map((plan) => plan.id))
|
||||
}, [plans])
|
||||
setIds(plans.map((plan) => plan.id));
|
||||
}, [plans]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : 'Plans'}>
|
||||
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Plans"}>
|
||||
<ListMenu
|
||||
onClear={clear}
|
||||
onCopy={copy}
|
||||
|
@ -98,22 +98,20 @@ export default function PlanList() {
|
|||
/>
|
||||
</DrawerHeader>
|
||||
<Page onAdd={onAdd} term={term} search={search}>
|
||||
{plans?.length === 0
|
||||
? (
|
||||
<List.Item
|
||||
title='No plans yet'
|
||||
description='A plan is a list of workouts for certain days.'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FlatList
|
||||
style={{ flex: 1 }}
|
||||
data={plans}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(set) => set.id?.toString() || ''}
|
||||
/>
|
||||
)}
|
||||
{plans?.length === 0 ? (
|
||||
<List.Item
|
||||
title="No plans yet"
|
||||
description="A plan is a list of workouts for certain days."
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
style={{ flex: 1 }}
|
||||
data={plans}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(set) => set.id?.toString() || ""}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
24
PlanPage.tsx
24
PlanPage.tsx
|
@ -1,21 +1,21 @@
|
|||
import { createStackNavigator } from '@react-navigation/stack'
|
||||
import EditPlan from './EditPlan'
|
||||
import EditSet from './EditSet'
|
||||
import { PlanPageParams } from './plan-page-params'
|
||||
import PlanList from './PlanList'
|
||||
import StartPlan from './StartPlan'
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import EditPlan from "./EditPlan";
|
||||
import EditSet from "./EditSet";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import PlanList from "./PlanList";
|
||||
import StartPlan from "./StartPlan";
|
||||
|
||||
const Stack = createStackNavigator<PlanPageParams>()
|
||||
const Stack = createStackNavigator<PlanPageParams>();
|
||||
|
||||
export default function PlanPage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
||||
>
|
||||
<Stack.Screen name='PlanList' component={PlanList} />
|
||||
<Stack.Screen name='EditPlan' component={EditPlan} />
|
||||
<Stack.Screen name='StartPlan' component={StartPlan} />
|
||||
<Stack.Screen name='EditSet' component={EditSet} />
|
||||
<Stack.Screen name="PlanList" component={PlanList} />
|
||||
<Stack.Screen name="EditPlan" component={EditPlan} />
|
||||
<Stack.Screen name="StartPlan" component={StartPlan} />
|
||||
<Stack.Screen name="EditSet" component={EditSet} />
|
||||
</Stack.Navigator>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
52
Routes.tsx
52
Routes.tsx
|
@ -1,57 +1,57 @@
|
|||
import { createDrawerNavigator } from '@react-navigation/drawer'
|
||||
import { IconButton } from 'react-native-paper'
|
||||
import GraphsPage from './GraphsPage'
|
||||
import { DrawerParamList } from './drawer-param-list'
|
||||
import HomePage from './HomePage'
|
||||
import PlanPage from './PlanPage'
|
||||
import SettingsPage from './SettingsPage'
|
||||
import TimerPage from './TimerPage'
|
||||
import useDark from './use-dark'
|
||||
import WorkoutsPage from './WorkoutsPage'
|
||||
import { createDrawerNavigator } from "@react-navigation/drawer";
|
||||
import { IconButton } from "react-native-paper";
|
||||
import GraphsPage from "./GraphsPage";
|
||||
import { DrawerParamList } from "./drawer-param-list";
|
||||
import HomePage from "./HomePage";
|
||||
import PlanPage from "./PlanPage";
|
||||
import SettingsPage from "./SettingsPage";
|
||||
import TimerPage from "./TimerPage";
|
||||
import useDark from "./use-dark";
|
||||
import WorkoutsPage from "./WorkoutsPage";
|
||||
|
||||
const Drawer = createDrawerNavigator<DrawerParamList>()
|
||||
const Drawer = createDrawerNavigator<DrawerParamList>();
|
||||
|
||||
export default function Routes() {
|
||||
const dark = useDark()
|
||||
const dark = useDark();
|
||||
|
||||
return (
|
||||
<Drawer.Navigator
|
||||
screenOptions={{
|
||||
headerTintColor: dark ? 'white' : 'black',
|
||||
headerTintColor: dark ? "white" : "black",
|
||||
swipeEdgeWidth: 1000,
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen
|
||||
name='Home'
|
||||
name="Home"
|
||||
component={HomePage}
|
||||
options={{ drawerIcon: () => <IconButton icon='home' /> }}
|
||||
options={{ drawerIcon: () => <IconButton icon="home" /> }}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name='Plans'
|
||||
name="Plans"
|
||||
component={PlanPage}
|
||||
options={{ drawerIcon: () => <IconButton icon='event' /> }}
|
||||
options={{ drawerIcon: () => <IconButton icon="event" /> }}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name='Graphs'
|
||||
name="Graphs"
|
||||
component={GraphsPage}
|
||||
options={{ drawerIcon: () => <IconButton icon='insights' /> }}
|
||||
options={{ drawerIcon: () => <IconButton icon="insights" /> }}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name='Workouts'
|
||||
name="Workouts"
|
||||
component={WorkoutsPage}
|
||||
options={{ drawerIcon: () => <IconButton icon='fitness-center' /> }}
|
||||
options={{ drawerIcon: () => <IconButton icon="fitness-center" /> }}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name='Timer'
|
||||
name="Timer"
|
||||
component={TimerPage}
|
||||
options={{ drawerIcon: () => <IconButton icon='access-time' /> }}
|
||||
options={{ drawerIcon: () => <IconButton icon="access-time" /> }}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name='Settings'
|
||||
name="Settings"
|
||||
component={SettingsPage}
|
||||
options={{ drawerIcon: () => <IconButton icon='settings' /> }}
|
||||
options={{ drawerIcon: () => <IconButton icon="settings" /> }}
|
||||
/>
|
||||
</Drawer.Navigator>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
46
Select.tsx
46
Select.tsx
|
@ -1,12 +1,12 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { Button, Menu, Subheading } from 'react-native-paper'
|
||||
import { ITEM_PADDING } from './constants'
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Button, Menu, Subheading } from "react-native-paper";
|
||||
import { ITEM_PADDING } from "./constants";
|
||||
|
||||
export interface Item {
|
||||
value: string
|
||||
label: string
|
||||
color?: string
|
||||
value: string;
|
||||
label: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function Select({
|
||||
|
@ -15,31 +15,31 @@ function Select({
|
|||
items,
|
||||
label,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
items: Item[]
|
||||
label?: string
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
items: Item[];
|
||||
label?: string;
|
||||
}) {
|
||||
const [show, setShow] = useState(false)
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const selected = useMemo(
|
||||
() => items.find((item) => item.value === value) || items[0],
|
||||
[items, value],
|
||||
)
|
||||
[items, value]
|
||||
);
|
||||
|
||||
const handlePress = useCallback(
|
||||
(newValue: string) => {
|
||||
onChange(newValue)
|
||||
setShow(false)
|
||||
onChange(newValue);
|
||||
setShow(false);
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: ITEM_PADDING,
|
||||
}}
|
||||
>
|
||||
|
@ -51,7 +51,7 @@ function Select({
|
|||
<Button
|
||||
onPress={() => setShow(true)}
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
>
|
||||
{selected?.label}
|
||||
|
@ -68,7 +68,7 @@ function Select({
|
|||
))}
|
||||
</Menu>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Select)
|
||||
export default React.memo(Select);
|
||||
|
|
85
SetItem.tsx
85
SetItem.tsx
|
@ -1,13 +1,13 @@
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/native'
|
||||
import { format } from 'date-fns'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Image } from 'react-native'
|
||||
import { List, Text } from 'react-native-paper'
|
||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from './constants'
|
||||
import GymSet from './gym-set'
|
||||
import { HomePageParams } from './home-page-params'
|
||||
import Settings from './settings'
|
||||
import useDark from './use-dark'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Image } from "react-native";
|
||||
import { List, Text } from "react-native-paper";
|
||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
||||
import GymSet from "./gym-set";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import Settings from "./settings";
|
||||
import useDark from "./use-dark";
|
||||
|
||||
export default function SetItem({
|
||||
item,
|
||||
|
@ -15,66 +15,63 @@ export default function SetItem({
|
|||
ids,
|
||||
setIds,
|
||||
}: {
|
||||
item: GymSet
|
||||
onRemove: () => void
|
||||
settings: Settings
|
||||
ids: number[]
|
||||
setIds: (value: number[]) => void
|
||||
item: GymSet;
|
||||
onRemove: () => void;
|
||||
settings: Settings;
|
||||
ids: number[];
|
||||
setIds: (value: number[]) => void;
|
||||
}) {
|
||||
const dark = useDark()
|
||||
const navigation = useNavigation<NavigationProp<HomePageParams>>()
|
||||
const dark = useDark();
|
||||
const navigation = useNavigation<NavigationProp<HomePageParams>>();
|
||||
|
||||
const longPress = useCallback(() => {
|
||||
if (ids.length > 0) return
|
||||
setIds([item.id])
|
||||
}, [ids.length, item.id, setIds])
|
||||
if (ids.length > 0) return;
|
||||
setIds([item.id]);
|
||||
}, [ids.length, item.id, setIds]);
|
||||
|
||||
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])
|
||||
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 backgroundColor = useMemo(() => {
|
||||
if (!ids.includes(item.id)) return
|
||||
if (dark) return DARK_RIPPLE
|
||||
return LIGHT_RIPPLE
|
||||
}, [dark, ids, item.id])
|
||||
if (!ids.includes(item.id)) return;
|
||||
if (dark) return DARK_RIPPLE;
|
||||
return LIGHT_RIPPLE;
|
||||
}, [dark, ids, item.id]);
|
||||
|
||||
const left = useCallback(() => {
|
||||
if (!settings.images || !item.image) return null
|
||||
if (!settings.images || !item.image) return null;
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: item.image }}
|
||||
style={{ height: 75, width: 75 }}
|
||||
/>
|
||||
)
|
||||
}, [item.image, settings.images])
|
||||
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
|
||||
);
|
||||
}, [item.image, settings.images]);
|
||||
|
||||
const right = useCallback(() => {
|
||||
if (!settings.showDate) return null
|
||||
if (!settings.showDate) return null;
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
color: dark ? '#909090ff' : '#717171ff',
|
||||
alignSelf: "center",
|
||||
color: dark ? "#909090ff" : "#717171ff",
|
||||
}}
|
||||
>
|
||||
{format(new Date(item.created), settings.date || 'P')}
|
||||
{format(new Date(item.created), settings.date || "P")}
|
||||
</Text>
|
||||
)
|
||||
}, [settings.showDate, item.created, settings.date, dark])
|
||||
);
|
||||
}, [settings.showDate, item.created, settings.date, dark]);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
onPress={press}
|
||||
title={item.name}
|
||||
description={`${item.reps} x ${item.weight}${item.unit || 'kg'}`}
|
||||
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
|
||||
onLongPress={longPress}
|
||||
style={{ backgroundColor }}
|
||||
left={left}
|
||||
right={right}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
178
SetList.tsx
178
SetList.tsx
|
@ -2,50 +2,50 @@ import {
|
|||
NavigationProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { FlatList } from 'react-native'
|
||||
import { List } from 'react-native-paper'
|
||||
import { Like } from 'typeorm'
|
||||
import { getNow, setRepo, settingsRepo } from './db'
|
||||
import DrawerHeader from './DrawerHeader'
|
||||
import GymSet, { defaultSet } from './gym-set'
|
||||
import { HomePageParams } from './home-page-params'
|
||||
import ListMenu from './ListMenu'
|
||||
import Page from './Page'
|
||||
import SetItem from './SetItem'
|
||||
import Settings from './settings'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { Like } from "typeorm";
|
||||
import { getNow, setRepo, settingsRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import GymSet, { defaultSet } from "./gym-set";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import ListMenu from "./ListMenu";
|
||||
import Page from "./Page";
|
||||
import SetItem from "./SetItem";
|
||||
import Settings from "./settings";
|
||||
|
||||
const limit = 15
|
||||
const limit = 15;
|
||||
|
||||
export default function SetList() {
|
||||
const [sets, setSets] = useState<GymSet[]>([])
|
||||
const [offset, setOffset] = useState(0)
|
||||
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 [sets, setSets] = useState<GymSet[]>([]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
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) => {
|
||||
const newSets = await setRepo.find({
|
||||
where: { name: Like(`%${value.trim()}%`), hidden: 0 as any },
|
||||
take: limit,
|
||||
skip: 0,
|
||||
order: { created: 'DESC' },
|
||||
})
|
||||
console.log(`${SetList.name}.refresh:`, { value, limit })
|
||||
setSets(newSets)
|
||||
setOffset(0)
|
||||
setEnd(false)
|
||||
}, [])
|
||||
order: { created: "DESC" },
|
||||
});
|
||||
console.log(`${SetList.name}.refresh:`, { value, limit });
|
||||
setSets(newSets);
|
||||
setOffset(0);
|
||||
setEnd(false);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refresh(term)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings)
|
||||
}, [refresh, term]),
|
||||
)
|
||||
refresh(term);
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [refresh, term])
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: GymSet }) => (
|
||||
|
@ -58,75 +58,75 @@ export default function SetList() {
|
|||
setIds={setIds}
|
||||
/>
|
||||
),
|
||||
[refresh, term, settings, ids],
|
||||
)
|
||||
[refresh, term, settings, ids]
|
||||
);
|
||||
|
||||
const next = useCallback(async () => {
|
||||
if (end) return
|
||||
const newOffset = offset + limit
|
||||
console.log(`${SetList.name}.next:`, { offset, newOffset, term })
|
||||
if (end) return;
|
||||
const newOffset = offset + limit;
|
||||
console.log(`${SetList.name}.next:`, { offset, newOffset, term });
|
||||
const newSets = await setRepo.find({
|
||||
where: { name: Like(`%${term}%`), hidden: 0 as any },
|
||||
take: limit,
|
||||
skip: newOffset,
|
||||
order: { created: 'DESC' },
|
||||
})
|
||||
if (newSets.length === 0) return setEnd(true)
|
||||
if (!sets) return
|
||||
setSets([...sets, ...newSets])
|
||||
if (newSets.length < limit) return setEnd(true)
|
||||
setOffset(newOffset)
|
||||
}, [term, end, offset, sets])
|
||||
order: { created: "DESC" },
|
||||
});
|
||||
if (newSets.length === 0) return setEnd(true);
|
||||
if (!sets) return;
|
||||
setSets([...sets, ...newSets]);
|
||||
if (newSets.length < limit) return setEnd(true);
|
||||
setOffset(newOffset);
|
||||
}, [term, end, offset, sets]);
|
||||
|
||||
const onAdd = useCallback(async () => {
|
||||
const now = await getNow()
|
||||
let set = sets[0]
|
||||
if (!set) set = { ...defaultSet }
|
||||
set.created = now
|
||||
delete set.id
|
||||
navigation.navigate('EditSet', { set })
|
||||
}, [navigation, sets])
|
||||
const now = await getNow();
|
||||
let set = sets[0];
|
||||
if (!set) set = { ...defaultSet };
|
||||
set.created = now;
|
||||
delete set.id;
|
||||
navigation.navigate("EditSet", { set });
|
||||
}, [navigation, sets]);
|
||||
|
||||
const search = useCallback(
|
||||
(value: string) => {
|
||||
setTerm(value)
|
||||
refresh(value)
|
||||
setTerm(value);
|
||||
refresh(value);
|
||||
},
|
||||
[refresh],
|
||||
)
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const edit = useCallback(() => {
|
||||
navigation.navigate('EditSets', { ids })
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
navigation.navigate("EditSets", { ids });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const copy = useCallback(async () => {
|
||||
const set = await setRepo.findOne({
|
||||
where: { id: ids.pop() },
|
||||
})
|
||||
delete set.id
|
||||
delete set.created
|
||||
navigation.navigate('EditSet', { set })
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
});
|
||||
delete set.id;
|
||||
delete set.created;
|
||||
navigation.navigate("EditSet", { set });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setIds([])
|
||||
}, [])
|
||||
setIds([]);
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
setIds([])
|
||||
await setRepo.delete(ids.length > 0 ? ids : {})
|
||||
await refresh(term)
|
||||
}, [ids, refresh, term])
|
||||
setIds([]);
|
||||
await setRepo.delete(ids.length > 0 ? ids : {});
|
||||
await refresh(term);
|
||||
}, [ids, refresh, term]);
|
||||
|
||||
const select = useCallback(() => {
|
||||
setIds(sets.map((set) => set.id))
|
||||
}, [sets])
|
||||
setIds(sets.map((set) => set.id));
|
||||
}, [sets]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : 'Home'}>
|
||||
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Home"}>
|
||||
<ListMenu
|
||||
onClear={clear}
|
||||
onCopy={copy}
|
||||
|
@ -138,24 +138,22 @@ export default function SetList() {
|
|||
</DrawerHeader>
|
||||
|
||||
<Page onAdd={onAdd} term={term} search={search}>
|
||||
{sets?.length === 0
|
||||
? (
|
||||
<List.Item
|
||||
title='No sets yet'
|
||||
description='A set is a group of repetitions. E.g. 8 reps of Squats.'
|
||||
{sets?.length === 0 ? (
|
||||
<List.Item
|
||||
title="No sets yet"
|
||||
description="A set is a group of repetitions. E.g. 8 reps of Squats."
|
||||
/>
|
||||
) : (
|
||||
settings && (
|
||||
<FlatList
|
||||
data={sets}
|
||||
style={{ flex: 1 }}
|
||||
renderItem={renderItem}
|
||||
onEndReached={next}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
settings && (
|
||||
<FlatList
|
||||
data={sets}
|
||||
style={{ flex: 1 }}
|
||||
renderItem={renderItem}
|
||||
onEndReached={next}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,33 @@
|
|||
import { View } from 'react-native'
|
||||
import { Button, Subheading } from 'react-native-paper'
|
||||
import { ITEM_PADDING } from './constants'
|
||||
import { View } from "react-native";
|
||||
import { Button, Subheading } from "react-native-paper";
|
||||
import { ITEM_PADDING } from "./constants";
|
||||
|
||||
export default function SettingButton(
|
||||
{ name: text, label, onPress }: {
|
||||
name: string
|
||||
label?: string
|
||||
onPress: () => void
|
||||
},
|
||||
) {
|
||||
export default function SettingButton({
|
||||
name: text,
|
||||
label,
|
||||
onPress,
|
||||
}: {
|
||||
name: string;
|
||||
label?: string;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
if (label) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: ITEM_PADDING,
|
||||
}}
|
||||
>
|
||||
<Subheading style={{ width: 100 }}>{label}</Subheading>
|
||||
<Button onPress={onPress}>{text}</Button>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
style={{ alignSelf: 'flex-start' }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Button style={{ alignSelf: "flex-start" }} onPress={onPress}>
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
414
SettingsPage.tsx
414
SettingsPage.tsx
|
@ -1,57 +1,57 @@
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/native'
|
||||
import { format } from 'date-fns'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { NativeModules, ScrollView } from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import { Dirs, FileSystem } from 'react-native-file-access'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import { MARGIN } 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 SettingButton from './SettingButton'
|
||||
import Settings from './settings'
|
||||
import Switch from './Switch'
|
||||
import { toast } from './toast'
|
||||
import { useTheme } from './use-theme'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { NativeModules, ScrollView } from "react-native";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { Dirs, FileSystem } from "react-native-file-access";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { MARGIN } 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 SettingButton from "./SettingButton";
|
||||
import Settings from "./settings";
|
||||
import Switch from "./Switch";
|
||||
import { toast } from "./toast";
|
||||
import { useTheme } from "./use-theme";
|
||||
|
||||
const twelveHours = [
|
||||
'dd/LL/yyyy',
|
||||
'dd/LL/yyyy, p',
|
||||
'ccc p',
|
||||
'p',
|
||||
'yyyy-MM-d',
|
||||
'yyyy-MM-d, p',
|
||||
'yyyy.MM.d',
|
||||
]
|
||||
"dd/LL/yyyy",
|
||||
"dd/LL/yyyy, p",
|
||||
"ccc p",
|
||||
"p",
|
||||
"yyyy-MM-d",
|
||||
"yyyy-MM-d, p",
|
||||
"yyyy.MM.d",
|
||||
];
|
||||
const twentyFours = [
|
||||
'dd/LL/yyyy',
|
||||
'dd/LL/yyyy, k:m',
|
||||
'ccc k:m',
|
||||
'k:m',
|
||||
'yyyy-MM-d',
|
||||
'yyyy-MM-d, k:m',
|
||||
'yyyy.MM.d',
|
||||
]
|
||||
"dd/LL/yyyy",
|
||||
"dd/LL/yyyy, k:m",
|
||||
"ccc k:m",
|
||||
"k:m",
|
||||
"yyyy-MM-d",
|
||||
"yyyy-MM-d, k:m",
|
||||
"yyyy.MM.d",
|
||||
];
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [ignoring, setIgnoring] = useState(false)
|
||||
const [term, setTerm] = useState('')
|
||||
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const { reset } = useNavigation<NavigationProp<DrawerParamList>>()
|
||||
const [ignoring, setIgnoring] = useState(false);
|
||||
const [term, setTerm] = useState("");
|
||||
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const { reset } = useNavigation<NavigationProp<DrawerParamList>>();
|
||||
|
||||
const { watch, setValue } = useForm<Settings>({
|
||||
defaultValues: () => settingsRepo.findOne({ where: {} }),
|
||||
})
|
||||
const settings = watch()
|
||||
});
|
||||
const settings = watch();
|
||||
|
||||
const {
|
||||
theme,
|
||||
|
@ -60,16 +60,16 @@ export default function SettingsPage() {
|
|||
setLightColor,
|
||||
darkColor,
|
||||
setDarkColor,
|
||||
} = useTheme()
|
||||
} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
NativeModules.SettingsModule.ignoringBattery(setIgnoring)
|
||||
NativeModules.SettingsModule.ignoringBattery(setIgnoring);
|
||||
NativeModules.SettingsModule.is24().then((is24: boolean) => {
|
||||
console.log(`${SettingsPage.name}.focus:`, { is24 })
|
||||
if (is24) setFormatOptions(twentyFours)
|
||||
else setFormatOptions(twelveHours)
|
||||
})
|
||||
}, [])
|
||||
console.log(`${SettingsPage.name}.focus:`, { is24 });
|
||||
if (is24) setFormatOptions(twentyFours);
|
||||
else setFormatOptions(twelveHours);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const update = useCallback((key: keyof Settings, value: unknown) => {
|
||||
return settingsRepo
|
||||
|
@ -77,98 +77,98 @@ export default function SettingsPage() {
|
|||
.update()
|
||||
.set({ [key]: value })
|
||||
.printSql()
|
||||
.execute()
|
||||
}, [])
|
||||
.execute();
|
||||
}, []);
|
||||
|
||||
const soundString = useMemo(() => {
|
||||
if (!settings.sound) return null
|
||||
const split = settings.sound.split('/')
|
||||
return split.pop()
|
||||
}, [settings.sound])
|
||||
if (!settings.sound) return null;
|
||||
const split = settings.sound.split("/");
|
||||
return split.pop();
|
||||
}, [settings.sound]);
|
||||
|
||||
const changeSound = useCallback(async () => {
|
||||
const { fileCopyUri } = await DocumentPicker.pickSingle({
|
||||
type: DocumentPicker.types.audio,
|
||||
copyTo: 'documentDirectory',
|
||||
})
|
||||
if (!fileCopyUri) return
|
||||
setValue('sound', fileCopyUri)
|
||||
await update('sound', fileCopyUri)
|
||||
toast('Sound will play after rest timers.')
|
||||
}, [setValue, update])
|
||||
copyTo: "documentDirectory",
|
||||
});
|
||||
if (!fileCopyUri) return;
|
||||
setValue("sound", fileCopyUri);
|
||||
await update("sound", fileCopyUri);
|
||||
toast("Sound will play after rest timers.");
|
||||
}, [setValue, update]);
|
||||
|
||||
const switches: Input<boolean>[] = useMemo(
|
||||
() => [
|
||||
{ name: 'Rest timers', value: settings.alarm, key: 'alarm' },
|
||||
{ name: 'Vibrate', value: settings.vibrate, key: 'vibrate' },
|
||||
{ name: 'Disable sound', value: settings.noSound, key: 'noSound' },
|
||||
{ name: 'Notifications', value: settings.notify, key: 'notify' },
|
||||
{ name: 'Show images', value: settings.images, key: 'images' },
|
||||
{ name: 'Show unit', value: settings.showUnit, key: 'showUnit' },
|
||||
{ name: 'Show steps', value: settings.steps, key: 'steps' },
|
||||
{ name: 'Show date', value: settings.showDate, key: 'showDate' },
|
||||
{ name: 'Automatic backup', value: settings.backup, key: 'backup' },
|
||||
{ name: "Rest timers", value: settings.alarm, key: "alarm" },
|
||||
{ name: "Vibrate", value: settings.vibrate, key: "vibrate" },
|
||||
{ name: "Disable sound", value: settings.noSound, key: "noSound" },
|
||||
{ name: "Notifications", value: settings.notify, key: "notify" },
|
||||
{ name: "Show images", value: settings.images, key: "images" },
|
||||
{ name: "Show unit", value: settings.showUnit, key: "showUnit" },
|
||||
{ name: "Show steps", value: settings.steps, key: "steps" },
|
||||
{ name: "Show date", value: settings.showDate, key: "showDate" },
|
||||
{ name: "Automatic backup", value: settings.backup, key: "backup" },
|
||||
],
|
||||
[settings],
|
||||
)
|
||||
[settings]
|
||||
);
|
||||
|
||||
const filter = useCallback(
|
||||
({ name }) => name.toLowerCase().includes(term.toLowerCase()),
|
||||
[term],
|
||||
)
|
||||
[term]
|
||||
);
|
||||
|
||||
const changeBoolean = useCallback(
|
||||
async (key: keyof Settings, value: boolean) => {
|
||||
setValue(key, value)
|
||||
await update(key, value)
|
||||
setValue(key, value);
|
||||
await update(key, value);
|
||||
switch (key) {
|
||||
case 'alarm':
|
||||
if (value) toast('Timers will now run after each set.')
|
||||
else toast('Stopped timers running after each set.')
|
||||
if (value && !ignoring) NativeModules.SettingsModule.ignoreBattery()
|
||||
return
|
||||
case 'vibrate':
|
||||
if (value) toast('Alarms will now vibrate.')
|
||||
else toast('Alarms will no longer vibrate.')
|
||||
return
|
||||
case 'notify':
|
||||
if (value) toast('Show notifications for new records.')
|
||||
else toast('Stopped notifications for new records.')
|
||||
return
|
||||
case 'images':
|
||||
if (value) toast('Show images for sets.')
|
||||
else toast('Hid images for sets.')
|
||||
return
|
||||
case 'showUnit':
|
||||
if (value) toast('Show option to select unit for sets.')
|
||||
else toast('Hid unit option for sets.')
|
||||
return
|
||||
case 'steps':
|
||||
if (value) toast('Show steps for a workout.')
|
||||
else toast('Hid steps for workouts.')
|
||||
return
|
||||
case 'showDate':
|
||||
if (value) toast('Show date for sets.')
|
||||
else toast('Hid date on sets.')
|
||||
return
|
||||
case 'noSound':
|
||||
if (value) toast('Disable sound on rest timer alarms.')
|
||||
else toast('Enabled sound for rest timer alarms.')
|
||||
return
|
||||
case 'backup':
|
||||
case "alarm":
|
||||
if (value) toast("Timers will now run after each set.");
|
||||
else toast("Stopped timers running after each set.");
|
||||
if (value && !ignoring) NativeModules.SettingsModule.ignoreBattery();
|
||||
return;
|
||||
case "vibrate":
|
||||
if (value) toast("Alarms will now vibrate.");
|
||||
else toast("Alarms will no longer vibrate.");
|
||||
return;
|
||||
case "notify":
|
||||
if (value) toast("Show notifications for new records.");
|
||||
else toast("Stopped notifications for new records.");
|
||||
return;
|
||||
case "images":
|
||||
if (value) toast("Show images for sets.");
|
||||
else toast("Hid images for sets.");
|
||||
return;
|
||||
case "showUnit":
|
||||
if (value) toast("Show option to select unit for sets.");
|
||||
else toast("Hid unit option for sets.");
|
||||
return;
|
||||
case "steps":
|
||||
if (value) toast("Show steps for a workout.");
|
||||
else toast("Hid steps for workouts.");
|
||||
return;
|
||||
case "showDate":
|
||||
if (value) toast("Show date for sets.");
|
||||
else toast("Hid date on sets.");
|
||||
return;
|
||||
case "noSound":
|
||||
if (value) toast("Disable sound on rest timer alarms.");
|
||||
else toast("Enabled sound for rest timer alarms.");
|
||||
return;
|
||||
case "backup":
|
||||
if (value) {
|
||||
const result = await DocumentPicker.pickDirectory()
|
||||
toast('Backup database daily.')
|
||||
NativeModules.BackupModule.start(result.uri)
|
||||
const result = await DocumentPicker.pickDirectory();
|
||||
toast("Backup database daily.");
|
||||
NativeModules.BackupModule.start(result.uri);
|
||||
} else {
|
||||
toast('Stopped backing up daily')
|
||||
NativeModules.BackupModule.stop()
|
||||
toast("Stopped backing up daily");
|
||||
NativeModules.BackupModule.stop();
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
},
|
||||
[ignoring, setValue, update],
|
||||
)
|
||||
[ignoring, setValue, update]
|
||||
);
|
||||
|
||||
const renderSwitch = useCallback(
|
||||
(item: Input<boolean>) => (
|
||||
|
@ -179,69 +179,69 @@ export default function SettingsPage() {
|
|||
title={item.name}
|
||||
/>
|
||||
),
|
||||
[changeBoolean],
|
||||
)
|
||||
[changeBoolean]
|
||||
);
|
||||
|
||||
const switchesMarkup = useMemo(
|
||||
() => switches.filter(filter).map((s) => renderSwitch(s)),
|
||||
[filter, switches, renderSwitch],
|
||||
)
|
||||
[filter, switches, renderSwitch]
|
||||
);
|
||||
|
||||
const changeString = useCallback(
|
||||
async (key: keyof Settings, value: string) => {
|
||||
setValue(key, value)
|
||||
await update(key, value)
|
||||
setValue(key, value);
|
||||
await update(key, value);
|
||||
switch (key) {
|
||||
case 'date':
|
||||
return toast('Changed date format')
|
||||
case 'darkColor':
|
||||
setDarkColor(value)
|
||||
return toast('Set primary color for dark mode.')
|
||||
case 'lightColor':
|
||||
setLightColor(value)
|
||||
return toast('Set primary color for light mode.')
|
||||
case 'vibrate':
|
||||
return toast('Set primary color for light mode.')
|
||||
case 'sound':
|
||||
return toast('Sound will play after rest timers.')
|
||||
case 'theme':
|
||||
setTheme(value as string)
|
||||
if (value === 'dark') toast('Theme will always be dark.')
|
||||
else if (value === 'light') toast('Theme will always be light.')
|
||||
else if (value === 'system') toast('Theme will follow system.')
|
||||
return
|
||||
case "date":
|
||||
return toast("Changed date format");
|
||||
case "darkColor":
|
||||
setDarkColor(value);
|
||||
return toast("Set primary color for dark mode.");
|
||||
case "lightColor":
|
||||
setLightColor(value);
|
||||
return toast("Set primary color for light mode.");
|
||||
case "vibrate":
|
||||
return toast("Set primary color for light mode.");
|
||||
case "sound":
|
||||
return toast("Sound will play after rest timers.");
|
||||
case "theme":
|
||||
setTheme(value as string);
|
||||
if (value === "dark") toast("Theme will always be dark.");
|
||||
else if (value === "light") toast("Theme will always be light.");
|
||||
else if (value === "system") toast("Theme will follow system.");
|
||||
return;
|
||||
}
|
||||
},
|
||||
[update, setTheme, setDarkColor, setLightColor, setValue],
|
||||
)
|
||||
[update, setTheme, setDarkColor, setLightColor, setValue]
|
||||
);
|
||||
|
||||
const selects: Input<string>[] = useMemo(() => {
|
||||
const today = new Date()
|
||||
const today = new Date();
|
||||
return [
|
||||
{ name: 'Theme', value: theme, items: themeOptions, key: 'theme' },
|
||||
{ name: "Theme", value: theme, items: themeOptions, key: "theme" },
|
||||
{
|
||||
name: 'Dark color',
|
||||
name: "Dark color",
|
||||
value: darkColor,
|
||||
items: lightOptions,
|
||||
key: 'darkColor',
|
||||
key: "darkColor",
|
||||
},
|
||||
{
|
||||
name: 'Light color',
|
||||
name: "Light color",
|
||||
value: lightColor,
|
||||
items: darkOptions,
|
||||
key: 'lightColor',
|
||||
key: "lightColor",
|
||||
},
|
||||
{
|
||||
name: 'Date format',
|
||||
name: "Date format",
|
||||
value: settings.date,
|
||||
items: formatOptions.map((option) => ({
|
||||
label: format(today, option),
|
||||
value: option,
|
||||
})),
|
||||
key: 'date',
|
||||
key: "date",
|
||||
},
|
||||
]
|
||||
}, [settings, darkColor, formatOptions, theme, lightColor])
|
||||
];
|
||||
}, [settings, darkColor, formatOptions, theme, lightColor]);
|
||||
|
||||
const renderSelect = useCallback(
|
||||
(item: Input<string>) => (
|
||||
|
@ -253,74 +253,72 @@ export default function SettingsPage() {
|
|||
items={item.items}
|
||||
/>
|
||||
),
|
||||
[changeString],
|
||||
)
|
||||
[changeString]
|
||||
);
|
||||
|
||||
const selectsMarkup = useMemo(
|
||||
() => selects.filter(filter).map(renderSelect),
|
||||
[filter, selects, renderSelect],
|
||||
)
|
||||
[filter, selects, renderSelect]
|
||||
);
|
||||
|
||||
const confirmDelete = useCallback(async () => {
|
||||
setDeleting(false)
|
||||
await AppDataSource.dropDatabase()
|
||||
await AppDataSource.destroy()
|
||||
await AppDataSource.initialize()
|
||||
toast('Database deleted.')
|
||||
}, [])
|
||||
setDeleting(false);
|
||||
await AppDataSource.dropDatabase();
|
||||
await AppDataSource.destroy();
|
||||
await AppDataSource.initialize();
|
||||
toast("Database deleted.");
|
||||
}, []);
|
||||
|
||||
const confirmImport = useCallback(async () => {
|
||||
setImporting(false)
|
||||
await AppDataSource.destroy()
|
||||
const file = await DocumentPicker.pickSingle()
|
||||
await FileSystem.cp(file.uri, Dirs.DatabaseDir + '/massive.db')
|
||||
await AppDataSource.initialize()
|
||||
await setRepo.createQueryBuilder().update().set({ image: null }).execute()
|
||||
await update('sound', null)
|
||||
const { alarm, backup } = await settingsRepo.findOne({ where: {} })
|
||||
console.log({ backup })
|
||||
const directory = await DocumentPicker.pickDirectory()
|
||||
if (backup) NativeModules.BackupModule.start(directory.uri)
|
||||
else NativeModules.BackupModule.stop()
|
||||
NativeModules.SettingsModule.ignoringBattery(
|
||||
(isIgnoring: boolean) => {
|
||||
if (alarm && !isIgnoring) NativeModules.SettingsModule.ignoreBattery()
|
||||
reset({ index: 0, routes: [{ name: 'Settings' }] })
|
||||
},
|
||||
)
|
||||
}, [reset, update])
|
||||
setImporting(false);
|
||||
await AppDataSource.destroy();
|
||||
const file = await DocumentPicker.pickSingle();
|
||||
await FileSystem.cp(file.uri, Dirs.DatabaseDir + "/massive.db");
|
||||
await AppDataSource.initialize();
|
||||
await setRepo.createQueryBuilder().update().set({ image: null }).execute();
|
||||
await update("sound", null);
|
||||
const { alarm, backup } = await settingsRepo.findOne({ where: {} });
|
||||
console.log({ backup });
|
||||
const directory = await DocumentPicker.pickDirectory();
|
||||
if (backup) NativeModules.BackupModule.start(directory.uri);
|
||||
else NativeModules.BackupModule.stop();
|
||||
NativeModules.SettingsModule.ignoringBattery((isIgnoring: boolean) => {
|
||||
if (alarm && !isIgnoring) NativeModules.SettingsModule.ignoreBattery();
|
||||
reset({ index: 0, routes: [{ name: "Settings" }] });
|
||||
});
|
||||
}, [reset, update]);
|
||||
|
||||
const exportDatabase = useCallback(async () => {
|
||||
const path = Dirs.DatabaseDir + '/massive.db'
|
||||
await FileSystem.cpExternal(path, 'massive.db', 'downloads')
|
||||
toast('Database exported. Check downloads.')
|
||||
}, [])
|
||||
const path = Dirs.DatabaseDir + "/massive.db";
|
||||
await FileSystem.cpExternal(path, "massive.db", "downloads");
|
||||
toast("Database exported. Check downloads.");
|
||||
}, []);
|
||||
|
||||
const buttons = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: soundString || 'Default',
|
||||
name: soundString || "Default",
|
||||
onPress: changeSound,
|
||||
label: 'Alarm sound',
|
||||
label: "Alarm sound",
|
||||
},
|
||||
{ name: 'Export database', onPress: exportDatabase },
|
||||
{ name: 'Import database', onPress: () => setImporting(true) },
|
||||
{ name: 'Delete database', onPress: () => setDeleting(true) },
|
||||
{ name: "Export database", onPress: exportDatabase },
|
||||
{ name: "Import database", onPress: () => setImporting(true) },
|
||||
{ name: "Delete database", onPress: () => setDeleting(true) },
|
||||
],
|
||||
[changeSound, exportDatabase, soundString],
|
||||
)
|
||||
[changeSound, exportDatabase, soundString]
|
||||
);
|
||||
|
||||
const buttonsMarkup = useMemo(
|
||||
() =>
|
||||
buttons.filter(filter).map((button) => (
|
||||
<SettingButton {...button} key={button.name} />
|
||||
)),
|
||||
[buttons, filter],
|
||||
)
|
||||
buttons
|
||||
.filter(filter)
|
||||
.map((button) => <SettingButton {...button} key={button.name} />),
|
||||
[buttons, filter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name='Settings' />
|
||||
<DrawerHeader name="Settings" />
|
||||
|
||||
<Page term={term} search={setTerm} style={{ flexGrow: 1 }}>
|
||||
<ScrollView style={{ marginTop: MARGIN, flex: 1 }}>
|
||||
|
@ -331,7 +329,7 @@ export default function SettingsPage() {
|
|||
</Page>
|
||||
|
||||
<ConfirmDialog
|
||||
title='Are you sure?'
|
||||
title="Are you sure?"
|
||||
onOk={confirmImport}
|
||||
setShow={setImporting}
|
||||
show={importing}
|
||||
|
@ -341,7 +339,7 @@ export default function SettingsPage() {
|
|||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
title='Are you sure?'
|
||||
title="Are you sure?"
|
||||
onOk={confirmDelete}
|
||||
setShow={setDeleting}
|
||||
show={deleting}
|
||||
|
@ -350,5 +348,5 @@ export default function SettingsPage() {
|
|||
reversed!
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Appbar, IconButton } from 'react-native-paper'
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { Appbar, IconButton } from "react-native-paper";
|
||||
|
||||
export default function StackHeader({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
children?: JSX.Element | JSX.Element[]
|
||||
title: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const navigation = useNavigation()
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
<IconButton
|
||||
icon='arrow-back'
|
||||
onPress={navigation.goBack}
|
||||
/>
|
||||
<IconButton icon="arrow-back" onPress={navigation.goBack} />
|
||||
<Appbar.Content title={title} />
|
||||
{children}
|
||||
</Appbar.Header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
158
StartPlan.tsx
158
StartPlan.tsx
|
@ -4,46 +4,46 @@ import {
|
|||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { FlatList, NativeModules, TextInput, View } from 'react-native'
|
||||
import { Button, IconButton, ProgressBar } from 'react-native-paper'
|
||||
import AppInput from './AppInput'
|
||||
import { getBestSet } from './best.service'
|
||||
import { PADDING } from './constants'
|
||||
import CountMany from './count-many'
|
||||
import { AppDataSource } from './data-source'
|
||||
import { getNow, setRepo, settingsRepo } from './db'
|
||||
import GymSet from './gym-set'
|
||||
import { PlanPageParams } from './plan-page-params'
|
||||
import Settings from './settings'
|
||||
import StackHeader from './StackHeader'
|
||||
import StartPlanItem from './StartPlanItem'
|
||||
import { toast } from './toast'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { FlatList, NativeModules, TextInput, View } from "react-native";
|
||||
import { Button, IconButton, ProgressBar } from "react-native-paper";
|
||||
import AppInput from "./AppInput";
|
||||
import { getBestSet } from "./best.service";
|
||||
import { PADDING } from "./constants";
|
||||
import CountMany from "./count-many";
|
||||
import { AppDataSource } from "./data-source";
|
||||
import { getNow, setRepo, settingsRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import Settings from "./settings";
|
||||
import StackHeader from "./StackHeader";
|
||||
import StartPlanItem from "./StartPlanItem";
|
||||
import { toast } from "./toast";
|
||||
|
||||
export default function StartPlan() {
|
||||
const { params } = useRoute<RouteProp<PlanPageParams, 'StartPlan'>>()
|
||||
const [reps, setReps] = useState(params.first?.reps.toString() || '0')
|
||||
const [weight, setWeight] = useState(params.first?.weight.toString() || '0')
|
||||
const [unit, setUnit] = useState<string>(params.first?.unit || 'kg')
|
||||
const [selected, setSelected] = useState(0)
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
const [counts, setCounts] = useState<CountMany[]>()
|
||||
const weightRef = useRef<TextInput>(null)
|
||||
const repsRef = useRef<TextInput>(null)
|
||||
const unitRef = useRef<TextInput>(null)
|
||||
const workouts = useMemo(() => params.plan.workouts.split(','), [params])
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>()
|
||||
const { params } = useRoute<RouteProp<PlanPageParams, "StartPlan">>();
|
||||
const [reps, setReps] = useState(params.first?.reps.toString() || "0");
|
||||
const [weight, setWeight] = useState(params.first?.weight.toString() || "0");
|
||||
const [unit, setUnit] = useState<string>(params.first?.unit || "kg");
|
||||
const [selected, setSelected] = useState(0);
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
const [counts, setCounts] = useState<CountMany[]>();
|
||||
const weightRef = useRef<TextInput>(null);
|
||||
const repsRef = useRef<TextInput>(null);
|
||||
const unitRef = useRef<TextInput>(null);
|
||||
const workouts = useMemo(() => params.plan.workouts.split(","), [params]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
const [selection, setSelection] = useState({
|
||||
start: 0,
|
||||
end: 0,
|
||||
})
|
||||
});
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const questions = workouts
|
||||
.map((workout, index) => `('${workout}',${index})`)
|
||||
.join(',')
|
||||
.join(",");
|
||||
const select = `
|
||||
SELECT workouts.name, COUNT(sets.id) as total, sets.sets
|
||||
FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts
|
||||
|
@ -54,45 +54,45 @@ export default function StartPlan() {
|
|||
ORDER BY workouts.sequence
|
||||
LIMIT -1
|
||||
OFFSET 1
|
||||
`
|
||||
const newCounts = await AppDataSource.manager.query(select)
|
||||
console.log(`${StartPlan.name}.focus:`, { newCounts })
|
||||
setCounts(newCounts)
|
||||
}, [workouts])
|
||||
`;
|
||||
const newCounts = await AppDataSource.manager.query(select);
|
||||
console.log(`${StartPlan.name}.focus:`, { newCounts });
|
||||
setCounts(newCounts);
|
||||
}, [workouts]);
|
||||
|
||||
const select = useCallback(
|
||||
async (index: number, newCounts?: CountMany[]) => {
|
||||
setSelected(index)
|
||||
if (!counts && !newCounts) return
|
||||
const workout = counts ? counts[index] : newCounts[index]
|
||||
console.log(`${StartPlan.name}.next:`, { workout })
|
||||
setSelected(index);
|
||||
if (!counts && !newCounts) return;
|
||||
const workout = counts ? counts[index] : newCounts[index];
|
||||
console.log(`${StartPlan.name}.next:`, { workout });
|
||||
const last = await setRepo.findOne({
|
||||
where: { name: workout.name },
|
||||
order: { created: 'desc' },
|
||||
})
|
||||
console.log({ last })
|
||||
if (!last) return
|
||||
delete last.id
|
||||
console.log(`${StartPlan.name}.select:`, { last })
|
||||
setReps(last.reps.toString())
|
||||
setWeight(last.weight.toString())
|
||||
setUnit(last.unit)
|
||||
order: { created: "desc" },
|
||||
});
|
||||
console.log({ last });
|
||||
if (!last) return;
|
||||
delete last.id;
|
||||
console.log(`${StartPlan.name}.select:`, { last });
|
||||
setReps(last.reps.toString());
|
||||
setWeight(last.weight.toString());
|
||||
setUnit(last.unit);
|
||||
},
|
||||
[counts],
|
||||
)
|
||||
[counts]
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings)
|
||||
refresh()
|
||||
}, [refresh]),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
refresh();
|
||||
}, [refresh])
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const now = await getNow()
|
||||
const workout = counts[selected]
|
||||
const best = await getBestSet(workout.name)
|
||||
delete best.id
|
||||
const now = await getNow();
|
||||
const workout = counts[selected];
|
||||
const best = await getBestSet(workout.name);
|
||||
delete best.id;
|
||||
const newSet: GymSet = {
|
||||
...best,
|
||||
weight: +weight,
|
||||
|
@ -100,34 +100,34 @@ export default function StartPlan() {
|
|||
unit,
|
||||
created: now,
|
||||
hidden: false,
|
||||
}
|
||||
await setRepo.save(newSet)
|
||||
await refresh()
|
||||
};
|
||||
await setRepo.save(newSet);
|
||||
await refresh();
|
||||
if (
|
||||
settings.notify &&
|
||||
(+weight > best.weight || (+reps > best.reps && +weight === best.weight))
|
||||
) {
|
||||
toast("Great work King! That's a new record.")
|
||||
toast("Great work King! That's a new record.");
|
||||
}
|
||||
if (!settings.alarm) return
|
||||
const milliseconds = Number(best.minutes) * 60 * 1000 +
|
||||
Number(best.seconds) * 1000
|
||||
NativeModules.AlarmModule.timer(milliseconds)
|
||||
}
|
||||
if (!settings.alarm) return;
|
||||
const milliseconds =
|
||||
Number(best.minutes) * 60 * 1000 + Number(best.seconds) * 1000;
|
||||
NativeModules.AlarmModule.timer(milliseconds);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title={params.plan.days.replace(/,/g, ', ')}>
|
||||
<StackHeader title={params.plan.days.replace(/,/g, ", ")}>
|
||||
<IconButton
|
||||
onPress={() => navigation.navigate('EditPlan', { plan: params.plan })}
|
||||
icon='edit'
|
||||
onPress={() => navigation.navigate("EditPlan", { plan: params.plan })}
|
||||
icon="edit"
|
||||
/>
|
||||
</StackHeader>
|
||||
<View style={{ padding: PADDING, flex: 1, flexDirection: 'column' }}>
|
||||
<View style={{ padding: PADDING, flex: 1, flexDirection: "column" }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<AppInput
|
||||
label='Reps'
|
||||
keyboardType='numeric'
|
||||
label="Reps"
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={setReps}
|
||||
onSubmitEditing={() => weightRef.current?.focus()}
|
||||
|
@ -136,8 +136,8 @@ export default function StartPlan() {
|
|||
innerRef={repsRef}
|
||||
/>
|
||||
<AppInput
|
||||
label='Weight'
|
||||
keyboardType='numeric'
|
||||
label="Weight"
|
||||
keyboardType="numeric"
|
||||
value={weight}
|
||||
onChangeText={setWeight}
|
||||
onSubmitEditing={handleSubmit}
|
||||
|
@ -146,8 +146,8 @@ export default function StartPlan() {
|
|||
/>
|
||||
{settings?.showUnit && (
|
||||
<AppInput
|
||||
autoCapitalize='none'
|
||||
label='Unit'
|
||||
autoCapitalize="none"
|
||||
label="Unit"
|
||||
value={unit}
|
||||
onChangeText={setUnit}
|
||||
innerRef={unitRef}
|
||||
|
@ -172,10 +172,10 @@ export default function StartPlan() {
|
|||
/>
|
||||
)}
|
||||
</View>
|
||||
<Button mode='outlined' icon='save' onPress={handleSubmit}>
|
||||
<Button mode="outlined" icon="save" onPress={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,109 +1,112 @@
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/native'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { GestureResponderEvent, ListRenderItemInfo, View } from 'react-native'
|
||||
import { List, Menu, RadioButton, useTheme } from 'react-native-paper'
|
||||
import { Like } from 'typeorm'
|
||||
import CountMany from './count-many'
|
||||
import { getNow, setRepo } from './db'
|
||||
import { PlanPageParams } from './plan-page-params'
|
||||
import { toast } from './toast'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { GestureResponderEvent, ListRenderItemInfo, View } from "react-native";
|
||||
import { List, Menu, RadioButton, useTheme } from "react-native-paper";
|
||||
import { Like } from "typeorm";
|
||||
import CountMany from "./count-many";
|
||||
import { getNow, setRepo } from "./db";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import { toast } from "./toast";
|
||||
|
||||
interface Props extends ListRenderItemInfo<CountMany> {
|
||||
onSelect: (index: number) => void
|
||||
selected: number
|
||||
onUndo: () => void
|
||||
onSelect: (index: number) => void;
|
||||
selected: number;
|
||||
onUndo: () => void;
|
||||
}
|
||||
|
||||
export default function StartPlanItem(props: Props) {
|
||||
const { index, item, onSelect, selected, onUndo } = props
|
||||
const { colors } = useTheme()
|
||||
const [anchor, setAnchor] = useState({ x: 0, y: 0 })
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const { navigate } = useNavigation<NavigationProp<PlanPageParams>>()
|
||||
const { index, item, onSelect, selected, onUndo } = props;
|
||||
const { colors } = useTheme();
|
||||
const [anchor, setAnchor] = useState({ x: 0, y: 0 });
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const { navigate } = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
const undo = useCallback(async () => {
|
||||
const now = await getNow()
|
||||
const created = now.split('T')[0]
|
||||
const now = await getNow();
|
||||
const created = now.split("T")[0];
|
||||
const first = await setRepo.findOne({
|
||||
where: {
|
||||
name: item.name,
|
||||
hidden: 0 as any,
|
||||
created: Like(`${created}%`),
|
||||
},
|
||||
order: { created: 'desc' },
|
||||
})
|
||||
setShowMenu(false)
|
||||
if (!first) return toast('Nothing to undo.')
|
||||
await setRepo.delete(first.id)
|
||||
onUndo()
|
||||
}, [setShowMenu, onUndo, item.name])
|
||||
order: { created: "desc" },
|
||||
});
|
||||
setShowMenu(false);
|
||||
if (!first) return toast("Nothing to undo.");
|
||||
await setRepo.delete(first.id);
|
||||
onUndo();
|
||||
}, [setShowMenu, onUndo, item.name]);
|
||||
|
||||
const longPress = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY })
|
||||
setShowMenu(true)
|
||||
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY });
|
||||
setShowMenu(true);
|
||||
},
|
||||
[setShowMenu, setAnchor],
|
||||
)
|
||||
[setShowMenu, setAnchor]
|
||||
);
|
||||
|
||||
const edit = useCallback(async () => {
|
||||
const now = await getNow()
|
||||
const created = now.split('T')[0]
|
||||
const now = await getNow();
|
||||
const created = now.split("T")[0];
|
||||
const first = await setRepo.findOne({
|
||||
where: {
|
||||
name: item.name,
|
||||
hidden: 0 as any,
|
||||
created: Like(`${created}%`),
|
||||
},
|
||||
order: { created: 'desc' },
|
||||
})
|
||||
setShowMenu(false)
|
||||
if (!first) return toast('Nothing to edit.')
|
||||
navigate('EditSet', { set: first })
|
||||
}, [item.name, navigate])
|
||||
order: { created: "desc" },
|
||||
});
|
||||
setShowMenu(false);
|
||||
if (!first) return toast("Nothing to edit.");
|
||||
navigate("EditSet", { set: first });
|
||||
}, [item.name, navigate]);
|
||||
|
||||
const left = useCallback(
|
||||
() => (
|
||||
<View style={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<View style={{ alignItems: "center", justifyContent: "center" }}>
|
||||
<RadioButton
|
||||
onPress={() => onSelect(index)}
|
||||
value={index.toString()}
|
||||
status={selected === index ? 'checked' : 'unchecked'}
|
||||
status={selected === index ? "checked" : "unchecked"}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
[index, selected, colors.primary, onSelect],
|
||||
)
|
||||
[index, selected, colors.primary, onSelect]
|
||||
);
|
||||
|
||||
const right = useCallback(() => (
|
||||
<View
|
||||
style={{
|
||||
width: '25%',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
anchor={anchor}
|
||||
visible={showMenu}
|
||||
onDismiss={() => setShowMenu(false)}
|
||||
const right = useCallback(
|
||||
() => (
|
||||
<View
|
||||
style={{
|
||||
width: "25%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Menu.Item leadingIcon='edit' onPress={edit} title='Edit' />
|
||||
<Menu.Item leadingIcon='undo' onPress={undo} title='Undo' />
|
||||
</Menu>
|
||||
</View>
|
||||
), [anchor, showMenu, edit, undo])
|
||||
<Menu
|
||||
anchor={anchor}
|
||||
visible={showMenu}
|
||||
onDismiss={() => setShowMenu(false)}
|
||||
>
|
||||
<Menu.Item leadingIcon="edit" onPress={edit} title="Edit" />
|
||||
<Menu.Item leadingIcon="undo" onPress={undo} title="Undo" />
|
||||
</Menu>
|
||||
</View>
|
||||
),
|
||||
[anchor, showMenu, edit, undo]
|
||||
);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
onLongPress={longPress}
|
||||
title={item.name}
|
||||
description={item.sets
|
||||
? `${item.total} / ${item.sets}`
|
||||
: item.total.toString()}
|
||||
description={
|
||||
item.sets ? `${item.total} / ${item.sets}` : item.total.toString()
|
||||
}
|
||||
onPress={() => onSelect(index)}
|
||||
left={left}
|
||||
right={right}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
30
Switch.tsx
30
Switch.tsx
|
@ -1,27 +1,27 @@
|
|||
import React from 'react'
|
||||
import { Platform, Pressable } from 'react-native'
|
||||
import { Switch as PaperSwitch, Text, useTheme } from 'react-native-paper'
|
||||
import { MARGIN } from './constants'
|
||||
import React from "react";
|
||||
import { Platform, Pressable } from "react-native";
|
||||
import { Switch as PaperSwitch, Text, useTheme } from "react-native-paper";
|
||||
import { MARGIN } from "./constants";
|
||||
|
||||
function Switch({
|
||||
value,
|
||||
onChange,
|
||||
title,
|
||||
}: {
|
||||
value?: boolean
|
||||
onChange: (value: boolean) => void
|
||||
title: string
|
||||
value?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
title: string;
|
||||
}) {
|
||||
const { colors } = useTheme()
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => onChange(!value)}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
marginBottom: Platform.OS === 'ios' ? MARGIN : null,
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
marginBottom: Platform.OS === "ios" ? MARGIN : null,
|
||||
}}
|
||||
>
|
||||
<PaperSwitch
|
||||
|
@ -30,13 +30,13 @@ function Switch({
|
|||
value={value}
|
||||
onValueChange={onChange}
|
||||
trackColor={{
|
||||
true: colors.primary + '80',
|
||||
true: colors.primary + "80",
|
||||
false: colors.surfaceDisabled,
|
||||
}}
|
||||
/>
|
||||
<Text>{title}</Text>
|
||||
</Pressable>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Switch)
|
||||
export default React.memo(Switch);
|
||||
|
|
|
@ -1,60 +1,60 @@
|
|||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Dimensions, NativeModules, View } from 'react-native'
|
||||
import { Button, Text, useTheme } from 'react-native-paper'
|
||||
import { ProgressCircle } from 'react-native-svg-charts'
|
||||
import AppFab from './AppFab'
|
||||
import { MARGIN, PADDING } from './constants'
|
||||
import { settingsRepo } from './db'
|
||||
import DrawerHeader from './DrawerHeader'
|
||||
import Settings from './settings'
|
||||
import useTimer from './use-timer'
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Dimensions, NativeModules, View } from "react-native";
|
||||
import { Button, Text, useTheme } from "react-native-paper";
|
||||
import { ProgressCircle } from "react-native-svg-charts";
|
||||
import AppFab from "./AppFab";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { settingsRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import Settings from "./settings";
|
||||
import useTimer from "./use-timer";
|
||||
|
||||
export interface TickEvent {
|
||||
minutes: string
|
||||
seconds: string
|
||||
minutes: string;
|
||||
seconds: string;
|
||||
}
|
||||
|
||||
export default function TimerPage() {
|
||||
const { minutes, seconds } = useTimer()
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
const { colors } = useTheme()
|
||||
const { minutes, seconds } = useTimer();
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
const { colors } = useTheme();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings)
|
||||
}, []),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const stop = () => {
|
||||
NativeModules.AlarmModule.stop()
|
||||
}
|
||||
NativeModules.AlarmModule.stop();
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
console.log(`${TimerPage.name}.add:`, settings)
|
||||
NativeModules.AlarmModule.add()
|
||||
}
|
||||
console.log(`${TimerPage.name}.add:`, settings);
|
||||
NativeModules.AlarmModule.add();
|
||||
};
|
||||
|
||||
const progress = useMemo(() => {
|
||||
return (Number(minutes) * 60 + Number(seconds)) / 210
|
||||
}, [minutes, seconds])
|
||||
return (Number(minutes) * 60 + Number(seconds)) / 210;
|
||||
}, [minutes, seconds]);
|
||||
|
||||
const left = useMemo(() => {
|
||||
return Dimensions.get('screen').width * 0.5 - 60
|
||||
}, [])
|
||||
return Dimensions.get("screen").width * 0.5 - 60;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name='Timer' />
|
||||
<DrawerHeader name="Timer" />
|
||||
<View style={{ flexGrow: 1, padding: PADDING }}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 70, position: 'absolute' }}>
|
||||
<Text style={{ fontSize: 70, position: "absolute" }}>
|
||||
{minutes}:{seconds}
|
||||
</Text>
|
||||
<ProgressCircle
|
||||
|
@ -62,14 +62,14 @@ export default function TimerPage() {
|
|||
progress={progress}
|
||||
strokeWidth={10}
|
||||
progressColor={colors.primary}
|
||||
backgroundColor={colors.primary + '80'}
|
||||
backgroundColor={colors.primary + "80"}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Button onPress={add} style={{ position: 'absolute', top: '82%', left }}>
|
||||
<Button onPress={add} style={{ position: "absolute", top: "82%", left }}>
|
||||
Add 1 min
|
||||
</Button>
|
||||
<AppFab icon='stop' onPress={stop} />
|
||||
<AppFab icon="stop" onPress={stop} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
132
ViewGraph.tsx
132
ViewGraph.tsx
|
@ -1,73 +1,73 @@
|
|||
import { RouteProp, useRoute } from '@react-navigation/native'
|
||||
import { format } from 'date-fns'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { FileSystem } from 'react-native-file-access'
|
||||
import { IconButton, List } from 'react-native-paper'
|
||||
import Share from 'react-native-share'
|
||||
import { captureScreen } from 'react-native-view-shot'
|
||||
import Chart from './Chart'
|
||||
import { GraphsPageParams } from './GraphsPage'
|
||||
import Select from './Select'
|
||||
import StackHeader from './StackHeader'
|
||||
import { PADDING } from './constants'
|
||||
import { setRepo } from './db'
|
||||
import GymSet from './gym-set'
|
||||
import { Metrics } from './metrics'
|
||||
import { Periods } from './periods'
|
||||
import Volume from './volume'
|
||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { FileSystem } from "react-native-file-access";
|
||||
import { IconButton, List } from "react-native-paper";
|
||||
import Share from "react-native-share";
|
||||
import { captureScreen } from "react-native-view-shot";
|
||||
import Chart from "./Chart";
|
||||
import { GraphsPageParams } from "./GraphsPage";
|
||||
import Select from "./Select";
|
||||
import StackHeader from "./StackHeader";
|
||||
import { PADDING } from "./constants";
|
||||
import { setRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
import { Metrics } from "./metrics";
|
||||
import { Periods } from "./periods";
|
||||
import Volume from "./volume";
|
||||
|
||||
export default function ViewGraph() {
|
||||
const { params } = useRoute<RouteProp<GraphsPageParams, 'ViewGraph'>>()
|
||||
const [weights, setWeights] = useState<GymSet[]>()
|
||||
const [volumes, setVolumes] = useState<Volume[]>()
|
||||
const [metric, setMetric] = useState(Metrics.Weight)
|
||||
const [period, setPeriod] = useState(Periods.Monthly)
|
||||
const { params } = useRoute<RouteProp<GraphsPageParams, "ViewGraph">>();
|
||||
const [weights, setWeights] = useState<GymSet[]>();
|
||||
const [volumes, setVolumes] = useState<Volume[]>();
|
||||
const [metric, setMetric] = useState(Metrics.Weight);
|
||||
const [period, setPeriod] = useState(Periods.Monthly);
|
||||
|
||||
useEffect(() => {
|
||||
let difference = '-7 days'
|
||||
if (period === Periods.Monthly) difference = '-1 months'
|
||||
else if (period === Periods.Yearly) difference = '-1 years'
|
||||
let group = '%Y-%m-%d'
|
||||
if (period === Periods.Yearly) group = '%Y-%m'
|
||||
let difference = "-7 days";
|
||||
if (period === Periods.Monthly) difference = "-1 months";
|
||||
else if (period === Periods.Yearly) difference = "-1 years";
|
||||
let group = "%Y-%m-%d";
|
||||
if (period === Periods.Yearly) group = "%Y-%m";
|
||||
const builder = setRepo
|
||||
.createQueryBuilder()
|
||||
.select("STRFTIME('%Y-%m-%d', created)", 'created')
|
||||
.addSelect('unit')
|
||||
.where('name = :name', { name: params.best.name })
|
||||
.andWhere('NOT hidden')
|
||||
.select("STRFTIME('%Y-%m-%d', created)", "created")
|
||||
.addSelect("unit")
|
||||
.where("name = :name", { name: params.best.name })
|
||||
.andWhere("NOT hidden")
|
||||
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
||||
difference,
|
||||
})
|
||||
.groupBy('name')
|
||||
.addGroupBy(`STRFTIME('${group}', created)`)
|
||||
.groupBy("name")
|
||||
.addGroupBy(`STRFTIME('${group}', created)`);
|
||||
switch (metric) {
|
||||
case Metrics.Weight:
|
||||
builder
|
||||
.addSelect('ROUND(MAX(weight), 2)', 'weight')
|
||||
.addSelect("ROUND(MAX(weight), 2)", "weight")
|
||||
.getRawMany()
|
||||
.then(setWeights)
|
||||
break
|
||||
.then(setWeights);
|
||||
break;
|
||||
case Metrics.Volume:
|
||||
builder
|
||||
.addSelect('ROUND(SUM(weight * reps), 2)', 'value')
|
||||
.addSelect("ROUND(SUM(weight * reps), 2)", "value")
|
||||
.getRawMany()
|
||||
.then(setVolumes)
|
||||
break
|
||||
.then(setVolumes);
|
||||
break;
|
||||
default:
|
||||
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
|
||||
builder
|
||||
.addSelect(
|
||||
'ROUND(MAX(weight / (1.0278 - 0.0278 * reps)), 2)',
|
||||
'weight',
|
||||
"ROUND(MAX(weight / (1.0278 - 0.0278 * reps)), 2)",
|
||||
"weight"
|
||||
)
|
||||
.getRawMany()
|
||||
.then((newWeights) => {
|
||||
console.log({ weights: newWeights })
|
||||
setWeights(newWeights)
|
||||
})
|
||||
console.log({ weights: newWeights });
|
||||
setWeights(newWeights);
|
||||
});
|
||||
}
|
||||
}, [params.best.name, metric, period])
|
||||
}, [params.best.name, metric, period]);
|
||||
|
||||
const charts = useMemo(() => {
|
||||
if (
|
||||
|
@ -75,21 +75,23 @@ export default function ViewGraph() {
|
|||
(metric === Metrics.Weight && weights?.length === 0) ||
|
||||
(metric === Metrics.OneRepMax && weights?.length === 0)
|
||||
) {
|
||||
return <List.Item title='No data yet.' />
|
||||
return <List.Item title="No data yet." />;
|
||||
}
|
||||
if (metric === Metrics.Volume && volumes?.length && weights?.length) {
|
||||
return (
|
||||
<Chart
|
||||
yData={volumes.map((v) => v.value)}
|
||||
yFormat={(value: number) =>
|
||||
`${value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}${
|
||||
volumes[0].unit || 'kg'
|
||||
}`}
|
||||
`${value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${
|
||||
volumes[0].unit || "kg"
|
||||
}`
|
||||
}
|
||||
xData={weights}
|
||||
xFormat={(_value, index) =>
|
||||
format(new Date(weights[index].created), 'd/M')}
|
||||
format(new Date(weights[index].created), "d/M")
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -98,10 +100,11 @@ export default function ViewGraph() {
|
|||
yFormat={(value) => `${value}${weights?.[0].unit}`}
|
||||
xData={weights || []}
|
||||
xFormat={(_value, index) =>
|
||||
format(new Date(weights?.[index].created), 'd/M')}
|
||||
format(new Date(weights?.[index].created), "d/M")
|
||||
}
|
||||
/>
|
||||
)
|
||||
}, [volumes, weights, metric])
|
||||
);
|
||||
}, [volumes, weights, metric]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -109,19 +112,20 @@ export default function ViewGraph() {
|
|||
<IconButton
|
||||
onPress={() =>
|
||||
captureScreen().then(async (uri) => {
|
||||
const base64 = await FileSystem.readFile(uri, 'base64')
|
||||
const url = `data:image/jpeg;base64,${base64}`
|
||||
const base64 = await FileSystem.readFile(uri, "base64");
|
||||
const url = `data:image/jpeg;base64,${base64}`;
|
||||
Share.open({
|
||||
type: 'image/jpeg',
|
||||
type: "image/jpeg",
|
||||
url,
|
||||
})
|
||||
})}
|
||||
icon='share'
|
||||
});
|
||||
})
|
||||
}
|
||||
icon="share"
|
||||
/>
|
||||
</StackHeader>
|
||||
<View style={{ padding: PADDING }}>
|
||||
<Select
|
||||
label='Metric'
|
||||
label="Metric"
|
||||
items={[
|
||||
{ value: Metrics.Volume, label: Metrics.Volume },
|
||||
{ value: Metrics.OneRepMax, label: Metrics.OneRepMax },
|
||||
|
@ -134,7 +138,7 @@ export default function ViewGraph() {
|
|||
value={metric}
|
||||
/>
|
||||
<Select
|
||||
label='Period'
|
||||
label="Period"
|
||||
items={[
|
||||
{ value: Periods.Weekly, label: Periods.Weekly },
|
||||
{ value: Periods.Monthly, label: Periods.Monthly },
|
||||
|
@ -146,5 +150,5 @@ export default function ViewGraph() {
|
|||
{charts}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,60 +1,57 @@
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/native'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { GestureResponderEvent, Image } from 'react-native'
|
||||
import { List, Menu, Text } from 'react-native-paper'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import { setRepo } from './db'
|
||||
import GymSet from './gym-set'
|
||||
import { WorkoutsPageParams } from './WorkoutsPage'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { GestureResponderEvent, Image } from "react-native";
|
||||
import { List, Menu, Text } from "react-native-paper";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { setRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||
|
||||
export default function WorkoutItem({
|
||||
item,
|
||||
onRemove,
|
||||
images,
|
||||
}: {
|
||||
item: GymSet
|
||||
onRemove: () => void
|
||||
images: boolean
|
||||
item: GymSet;
|
||||
onRemove: () => void;
|
||||
images: boolean;
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const [anchor, setAnchor] = useState({ x: 0, y: 0 })
|
||||
const [showRemove, setShowRemove] = useState('')
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [anchor, setAnchor] = useState({ x: 0, y: 0 });
|
||||
const [showRemove, setShowRemove] = useState("");
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
await setRepo.delete({ name: item.name })
|
||||
setShowMenu(false)
|
||||
onRemove()
|
||||
}, [setShowMenu, onRemove, item.name])
|
||||
await setRepo.delete({ name: item.name });
|
||||
setShowMenu(false);
|
||||
onRemove();
|
||||
}, [setShowMenu, onRemove, item.name]);
|
||||
|
||||
const longPress = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY })
|
||||
setShowMenu(true)
|
||||
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY });
|
||||
setShowMenu(true);
|
||||
},
|
||||
[setShowMenu, setAnchor],
|
||||
)
|
||||
[setShowMenu, setAnchor]
|
||||
);
|
||||
|
||||
const description = useMemo(() => {
|
||||
const seconds = item.seconds?.toString().padStart(2, '0')
|
||||
return `${item.sets} x ${item.minutes || 0}:${seconds}`
|
||||
}, [item])
|
||||
const seconds = item.seconds?.toString().padStart(2, "0");
|
||||
return `${item.sets} x ${item.minutes || 0}:${seconds}`;
|
||||
}, [item]);
|
||||
|
||||
const left = useCallback(() => {
|
||||
if (!images || !item.image) return null
|
||||
if (!images || !item.image) return null;
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: item.image }}
|
||||
style={{ height: 75, width: 75 }}
|
||||
/>
|
||||
)
|
||||
}, [item.image, images])
|
||||
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
|
||||
);
|
||||
}, [item.image, images]);
|
||||
|
||||
const right = useCallback(() => {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
|
@ -63,22 +60,22 @@ export default function WorkoutItem({
|
|||
onDismiss={() => setShowMenu(false)}
|
||||
>
|
||||
<Menu.Item
|
||||
leadingIcon='delete'
|
||||
leadingIcon="delete"
|
||||
onPress={() => {
|
||||
setShowRemove(item.name)
|
||||
setShowMenu(false)
|
||||
setShowRemove(item.name);
|
||||
setShowMenu(false);
|
||||
}}
|
||||
title='Delete'
|
||||
title="Delete"
|
||||
/>
|
||||
</Menu>
|
||||
</Text>
|
||||
)
|
||||
}, [anchor, showMenu, item.name])
|
||||
);
|
||||
}, [anchor, showMenu, item.name]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<List.Item
|
||||
onPress={() => navigation.navigate('EditWorkout', { value: item })}
|
||||
onPress={() => navigation.navigate("EditWorkout", { value: item })}
|
||||
title={item.name}
|
||||
description={description}
|
||||
onLongPress={longPress}
|
||||
|
@ -88,12 +85,12 @@ export default function WorkoutItem({
|
|||
<ConfirmDialog
|
||||
title={`Delete ${showRemove}`}
|
||||
show={!!showRemove}
|
||||
setShow={(show) => (show ? null : setShowRemove(''))}
|
||||
setShow={(show) => (show ? null : setShowRemove(""))}
|
||||
onOk={remove}
|
||||
>
|
||||
This irreversibly deletes ALL sets related to this workout. Are you
|
||||
sure?
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
142
WorkoutList.tsx
142
WorkoutList.tsx
|
@ -2,50 +2,50 @@ import {
|
|||
NavigationProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { FlatList } from 'react-native'
|
||||
import { List } from 'react-native-paper'
|
||||
import { setRepo, settingsRepo } from './db'
|
||||
import DrawerHeader from './DrawerHeader'
|
||||
import GymSet from './gym-set'
|
||||
import Page from './Page'
|
||||
import SetList from './SetList'
|
||||
import Settings from './settings'
|
||||
import WorkoutItem from './WorkoutItem'
|
||||
import { WorkoutsPageParams } from './WorkoutsPage'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { setRepo, settingsRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import GymSet from "./gym-set";
|
||||
import Page from "./Page";
|
||||
import SetList from "./SetList";
|
||||
import Settings from "./settings";
|
||||
import WorkoutItem from "./WorkoutItem";
|
||||
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||
|
||||
const limit = 15
|
||||
const limit = 15;
|
||||
|
||||
export default function WorkoutList() {
|
||||
const [workouts, setWorkouts] = useState<GymSet[]>()
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [term, setTerm] = useState('')
|
||||
const [end, setEnd] = useState(false)
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
|
||||
const [workouts, setWorkouts] = useState<GymSet[]>();
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [term, setTerm] = useState("");
|
||||
const [end, setEnd] = useState(false);
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
||||
|
||||
const refresh = useCallback(async (value: string) => {
|
||||
const newWorkouts = await setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.where('name LIKE :name', { name: `%${value.trim()}%` })
|
||||
.groupBy('name')
|
||||
.orderBy('name')
|
||||
.where("name LIKE :name", { name: `%${value.trim()}%` })
|
||||
.groupBy("name")
|
||||
.orderBy("name")
|
||||
.limit(limit)
|
||||
.getMany()
|
||||
console.log(`${WorkoutList.name}`, { newWorkout: newWorkouts[0] })
|
||||
setWorkouts(newWorkouts)
|
||||
setOffset(0)
|
||||
setEnd(false)
|
||||
}, [])
|
||||
.getMany();
|
||||
console.log(`${WorkoutList.name}`, { newWorkout: newWorkouts[0] });
|
||||
setWorkouts(newWorkouts);
|
||||
setOffset(0);
|
||||
setEnd(false);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refresh(term)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings)
|
||||
}, [refresh, term]),
|
||||
)
|
||||
refresh(term);
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [refresh, term])
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: GymSet }) => (
|
||||
|
@ -56,69 +56,67 @@ export default function WorkoutList() {
|
|||
onRemove={() => refresh(term)}
|
||||
/>
|
||||
),
|
||||
[refresh, term, settings?.images],
|
||||
)
|
||||
[refresh, term, settings?.images]
|
||||
);
|
||||
|
||||
const next = useCallback(async () => {
|
||||
if (end) return
|
||||
const newOffset = offset + limit
|
||||
if (end) return;
|
||||
const newOffset = offset + limit;
|
||||
console.log(`${SetList.name}.next:`, {
|
||||
offset,
|
||||
limit,
|
||||
newOffset,
|
||||
term,
|
||||
})
|
||||
});
|
||||
const newWorkouts = await setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.where('name LIKE :name', { name: `%${term.trim()}%` })
|
||||
.groupBy('name')
|
||||
.orderBy('name')
|
||||
.where("name LIKE :name", { name: `%${term.trim()}%` })
|
||||
.groupBy("name")
|
||||
.orderBy("name")
|
||||
.limit(limit)
|
||||
.offset(newOffset)
|
||||
.getMany()
|
||||
if (newWorkouts.length === 0) return setEnd(true)
|
||||
if (!workouts) return
|
||||
setWorkouts([...workouts, ...newWorkouts])
|
||||
if (newWorkouts.length < limit) return setEnd(true)
|
||||
setOffset(newOffset)
|
||||
}, [term, end, offset, workouts])
|
||||
.getMany();
|
||||
if (newWorkouts.length === 0) return setEnd(true);
|
||||
if (!workouts) return;
|
||||
setWorkouts([...workouts, ...newWorkouts]);
|
||||
if (newWorkouts.length < limit) return setEnd(true);
|
||||
setOffset(newOffset);
|
||||
}, [term, end, offset, workouts]);
|
||||
|
||||
const onAdd = useCallback(async () => {
|
||||
navigation.navigate('EditWorkout', {
|
||||
navigation.navigate("EditWorkout", {
|
||||
value: new GymSet(),
|
||||
})
|
||||
}, [navigation])
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const search = useCallback(
|
||||
(value: string) => {
|
||||
setTerm(value)
|
||||
refresh(value)
|
||||
setTerm(value);
|
||||
refresh(value);
|
||||
},
|
||||
[refresh],
|
||||
)
|
||||
[refresh]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name='Workouts' />
|
||||
<DrawerHeader name="Workouts" />
|
||||
<Page onAdd={onAdd} term={term} search={search}>
|
||||
{workouts?.length === 0
|
||||
? (
|
||||
<List.Item
|
||||
title='No workouts yet.'
|
||||
description='A workout is something you do at the gym. For example Deadlifts are a workout.'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FlatList
|
||||
data={workouts}
|
||||
style={{ flex: 1 }}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(w) => w.name}
|
||||
onEndReached={next}
|
||||
/>
|
||||
)}
|
||||
{workouts?.length === 0 ? (
|
||||
<List.Item
|
||||
title="No workouts yet."
|
||||
description="A workout is something you do at the gym. For example Deadlifts are a workout."
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
data={workouts}
|
||||
style={{ flex: 1 }}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(w) => w.name}
|
||||
onEndReached={next}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import { createStackNavigator } from '@react-navigation/stack'
|
||||
import EditWorkout from './EditWorkout'
|
||||
import GymSet from './gym-set'
|
||||
import WorkoutList from './WorkoutList'
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import EditWorkout from "./EditWorkout";
|
||||
import GymSet from "./gym-set";
|
||||
import WorkoutList from "./WorkoutList";
|
||||
|
||||
export type WorkoutsPageParams = {
|
||||
WorkoutList: {}
|
||||
WorkoutList: {};
|
||||
EditWorkout: {
|
||||
value: GymSet
|
||||
}
|
||||
}
|
||||
value: GymSet;
|
||||
};
|
||||
};
|
||||
|
||||
const Stack = createStackNavigator<WorkoutsPageParams>()
|
||||
const Stack = createStackNavigator<WorkoutsPageParams>();
|
||||
|
||||
export default function WorkoutsPage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
||||
>
|
||||
<Stack.Screen name='WorkoutList' component={WorkoutList} />
|
||||
<Stack.Screen name='EditWorkout' component={EditWorkout} />
|
||||
<Stack.Screen name="WorkoutList" component={WorkoutList} />
|
||||
<Stack.Screen name="EditWorkout" component={EditWorkout} />
|
||||
</Stack.Navigator>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { setRepo } from './db'
|
||||
import GymSet from './gym-set'
|
||||
import { setRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
|
||||
export const getBestSet = async (name: string): Promise<GymSet> => {
|
||||
return setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addSelect('MAX(weight)', 'weight')
|
||||
.where('name = :name', { name })
|
||||
.groupBy('name')
|
||||
.addGroupBy('reps')
|
||||
.orderBy('weight', 'DESC')
|
||||
.addOrderBy('reps', 'DESC')
|
||||
.getOne()
|
||||
}
|
||||
.addSelect("MAX(weight)", "weight")
|
||||
.where("name = :name", { name })
|
||||
.groupBy("name")
|
||||
.addGroupBy("reps")
|
||||
.orderBy("weight", "DESC")
|
||||
.addOrderBy("reps", "DESC")
|
||||
.getOne();
|
||||
};
|
||||
|
|
52
colors.ts
52
colors.ts
|
@ -1,41 +1,41 @@
|
|||
import { DefaultTheme, MD3DarkTheme } from 'react-native-paper'
|
||||
import { DefaultTheme, MD3DarkTheme } from "react-native-paper";
|
||||
|
||||
export const lightColors = [
|
||||
{ hex: MD3DarkTheme.colors.primary, name: 'Purple' },
|
||||
{ hex: '#B3E5FC', name: 'Blue' },
|
||||
{ hex: '#FA8072', name: 'Salmon' },
|
||||
{ hex: '#FFC0CB', name: 'Pink' },
|
||||
{ hex: '#E9DCC9', name: 'Linen' },
|
||||
]
|
||||
{ hex: MD3DarkTheme.colors.primary, name: "Purple" },
|
||||
{ hex: "#B3E5FC", name: "Blue" },
|
||||
{ hex: "#FA8072", name: "Salmon" },
|
||||
{ hex: "#FFC0CB", name: "Pink" },
|
||||
{ hex: "#E9DCC9", name: "Linen" },
|
||||
];
|
||||
|
||||
export const darkColors = [
|
||||
{ hex: DefaultTheme.colors.primary, name: 'Purple' },
|
||||
{ hex: '#0051a9', name: 'Blue' },
|
||||
{ hex: '#000000', name: 'Black' },
|
||||
{ hex: '#863c3c', name: 'Red' },
|
||||
{ hex: '#1c6000', name: 'Kermit' },
|
||||
]
|
||||
{ hex: DefaultTheme.colors.primary, name: "Purple" },
|
||||
{ hex: "#0051a9", name: "Blue" },
|
||||
{ hex: "#000000", name: "Black" },
|
||||
{ hex: "#863c3c", name: "Red" },
|
||||
{ hex: "#1c6000", name: "Kermit" },
|
||||
];
|
||||
|
||||
export const colorShade = (color: any, amount: number) => {
|
||||
color = color.replace(/^#/, '')
|
||||
color = color.replace(/^#/, "");
|
||||
if (color.length === 3) {
|
||||
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]
|
||||
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
|
||||
}
|
||||
|
||||
let [r, g, b] = color.match(/.{2}/g)
|
||||
;[r, g, b] = [
|
||||
let [r, g, b] = color.match(/.{2}/g);
|
||||
[r, g, b] = [
|
||||
parseInt(r, 16) + amount,
|
||||
parseInt(g, 16) + amount,
|
||||
parseInt(b, 16) + amount,
|
||||
]
|
||||
];
|
||||
|
||||
r = Math.max(Math.min(255, r), 0).toString(16)
|
||||
g = Math.max(Math.min(255, g), 0).toString(16)
|
||||
b = Math.max(Math.min(255, b), 0).toString(16)
|
||||
r = Math.max(Math.min(255, r), 0).toString(16);
|
||||
g = Math.max(Math.min(255, g), 0).toString(16);
|
||||
b = Math.max(Math.min(255, b), 0).toString(16);
|
||||
|
||||
const rr = (r.length < 2 ? '0' : '') + r
|
||||
const gg = (g.length < 2 ? '0' : '') + g
|
||||
const bb = (b.length < 2 ? '0' : '') + b
|
||||
const rr = (r.length < 2 ? "0" : "") + r;
|
||||
const gg = (g.length < 2 ? "0" : "") + g;
|
||||
const bb = (b.length < 2 ? "0" : "") + b;
|
||||
|
||||
return `#${rr}${gg}${bb}`
|
||||
}
|
||||
return `#${rr}${gg}${bb}`;
|
||||
};
|
||||
|
|
10
constants.ts
10
constants.ts
|
@ -1,5 +1,5 @@
|
|||
export const MARGIN = 10
|
||||
export const PADDING = 10
|
||||
export const ITEM_PADDING = 8
|
||||
export const DARK_RIPPLE = '#444444'
|
||||
export const LIGHT_RIPPLE = '#c2c2c2'
|
||||
export const MARGIN = 10;
|
||||
export const PADDING = 10;
|
||||
export const ITEM_PADDING = 8;
|
||||
export const DARK_RIPPLE = "#444444";
|
||||
export const LIGHT_RIPPLE = "#c2c2c2";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export default interface CountMany {
|
||||
name: string
|
||||
total: number
|
||||
sets?: number
|
||||
name: string;
|
||||
total: number;
|
||||
sets?: number;
|
||||
}
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
import { DataSource } from 'typeorm'
|
||||
import GymSet from './gym-set'
|
||||
import { Sets1667185586014 as sets1667185586014 } from './migrations/1667185586014-sets'
|
||||
import { plans1667186124792 } from './migrations/1667186124792-plans'
|
||||
import { settings1667186130041 } from './migrations/1667186130041-settings'
|
||||
import { addSound1667186139844 } from './migrations/1667186139844-add-sound'
|
||||
import { addHidden1667186159379 } from './migrations/1667186159379-add-hidden'
|
||||
import { addNotify1667186166140 } from './migrations/1667186166140-add-notify'
|
||||
import { addImage1667186171548 } from './migrations/1667186171548-add-image'
|
||||
import { addImages1667186179488 } from './migrations/1667186179488-add-images'
|
||||
import { insertSettings1667186203827 } from './migrations/1667186203827-insert-settings'
|
||||
import { addSteps1667186211251 } from './migrations/1667186211251-add-steps'
|
||||
import { addSets1667186250618 } from './migrations/1667186250618-add-sets'
|
||||
import { addMinutes1667186255650 } from './migrations/1667186255650-add-minutes'
|
||||
import { addSeconds1667186259174 } from './migrations/1667186259174-add-seconds'
|
||||
import { addShowUnit1667186265588 } from './migrations/1667186265588-add-show-unit'
|
||||
import { addColor1667186320954 } from './migrations/1667186320954-add-color'
|
||||
import { addSteps1667186348425 } from './migrations/1667186348425-add-steps'
|
||||
import { addDate1667186431804 } from './migrations/1667186431804-add-date'
|
||||
import { addShowDate1667186435051 } from './migrations/1667186435051-add-show-date'
|
||||
import { addTheme1667186439366 } from './migrations/1667186439366-add-theme'
|
||||
import { addShowSets1667186443614 } from './migrations/1667186443614-add-show-sets'
|
||||
import { addSetsCreated1667186451005 } from './migrations/1667186451005-add-sets-created'
|
||||
import { addNoSound1667186456118 } from './migrations/1667186456118-add-no-sound'
|
||||
import { dropMigrations1667190214743 } from './migrations/1667190214743-drop-migrations'
|
||||
import { splitColor1669420187764 } from './migrations/1669420187764-split-color'
|
||||
import { addBackup1678334268359 } from './migrations/1678334268359-add-backup'
|
||||
import { Plan } from './plan'
|
||||
import Settings from './settings'
|
||||
import { DataSource } from "typeorm";
|
||||
import GymSet from "./gym-set";
|
||||
import { Sets1667185586014 as sets1667185586014 } from "./migrations/1667185586014-sets";
|
||||
import { plans1667186124792 } from "./migrations/1667186124792-plans";
|
||||
import { settings1667186130041 } from "./migrations/1667186130041-settings";
|
||||
import { addSound1667186139844 } from "./migrations/1667186139844-add-sound";
|
||||
import { addHidden1667186159379 } from "./migrations/1667186159379-add-hidden";
|
||||
import { addNotify1667186166140 } from "./migrations/1667186166140-add-notify";
|
||||
import { addImage1667186171548 } from "./migrations/1667186171548-add-image";
|
||||
import { addImages1667186179488 } from "./migrations/1667186179488-add-images";
|
||||
import { insertSettings1667186203827 } from "./migrations/1667186203827-insert-settings";
|
||||
import { addSteps1667186211251 } from "./migrations/1667186211251-add-steps";
|
||||
import { addSets1667186250618 } from "./migrations/1667186250618-add-sets";
|
||||
import { addMinutes1667186255650 } from "./migrations/1667186255650-add-minutes";
|
||||
import { addSeconds1667186259174 } from "./migrations/1667186259174-add-seconds";
|
||||
import { addShowUnit1667186265588 } from "./migrations/1667186265588-add-show-unit";
|
||||
import { addColor1667186320954 } from "./migrations/1667186320954-add-color";
|
||||
import { addSteps1667186348425 } from "./migrations/1667186348425-add-steps";
|
||||
import { addDate1667186431804 } from "./migrations/1667186431804-add-date";
|
||||
import { addShowDate1667186435051 } from "./migrations/1667186435051-add-show-date";
|
||||
import { addTheme1667186439366 } from "./migrations/1667186439366-add-theme";
|
||||
import { addShowSets1667186443614 } from "./migrations/1667186443614-add-show-sets";
|
||||
import { addSetsCreated1667186451005 } from "./migrations/1667186451005-add-sets-created";
|
||||
import { addNoSound1667186456118 } from "./migrations/1667186456118-add-no-sound";
|
||||
import { dropMigrations1667190214743 } from "./migrations/1667190214743-drop-migrations";
|
||||
import { splitColor1669420187764 } from "./migrations/1669420187764-split-color";
|
||||
import { addBackup1678334268359 } from "./migrations/1678334268359-add-backup";
|
||||
import { Plan } from "./plan";
|
||||
import Settings from "./settings";
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'react-native',
|
||||
database: 'massive.db',
|
||||
location: 'default',
|
||||
type: "react-native",
|
||||
database: "massive.db",
|
||||
location: "default",
|
||||
entities: [GymSet, Plan, Settings],
|
||||
migrationsRun: true,
|
||||
migrationsTableName: 'typeorm_migrations',
|
||||
migrationsTableName: "typeorm_migrations",
|
||||
migrations: [
|
||||
sets1667185586014,
|
||||
plans1667186124792,
|
||||
|
@ -62,4 +62,4 @@ export const AppDataSource = new DataSource({
|
|||
splitColor1669420187764,
|
||||
addBackup1678334268359,
|
||||
],
|
||||
})
|
||||
});
|
||||
|
|
22
db.ts
22
db.ts
|
@ -1,15 +1,15 @@
|
|||
import { AppDataSource } from './data-source'
|
||||
import GymSet from './gym-set'
|
||||
import { Plan } from './plan'
|
||||
import Settings from './settings'
|
||||
import { AppDataSource } from "./data-source";
|
||||
import GymSet from "./gym-set";
|
||||
import { Plan } from "./plan";
|
||||
import Settings from "./settings";
|
||||
|
||||
export const setRepo = AppDataSource.manager.getRepository(GymSet)
|
||||
export const planRepo = AppDataSource.manager.getRepository(Plan)
|
||||
export const settingsRepo = AppDataSource.manager.getRepository(Settings)
|
||||
export const setRepo = AppDataSource.manager.getRepository(GymSet);
|
||||
export const planRepo = AppDataSource.manager.getRepository(Plan);
|
||||
export const settingsRepo = AppDataSource.manager.getRepository(Settings);
|
||||
|
||||
export const getNow = async (): Promise<string> => {
|
||||
const query = await AppDataSource.manager.query(
|
||||
"SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now",
|
||||
)
|
||||
return query[0].now
|
||||
}
|
||||
"SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now"
|
||||
);
|
||||
return query[0].now;
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export type DrawerParamList = {
|
||||
Home: {}
|
||||
Settings: {}
|
||||
Graphs: {}
|
||||
Plans: {}
|
||||
Workouts: {}
|
||||
Timer: {}
|
||||
}
|
||||
Home: {};
|
||||
Settings: {};
|
||||
Graphs: {};
|
||||
Plans: {};
|
||||
Workouts: {};
|
||||
Timer: {};
|
||||
};
|
||||
|
|
60
gym-set.ts
60
gym-set.ts
|
@ -1,53 +1,53 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity('sets')
|
||||
@Entity("sets")
|
||||
export default class GymSet {
|
||||
@PrimaryGeneratedColumn()
|
||||
id?: number
|
||||
id?: number;
|
||||
|
||||
@Column('text')
|
||||
name: string
|
||||
@Column("text")
|
||||
name: string;
|
||||
|
||||
@Column('int')
|
||||
reps: number
|
||||
@Column("int")
|
||||
reps: number;
|
||||
|
||||
@Column('int')
|
||||
weight: number
|
||||
@Column("int")
|
||||
weight: number;
|
||||
|
||||
@Column('int')
|
||||
sets = 3
|
||||
@Column("int")
|
||||
sets = 3;
|
||||
|
||||
@Column('int')
|
||||
minutes = 3
|
||||
@Column("int")
|
||||
minutes = 3;
|
||||
|
||||
@Column('int')
|
||||
seconds = 30
|
||||
@Column("int")
|
||||
seconds = 30;
|
||||
|
||||
@Column('boolean')
|
||||
hidden = false
|
||||
@Column("boolean")
|
||||
hidden = false;
|
||||
|
||||
@Column('text')
|
||||
created: string
|
||||
@Column("text")
|
||||
created: string;
|
||||
|
||||
@Column('text')
|
||||
unit: string
|
||||
@Column("text")
|
||||
unit: string;
|
||||
|
||||
@Column('text')
|
||||
image: string
|
||||
@Column("text")
|
||||
image: string;
|
||||
|
||||
@Column('text')
|
||||
steps?: string
|
||||
@Column("text")
|
||||
steps?: string;
|
||||
}
|
||||
|
||||
export const defaultSet: GymSet = {
|
||||
created: '',
|
||||
name: '',
|
||||
image: '',
|
||||
created: "",
|
||||
name: "",
|
||||
image: "",
|
||||
hidden: false,
|
||||
minutes: 3,
|
||||
seconds: 30,
|
||||
reps: 0,
|
||||
sets: 0,
|
||||
unit: 'kg',
|
||||
unit: "kg",
|
||||
weight: 0,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import GymSet from './gym-set'
|
||||
import GymSet from "./gym-set";
|
||||
|
||||
export type HomePageParams = {
|
||||
Sets: {}
|
||||
Sets: {};
|
||||
EditSet: {
|
||||
set: GymSet
|
||||
}
|
||||
set: GymSet;
|
||||
};
|
||||
EditSets: {
|
||||
ids: number[]
|
||||
}
|
||||
}
|
||||
ids: number[];
|
||||
};
|
||||
};
|
||||
|
|
12
input.ts
12
input.ts
|
@ -1,9 +1,9 @@
|
|||
import { Item } from './Select'
|
||||
import Settings from './settings'
|
||||
import { Item } from "./Select";
|
||||
import Settings from "./settings";
|
||||
|
||||
export default interface Input<T> {
|
||||
name: string
|
||||
key: keyof Settings
|
||||
value?: T
|
||||
items?: Item[]
|
||||
name: string;
|
||||
key: keyof Settings;
|
||||
value?: T;
|
||||
items?: Item[];
|
||||
}
|
||||
|
|
26
jestSetup.ts
26
jestSetup.ts
|
@ -1,21 +1,21 @@
|
|||
import { NativeModules } from 'react-native'
|
||||
import 'react-native-gesture-handler/jestSetup'
|
||||
import { NativeModules } from "react-native";
|
||||
import "react-native-gesture-handler/jestSetup";
|
||||
|
||||
NativeModules.RNViewShot = NativeModules.RNViewShot || {
|
||||
captureScreen: jest.fn(),
|
||||
}
|
||||
};
|
||||
NativeModules.SettingsModule = NativeModules.SettingsModule || {
|
||||
ignoringBattery: jest.fn(),
|
||||
is24: jest.fn(() => Promise.resolve(true)),
|
||||
}
|
||||
};
|
||||
|
||||
jest.mock('react-native-file-access', () => jest.fn())
|
||||
jest.mock('react-native-share', () => jest.fn())
|
||||
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
|
||||
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')
|
||||
jest.mock("react-native-file-access", () => jest.fn());
|
||||
jest.mock("react-native-share", () => jest.fn());
|
||||
jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
|
||||
jest.mock("react-native/Libraries/EventEmitter/NativeEventEmitter");
|
||||
|
||||
jest.mock('react-native-reanimated', () => {
|
||||
const Reanimated = require('react-native-reanimated/mock')
|
||||
Reanimated.default.call = () => {}
|
||||
return Reanimated
|
||||
})
|
||||
jest.mock("react-native-reanimated", () => {
|
||||
const Reanimated = require("react-native-reanimated/mock");
|
||||
Reanimated.default.call = () => {};
|
||||
return Reanimated;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum Metrics {
|
||||
Weight = 'Best weight',
|
||||
Volume = 'Volume',
|
||||
OneRepMax = 'One rep max',
|
||||
Weight = "Best weight",
|
||||
Volume = "Volume",
|
||||
OneRepMax = "One rep max",
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import { NavigationContainer } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import React from "react";
|
||||
import {
|
||||
DefaultTheme,
|
||||
MD3DarkTheme,
|
||||
Provider as PaperProvider,
|
||||
} from 'react-native-paper'
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
|
||||
import { ThemeContext } from './use-theme'
|
||||
} from "react-native-paper";
|
||||
import MaterialIcon from "react-native-vector-icons/MaterialIcons";
|
||||
import { ThemeContext } from "./use-theme";
|
||||
|
||||
export const MockProviders = ({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element | JSX.Element[]
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}) => (
|
||||
<PaperProvider settings={{ icon: (props) => <MaterialIcon {...props} /> }}>
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
theme: 'system',
|
||||
theme: "system",
|
||||
setTheme: jest.fn(),
|
||||
lightColor: DefaultTheme.colors.primary,
|
||||
darkColor: MD3DarkTheme.colors.primary,
|
||||
|
@ -27,4 +27,4 @@ export const MockProviders = ({
|
|||
<NavigationContainer>{children}</NavigationContainer>
|
||||
</ThemeContext.Provider>
|
||||
</PaperProvider>
|
||||
)
|
||||
);
|
||||
|
|
14
options.ts
14
options.ts
|
@ -1,19 +1,19 @@
|
|||
import { darkColors, lightColors } from './colors'
|
||||
import { darkColors, lightColors } from "./colors";
|
||||
|
||||
export const themeOptions = [
|
||||
{ label: 'System', value: 'system' },
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
{ label: 'Light', value: 'light' },
|
||||
]
|
||||
{ label: "System", value: "system" },
|
||||
{ label: "Dark", value: "dark" },
|
||||
{ label: "Light", value: "light" },
|
||||
];
|
||||
|
||||
export const lightOptions = lightColors.map((color) => ({
|
||||
label: color.name,
|
||||
value: color.hex,
|
||||
color: color.hex,
|
||||
}))
|
||||
}));
|
||||
|
||||
export const darkOptions = darkColors.map((color) => ({
|
||||
label: color.name,
|
||||
value: color.hex,
|
||||
color: color.hex,
|
||||
}))
|
||||
}));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum Periods {
|
||||
Weekly = 'This week',
|
||||
Monthly = 'This month',
|
||||
Yearly = 'This year',
|
||||
Weekly = "This week",
|
||||
Monthly = "This month",
|
||||
Yearly = "This year",
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import GymSet from './gym-set'
|
||||
import { Plan } from './plan'
|
||||
import GymSet from "./gym-set";
|
||||
import { Plan } from "./plan";
|
||||
|
||||
export type PlanPageParams = {
|
||||
PlanList: {}
|
||||
PlanList: {};
|
||||
EditPlan: {
|
||||
plan: Plan
|
||||
}
|
||||
plan: Plan;
|
||||
};
|
||||
StartPlan: {
|
||||
plan: Plan
|
||||
first?: GymSet
|
||||
}
|
||||
plan: Plan;
|
||||
first?: GymSet;
|
||||
};
|
||||
EditSet: {
|
||||
set: GymSet
|
||||
}
|
||||
}
|
||||
set: GymSet;
|
||||
};
|
||||
};
|
||||
|
|
14
plan.ts
14
plan.ts
|
@ -1,13 +1,13 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity('plans')
|
||||
@Entity("plans")
|
||||
export class Plan {
|
||||
@PrimaryGeneratedColumn()
|
||||
id?: number
|
||||
id?: number;
|
||||
|
||||
@Column('text')
|
||||
days: string
|
||||
@Column("text")
|
||||
days: string;
|
||||
|
||||
@Column('text')
|
||||
workouts: string
|
||||
@Column("text")
|
||||
workouts: string;
|
||||
}
|
||||
|
|
8
route.ts
8
route.ts
|
@ -1,7 +1,7 @@
|
|||
import { DrawerParamList } from './drawer-param-list'
|
||||
import { DrawerParamList } from "./drawer-param-list";
|
||||
|
||||
export default interface Route {
|
||||
name: keyof DrawerParamList
|
||||
component: React.ComponentType<any>
|
||||
icon: string
|
||||
name: keyof DrawerParamList;
|
||||
component: React.ComponentType<any>;
|
||||
icon: string;
|
||||
}
|
||||
|
|
62
settings.ts
62
settings.ts
|
@ -1,49 +1,49 @@
|
|||
import { Column, Entity, PrimaryColumn } from 'typeorm'
|
||||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export default class Settings {
|
||||
@PrimaryColumn('boolean')
|
||||
alarm: boolean
|
||||
@PrimaryColumn("boolean")
|
||||
alarm: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
vibrate: boolean
|
||||
@Column("boolean")
|
||||
vibrate: boolean;
|
||||
|
||||
@Column('text')
|
||||
sound: string
|
||||
@Column("text")
|
||||
sound: string;
|
||||
|
||||
@Column('boolean')
|
||||
notify: boolean
|
||||
@Column("boolean")
|
||||
notify: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
images: boolean
|
||||
@Column("boolean")
|
||||
images: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
showUnit: boolean
|
||||
@Column("boolean")
|
||||
showUnit: boolean;
|
||||
|
||||
@Column('text')
|
||||
lightColor?: string
|
||||
@Column("text")
|
||||
lightColor?: string;
|
||||
|
||||
@Column('text')
|
||||
darkColor?: string
|
||||
@Column("text")
|
||||
darkColor?: string;
|
||||
|
||||
@Column('boolean')
|
||||
steps: boolean
|
||||
@Column("boolean")
|
||||
steps: boolean;
|
||||
|
||||
@Column('text')
|
||||
date: string
|
||||
@Column("text")
|
||||
date: string;
|
||||
|
||||
@Column('boolean')
|
||||
showDate: boolean
|
||||
@Column("boolean")
|
||||
showDate: boolean;
|
||||
|
||||
@Column('text')
|
||||
theme: string
|
||||
@Column("text")
|
||||
theme: string;
|
||||
|
||||
@Column('boolean')
|
||||
showSets: boolean
|
||||
@Column("boolean")
|
||||
showSets: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
noSound: boolean
|
||||
@Column("boolean")
|
||||
noSound: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
backup: boolean
|
||||
@Column("boolean")
|
||||
backup: boolean;
|
||||
}
|
||||
|
|
16
time.ts
16
time.ts
|
@ -1,9 +1,9 @@
|
|||
export const DAYS = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
]
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
|
|
6
toast.ts
6
toast.ts
|
@ -1,7 +1,7 @@
|
|||
import { DeviceEventEmitter } from 'react-native'
|
||||
import { DeviceEventEmitter } from "react-native";
|
||||
|
||||
export const TOAST = 'toast'
|
||||
export const TOAST = "toast";
|
||||
|
||||
export function toast(value: string) {
|
||||
DeviceEventEmitter.emit(TOAST, { value })
|
||||
DeviceEventEmitter.emit(TOAST, { value });
|
||||
}
|
||||
|
|
14
use-dark.ts
14
use-dark.ts
|
@ -1,11 +1,11 @@
|
|||
import { useColorScheme } from 'react-native'
|
||||
import { useTheme } from './use-theme'
|
||||
import { useColorScheme } from "react-native";
|
||||
import { useTheme } from "./use-theme";
|
||||
|
||||
export default function useDark() {
|
||||
const dark = useColorScheme() === 'dark'
|
||||
const { theme } = useTheme()
|
||||
const dark = useColorScheme() === "dark";
|
||||
const { theme } = useTheme();
|
||||
|
||||
if (theme === 'dark') return true
|
||||
if (theme === 'light') return false
|
||||
return dark
|
||||
if (theme === "dark") return true;
|
||||
if (theme === "light") return false;
|
||||
return dark;
|
||||
}
|
||||
|
|
22
use-theme.ts
22
use-theme.ts
|
@ -1,22 +1,22 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
import { MD3DarkTheme, MD3LightTheme } from 'react-native-paper'
|
||||
import { createContext, useContext } from "react";
|
||||
import { MD3DarkTheme, MD3LightTheme } from "react-native-paper";
|
||||
|
||||
export const ThemeContext = createContext<{
|
||||
theme: string
|
||||
lightColor: string
|
||||
setTheme: (value: string) => void
|
||||
setLightColor: (value: string) => void
|
||||
darkColor: string
|
||||
setDarkColor: (value: string) => void
|
||||
theme: string;
|
||||
lightColor: string;
|
||||
setTheme: (value: string) => void;
|
||||
setLightColor: (value: string) => void;
|
||||
darkColor: string;
|
||||
setDarkColor: (value: string) => void;
|
||||
}>({
|
||||
theme: 'system',
|
||||
theme: "system",
|
||||
lightColor: MD3DarkTheme.colors.primary,
|
||||
setTheme: () => null,
|
||||
setLightColor: () => null,
|
||||
darkColor: MD3LightTheme.colors.primary,
|
||||
setDarkColor: () => null,
|
||||
})
|
||||
});
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext)
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
|
|
36
use-timer.ts
36
use-timer.ts
|
@ -1,25 +1,25 @@
|
|||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { NativeEventEmitter } from 'react-native'
|
||||
import { TickEvent } from './TimerPage'
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { NativeEventEmitter } from "react-native";
|
||||
import { TickEvent } from "./TimerPage";
|
||||
|
||||
export default function useTimer() {
|
||||
const [minutes, setMinutes] = useState('00')
|
||||
const [seconds, setSeconds] = useState('00')
|
||||
const [minutes, setMinutes] = useState("00");
|
||||
const [seconds, setSeconds] = useState("00");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setMinutes('00')
|
||||
setSeconds('00')
|
||||
const emitter = new NativeEventEmitter()
|
||||
const listener = emitter.addListener('tick', (event: TickEvent) => {
|
||||
console.log(`${useTimer.name}.tick:`, { event })
|
||||
setMinutes(event.minutes)
|
||||
setSeconds(event.seconds)
|
||||
})
|
||||
return listener.remove
|
||||
}, []),
|
||||
)
|
||||
setMinutes("00");
|
||||
setSeconds("00");
|
||||
const emitter = new NativeEventEmitter();
|
||||
const listener = emitter.addListener("tick", (event: TickEvent) => {
|
||||
console.log(`${useTimer.name}.tick:`, { event });
|
||||
setMinutes(event.minutes);
|
||||
setSeconds(event.seconds);
|
||||
});
|
||||
return listener.remove;
|
||||
}, [])
|
||||
);
|
||||
|
||||
return { minutes, seconds }
|
||||
return { minutes, seconds };
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default interface Volume {
|
||||
name: string
|
||||
created: string
|
||||
value: number
|
||||
unit: string
|
||||
name: string;
|
||||
created: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
}
|
||||
|
|
32
write.ts
32
write.ts
|
@ -1,21 +1,21 @@
|
|||
import { PermissionsAndroid, Platform } from 'react-native'
|
||||
import { Dirs, FileSystem } from 'react-native-file-access'
|
||||
import { toast } from './toast'
|
||||
import { PermissionsAndroid, Platform } from "react-native";
|
||||
import { Dirs, FileSystem } from "react-native-file-access";
|
||||
import { toast } from "./toast";
|
||||
|
||||
export const write = async (name: string, data: string) => {
|
||||
const filePath = `${Dirs.DocumentDir}/${name}`
|
||||
const filePath = `${Dirs.DocumentDir}/${name}`;
|
||||
const permission = async () => {
|
||||
if (Platform.OS !== 'android') return true
|
||||
if (Platform.OS !== "android") return true;
|
||||
const granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
||||
)
|
||||
return granted === PermissionsAndroid.RESULTS.GRANTED
|
||||
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
|
||||
);
|
||||
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
||||
};
|
||||
const granted = await permission();
|
||||
if (!granted) return;
|
||||
await FileSystem.writeFile(filePath, data);
|
||||
if (Platform.OS === "android") {
|
||||
await FileSystem.cpExternal(filePath, name, "downloads");
|
||||
}
|
||||
const granted = await permission()
|
||||
if (!granted) return
|
||||
await FileSystem.writeFile(filePath, data)
|
||||
if (Platform.OS === 'android') {
|
||||
await FileSystem.cpExternal(filePath, name, 'downloads')
|
||||
}
|
||||
toast(`Downloaded ${name}`)
|
||||
}
|
||||
toast(`Downloaded ${name}`);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue