Add weight page
This commit is contained in:
parent
7928cab4c1
commit
ff7cd2fe54
|
@ -4,7 +4,6 @@ import { View } from "react-native";
|
||||||
import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
|
import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
|
||||||
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import GymSet from "./gym-set";
|
|
||||||
import useDark from "./use-dark";
|
import useDark from "./use-dark";
|
||||||
|
|
||||||
export default function Chart({
|
export default function Chart({
|
||||||
|
@ -14,7 +13,7 @@ export default function Chart({
|
||||||
yFormat,
|
yFormat,
|
||||||
}: {
|
}: {
|
||||||
yData: number[];
|
yData: number[];
|
||||||
xData: GymSet[];
|
xData: unknown[];
|
||||||
xFormat: (value: any, index: number) => string;
|
xFormat: (value: any, index: number) => string;
|
||||||
yFormat: (value: any) => string;
|
yFormat: (value: any) => string;
|
||||||
}) {
|
}) {
|
||||||
|
|
242
EditWeight.tsx
242
EditWeight.tsx
|
@ -8,130 +8,53 @@ import {
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { NativeModules, TextInput, View } from "react-native";
|
import { TextInput, View } from "react-native";
|
||||||
import DocumentPicker from "react-native-document-picker";
|
import { Button, IconButton } from "react-native-paper";
|
||||||
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
|
|
||||||
import AppInput from "./AppInput";
|
import AppInput from "./AppInput";
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import { getNow, setRepo, settingsRepo } from "./db";
|
import { getNow, settingsRepo, weightRepo } from "./db";
|
||||||
import GymSet, {
|
|
||||||
GYM_SET_CREATED,
|
|
||||||
GYM_SET_DELETED,
|
|
||||||
GYM_SET_UPDATED,
|
|
||||||
} from "./gym-set";
|
|
||||||
import { HomePageParams } from "./home-page-params";
|
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
import StackHeader from "./StackHeader";
|
import StackHeader from "./StackHeader";
|
||||||
import { toast } from "./toast";
|
import Weight from "./weight";
|
||||||
import { fixNumeric } from "./fix-numeric";
|
import { WeightPageParams } from "./WeightPage";
|
||||||
import { emitter } from "./emitter";
|
|
||||||
|
|
||||||
export default function EditWeight() {
|
export default function EditWeight() {
|
||||||
const { params } = useRoute<RouteProp<HomePageParams, "EditSet">>();
|
const { params } = useRoute<RouteProp<WeightPageParams, "EditWeight">>();
|
||||||
const { set } = params;
|
const { weight } = params;
|
||||||
const { navigate } = useNavigation<NavigationProp<HomePageParams>>();
|
const { navigate } = useNavigation<NavigationProp<WeightPageParams>>();
|
||||||
const [settings, setSettings] = useState<Settings>({} as Settings);
|
const [settings, setSettings] = useState<Settings>({} as Settings);
|
||||||
const [name, setName] = useState(set.name);
|
const [value, setValue] = useState(weight.value?.toString());
|
||||||
const [reps, setReps] = useState(set.reps?.toString());
|
const [unit, setUnit] = useState(weight.unit);
|
||||||
const [weight, setWeight] = useState(set.weight?.toString());
|
|
||||||
const [newImage, setNewImage] = useState(set.image);
|
|
||||||
const [unit, setUnit] = useState(set.unit);
|
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
|
||||||
const [created, setCreated] = useState<Date>(
|
const [created, setCreated] = useState<Date>(
|
||||||
set.created ? new Date(set.created) : new Date()
|
weight.created ? new Date(weight.created) : new Date()
|
||||||
);
|
);
|
||||||
|
const [showDelete, setShowDelete] = useState(false);
|
||||||
const [createdDirty, setCreatedDirty] = useState(false);
|
const [createdDirty, setCreatedDirty] = useState(false);
|
||||||
const [showRemove, setShowRemove] = useState(false);
|
|
||||||
const [removeImage, setRemoveImage] = useState(false);
|
|
||||||
const weightRef = useRef<TextInput>(null);
|
|
||||||
const repsRef = useRef<TextInput>(null);
|
|
||||||
const unitRef = useRef<TextInput>(null);
|
const unitRef = useRef<TextInput>(null);
|
||||||
|
|
||||||
const [selection, setSelection] = useState({
|
|
||||||
start: 0,
|
|
||||||
end: set.reps?.toString().length,
|
|
||||||
});
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
const startTimer = useCallback(
|
|
||||||
async (value: string) => {
|
|
||||||
if (!settings.alarm) return;
|
|
||||||
const first = await setRepo.findOne({ where: { name: value } });
|
|
||||||
const milliseconds =
|
|
||||||
(first?.minutes ?? 3) * 60 * 1000 + (first?.seconds ?? 0) * 1000;
|
|
||||||
if (milliseconds) NativeModules.AlarmModule.timer(milliseconds);
|
|
||||||
},
|
|
||||||
[settings]
|
|
||||||
);
|
|
||||||
|
|
||||||
const notify = (value: Partial<GymSet>) => {
|
|
||||||
if (!settings.notify) return navigate("Sets");
|
|
||||||
if (
|
|
||||||
value.weight > set.weight ||
|
|
||||||
(value.reps > set.reps && value.weight === set.weight)
|
|
||||||
) {
|
|
||||||
toast("Great work King! That's a new record.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const added = async (value: GymSet) => {
|
|
||||||
console.log(`${EditSet.name}.added:`, value);
|
|
||||||
emitter.emit(GYM_SET_CREATED);
|
|
||||||
startTimer(value.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!name) return;
|
if (!value) return;
|
||||||
|
|
||||||
const newSet: Partial<GymSet> = {
|
const newWeight: Partial<Weight> = {
|
||||||
id: set.id,
|
id: weight.id,
|
||||||
name,
|
value: Number(value),
|
||||||
reps: Number(reps),
|
|
||||||
weight: Number(weight),
|
|
||||||
unit,
|
unit,
|
||||||
minutes: Number(set.minutes ?? 3),
|
|
||||||
seconds: Number(set.seconds ?? 30),
|
|
||||||
sets: set.sets ?? 3,
|
|
||||||
hidden: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
newSet.image = newImage;
|
if (createdDirty) newWeight.created = created.toISOString();
|
||||||
if (!newImage && !removeImage) {
|
else if (typeof weight.id !== "number") newWeight.created = await getNow();
|
||||||
newSet.image = await setRepo
|
|
||||||
.findOne({ where: { name } })
|
|
||||||
.then((s) => s?.image);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createdDirty) newSet.created = created.toISOString();
|
await weightRepo.save(newWeight);
|
||||||
if (typeof set.id !== "number") newSet.created = await getNow();
|
navigate("Weights");
|
||||||
|
|
||||||
const saved = await setRepo.save(newSet);
|
|
||||||
notify(newSet);
|
|
||||||
if (typeof set.id !== "number") return added(saved);
|
|
||||||
else emitter.emit(GYM_SET_UPDATED, saved);
|
|
||||||
navigate("Sets");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeImage = useCallback(async () => {
|
|
||||||
const { fileCopyUri } = await DocumentPicker.pickSingle({
|
|
||||||
type: DocumentPicker.types.images,
|
|
||||||
copyTo: "documentDirectory",
|
|
||||||
});
|
|
||||||
if (fileCopyUri) setNewImage(fileCopyUri);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(async () => {
|
|
||||||
setNewImage("");
|
|
||||||
setRemoveImage(true);
|
|
||||||
setShowRemove(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const pickDate = useCallback(() => {
|
const pickDate = useCallback(() => {
|
||||||
DateTimePickerAndroid.open({
|
DateTimePickerAndroid.open({
|
||||||
value: created,
|
value: created,
|
||||||
|
@ -139,108 +62,44 @@ export default function EditWeight() {
|
||||||
if (date === created) return;
|
if (date === created) return;
|
||||||
setCreated(date);
|
setCreated(date);
|
||||||
setCreatedDirty(true);
|
setCreatedDirty(true);
|
||||||
DateTimePickerAndroid.open({
|
|
||||||
value: date,
|
|
||||||
onChange: (__, time) => setCreated(time),
|
|
||||||
mode: "time",
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
mode: "date",
|
mode: "date",
|
||||||
});
|
});
|
||||||
}, [created]);
|
}, [created]);
|
||||||
|
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
await setRepo.delete(set.id);
|
if (!weight.id) return;
|
||||||
emitter.emit(GYM_SET_DELETED);
|
await weightRepo.delete(weight.id);
|
||||||
navigate("Sets");
|
navigate("Weights");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader title={typeof set.id === "number" ? "Edit set" : "Add set"}>
|
<StackHeader
|
||||||
{typeof set.id === "number" ? (
|
title={typeof weight.id === "number" ? "Edit weight" : "Add weight"}
|
||||||
|
>
|
||||||
|
{typeof weight.id === "number" ? (
|
||||||
<IconButton onPress={() => setShowDelete(true)} icon="delete" />
|
<IconButton onPress={() => setShowDelete(true)} icon="delete" />
|
||||||
) : null}
|
) : null}
|
||||||
</StackHeader>
|
</StackHeader>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Delete set"
|
title="Delete weight"
|
||||||
show={showDelete}
|
show={showDelete}
|
||||||
onOk={remove}
|
onOk={remove}
|
||||||
setShow={setShowDelete}
|
setShow={setShowDelete}
|
||||||
>
|
>
|
||||||
<>Are you sure you want to delete {name}</>
|
<>Are you sure you want to delete {value}</>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<View style={{ padding: PADDING, flex: 1 }}>
|
<View style={{ padding: PADDING, flex: 1 }}>
|
||||||
<AppInput
|
<AppInput
|
||||||
label="Name"
|
label="Value"
|
||||||
value={name}
|
value={value}
|
||||||
onChangeText={setName}
|
onChangeText={setValue}
|
||||||
autoCorrect={false}
|
keyboardType="numeric"
|
||||||
autoFocus={!name}
|
onSubmitEditing={() => unitRef.current?.focus()}
|
||||||
onSubmitEditing={() => repsRef.current?.focus()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={{ flexDirection: "row" }}>
|
|
||||||
<AppInput
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
marginBottom: MARGIN,
|
|
||||||
}}
|
|
||||||
label="Reps"
|
|
||||||
keyboardType="numeric"
|
|
||||||
value={reps}
|
|
||||||
onChangeText={(newReps) => {
|
|
||||||
const fixed = fixNumeric(newReps);
|
|
||||||
setReps(fixed);
|
|
||||||
if (fixed.length !== newReps.length)
|
|
||||||
toast("Reps must be a number");
|
|
||||||
}}
|
|
||||||
onSubmitEditing={() => weightRef.current?.focus()}
|
|
||||||
selection={selection}
|
|
||||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
|
||||||
innerRef={repsRef}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon="add"
|
|
||||||
onPress={() => setReps((Number(reps) + 1).toString())}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon="remove"
|
|
||||||
onPress={() => setReps((Number(reps) - 1).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
marginBottom: MARGIN,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppInput
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
label="Weight"
|
|
||||||
keyboardType="numeric"
|
|
||||||
value={weight}
|
|
||||||
onChangeText={(newWeight) => {
|
|
||||||
const fixed = fixNumeric(newWeight);
|
|
||||||
setWeight(fixed);
|
|
||||||
if (fixed.length !== newWeight.length)
|
|
||||||
toast("Weight must be a number");
|
|
||||||
}}
|
|
||||||
onSubmitEditing={handleSubmit}
|
|
||||||
innerRef={weightRef}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon="add"
|
|
||||||
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon="remove"
|
|
||||||
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{settings.showUnit && (
|
{settings.showUnit && (
|
||||||
<AppInput
|
<AppInput
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
@ -258,46 +117,17 @@ export default function EditWeight() {
|
||||||
onPressOut={pickDate}
|
onPressOut={pickDate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{settings.images && newImage && (
|
|
||||||
<TouchableRipple
|
|
||||||
style={{ marginBottom: MARGIN }}
|
|
||||||
onPress={changeImage}
|
|
||||||
onLongPress={() => setShowRemove(true)}
|
|
||||||
>
|
|
||||||
<Card.Cover source={{ uri: newImage }} />
|
|
||||||
</TouchableRipple>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{settings.images && !newImage && (
|
|
||||||
<Button
|
|
||||||
style={{ marginBottom: MARGIN }}
|
|
||||||
onPress={changeImage}
|
|
||||||
icon="add-photo-alternate"
|
|
||||||
>
|
|
||||||
Image
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!name}
|
disabled={!value}
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
icon="save"
|
icon="content-save"
|
||||||
style={{ margin: MARGIN }}
|
style={{ margin: MARGIN }}
|
||||||
onPress={handleSubmit}
|
onPress={handleSubmit}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Remove image"
|
|
||||||
onOk={handleRemove}
|
|
||||||
show={showRemove}
|
|
||||||
setShow={setShowRemove}
|
|
||||||
>
|
|
||||||
Are you sure you want to remove the image?
|
|
||||||
</ConfirmDialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,9 @@ export default function Routes() {
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="Graphs"
|
name="Graphs"
|
||||||
component={GraphsPage}
|
component={GraphsPage}
|
||||||
options={{ drawerIcon: () => <IconButton icon="chart-line" /> }}
|
options={{
|
||||||
|
drawerIcon: () => <IconButton icon="chart-bell-curve-cumulative" />,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="Workouts"
|
name="Workouts"
|
||||||
|
|
|
@ -68,10 +68,10 @@ export default function StartPlanItem(props: Props) {
|
||||||
navigate("EditSet", { set: first });
|
navigate("EditSet", { set: first });
|
||||||
}, [item.name, navigate]);
|
}, [item.name, navigate]);
|
||||||
|
|
||||||
const view = () => {
|
const view = useCallback(() => {
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
navigateHome("Sets", { search: item.name });
|
navigateHome("Sets", { search: item.name });
|
||||||
};
|
}, [item.name, navigateHome]);
|
||||||
|
|
||||||
const left = useCallback(
|
const left = useCallback(
|
||||||
() => (
|
() => (
|
||||||
|
@ -106,7 +106,7 @@ export default function StartPlanItem(props: Props) {
|
||||||
</Menu>
|
</Menu>
|
||||||
</View>
|
</View>
|
||||||
),
|
),
|
||||||
[anchor, showMenu, edit, undo]
|
[anchor, showMenu, edit, undo, view]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
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 { PADDING } from "./constants";
|
||||||
|
import { weightRepo } from "./db";
|
||||||
|
import { Periods } from "./periods";
|
||||||
|
import Select from "./Select";
|
||||||
|
import StackHeader from "./StackHeader";
|
||||||
|
import Weight from "./weight";
|
||||||
|
|
||||||
|
export default function ViewWeightGraph() {
|
||||||
|
const [weights, setWeights] = useState<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";
|
||||||
|
|
||||||
|
weightRepo
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select("STRFTIME('%Y-%m-%d', created)", "created")
|
||||||
|
.addSelect("unit")
|
||||||
|
.addSelect("value")
|
||||||
|
.where("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
||||||
|
difference,
|
||||||
|
})
|
||||||
|
.groupBy(`STRFTIME('${group}', created)`)
|
||||||
|
.getRawMany()
|
||||||
|
.then(setWeights);
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
|
const charts = useMemo(() => {
|
||||||
|
if (weights?.length === 0) {
|
||||||
|
return <List.Item title="No data yet." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chart
|
||||||
|
yData={weights?.map((set) => set.value) || []}
|
||||||
|
yFormat={(value) => `${value}${weights?.[0].unit}`}
|
||||||
|
xData={weights || []}
|
||||||
|
xFormat={(_value, index) =>
|
||||||
|
format(new Date(weights?.[index].created), "d/M")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [weights]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StackHeader title="Weight graph">
|
||||||
|
<IconButton
|
||||||
|
onPress={() =>
|
||||||
|
captureScreen().then(async (uri) => {
|
||||||
|
const base64 = await FileSystem.readFile(uri, "base64");
|
||||||
|
const url = `data:image/jpeg;base64,${base64}`;
|
||||||
|
Share.open({
|
||||||
|
type: "image/jpeg",
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
icon="share"
|
||||||
|
/>
|
||||||
|
</StackHeader>
|
||||||
|
<View style={{ padding: PADDING }}>
|
||||||
|
<Select
|
||||||
|
label="Period"
|
||||||
|
items={[
|
||||||
|
{ value: Periods.Weekly, label: Periods.Weekly },
|
||||||
|
{ value: Periods.Monthly, label: Periods.Monthly },
|
||||||
|
{ value: Periods.Yearly, label: Periods.Yearly },
|
||||||
|
]}
|
||||||
|
onChange={(value) => setPeriod(value as Periods)}
|
||||||
|
value={period}
|
||||||
|
/>
|
||||||
|
{charts}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { List, Text } from "react-native-paper";
|
||||||
|
import Settings from "./settings";
|
||||||
|
import Weight from "./weight";
|
||||||
|
import { WeightPageParams } from "./WeightPage";
|
||||||
|
|
||||||
|
export default function WeightItem({
|
||||||
|
item,
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
item: Weight;
|
||||||
|
settings: Settings;
|
||||||
|
}) {
|
||||||
|
const navigation = useNavigation<NavigationProp<WeightPageParams>>();
|
||||||
|
|
||||||
|
const press = useCallback(() => {
|
||||||
|
navigation.navigate("EditWeight", { weight: item });
|
||||||
|
}, [item, navigation]);
|
||||||
|
|
||||||
|
const description = useCallback(() => {
|
||||||
|
return <Text>{format(new Date(item.created), settings.date || "P")}</Text>;
|
||||||
|
}, [item.created, settings.date]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
onPress={press}
|
||||||
|
title={`${item.value}${item.unit || "kg"}`}
|
||||||
|
description={description}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
188
WeightList.tsx
188
WeightList.tsx
|
@ -1,174 +1,114 @@
|
||||||
import {
|
import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
RouteProp,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
useRoute,
|
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { FlatList } from "react-native";
|
import { FlatList } from "react-native";
|
||||||
import { List } from "react-native-paper";
|
import { IconButton, List } from "react-native-paper";
|
||||||
import { Like } from "typeorm";
|
import { Like } from "typeorm";
|
||||||
import { LIMIT } from "./constants";
|
import { LIMIT } from "./constants";
|
||||||
import { getNow, setRepo, settingsRepo } from "./db";
|
import { getNow, settingsRepo, weightRepo } from "./db";
|
||||||
import DrawerHeader from "./DrawerHeader";
|
import DrawerHeader from "./DrawerHeader";
|
||||||
import { emitter } from "./emitter";
|
|
||||||
import GymSet, {
|
|
||||||
defaultSet,
|
|
||||||
GYM_SET_CREATED,
|
|
||||||
GYM_SET_DELETED,
|
|
||||||
GYM_SET_UPDATED,
|
|
||||||
} from "./gym-set";
|
|
||||||
import { HomePageParams } from "./home-page-params";
|
|
||||||
import ListMenu from "./ListMenu";
|
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
import SetItem from "./SetItem";
|
import Settings from "./settings";
|
||||||
import Settings, { SETTINGS } from "./settings";
|
import { default as Weight, defaultWeight } from "./weight";
|
||||||
|
import WeightItem from "./WeightItem";
|
||||||
|
import { WeightPageParams } from "./WeightPage";
|
||||||
|
|
||||||
export default function WeightList() {
|
export default function WeightList() {
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [sets, setSets] = useState<GymSet[]>();
|
const [weights, setWeights] = useState<Weight[]>();
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const [end, setEnd] = useState(false);
|
const [end, setEnd] = useState(false);
|
||||||
const [settings, setSettings] = useState<Settings>();
|
const [settings, setSettings] = useState<Settings>();
|
||||||
const [ids, setIds] = useState<number[]>([]);
|
const { navigate } = useNavigation<NavigationProp<WeightPageParams>>();
|
||||||
const navigation = useNavigation<NavigationProp<HomePageParams>>();
|
const [term, setTerm] = useState("");
|
||||||
const { params } = useRoute<RouteProp<HomePageParams, "Sets">>();
|
|
||||||
const [term, setTerm] = useState(params?.search || "");
|
|
||||||
|
|
||||||
const reset = useCallback(
|
const reset = useCallback(
|
||||||
async (value: string) => {
|
async (value: string) => {
|
||||||
const newSets = await setRepo.find({
|
const newWeights = await weightRepo.find({
|
||||||
where: { name: Like(`%${value.trim()}%`), hidden: 0 as any },
|
where: [
|
||||||
|
{
|
||||||
|
value: isNaN(Number(term)) ? undefined : Number(term),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
created: Like(`%${term}%`),
|
||||||
|
},
|
||||||
|
],
|
||||||
take: LIMIT,
|
take: LIMIT,
|
||||||
skip: 0,
|
skip: 0,
|
||||||
order: { created: "DESC" },
|
order: { created: "DESC" },
|
||||||
});
|
});
|
||||||
console.log(`${SetList.name}.reset:`, { value, offset });
|
console.log(`${WeightList.name}.reset:`, { value, offset });
|
||||||
setSets(newSets);
|
setWeights(newWeights);
|
||||||
setEnd(false);
|
setEnd(false);
|
||||||
},
|
},
|
||||||
[offset]
|
[offset, term]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useFocusEffect(
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
useCallback(() => {
|
||||||
reset("");
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
reset(term);
|
||||||
}, []);
|
}, [term, reset])
|
||||||
|
);
|
||||||
useEffect(() => {
|
|
||||||
const updated = (gymSet: GymSet) => {
|
|
||||||
if (!sets) console.log({ sets });
|
|
||||||
console.log(`${SetList.name}.updated:`, { gymSet, length: sets.length });
|
|
||||||
const newSets = sets.map((set) => {
|
|
||||||
if (set.id !== gymSet.id) return set;
|
|
||||||
if (gymSet.created === undefined) gymSet.created = set.created;
|
|
||||||
return gymSet;
|
|
||||||
});
|
|
||||||
setSets(newSets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const descriptions = [
|
|
||||||
emitter.addListener(SETTINGS, () => {
|
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
|
||||||
}),
|
|
||||||
emitter.addListener(GYM_SET_UPDATED, updated),
|
|
||||||
emitter.addListener(GYM_SET_CREATED, () => reset("")),
|
|
||||||
emitter.addListener(GYM_SET_DELETED, () => reset("")),
|
|
||||||
];
|
|
||||||
return () => descriptions.forEach((description) => description.remove());
|
|
||||||
}, [sets]);
|
|
||||||
|
|
||||||
const search = (value: string) => {
|
const search = (value: string) => {
|
||||||
console.log(`${SetList.name}.search:`, value);
|
console.log(`${WeightList.name}.search:`, value);
|
||||||
setTerm(value);
|
setTerm(value);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
reset(value);
|
reset(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(`${SetList.name}.useEffect:`, params);
|
|
||||||
if (params?.search) search(params.search);
|
|
||||||
}, [params]);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item }: { item: GymSet }) => (
|
({ item }: { item: Weight }) => (
|
||||||
<SetItem
|
<WeightItem settings={settings} item={item} key={item.id} />
|
||||||
settings={settings}
|
|
||||||
item={item}
|
|
||||||
key={item.id}
|
|
||||||
ids={ids}
|
|
||||||
setIds={setIds}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
[settings, ids]
|
[settings]
|
||||||
);
|
);
|
||||||
|
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
console.log({ end, refreshing });
|
console.log({ end, refreshing });
|
||||||
if (end || refreshing) return;
|
if (end || refreshing) return;
|
||||||
const newOffset = offset + LIMIT;
|
const newOffset = offset + LIMIT;
|
||||||
console.log(`${SetList.name}.next:`, { offset, newOffset, term });
|
console.log(`${WeightList.name}.next:`, { offset, newOffset, term });
|
||||||
const newSets = await setRepo.find({
|
const newWeights = await weightRepo.find({
|
||||||
where: { name: Like(`%${term}%`), hidden: 0 as any },
|
where: [
|
||||||
|
{
|
||||||
|
value: Number(term),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
created: Like(`%${term}%`),
|
||||||
|
},
|
||||||
|
],
|
||||||
take: LIMIT,
|
take: LIMIT,
|
||||||
skip: newOffset,
|
skip: newOffset,
|
||||||
order: { created: "DESC" },
|
order: { created: "DESC" },
|
||||||
});
|
});
|
||||||
if (newSets.length === 0) return setEnd(true);
|
if (newWeights.length === 0) return setEnd(true);
|
||||||
if (!sets) return;
|
if (!weights) return;
|
||||||
const map = new Map<number, GymSet>();
|
const map = new Map<number, Weight>();
|
||||||
for (const set of sets) map.set(set.id, set);
|
for (const weight of weights) map.set(weight.id, weight);
|
||||||
for (const set of newSets) map.set(set.id, set);
|
for (const weight of newWeights) map.set(weight.id, weight);
|
||||||
const unique = Array.from(map.values());
|
const unique = Array.from(map.values());
|
||||||
setSets(unique);
|
setWeights(unique);
|
||||||
if (newSets.length < LIMIT) return setEnd(true);
|
if (newWeights.length < LIMIT) return setEnd(true);
|
||||||
setOffset(newOffset);
|
setOffset(newOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAdd = useCallback(async () => {
|
const onAdd = useCallback(async () => {
|
||||||
const now = await getNow();
|
const now = await getNow();
|
||||||
let set = sets?.[0];
|
let weight = weights?.[0];
|
||||||
if (!set) set = { ...defaultSet };
|
if (!weight) weight = { ...defaultWeight };
|
||||||
set.created = now;
|
weight.created = now;
|
||||||
delete set.id;
|
delete weight.id;
|
||||||
navigation.navigate("EditSet", { set });
|
navigate("EditWeight", { weight });
|
||||||
}, [navigation, sets]);
|
}, [navigate, weights]);
|
||||||
|
|
||||||
const edit = useCallback(() => {
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
|
||||||
setIds([]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const remove = async () => {
|
|
||||||
setIds([]);
|
|
||||||
await setRepo.delete(ids.length > 0 ? ids : {});
|
|
||||||
return reset(term);
|
|
||||||
};
|
|
||||||
|
|
||||||
const select = useCallback(() => {
|
|
||||||
if (!sets) return;
|
|
||||||
if (ids.length === sets.length) return setIds([]);
|
|
||||||
setIds(sets.map((set) => set.id));
|
|
||||||
}, [sets, ids]);
|
|
||||||
|
|
||||||
const getContent = () => {
|
const getContent = () => {
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
if (sets?.length === 0)
|
if (weights?.length === 0)
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No sets yet"
|
title="No sets yet"
|
||||||
|
@ -177,7 +117,7 @@ export default function WeightList() {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={sets ?? []}
|
data={weights ?? []}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
onEndReached={next}
|
onEndReached={next}
|
||||||
|
@ -194,14 +134,10 @@ export default function WeightList() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Home"}>
|
<DrawerHeader name="Weight">
|
||||||
<ListMenu
|
<IconButton
|
||||||
onClear={clear}
|
onPress={() => navigate("ViewWeightGraph")}
|
||||||
onCopy={copy}
|
icon="chart-bell-curve-cumulative"
|
||||||
onDelete={remove}
|
|
||||||
onEdit={edit}
|
|
||||||
ids={ids}
|
|
||||||
onSelect={select}
|
|
||||||
/>
|
/>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { createStackNavigator } from "@react-navigation/stack";
|
import { createStackNavigator } from "@react-navigation/stack";
|
||||||
import EditSet from "./EditSet";
|
import EditWeight from "./EditWeight";
|
||||||
import SetList from "./SetList";
|
import ViewWeightGraph from "./ViewWeightGraph";
|
||||||
|
import Weight from "./weight";
|
||||||
|
import WeightList from "./WeightList";
|
||||||
|
|
||||||
export type WeightPageParams = {
|
export type WeightPageParams = {
|
||||||
Weights: {};
|
Weights: {};
|
||||||
EditWeight: {
|
EditWeight: {
|
||||||
weight: any;
|
weight: Weight;
|
||||||
};
|
};
|
||||||
|
ViewWeightGraph: {};
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createStackNavigator<WeightPageParams>();
|
const Stack = createStackNavigator<WeightPageParams>();
|
||||||
|
@ -16,8 +19,9 @@ export default function WeightPage() {
|
||||||
<Stack.Navigator
|
<Stack.Navigator
|
||||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
screenOptions={{ headerShown: false, animationEnabled: false }}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="Weights" component={SetList} />
|
<Stack.Screen name="Weights" component={WeightList} />
|
||||||
<Stack.Screen name="EditWeight" component={EditSet} />
|
<Stack.Screen name="EditWeight" component={EditWeight} />
|
||||||
|
<Stack.Screen name="ViewWeightGraph" component={ViewWeightGraph} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,14 +26,16 @@ import { dropMigrations1667190214743 } from "./migrations/1667190214743-drop-mig
|
||||||
import { splitColor1669420187764 } from "./migrations/1669420187764-split-color";
|
import { splitColor1669420187764 } from "./migrations/1669420187764-split-color";
|
||||||
import { addBackup1678334268359 } from "./migrations/1678334268359-add-backup";
|
import { addBackup1678334268359 } from "./migrations/1678334268359-add-backup";
|
||||||
import { planTitle1692654882408 } from "./migrations/1692654882408-plan-title";
|
import { planTitle1692654882408 } from "./migrations/1692654882408-plan-title";
|
||||||
|
import { weight1697766633971 } from "./migrations/1697766633971-weight";
|
||||||
import { Plan } from "./plan";
|
import { Plan } from "./plan";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
import Weight from "./weight";
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: "react-native",
|
type: "react-native",
|
||||||
database: "massive.db",
|
database: "massive.db",
|
||||||
location: "default",
|
location: "default",
|
||||||
entities: [GymSet, Plan, Settings],
|
entities: [GymSet, Plan, Settings, Weight],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTableName: "typeorm_migrations",
|
migrationsTableName: "typeorm_migrations",
|
||||||
migrations: [
|
migrations: [
|
||||||
|
@ -63,5 +65,6 @@ export const AppDataSource = new DataSource({
|
||||||
splitColor1669420187764,
|
splitColor1669420187764,
|
||||||
addBackup1678334268359,
|
addBackup1678334268359,
|
||||||
planTitle1692654882408,
|
planTitle1692654882408,
|
||||||
|
weight1697766633971,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
2
db.ts
2
db.ts
|
@ -2,10 +2,12 @@ import { AppDataSource } from "./data-source";
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
import { Plan } from "./plan";
|
import { Plan } from "./plan";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
import Weight from "./weight";
|
||||||
|
|
||||||
export const setRepo = AppDataSource.manager.getRepository(GymSet);
|
export const setRepo = AppDataSource.manager.getRepository(GymSet);
|
||||||
export const planRepo = AppDataSource.manager.getRepository(Plan);
|
export const planRepo = AppDataSource.manager.getRepository(Plan);
|
||||||
export const settingsRepo = AppDataSource.manager.getRepository(Settings);
|
export const settingsRepo = AppDataSource.manager.getRepository(Settings);
|
||||||
|
export const weightRepo = AppDataSource.manager.getRepository(Weight);
|
||||||
|
|
||||||
export const getNow = async (): Promise<string> => {
|
export const getNow = async (): Promise<string> => {
|
||||||
const query = await AppDataSource.manager.query(
|
const query = await AppDataSource.manager.query(
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class weight1697766633971 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS weights (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
value INTEGER NOT NULL,
|
||||||
|
created TEXT NOT NULL,
|
||||||
|
unit TEXT DEFAULT 'kg'
|
||||||
|
)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query("DROP TABLE weights");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity("weights")
|
||||||
|
export default class Weight {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
@Column("int")
|
||||||
|
value: number;
|
||||||
|
|
||||||
|
@Column("text")
|
||||||
|
created: string;
|
||||||
|
|
||||||
|
@Column("text")
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultWeight: Weight = {
|
||||||
|
created: "",
|
||||||
|
unit: "kg",
|
||||||
|
value: 0,
|
||||||
|
};
|
Loading…
Reference in New Issue