Compare commits
No commits in common. "master" and "Nuice-master" have entirely different histories.
master
...
Nuice-mast
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -74,4 +74,3 @@ massive-build
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
coverage
|
coverage
|
||||||
profiles
|
|
||||||
|
|
132
App.tsx
132
App.tsx
|
@ -1,23 +1,22 @@
|
||||||
import {
|
import {
|
||||||
NavigationContainer,
|
|
||||||
DarkTheme as NavigationDarkTheme,
|
DarkTheme as NavigationDarkTheme,
|
||||||
DefaultTheme as NavigationDefaultTheme,
|
DefaultTheme as NavigationDefaultTheme,
|
||||||
|
NavigationContainer,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useColorScheme } from "react-native";
|
import { DeviceEventEmitter, useColorScheme } from "react-native";
|
||||||
import {
|
import {
|
||||||
MD3DarkTheme as PaperDarkTheme,
|
MD3DarkTheme as PaperDarkTheme,
|
||||||
MD3LightTheme as PaperDefaultTheme,
|
MD3LightTheme as PaperDefaultTheme,
|
||||||
Provider as PaperProvider,
|
Provider as PaperProvider,
|
||||||
|
Snackbar,
|
||||||
} from "react-native-paper";
|
} from "react-native-paper";
|
||||||
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
|
import MaterialIcon from "react-native-vector-icons/MaterialIcons";
|
||||||
import AppSnack from "./AppSnack";
|
|
||||||
import AppStack from "./AppStack";
|
|
||||||
import FatalError from "./FatalError";
|
|
||||||
import { AppDataSource } from "./data-source";
|
import { AppDataSource } from "./data-source";
|
||||||
import { settingsRepo } from "./db";
|
import { settingsRepo } from "./db";
|
||||||
|
import Routes from "./Routes";
|
||||||
|
import { TOAST } from "./toast";
|
||||||
import { ThemeContext } from "./use-theme";
|
import { ThemeContext } from "./use-theme";
|
||||||
import TimerProgress from "./TimerProgress";
|
|
||||||
|
|
||||||
export const CombinedDefaultTheme = {
|
export const CombinedDefaultTheme = {
|
||||||
...NavigationDefaultTheme,
|
...NavigationDefaultTheme,
|
||||||
|
@ -38,53 +37,55 @@ export const CombinedDarkTheme = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
console.log("Re rendered app");
|
const phoneTheme = useColorScheme();
|
||||||
const systemTheme = useColorScheme();
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
const [snackbar, setSnackbar] = useState("");
|
||||||
|
const [appTheme, setAppTheme] = useState("system");
|
||||||
|
|
||||||
const [appSettings, setAppSettings] = useState({
|
const [lightColor, setLightColor] = useState<string>(
|
||||||
startup: undefined,
|
CombinedDefaultTheme.colors.primary
|
||||||
theme: "system",
|
);
|
||||||
lightColor: CombinedDefaultTheme.colors.primary,
|
|
||||||
darkColor: CombinedDarkTheme.colors.primary,
|
const [darkColor, setDarkColor] = useState<string>(
|
||||||
});
|
CombinedDarkTheme.colors.primary
|
||||||
const [error, setError] = useState("");
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!AppDataSource.isInitialized)
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
await AppDataSource.initialize().catch((e) => setError(e.toString()));
|
const settings = await settingsRepo.findOne({ where: {} });
|
||||||
const gotSettings = await settingsRepo.findOne({ where: {} });
|
setAppTheme(settings.theme);
|
||||||
console.log({ gotSettings });
|
if (settings.lightColor) setLightColor(settings.lightColor);
|
||||||
setAppSettings({
|
if (settings.darkColor) setDarkColor(settings.darkColor);
|
||||||
startup: gotSettings.startup,
|
setInitialized(true);
|
||||||
theme: gotSettings.theme,
|
|
||||||
lightColor:
|
|
||||||
gotSettings.lightColor || CombinedDefaultTheme.colors.primary,
|
|
||||||
darkColor: gotSettings.darkColor || CombinedDarkTheme.colors.primary,
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
const description = DeviceEventEmitter.addListener(
|
||||||
|
TOAST,
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
setSnackbar(value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return description.remove;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const paperTheme = useMemo(() => {
|
const paperTheme = useMemo(() => {
|
||||||
const darkTheme = {
|
const darkTheme = lightColor
|
||||||
...CombinedDarkTheme,
|
? {
|
||||||
colors: {
|
...CombinedDarkTheme,
|
||||||
...CombinedDarkTheme.colors,
|
colors: { ...CombinedDarkTheme.colors, primary: darkColor },
|
||||||
primary: appSettings.darkColor,
|
}
|
||||||
},
|
: CombinedDarkTheme;
|
||||||
};
|
const lightTheme = lightColor
|
||||||
const lightTheme = {
|
? {
|
||||||
...CombinedDefaultTheme,
|
...CombinedDefaultTheme,
|
||||||
colors: {
|
colors: { ...CombinedDefaultTheme.colors, primary: lightColor },
|
||||||
...CombinedDefaultTheme.colors,
|
}
|
||||||
primary: appSettings.lightColor,
|
: CombinedDefaultTheme;
|
||||||
},
|
let value = phoneTheme === "dark" ? darkTheme : lightTheme;
|
||||||
};
|
if (appTheme === "dark") value = darkTheme;
|
||||||
let theme = systemTheme === "dark" ? darkTheme : lightTheme;
|
else if (appTheme === "light") value = lightTheme;
|
||||||
if (appSettings.theme === "dark") theme = darkTheme;
|
return value;
|
||||||
else if (appSettings.theme === "light") theme = lightTheme;
|
}, [phoneTheme, appTheme, lightColor, darkColor]);
|
||||||
return theme;
|
|
||||||
}, [systemTheme, appSettings]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaperProvider
|
<PaperProvider
|
||||||
|
@ -92,33 +93,34 @@ const App = () => {
|
||||||
settings={{ icon: (props) => <MaterialIcon {...props} /> }}
|
settings={{ icon: (props) => <MaterialIcon {...props} /> }}
|
||||||
>
|
>
|
||||||
<NavigationContainer theme={paperTheme}>
|
<NavigationContainer theme={paperTheme}>
|
||||||
{error && (
|
{initialized && (
|
||||||
<FatalError
|
|
||||||
message={error}
|
|
||||||
setAppSettings={setAppSettings}
|
|
||||||
setError={setError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{appSettings.startup !== undefined && (
|
|
||||||
<ThemeContext.Provider
|
<ThemeContext.Provider
|
||||||
value={{
|
value={{
|
||||||
theme: appSettings.theme,
|
theme: appTheme,
|
||||||
setTheme: (theme) => setAppSettings({ ...appSettings, theme }),
|
setTheme: setAppTheme,
|
||||||
lightColor: appSettings.lightColor,
|
lightColor,
|
||||||
setLightColor: (color) =>
|
setLightColor,
|
||||||
setAppSettings({ ...appSettings, lightColor: color }),
|
darkColor,
|
||||||
darkColor: appSettings.darkColor,
|
setDarkColor,
|
||||||
setDarkColor: (color) =>
|
|
||||||
setAppSettings({ ...appSettings, darkColor: color }),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppStack startup={appSettings.startup} />
|
<Routes />
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
)}
|
)}
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
|
||||||
<AppSnack textColor={paperTheme.colors.background} />
|
<Snackbar
|
||||||
<TimerProgress />
|
duration={3000}
|
||||||
|
onDismiss={() => setSnackbar("")}
|
||||||
|
visible={!!snackbar}
|
||||||
|
action={{
|
||||||
|
label: "Close",
|
||||||
|
onPress: () => setSnackbar(""),
|
||||||
|
textColor: paperTheme.colors.background,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{snackbar}
|
||||||
|
</Snackbar>
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
import { createDrawerNavigator } from "@react-navigation/drawer";
|
|
||||||
import { StackScreenProps } from "@react-navigation/stack";
|
|
||||||
import { IconButton, useTheme } from "react-native-paper";
|
|
||||||
import { DrawerParams } from "./drawer-params";
|
|
||||||
import ExerciseList from "./ExerciseList";
|
|
||||||
import GraphsList from "./GraphsList";
|
|
||||||
import InsightsPage from "./InsightsPage";
|
|
||||||
import PlanList from "./PlanList";
|
|
||||||
import SetList from "./SetList";
|
|
||||||
import SettingsPage from "./SettingsPage";
|
|
||||||
import TimerPage from "./TimerPage";
|
|
||||||
import WeightList from "./WeightList";
|
|
||||||
|
|
||||||
const Drawer = createDrawerNavigator<DrawerParams>();
|
|
||||||
|
|
||||||
interface AppDrawerParams {
|
|
||||||
startup: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppDrawer({
|
|
||||||
route,
|
|
||||||
}: StackScreenProps<{ startup: AppDrawerParams }>) {
|
|
||||||
const { dark } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer.Navigator
|
|
||||||
screenOptions={{
|
|
||||||
headerTintColor: dark ? "white" : "black",
|
|
||||||
swipeEdgeWidth: 1000,
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
initialRouteName={
|
|
||||||
(route.params.startup as keyof DrawerParams) || "History"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="History"
|
|
||||||
component={SetList}
|
|
||||||
options={{ drawerIcon: () => <IconButton icon="history" /> }}
|
|
||||||
/>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="Exercises"
|
|
||||||
component={ExerciseList}
|
|
||||||
options={{ drawerIcon: () => <IconButton icon="dumbbell" /> }}
|
|
||||||
/>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="Plans"
|
|
||||||
component={PlanList}
|
|
||||||
options={{ drawerIcon: () => <IconButton icon="calendar-outline" /> }}
|
|
||||||
/>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="Graphs"
|
|
||||||
component={GraphsList}
|
|
||||||
options={{
|
|
||||||
drawerIcon: () => <IconButton icon="chart-bell-curve-cumulative" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="Timer"
|
|
||||||
component={TimerPage}
|
|
||||||
options={{ drawerIcon: () => <IconButton icon="timer-outline" /> }}
|
|
||||||
/>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="Weight"
|
|
||||||
component={WeightList}
|
|
||||||
options={{ drawerIcon: () => <IconButton icon="scale-bathroom" /> }}
|
|
||||||
/>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="Insights"
|
|
||||||
component={InsightsPage}
|
|
||||||
options={{
|
|
||||||
drawerIcon: () => <IconButton icon="lightbulb-on-outline" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="Settings"
|
|
||||||
component={SettingsPage}
|
|
||||||
options={{ drawerIcon: () => <IconButton icon="cog-outline" /> }}
|
|
||||||
/>
|
|
||||||
</Drawer.Navigator>
|
|
||||||
);
|
|
||||||
}
|
|
16
AppFab.tsx
16
AppFab.tsx
|
@ -1,14 +1,24 @@
|
||||||
import { ComponentProps } from "react";
|
import { ComponentProps, useMemo } from "react";
|
||||||
import { FAB, useTheme } from "react-native-paper";
|
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>>) {
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FAB
|
<FAB
|
||||||
icon="plus"
|
icon="add"
|
||||||
testID="add"
|
testID="add"
|
||||||
color={colors.background}
|
color={fabColor}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: 20,
|
right: 20,
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import React, { ComponentProps, Ref } from "react";
|
import React, { ComponentProps, Ref } from "react";
|
||||||
import { TextInput, useTheme } from "react-native-paper";
|
import { TextInput } from "react-native-paper";
|
||||||
import { CombinedDefaultTheme } from "./App";
|
import { CombinedDefaultTheme } from "./App";
|
||||||
import { MARGIN } from "./constants";
|
import { MARGIN } from "./constants";
|
||||||
|
import useDark from "./use-dark";
|
||||||
|
|
||||||
function AppInput(
|
function AppInput(
|
||||||
props: Partial<ComponentProps<typeof TextInput>> & {
|
props: Partial<ComponentProps<typeof TextInput>> & {
|
||||||
innerRef?: Ref<any>;
|
innerRef?: Ref<any>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { dark } = useTheme();
|
const dark = useDark();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { useWindowDimensions } from "react-native";
|
|
||||||
import { PieChart } from "react-native-chart-kit";
|
|
||||||
import { useTheme } from "react-native-paper";
|
|
||||||
|
|
||||||
export interface Option {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppPieChart({ options }: { options: Option[] }) {
|
|
||||||
const { width } = useWindowDimensions();
|
|
||||||
const { colors } = useTheme();
|
|
||||||
|
|
||||||
const pieChartColors = [
|
|
||||||
"#FF7F50", // Coral
|
|
||||||
"#1E90FF", // Dodger Blue
|
|
||||||
"#32CD32", // Lime Green
|
|
||||||
"#BA55D3", // Medium Orchid
|
|
||||||
"#FFD700", // Gold
|
|
||||||
"#48D1CC", // Medium Turquoise
|
|
||||||
"#FF69B4", // Hot Pink
|
|
||||||
];
|
|
||||||
|
|
||||||
const data = options.map((option, index) => ({
|
|
||||||
name: option.label,
|
|
||||||
value: option.value,
|
|
||||||
color: pieChartColors[index],
|
|
||||||
legendFontColor: colors.onSurface,
|
|
||||||
legendFontSize: 15,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PieChart
|
|
||||||
data={data}
|
|
||||||
paddingLeft="0"
|
|
||||||
width={width}
|
|
||||||
height={220}
|
|
||||||
chartConfig={{
|
|
||||||
backgroundColor: "#e26a00",
|
|
||||||
backgroundGradientFrom: "#fb8c00",
|
|
||||||
backgroundGradientTo: "#ffa726",
|
|
||||||
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
|
|
||||||
labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
|
|
||||||
style: {
|
|
||||||
borderRadius: 16,
|
|
||||||
},
|
|
||||||
propsForDots: {
|
|
||||||
r: "6",
|
|
||||||
strokeWidth: "2",
|
|
||||||
stroke: "#ffa726",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
accessor={"value"}
|
|
||||||
backgroundColor={"transparent"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
33
AppSnack.tsx
33
AppSnack.tsx
|
@ -1,33 +0,0 @@
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Snackbar } from "react-native-paper";
|
|
||||||
import { emitter } from "./emitter";
|
|
||||||
import { TOAST } from "./toast";
|
|
||||||
|
|
||||||
export default function AppSnack({ textColor }: { textColor: string }) {
|
|
||||||
const [snackbar, setSnackbar] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const description = emitter.addListener(
|
|
||||||
TOAST,
|
|
||||||
({ value }: { value: string }) => {
|
|
||||||
setSnackbar(value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return description.remove;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Snackbar
|
|
||||||
duration={3000}
|
|
||||||
onDismiss={() => setSnackbar("")}
|
|
||||||
visible={!!snackbar}
|
|
||||||
action={{
|
|
||||||
label: "Close",
|
|
||||||
onPress: () => setSnackbar(""),
|
|
||||||
textColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{snackbar}
|
|
||||||
</Snackbar>
|
|
||||||
);
|
|
||||||
}
|
|
74
AppStack.tsx
74
AppStack.tsx
|
@ -1,74 +0,0 @@
|
||||||
import { createStackNavigator } from "@react-navigation/stack";
|
|
||||||
import AppDrawer from "./AppDrawer";
|
|
||||||
import EditExercise from "./EditExercise";
|
|
||||||
import EditExercises from "./EditExercises";
|
|
||||||
import EditPlan from "./EditPlan";
|
|
||||||
import EditSet from "./EditSet";
|
|
||||||
import EditSets from "./EditSets";
|
|
||||||
import EditWeight from "./EditWeight";
|
|
||||||
import GymSet from "./gym-set";
|
|
||||||
import { Plan } from "./plan";
|
|
||||||
import StartPlan from "./StartPlan";
|
|
||||||
import ViewGraph from "./ViewGraph";
|
|
||||||
import ViewSetList from "./ViewSetList";
|
|
||||||
import ViewWeightGraph from "./ViewWeightGraph";
|
|
||||||
import Weight from "./weight";
|
|
||||||
|
|
||||||
export type StackParams = {
|
|
||||||
Drawer: {};
|
|
||||||
EditSet: {
|
|
||||||
set: Partial<GymSet>;
|
|
||||||
};
|
|
||||||
EditSets: {
|
|
||||||
ids: number[];
|
|
||||||
};
|
|
||||||
EditPlan: {
|
|
||||||
plan: Partial<Plan>;
|
|
||||||
};
|
|
||||||
StartPlan: {
|
|
||||||
plan: Plan;
|
|
||||||
first: Partial<GymSet>;
|
|
||||||
};
|
|
||||||
ViewGraph: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
EditWeight: {
|
|
||||||
weight: Partial<Weight>;
|
|
||||||
};
|
|
||||||
ViewWeightGraph: {};
|
|
||||||
EditExercise: {
|
|
||||||
gymSet: Partial<GymSet>;
|
|
||||||
};
|
|
||||||
EditExercises: {
|
|
||||||
names: string[];
|
|
||||||
};
|
|
||||||
ViewSetList: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Stack = createStackNavigator<StackParams>();
|
|
||||||
|
|
||||||
export default function AppStack({ startup }: { startup: string }) {
|
|
||||||
return (
|
|
||||||
<Stack.Navigator
|
|
||||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
|
||||||
>
|
|
||||||
<Stack.Screen
|
|
||||||
name="Drawer"
|
|
||||||
component={AppDrawer}
|
|
||||||
initialParams={{ startup }}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name="EditSet" component={EditSet} />
|
|
||||||
<Stack.Screen name="EditSets" component={EditSets} />
|
|
||||||
<Stack.Screen name="EditPlan" component={EditPlan} />
|
|
||||||
<Stack.Screen name="StartPlan" component={StartPlan} />
|
|
||||||
<Stack.Screen name="ViewGraph" component={ViewGraph} />
|
|
||||||
<Stack.Screen name="EditWeight" component={EditWeight} />
|
|
||||||
<Stack.Screen name="ViewWeightGraph" component={ViewWeightGraph} />
|
|
||||||
<Stack.Screen name="EditExercise" component={EditExercise} />
|
|
||||||
<Stack.Screen name="EditExercises" component={EditExercises} />
|
|
||||||
<Stack.Screen name="ViewSetList" component={ViewSetList} />
|
|
||||||
</Stack.Navigator>
|
|
||||||
);
|
|
||||||
}
|
|
106
Chart.tsx
106
Chart.tsx
|
@ -1,50 +1,70 @@
|
||||||
import { useMemo } from "react";
|
import { useTheme } from "@react-navigation/native";
|
||||||
import { useWindowDimensions } from "react-native";
|
import * as shape from "d3-shape";
|
||||||
import { LineChart } from "react-native-chart-kit";
|
import { View } from "react-native";
|
||||||
import { AbstractChartConfig } from "react-native-chart-kit/dist/AbstractChart";
|
import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
|
||||||
import { useTheme } from "react-native-paper";
|
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
||||||
|
import { MARGIN, PADDING } from "./constants";
|
||||||
|
import GymSet from "./gym-set";
|
||||||
|
import useDark from "./use-dark";
|
||||||
|
|
||||||
interface ChartProps {
|
export default function Chart({
|
||||||
labels: string[];
|
yData,
|
||||||
data: number[];
|
xFormat,
|
||||||
}
|
xData,
|
||||||
|
yFormat,
|
||||||
export default function Chart({ labels, data }: ChartProps) {
|
}: {
|
||||||
const { width } = useWindowDimensions();
|
yData: number[];
|
||||||
|
xData: GymSet[];
|
||||||
|
xFormat: (value: any, index: number) => string;
|
||||||
|
yFormat: (value: any) => string;
|
||||||
|
}) {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
const dark = useDark();
|
||||||
const config: AbstractChartConfig = {
|
const axesSvg = {
|
||||||
backgroundGradientFrom: colors.background,
|
fontSize: 10,
|
||||||
backgroundGradientTo: colors.elevation.level1,
|
fill: dark
|
||||||
color: () => colors.primary,
|
? CombinedDarkTheme.colors.text
|
||||||
|
: CombinedDefaultTheme.colors.text,
|
||||||
};
|
};
|
||||||
|
const verticalContentInset = { top: 10, bottom: 10 };
|
||||||
const pruned = useMemo(() => {
|
const xAxisHeight = 30;
|
||||||
if (labels.length < 3) return labels;
|
|
||||||
const newPruned = [labels[0]];
|
|
||||||
const centerIndex = Math.floor(labels.length / 2);
|
|
||||||
for (let i = 1; i < labels.length - 1; i++) {
|
|
||||||
if (i === centerIndex) newPruned[i] = labels[i];
|
|
||||||
else newPruned[i] = "";
|
|
||||||
}
|
|
||||||
newPruned.push(labels[labels.length - 1]);
|
|
||||||
return newPruned;
|
|
||||||
}, [labels]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<>
|
||||||
height={400}
|
<View
|
||||||
width={width - 20}
|
style={{
|
||||||
data={{
|
height: 300,
|
||||||
labels: pruned,
|
padding: PADDING,
|
||||||
datasets: [
|
flexDirection: "row",
|
||||||
{
|
}}
|
||||||
data,
|
>
|
||||||
},
|
<YAxis
|
||||||
],
|
data={yData}
|
||||||
}}
|
style={{ marginBottom: xAxisHeight }}
|
||||||
bezier
|
contentInset={verticalContentInset}
|
||||||
chartConfig={config}
|
svg={axesSvg}
|
||||||
/>
|
formatLabel={yFormat}
|
||||||
|
/>
|
||||||
|
<View style={{ flex: 1, marginLeft: MARGIN }}>
|
||||||
|
<LineChart
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
data={yData}
|
||||||
|
contentInset={verticalContentInset}
|
||||||
|
curve={shape.curveBasis}
|
||||||
|
svg={{
|
||||||
|
stroke: colors.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid />
|
||||||
|
</LineChart>
|
||||||
|
<XAxis
|
||||||
|
data={xData}
|
||||||
|
formatLabel={xFormat}
|
||||||
|
contentInset={{ left: 15, right: 16 }}
|
||||||
|
svg={axesSvg}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { DrawerNavigationProp } from "@react-navigation/drawer";
|
import { DrawerNavigationProp } from "@react-navigation/drawer";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import { Appbar, IconButton } from "react-native-paper";
|
import { Appbar, IconButton } from "react-native-paper";
|
||||||
import { DrawerParams } from "./drawer-params";
|
import { DrawerParamList } from "./drawer-param-list";
|
||||||
|
|
||||||
export default function DrawerHeader({
|
export default function DrawerHeader({
|
||||||
name,
|
name,
|
||||||
|
@ -10,7 +10,7 @@ export default function DrawerHeader({
|
||||||
name: string;
|
name: string;
|
||||||
children?: JSX.Element | JSX.Element[];
|
children?: JSX.Element | JSX.Element[];
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<DrawerNavigationProp<DrawerParams>>();
|
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
|
|
229
EditPlan.tsx
229
EditPlan.tsx
|
@ -4,50 +4,30 @@ import {
|
||||||
useNavigation,
|
useNavigation,
|
||||||
useRoute,
|
useRoute,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import { ScrollView, StyleSheet, View } from "react-native";
|
||||||
FlatList,
|
import { Button, IconButton, Text } from "react-native-paper";
|
||||||
Pressable,
|
import { MARGIN, PADDING } from "./constants";
|
||||||
ScrollView,
|
import { planRepo, setRepo } from "./db";
|
||||||
StyleSheet,
|
import { defaultSet } from "./gym-set";
|
||||||
View,
|
import { PlanPageParams } from "./plan-page-params";
|
||||||
} from "react-native";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
Switch as PaperSwitch,
|
|
||||||
Text,
|
|
||||||
} from "react-native-paper";
|
|
||||||
import AppInput from "./AppInput";
|
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import PrimaryButton from "./PrimaryButton";
|
|
||||||
import StackHeader from "./StackHeader";
|
import StackHeader from "./StackHeader";
|
||||||
import Switch from "./Switch";
|
import Switch from "./Switch";
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { DAYS } from "./time";
|
||||||
import { DAYS } from "./days";
|
import AppInput from "./AppInput";
|
||||||
import { planRepo, setRepo } from "./db";
|
|
||||||
import { DrawerParams } from "./drawer-params";
|
|
||||||
import GymSet, { defaultSet } from "./gym-set";
|
|
||||||
import { toast } from "./toast";
|
|
||||||
|
|
||||||
export default function EditPlan() {
|
export default function EditPlan() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "EditPlan">>();
|
const { params } = useRoute<RouteProp<PlanPageParams, "EditPlan">>();
|
||||||
const { plan } = params;
|
const { plan } = params;
|
||||||
const [title, setTitle] = useState<string>(plan?.title);
|
const [title, setTitle] = useState<string>(plan?.title);
|
||||||
const [names, setNames] = useState<string[]>();
|
|
||||||
|
|
||||||
const [days, setDays] = useState<string[]>(
|
const [days, setDays] = useState<string[]>(
|
||||||
plan.days ? plan.days.split(",") : []
|
plan.days ? plan.days.split(",") : []
|
||||||
);
|
);
|
||||||
|
const [workouts, setWorkouts] = useState<string[]>(
|
||||||
const [exercises, setExercises] = useState<string[]>(
|
plan.workouts ? plan.workouts.split(",") : []
|
||||||
plan.exercises ? plan.exercises.split(",") : []
|
|
||||||
);
|
);
|
||||||
|
const [names, setNames] = useState<string[]>([]);
|
||||||
const { navigate: drawerNavigate } =
|
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||||
useNavigation<NavigationProp<DrawerParams>>();
|
|
||||||
const { navigate: stackNavigate } =
|
|
||||||
useNavigation<NavigationProp<StackParams>>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRepo
|
setRepo
|
||||||
|
@ -57,35 +37,33 @@ export default function EditPlan() {
|
||||||
.orderBy("name")
|
.orderBy("name")
|
||||||
.getRawMany()
|
.getRawMany()
|
||||||
.then((values) => {
|
.then((values) => {
|
||||||
const newNames = values.map((value) => value.name);
|
console.log(EditPlan.name, { values });
|
||||||
console.log(EditPlan.name, { newNames });
|
setNames(values.map((value) => value.name));
|
||||||
setNames(newNames);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = useCallback(async () => {
|
||||||
console.log(`${EditPlan.name}.save`, { days, exercises, plan });
|
console.log(`${EditPlan.name}.save`, { days, workouts, plan });
|
||||||
if (!days || !exercises) return;
|
if (!days || !workouts) return;
|
||||||
const newExercises = exercises.filter((exercise) => exercise).join(",");
|
const newWorkouts = workouts.filter((workout) => workout).join(",");
|
||||||
const newDays = days.filter((day) => day).join(",");
|
const newDays = days.filter((day) => day).join(",");
|
||||||
const saved = await planRepo.save({
|
await planRepo.save({
|
||||||
title: title,
|
title: title,
|
||||||
days: newDays,
|
days: newDays,
|
||||||
exercises: newExercises,
|
workouts: newWorkouts,
|
||||||
id: plan.id,
|
id: plan.id,
|
||||||
});
|
});
|
||||||
if (saved.id === 1) toast("Tap your plan again to begin using it");
|
}, [title, days, workouts, plan]);
|
||||||
}, [title, days, exercises, plan]);
|
|
||||||
|
|
||||||
const toggleExercise = useCallback(
|
const toggleWorkout = useCallback(
|
||||||
(on: boolean, name: string) => {
|
(on: boolean, name: string) => {
|
||||||
if (on) {
|
if (on) {
|
||||||
setExercises([...exercises, name]);
|
setWorkouts([...workouts, name]);
|
||||||
} else {
|
} else {
|
||||||
setExercises(exercises.filter((exercise) => exercise !== name));
|
setWorkouts(workouts.filter((workout) => workout !== name));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setExercises, exercises]
|
[setWorkouts, workouts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleDay = useCallback(
|
const toggleDay = useCallback(
|
||||||
|
@ -99,60 +77,6 @@ export default function EditPlan() {
|
||||||
[setDays, days]
|
[setDays, days]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDay = (day: string) => (
|
|
||||||
<Switch
|
|
||||||
key={day}
|
|
||||||
onChange={(value) => toggleDay(value, day)}
|
|
||||||
value={days.includes(day)}
|
|
||||||
title={day}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderExercise = (name: string, index: number, movable: boolean) => (
|
|
||||||
<Pressable
|
|
||||||
onPress={() => toggleExercise(!exercises.includes(name), name)}
|
|
||||||
style={{ flexDirection: "row", alignItems: "center" }}
|
|
||||||
key={name}
|
|
||||||
>
|
|
||||||
<PaperSwitch
|
|
||||||
value={exercises.includes(name)}
|
|
||||||
style={{ marginRight: MARGIN }}
|
|
||||||
onValueChange={(value) => toggleExercise(value, name)}
|
|
||||||
/>
|
|
||||||
<Text>{name}</Text>
|
|
||||||
{movable && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
icon="arrow-up"
|
|
||||||
style={{ marginLeft: "auto" }}
|
|
||||||
onPressIn={() => moveUp(index)}
|
|
||||||
/>
|
|
||||||
<IconButton icon="arrow-down" onPressIn={() => moveDown(index)} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
|
|
||||||
const moveDown = (from: number) => {
|
|
||||||
if (from === exercises.length - 1) return;
|
|
||||||
const to = from + 1;
|
|
||||||
const newExercises = [...exercises];
|
|
||||||
const copy = newExercises[from];
|
|
||||||
newExercises[from] = newExercises[to];
|
|
||||||
newExercises[to] = copy;
|
|
||||||
setExercises(newExercises);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveUp = (from: number) => {
|
|
||||||
if (from === 0) return;
|
|
||||||
const to = from - 1;
|
|
||||||
const newExercises = [...exercises];
|
|
||||||
const copy = newExercises[from];
|
|
||||||
newExercises[from] = newExercises[to];
|
|
||||||
newExercises[to] = copy;
|
|
||||||
setExercises(newExercises);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader
|
<StackHeader
|
||||||
|
@ -165,64 +89,64 @@ export default function EditPlan() {
|
||||||
const newPlan = await planRepo.findOne({
|
const newPlan = await planRepo.findOne({
|
||||||
where: { id: plan.id },
|
where: { id: plan.id },
|
||||||
});
|
});
|
||||||
let first: Partial<GymSet> = await setRepo.findOne({
|
let first = await setRepo.findOne({
|
||||||
where: { name: exercises[0] },
|
where: { name: workouts[0] },
|
||||||
order: { created: "desc" },
|
order: { created: "desc" },
|
||||||
});
|
});
|
||||||
if (!first) first = { ...defaultSet, name: exercises[0] };
|
if (!first) first = { ...defaultSet, name: workouts[0] };
|
||||||
delete first.id;
|
delete first.id;
|
||||||
stackNavigate("StartPlan", { plan: newPlan, first });
|
navigation.navigate("StartPlan", { plan: newPlan, first });
|
||||||
}}
|
}}
|
||||||
icon="play"
|
icon="play-arrow"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StackHeader>
|
</StackHeader>
|
||||||
<ScrollView style={{ padding: PADDING, flex: 1 }}>
|
<View style={{ padding: PADDING, flex: 1 }}>
|
||||||
<AppInput
|
<ScrollView style={{ flex: 1 }}>
|
||||||
label="Title"
|
<AppInput
|
||||||
value={title}
|
label="Title"
|
||||||
placeholder={days.join(", ")}
|
value={title}
|
||||||
onChangeText={(value) => setTitle(value)}
|
onChangeText={(value) => setTitle(value)}
|
||||||
/>
|
/>
|
||||||
|
<Text style={styles.title}>Days</Text>
|
||||||
|
{DAYS.map((day) => (
|
||||||
|
<Switch
|
||||||
|
key={day}
|
||||||
|
onChange={(value) => toggleDay(value, day)}
|
||||||
|
value={days.includes(day)}
|
||||||
|
title={day}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
<Text style={styles.title}>Days</Text>
|
<Button
|
||||||
{DAYS.map((day) => renderDay(day))}
|
disabled={workouts.length === 0 && days.length === 0}
|
||||||
|
style={styles.button}
|
||||||
<Text style={[styles.title, { marginTop: MARGIN }]}>Exercises</Text>
|
mode="outlined"
|
||||||
{exercises.map((exercise, index) =>
|
icon="save"
|
||||||
renderExercise(exercise, index, true)
|
onPress={async () => {
|
||||||
)}
|
await save();
|
||||||
{names?.length === 0 && (
|
navigation.navigate("PlanList");
|
||||||
<>
|
}}
|
||||||
<Text>No exercises yet.</Text>
|
>
|
||||||
<Button
|
Save
|
||||||
onPress={() =>
|
</Button>
|
||||||
stackNavigate("EditExercise", { gymSet: defaultSet })
|
</View>
|
||||||
}
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
>
|
|
||||||
Add some?
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{names !== undefined &&
|
|
||||||
names
|
|
||||||
.filter((name) => !exercises.includes(name))
|
|
||||||
.map((name, index) => renderExercise(name, index, false))}
|
|
||||||
<View style={{ marginBottom: MARGIN }}></View>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<PrimaryButton
|
|
||||||
disabled={exercises.length === 0 && days.length === 0}
|
|
||||||
icon="content-save"
|
|
||||||
onPress={async () => {
|
|
||||||
await save();
|
|
||||||
drawerNavigate("Plans");
|
|
||||||
}}
|
|
||||||
style={{ margin: MARGIN }}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</PrimaryButton>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -232,4 +156,5 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
marginBottom: MARGIN,
|
marginBottom: MARGIN,
|
||||||
},
|
},
|
||||||
|
button: {},
|
||||||
});
|
});
|
||||||
|
|
231
EditSet.tsx
231
EditSet.tsx
|
@ -10,50 +10,37 @@ 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 { NativeModules, TextInput, View } from "react-native";
|
||||||
import DocumentPicker from "react-native-document-picker";
|
import DocumentPicker from "react-native-document-picker";
|
||||||
import {
|
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
TouchableRipple,
|
|
||||||
} from "react-native-paper";
|
|
||||||
import { check, PERMISSIONS, request, RESULTS } from "react-native-permissions";
|
|
||||||
import AppInput from "./AppInput";
|
import AppInput from "./AppInput";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import { convert } from "./conversions";
|
|
||||||
import { getNow, setRepo, settingsRepo } from "./db";
|
import { getNow, setRepo, settingsRepo } from "./db";
|
||||||
import { DrawerParams } from "./drawer-params";
|
|
||||||
import { fixNumeric } from "./fix-numeric";
|
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
import Select from "./Select";
|
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 { toast } from "./toast";
|
||||||
import PrimaryButton from "./PrimaryButton";
|
import { fixNumeric } from "./fix-numeric";
|
||||||
|
|
||||||
export default function EditSet() {
|
export default function EditSet() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "EditSet">>();
|
const { params } = useRoute<RouteProp<HomePageParams, "EditSet">>();
|
||||||
const { set } = params;
|
const { set } = params;
|
||||||
const { navigate } = useNavigation<NavigationProp<DrawerParams>>();
|
const { navigate } = useNavigation<NavigationProp<HomePageParams>>();
|
||||||
const [settings, setSettings] = useState<Settings>({} as Settings);
|
const [settings, setSettings] = useState<Settings>({} as Settings);
|
||||||
const [name, setName] = useState(set.name);
|
const [name, setName] = useState(set.name);
|
||||||
const [reps, setReps] = useState(set.reps?.toString());
|
const [reps, setReps] = useState(set.reps?.toString());
|
||||||
const [weight, setWeight] = useState(set.weight?.toString());
|
const [weight, setWeight] = useState(set.weight?.toString());
|
||||||
const [newImage, setNewImage] = useState(set.image);
|
const [newImage, setNewImage] = useState(set.image);
|
||||||
const [unit, setUnit] = useState(set.unit);
|
const [unit, setUnit] = useState(set.unit);
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
|
||||||
const [created, setCreated] = useState<Date>(
|
const [created, setCreated] = useState<Date>(
|
||||||
set.created ? new Date(set.created) : new Date()
|
set.created ? new Date(set.created) : new Date()
|
||||||
);
|
);
|
||||||
const [createdDirty, setCreatedDirty] = useState(false);
|
const [createdDirty, setCreatedDirty] = useState(false);
|
||||||
const [showRemoveImage, setShowRemoveImage] = useState(false);
|
const [showRemove, setShowRemove] = useState(false);
|
||||||
const [removeImage, setRemoveImage] = useState(false);
|
const [removeImage, setRemoveImage] = useState(false);
|
||||||
const [setOptions, setSets] = useState<GymSet[]>([]);
|
|
||||||
const weightRef = useRef<TextInput>(null);
|
const weightRef = useRef<TextInput>(null);
|
||||||
const repsRef = useRef<TextInput>(null);
|
const repsRef = useRef<TextInput>(null);
|
||||||
|
const unitRef = useRef<TextInput>(null);
|
||||||
|
|
||||||
const [selection, setSelection] = useState({
|
const [selection, setSelection] = useState({
|
||||||
start: 0,
|
start: 0,
|
||||||
|
@ -72,46 +59,33 @@ export default function EditSet() {
|
||||||
const first = await setRepo.findOne({ where: { name: value } });
|
const first = await setRepo.findOne({ where: { name: value } });
|
||||||
const milliseconds =
|
const milliseconds =
|
||||||
(first?.minutes ?? 3) * 60 * 1000 + (first?.seconds ?? 0) * 1000;
|
(first?.minutes ?? 3) * 60 * 1000 + (first?.seconds ?? 0) * 1000;
|
||||||
console.log(`${EditSet.name}.timer:`, { milliseconds });
|
|
||||||
const canNotify = await check(PERMISSIONS.ANDROID.POST_NOTIFICATIONS);
|
|
||||||
if (canNotify === RESULTS.DENIED)
|
|
||||||
await request(PERMISSIONS.ANDROID.POST_NOTIFICATIONS);
|
|
||||||
if (milliseconds) NativeModules.AlarmModule.timer(milliseconds);
|
if (milliseconds) NativeModules.AlarmModule.timer(milliseconds);
|
||||||
},
|
},
|
||||||
[settings]
|
[settings]
|
||||||
);
|
);
|
||||||
|
|
||||||
const notify = (value: Partial<GymSet>) => {
|
const added = async (value: GymSet) => {
|
||||||
if (!settings.notify) return navigate("History");
|
startTimer(value.name);
|
||||||
|
console.log(`${EditSet.name}.add`, { set: value });
|
||||||
|
if (!settings.notify) return;
|
||||||
if (
|
if (
|
||||||
value.weight > set.weight ||
|
value.weight > set.weight ||
|
||||||
(value.reps > set.reps && value.weight === set.weight)
|
(value.reps > set.reps && value.weight === set.weight)
|
||||||
) {
|
) {
|
||||||
toast("Great work King! That's a new record.");
|
toast("Great work King! That's a new record.");
|
||||||
}
|
}
|
||||||
};
|
navigate("Sets", { added: value.id });
|
||||||
|
|
||||||
const added = async (value: GymSet) => {
|
|
||||||
console.log(`${EditSet.name}.added:`, value);
|
|
||||||
startTimer(value.name);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
|
|
||||||
let newWeight = Number(weight || 0);
|
|
||||||
let newUnit = unit;
|
|
||||||
if (settings.autoConvert && unit !== settings.autoConvert) {
|
|
||||||
newUnit = settings.autoConvert;
|
|
||||||
newWeight = convert(newWeight, unit, settings.autoConvert);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSet: Partial<GymSet> = {
|
const newSet: Partial<GymSet> = {
|
||||||
id: set.id,
|
id: set.id,
|
||||||
name,
|
name,
|
||||||
reps: Number(reps || 0),
|
reps: Number(reps),
|
||||||
weight: newWeight,
|
weight: Number(weight),
|
||||||
unit: newUnit,
|
unit,
|
||||||
minutes: Number(set.minutes ?? 3),
|
minutes: Number(set.minutes ?? 3),
|
||||||
seconds: Number(set.seconds ?? 30),
|
seconds: Number(set.seconds ?? 30),
|
||||||
sets: set.sets ?? 3,
|
sets: set.sets ?? 3,
|
||||||
|
@ -129,9 +103,9 @@ export default function EditSet() {
|
||||||
if (typeof set.id !== "number") newSet.created = await getNow();
|
if (typeof set.id !== "number") newSet.created = await getNow();
|
||||||
|
|
||||||
const saved = await setRepo.save(newSet);
|
const saved = await setRepo.save(newSet);
|
||||||
notify(newSet);
|
if (typeof set.id !== "number") return added(saved);
|
||||||
if (typeof set.id !== "number") added(saved);
|
if (createdDirty) navigate("Sets", { reset: saved.id });
|
||||||
navigate("History");
|
else navigate("Sets", { refresh: saved.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeImage = useCallback(async () => {
|
const changeImage = useCallback(async () => {
|
||||||
|
@ -145,7 +119,7 @@ export default function EditSet() {
|
||||||
const handleRemove = useCallback(async () => {
|
const handleRemove = useCallback(async () => {
|
||||||
setNewImage("");
|
setNewImage("");
|
||||||
setRemoveImage(true);
|
setRemoveImage(true);
|
||||||
setShowRemoveImage(false);
|
setShowRemove(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pickDate = useCallback(() => {
|
const pickDate = useCallback(() => {
|
||||||
|
@ -165,75 +139,28 @@ export default function EditSet() {
|
||||||
});
|
});
|
||||||
}, [created]);
|
}, [created]);
|
||||||
|
|
||||||
const remove = async () => {
|
|
||||||
await setRepo.delete(set.id);
|
|
||||||
navigate("History");
|
|
||||||
};
|
|
||||||
|
|
||||||
const openMenu = async () => {
|
|
||||||
if (setOptions.length > 0) return setShowMenu(true);
|
|
||||||
const latestSets = await setRepo
|
|
||||||
.createQueryBuilder()
|
|
||||||
.select()
|
|
||||||
.addSelect("MAX(created) as created")
|
|
||||||
.groupBy("name")
|
|
||||||
.getMany();
|
|
||||||
setSets(latestSets);
|
|
||||||
setShowMenu(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const select = (setOption: GymSet) => {
|
|
||||||
setName(setOption.name);
|
|
||||||
setReps(setOption.reps.toString());
|
|
||||||
setWeight(setOption.weight.toString());
|
|
||||||
setNewImage(setOption.image);
|
|
||||||
setUnit(setOption.unit);
|
|
||||||
setSelection({
|
|
||||||
start: 0,
|
|
||||||
end: setOption.reps.toString().length,
|
|
||||||
});
|
|
||||||
setShowMenu(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader title={typeof set.id === "number" ? "Edit set" : "Add set"}>
|
<StackHeader
|
||||||
{typeof set.id === "number" ? (
|
title={typeof set.id === "number" ? "Edit set" : "Add set"}
|
||||||
<IconButton onPress={() => setShowDelete(true)} icon="delete" />
|
/>
|
||||||
) : null}
|
|
||||||
</StackHeader>
|
|
||||||
|
|
||||||
<View style={{ padding: PADDING, flex: 1 }}>
|
<View style={{ padding: PADDING, flex: 1 }}>
|
||||||
<View>
|
<AppInput
|
||||||
<AppInput
|
label="Name"
|
||||||
label="Name"
|
value={name}
|
||||||
value={name}
|
onChangeText={setName}
|
||||||
onChangeText={setName}
|
autoCorrect={false}
|
||||||
autoCorrect={false}
|
autoFocus={!name}
|
||||||
autoFocus={!name}
|
onSubmitEditing={() => repsRef.current?.focus()}
|
||||||
onSubmitEditing={() => repsRef.current?.focus()}
|
/>
|
||||||
/>
|
|
||||||
<View
|
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
|
||||||
>
|
|
||||||
<Menu
|
|
||||||
visible={showMenu}
|
|
||||||
onDismiss={() => setShowMenu(false)}
|
|
||||||
anchor={<IconButton icon="menu-down" onPress={openMenu} />}
|
|
||||||
>
|
|
||||||
{setOptions.map((setOption) => (
|
|
||||||
<Menu.Item
|
|
||||||
title={setOption.name}
|
|
||||||
key={setOption.id}
|
|
||||||
onPress={() => select(setOption)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View>
|
<View style={{ flexDirection: "row" }}>
|
||||||
<AppInput
|
<AppInput
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginBottom: MARGIN,
|
||||||
|
}}
|
||||||
label="Reps"
|
label="Reps"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={reps}
|
value={reps}
|
||||||
|
@ -248,22 +175,24 @@ export default function EditSet() {
|
||||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
||||||
innerRef={repsRef}
|
innerRef={repsRef}
|
||||||
/>
|
/>
|
||||||
<View
|
<IconButton
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
icon="add"
|
||||||
>
|
onPress={() => setReps((Number(reps) + 1).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="plus"
|
<IconButton
|
||||||
onPress={() => setReps((Number(reps) + 1).toString())}
|
icon="remove"
|
||||||
/>
|
onPress={() => setReps((Number(reps) - 1).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="minus"
|
|
||||||
onPress={() => setReps((Number(reps) - 1).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: MARGIN,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AppInput
|
<AppInput
|
||||||
|
style={{ flex: 1 }}
|
||||||
label="Weight"
|
label="Weight"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={weight}
|
value={weight}
|
||||||
|
@ -276,38 +205,30 @@ export default function EditSet() {
|
||||||
onSubmitEditing={handleSubmit}
|
onSubmitEditing={handleSubmit}
|
||||||
innerRef={weightRef}
|
innerRef={weightRef}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
<View
|
icon="add"
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
||||||
>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="plus"
|
icon="remove"
|
||||||
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
|
||||||
icon="minus"
|
|
||||||
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{settings.showUnit && (
|
{settings.showUnit && (
|
||||||
<Select
|
<AppInput
|
||||||
value={unit}
|
autoCapitalize="none"
|
||||||
onChange={setUnit}
|
|
||||||
items={[
|
|
||||||
{ label: "kg", value: "kg" },
|
|
||||||
{ label: "lb", value: "lb" },
|
|
||||||
{ label: "stone", value: "stone" },
|
|
||||||
]}
|
|
||||||
label="Unit"
|
label="Unit"
|
||||||
|
value={unit}
|
||||||
|
onChangeText={setUnit}
|
||||||
|
innerRef={unitRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{settings.showDate && (
|
{settings.showDate && (
|
||||||
<AppInput
|
<AppInput
|
||||||
label="Created"
|
label="Created"
|
||||||
value={format(created, settings.date || "Pp")}
|
value={format(created, settings.date || "P")}
|
||||||
onPressOut={pickDate}
|
onPressOut={pickDate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -316,7 +237,7 @@ export default function EditSet() {
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
style={{ marginBottom: MARGIN }}
|
style={{ marginBottom: MARGIN }}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
onLongPress={() => setShowRemoveImage(true)}
|
onLongPress={() => setShowRemove(true)}
|
||||||
>
|
>
|
||||||
<Card.Cover source={{ uri: newImage }} />
|
<Card.Cover source={{ uri: newImage }} />
|
||||||
</TouchableRipple>
|
</TouchableRipple>
|
||||||
|
@ -326,39 +247,31 @@ export default function EditSet() {
|
||||||
<Button
|
<Button
|
||||||
style={{ marginBottom: MARGIN }}
|
style={{ marginBottom: MARGIN }}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
icon="image-plus"
|
icon="add-photo-alternate"
|
||||||
>
|
>
|
||||||
Image
|
Image
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<PrimaryButton
|
<Button
|
||||||
disabled={!name}
|
disabled={!name}
|
||||||
icon="content-save"
|
mode="outlined"
|
||||||
|
icon="save"
|
||||||
style={{ margin: MARGIN }}
|
style={{ margin: MARGIN }}
|
||||||
onPress={handleSubmit}
|
onPress={handleSubmit}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Remove image"
|
title="Remove image"
|
||||||
onOk={handleRemove}
|
onOk={handleRemove}
|
||||||
show={showRemoveImage}
|
show={showRemove}
|
||||||
setShow={setShowRemoveImage}
|
setShow={setShowRemove}
|
||||||
>
|
>
|
||||||
Are you sure you want to remove the image?
|
Are you sure you want to remove the image?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Delete set"
|
|
||||||
show={showDelete}
|
|
||||||
onOk={remove}
|
|
||||||
setShow={setShowDelete}
|
|
||||||
>
|
|
||||||
<>Are you sure you want to delete {name}</>
|
|
||||||
</ConfirmDialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
102
EditSets.tsx
102
EditSets.tsx
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
NavigationProp,
|
|
||||||
RouteProp,
|
RouteProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
|
@ -11,21 +10,18 @@ import DocumentPicker from "react-native-document-picker";
|
||||||
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
|
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import AppInput from "./AppInput";
|
import AppInput from "./AppInput";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import Select from "./Select";
|
|
||||||
import StackHeader from "./StackHeader";
|
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import { setRepo, settingsRepo } from "./db";
|
import { setRepo, settingsRepo } from "./db";
|
||||||
import { DrawerParams } from "./drawer-params";
|
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
|
import { HomePageParams } from "./home-page-params";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
import PrimaryButton from "./PrimaryButton";
|
import StackHeader from "./StackHeader";
|
||||||
|
|
||||||
export default function EditSets() {
|
export default function EditSets() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "EditSets">>();
|
const { params } = useRoute<RouteProp<HomePageParams, "EditSets">>();
|
||||||
const { ids } = params;
|
const { ids } = params;
|
||||||
const { navigate } = useNavigation<NavigationProp<DrawerParams>>();
|
const navigation = useNavigation();
|
||||||
const [settings, setSettings] = useState<Settings>({} as Settings);
|
const [settings, setSettings] = useState<Settings>({} as Settings);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [reps, setReps] = useState("");
|
const [reps, setReps] = useState("");
|
||||||
|
@ -55,8 +51,8 @@ export default function EditSets() {
|
||||||
}, [ids])
|
}, [ids])
|
||||||
);
|
);
|
||||||
|
|
||||||
const save = async () => {
|
const handleSubmit = async () => {
|
||||||
console.log(`${EditSets.name}.save:`, { uri: newImage, name });
|
console.log(`${EditSets.name}.handleSubmit:`, { uri: newImage, name });
|
||||||
const update: Partial<GymSet> = {};
|
const update: Partial<GymSet> = {};
|
||||||
if (name) update.name = name;
|
if (name) update.name = name;
|
||||||
if (reps) update.reps = Number(reps);
|
if (reps) update.reps = Number(reps);
|
||||||
|
@ -64,7 +60,7 @@ export default function EditSets() {
|
||||||
if (unit) update.unit = unit;
|
if (unit) update.unit = unit;
|
||||||
if (newImage) update.image = newImage;
|
if (newImage) update.image = newImage;
|
||||||
if (Object.keys(update).length > 0) await setRepo.update(ids, update);
|
if (Object.keys(update).length > 0) await setRepo.update(ids, update);
|
||||||
navigate("History");
|
navigation.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeImage = useCallback(async () => {
|
const changeImage = useCallback(async () => {
|
||||||
|
@ -93,8 +89,16 @@ export default function EditSets() {
|
||||||
autoFocus={!name}
|
autoFocus={!name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: MARGIN,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AppInput
|
<AppInput
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
label={`Reps: ${oldReps}`}
|
label={`Reps: ${oldReps}`}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={reps}
|
value={reps}
|
||||||
|
@ -103,53 +107,46 @@ export default function EditSets() {
|
||||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
||||||
autoFocus={!!name}
|
autoFocus={!!name}
|
||||||
/>
|
/>
|
||||||
<View
|
<IconButton
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
icon="add"
|
||||||
>
|
onPress={() => setReps((Number(reps) + 1).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="plus"
|
<IconButton
|
||||||
onPress={() => setReps((Number(reps) + 1).toString())}
|
icon="remove"
|
||||||
/>
|
onPress={() => setReps((Number(reps) - 1).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="minus"
|
|
||||||
onPress={() => setReps((Number(reps) - 1).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: MARGIN,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AppInput
|
<AppInput
|
||||||
|
style={{ flex: 1 }}
|
||||||
label={`Weights: ${weights}`}
|
label={`Weights: ${weights}`}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={weight}
|
value={weight}
|
||||||
onChangeText={setWeight}
|
onChangeText={setWeight}
|
||||||
onSubmitEditing={save}
|
onSubmitEditing={handleSubmit}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="add"
|
||||||
|
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="remove"
|
||||||
|
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
||||||
/>
|
/>
|
||||||
<View
|
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon="plus"
|
|
||||||
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon="minus"
|
|
||||||
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{settings.showUnit && (
|
{settings.showUnit && (
|
||||||
<Select
|
<AppInput
|
||||||
value={unit}
|
autoCapitalize="none"
|
||||||
onChange={setUnit}
|
|
||||||
items={[
|
|
||||||
{ label: "", value: "" },
|
|
||||||
{ label: "kg", value: "kg" },
|
|
||||||
{ label: "lb", value: "lb" },
|
|
||||||
{ label: "stone", value: "stone" },
|
|
||||||
]}
|
|
||||||
label={`Units: ${units}`}
|
label={`Units: ${units}`}
|
||||||
|
value={unit}
|
||||||
|
onChangeText={setUnit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -175,20 +172,21 @@ export default function EditSets() {
|
||||||
<Button
|
<Button
|
||||||
style={{ marginBottom: MARGIN }}
|
style={{ marginBottom: MARGIN }}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
icon="image-plus"
|
icon="add-photo-alternate"
|
||||||
>
|
>
|
||||||
Image
|
Image
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<PrimaryButton
|
<Button
|
||||||
icon="content-save"
|
mode="outlined"
|
||||||
|
icon="save"
|
||||||
style={{ margin: MARGIN }}
|
style={{ margin: MARGIN }}
|
||||||
onPress={save}
|
onPress={handleSubmit}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
163
EditWeight.tsx
163
EditWeight.tsx
|
@ -1,163 +0,0 @@
|
||||||
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
|
|
||||||
import {
|
|
||||||
NavigationProp,
|
|
||||||
RouteProp,
|
|
||||||
useFocusEffect,
|
|
||||||
useNavigation,
|
|
||||||
useRoute,
|
|
||||||
} from "@react-navigation/native";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { TextInput, View } from "react-native";
|
|
||||||
import { IconButton } from "react-native-paper";
|
|
||||||
import AppInput from "./AppInput";
|
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
|
||||||
import PrimaryButton from "./PrimaryButton";
|
|
||||||
import Select from "./Select";
|
|
||||||
import StackHeader from "./StackHeader";
|
|
||||||
import { MARGIN, PADDING } from "./constants";
|
|
||||||
import { AppDataSource } from "./data-source";
|
|
||||||
import { getNow, settingsRepo, weightRepo } from "./db";
|
|
||||||
import { DrawerParams } from "./drawer-params";
|
|
||||||
import Settings from "./settings";
|
|
||||||
import { toast } from "./toast";
|
|
||||||
import Weight from "./weight";
|
|
||||||
|
|
||||||
export default function EditWeight() {
|
|
||||||
const { params } = useRoute<RouteProp<StackParams, "EditWeight">>();
|
|
||||||
const { weight } = params;
|
|
||||||
const { navigate } = useNavigation<NavigationProp<DrawerParams>>();
|
|
||||||
const [settings, setSettings] = useState<Settings>({} as Settings);
|
|
||||||
const [value, setValue] = useState(weight.value?.toString());
|
|
||||||
const [unit, setUnit] = useState(weight.unit);
|
|
||||||
const [created, setCreated] = useState<Date>(
|
|
||||||
weight.created ? new Date(weight.created) : new Date()
|
|
||||||
);
|
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
|
||||||
const [createdDirty, setCreatedDirty] = useState(false);
|
|
||||||
const unitRef = useRef<TextInput>(null);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!value) return;
|
|
||||||
|
|
||||||
const newWeight: Partial<Weight> = {
|
|
||||||
id: weight.id,
|
|
||||||
value: Number(value),
|
|
||||||
unit,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (createdDirty) newWeight.created = created.toISOString();
|
|
||||||
else if (typeof weight.id !== "number") newWeight.created = await getNow();
|
|
||||||
|
|
||||||
await weightRepo.save(newWeight);
|
|
||||||
if (settings.notify) await checkWeekly();
|
|
||||||
navigate("Weight");
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkWeekly = async () => {
|
|
||||||
const select = `
|
|
||||||
WITH weekly_weights AS (
|
|
||||||
SELECT
|
|
||||||
strftime('%W', created) AS week_number,
|
|
||||||
AVG(value) AS weekly_average
|
|
||||||
FROM weights
|
|
||||||
WHERE strftime('%W', created) = strftime('%W', 'now')
|
|
||||||
GROUP BY week_number
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
((SELECT value FROM weights WHERE strftime('%W', created) = strftime('%W', 'now') ORDER BY created LIMIT 1) - weekly_weights.weekly_average) / (SELECT value FROM weights WHERE strftime('%W', created) = strftime('%W', 'now') ORDER BY created LIMIT 1) * 100 AS loss
|
|
||||||
FROM weekly_weights
|
|
||||||
WHERE week_number = strftime('%W', 'now')
|
|
||||||
`;
|
|
||||||
const result = await AppDataSource.manager.query(select);
|
|
||||||
console.log(`${EditWeight.name}.checkWeekly:`, result);
|
|
||||||
if (result.length && result[0].loss > 1)
|
|
||||||
toast("Weight loss should be <= 1% per week.");
|
|
||||||
};
|
|
||||||
|
|
||||||
const pickDate = useCallback(() => {
|
|
||||||
DateTimePickerAndroid.open({
|
|
||||||
value: created,
|
|
||||||
onChange: (_, date) => {
|
|
||||||
if (date === created) return;
|
|
||||||
setCreated(date);
|
|
||||||
setCreatedDirty(true);
|
|
||||||
},
|
|
||||||
mode: "date",
|
|
||||||
});
|
|
||||||
}, [created]);
|
|
||||||
|
|
||||||
const remove = async () => {
|
|
||||||
if (!weight.id) return;
|
|
||||||
await weightRepo.delete(weight.id);
|
|
||||||
navigate("Weight");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StackHeader
|
|
||||||
title={typeof weight.id === "number" ? "Edit weight" : "Add weight"}
|
|
||||||
>
|
|
||||||
{typeof weight.id === "number" ? (
|
|
||||||
<IconButton onPress={() => setShowDelete(true)} icon="delete" />
|
|
||||||
) : null}
|
|
||||||
</StackHeader>
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Delete weight"
|
|
||||||
show={showDelete}
|
|
||||||
onOk={remove}
|
|
||||||
setShow={setShowDelete}
|
|
||||||
>
|
|
||||||
<>Are you sure you want to delete {value}</>
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<View style={{ padding: PADDING, flex: 1 }}>
|
|
||||||
<AppInput
|
|
||||||
label="Weight"
|
|
||||||
value={value}
|
|
||||||
onChangeText={setValue}
|
|
||||||
keyboardType="numeric"
|
|
||||||
onSubmitEditing={submit}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
{settings.showUnit && (
|
|
||||||
<Select
|
|
||||||
value={unit}
|
|
||||||
onChange={setUnit}
|
|
||||||
items={[
|
|
||||||
{ label: "kg", value: "kg" },
|
|
||||||
{ label: "lb", value: "lb" },
|
|
||||||
{ label: "stone", value: "stone" },
|
|
||||||
]}
|
|
||||||
label="Unit"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{settings.showDate && (
|
|
||||||
<AppInput
|
|
||||||
label="Created"
|
|
||||||
value={format(created, settings.date || "Pp")}
|
|
||||||
onPressOut={pickDate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<PrimaryButton
|
|
||||||
disabled={!value}
|
|
||||||
icon="content-save"
|
|
||||||
style={{ margin: MARGIN }}
|
|
||||||
onPress={submit}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</PrimaryButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
NavigationProp,
|
|
||||||
RouteProp,
|
RouteProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
|
@ -8,25 +7,22 @@ import {
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { ScrollView, TextInput, View } from "react-native";
|
import { ScrollView, TextInput, View } from "react-native";
|
||||||
import DocumentPicker from "react-native-document-picker";
|
import DocumentPicker from "react-native-document-picker";
|
||||||
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
|
import { Button, Card, TouchableRipple } from "react-native-paper";
|
||||||
import AppInput from "./AppInput";
|
import AppInput from "./AppInput";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import PrimaryButton from "./PrimaryButton";
|
|
||||||
import StackHeader from "./StackHeader";
|
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import { getNow, planRepo, setRepo, settingsRepo } from "./db";
|
import { getNow, planRepo, setRepo, settingsRepo } from "./db";
|
||||||
import { DrawerParams } from "./drawer-params";
|
|
||||||
import { fixNumeric } from "./fix-numeric";
|
import { fixNumeric } from "./fix-numeric";
|
||||||
import GymSet, { defaultSet } from "./gym-set";
|
import { defaultSet } from "./gym-set";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
import StackHeader from "./StackHeader";
|
||||||
import { toast } from "./toast";
|
import { toast } from "./toast";
|
||||||
|
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||||
|
|
||||||
export default function EditExercise() {
|
export default function EditWorkout() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "EditExercise">>();
|
const { params } = useRoute<RouteProp<WorkoutsPageParams, "EditWorkout">>();
|
||||||
const [removeImage, setRemoveImage] = useState(false);
|
const [removeImage, setRemoveImage] = useState(false);
|
||||||
const [showRemoveImage, setShowRemoveImage] = useState(false);
|
const [showRemove, setShowRemove] = useState(false);
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
|
||||||
const [name, setName] = useState(params.gymSet.name);
|
const [name, setName] = useState(params.gymSet.name);
|
||||||
const [steps, setSteps] = useState(params.gymSet.steps);
|
const [steps, setSteps] = useState(params.gymSet.steps);
|
||||||
const [uri, setUri] = useState(params.gymSet.image);
|
const [uri, setUri] = useState(params.gymSet.image);
|
||||||
|
@ -37,7 +33,7 @@ export default function EditExercise() {
|
||||||
params.gymSet.seconds?.toString() ?? "30"
|
params.gymSet.seconds?.toString() ?? "30"
|
||||||
);
|
);
|
||||||
const [sets, setSets] = useState(params.gymSet.sets?.toString() ?? "3");
|
const [sets, setSets] = useState(params.gymSet.sets?.toString() ?? "3");
|
||||||
const { navigate } = useNavigation<NavigationProp<DrawerParams>>();
|
const navigation = useNavigation();
|
||||||
const setsRef = useRef<TextInput>(null);
|
const setsRef = useRef<TextInput>(null);
|
||||||
const stepsRef = useRef<TextInput>(null);
|
const stepsRef = useRef<TextInput>(null);
|
||||||
const minutesRef = useRef<TextInput>(null);
|
const minutesRef = useRef<TextInput>(null);
|
||||||
|
@ -46,33 +42,29 @@ export default function EditExercise() {
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
settingsRepo.findOne({ where: {} }).then((gotSettings) => {
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
setSettings(gotSettings);
|
}, [])
|
||||||
if (params.gymSet.id) return;
|
|
||||||
setSets(gotSettings.defaultSets?.toString() ?? "3");
|
|
||||||
setMinutes(gotSettings.defaultMinutes?.toString() ?? "3");
|
|
||||||
setSeconds(gotSettings.defaultSeconds?.toString() ?? "30");
|
|
||||||
});
|
|
||||||
}, [params.gymSet.id])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
const newExercise = {
|
await setRepo.update(
|
||||||
name: name || params.gymSet.name,
|
{ name: params.gymSet.name },
|
||||||
sets: Number(sets),
|
{
|
||||||
minutes: Number(minutes),
|
name: name || params.gymSet.name,
|
||||||
seconds: Number(seconds),
|
sets: Number(sets),
|
||||||
steps,
|
minutes: +minutes,
|
||||||
image: removeImage ? "" : uri,
|
seconds: +seconds,
|
||||||
} as GymSet;
|
steps,
|
||||||
await setRepo.update({ name: params.gymSet.name }, newExercise);
|
image: removeImage ? "" : uri,
|
||||||
|
}
|
||||||
|
);
|
||||||
await planRepo.query(
|
await planRepo.query(
|
||||||
`UPDATE plans
|
`UPDATE plans
|
||||||
SET exercises = REPLACE(exercises, $1, $2)
|
SET workouts = REPLACE(workouts, $1, $2)
|
||||||
WHERE exercises LIKE $3`,
|
WHERE workouts LIKE $3`,
|
||||||
[params.gymSet.name, name, `%${params.gymSet.name}%`]
|
[params.gymSet.name, name, `%${params.gymSet.name}%`]
|
||||||
);
|
);
|
||||||
navigate("Exercises", { update: newExercise });
|
navigation.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
const add = async () => {
|
const add = async () => {
|
||||||
|
@ -82,18 +74,13 @@ export default function EditExercise() {
|
||||||
name,
|
name,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
image: uri,
|
image: uri,
|
||||||
minutes: minutes ? Number(minutes) : 3,
|
minutes: minutes ? +minutes : 3,
|
||||||
seconds: seconds ? Number(seconds) : 30,
|
seconds: seconds ? +seconds : 30,
|
||||||
sets: sets ? Number(sets) : 3,
|
sets: sets ? +sets : 3,
|
||||||
steps,
|
steps,
|
||||||
created: now,
|
created: now,
|
||||||
});
|
});
|
||||||
navigate("Exercises");
|
navigation.goBack();
|
||||||
};
|
|
||||||
|
|
||||||
const remove = async () => {
|
|
||||||
await setRepo.delete({ name: params.gymSet.name });
|
|
||||||
navigate("Exercises");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
|
@ -112,7 +99,7 @@ export default function EditExercise() {
|
||||||
const handleRemove = useCallback(async () => {
|
const handleRemove = useCallback(async () => {
|
||||||
setUri("");
|
setUri("");
|
||||||
setRemoveImage(true);
|
setRemoveImage(true);
|
||||||
setShowRemoveImage(false);
|
setShowRemove(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submitName = () => {
|
const submitName = () => {
|
||||||
|
@ -123,12 +110,8 @@ export default function EditExercise() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader
|
<StackHeader
|
||||||
title={params.gymSet.name ? "Edit exercise" : "Add exercise"}
|
title={params.gymSet.name ? "Edit workout" : "Add workout"}
|
||||||
>
|
/>
|
||||||
{typeof params.gymSet.id === "number" ? (
|
|
||||||
<IconButton onPress={() => setShowDelete(true)} icon="delete" />
|
|
||||||
) : null}
|
|
||||||
</StackHeader>
|
|
||||||
<View style={{ padding: PADDING, flex: 1 }}>
|
<View style={{ padding: PADDING, flex: 1 }}>
|
||||||
<ScrollView style={{ flex: 1 }}>
|
<ScrollView style={{ flex: 1 }}>
|
||||||
<AppInput
|
<AppInput
|
||||||
|
@ -158,7 +141,7 @@ export default function EditExercise() {
|
||||||
if (fixed.length !== newSets.length)
|
if (fixed.length !== newSets.length)
|
||||||
toast("Sets must be a number");
|
toast("Sets must be a number");
|
||||||
}}
|
}}
|
||||||
label="Sets per exercise"
|
label="Sets per workout"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
onSubmitEditing={() => minutesRef.current?.focus()}
|
onSubmitEditing={() => minutesRef.current?.focus()}
|
||||||
/>
|
/>
|
||||||
|
@ -191,7 +174,7 @@ export default function EditExercise() {
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
style={{ marginBottom: MARGIN }}
|
style={{ marginBottom: MARGIN }}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
onLongPress={() => setShowRemoveImage(true)}
|
onLongPress={() => setShowRemove(true)}
|
||||||
>
|
>
|
||||||
<Card.Cover source={{ uri }} />
|
<Card.Cover source={{ uri }} />
|
||||||
</TouchableRipple>
|
</TouchableRipple>
|
||||||
|
@ -200,33 +183,23 @@ export default function EditExercise() {
|
||||||
<Button
|
<Button
|
||||||
style={{ marginBottom: MARGIN }}
|
style={{ marginBottom: MARGIN }}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
icon="image-plus"
|
icon="add-photo-alternate"
|
||||||
>
|
>
|
||||||
Image
|
Image
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<PrimaryButton disabled={!name} icon="content-save" onPress={save}>
|
<Button disabled={!name} mode="outlined" icon="save" onPress={save}>
|
||||||
Save
|
Save
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Remove image"
|
title="Remove image"
|
||||||
onOk={handleRemove}
|
onOk={handleRemove}
|
||||||
show={showRemoveImage}
|
show={showRemove}
|
||||||
setShow={setShowRemoveImage}
|
setShow={setShowRemove}
|
||||||
>
|
>
|
||||||
Are you sure you want to remove the image?
|
Are you sure you want to remove the image?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Delete set"
|
|
||||||
show={showDelete}
|
|
||||||
onOk={remove}
|
|
||||||
setShow={setShowDelete}
|
|
||||||
>
|
|
||||||
<>Are you sure you want to delete {name}</>
|
|
||||||
</ConfirmDialog>
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
|
@ -11,19 +11,17 @@ import DocumentPicker from "react-native-document-picker";
|
||||||
import { Button, Card, TouchableRipple } from "react-native-paper";
|
import { Button, Card, TouchableRipple } from "react-native-paper";
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import AppInput from "./AppInput";
|
import AppInput from "./AppInput";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import { planRepo, setRepo, settingsRepo } from "./db";
|
import { planRepo, setRepo, settingsRepo } from "./db";
|
||||||
import { DrawerParams } from "./drawer-params";
|
|
||||||
import { fixNumeric } from "./fix-numeric";
|
import { fixNumeric } from "./fix-numeric";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
import StackHeader from "./StackHeader";
|
import StackHeader from "./StackHeader";
|
||||||
import { toast } from "./toast";
|
import { toast } from "./toast";
|
||||||
import PrimaryButton from "./PrimaryButton";
|
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||||
|
|
||||||
export default function EditExercises() {
|
export default function EditWorkouts() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "EditExercises">>();
|
const { params } = useRoute<RouteProp<WorkoutsPageParams, "EditWorkouts">>();
|
||||||
const [removeImage, setRemoveImage] = useState(false);
|
const [removeImage, setRemoveImage] = useState(false);
|
||||||
const [showRemove, setShowRemove] = useState(false);
|
const [showRemove, setShowRemove] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
@ -37,7 +35,7 @@ export default function EditExercises() {
|
||||||
const [oldSeconds, setOldSeconds] = useState("");
|
const [oldSeconds, setOldSeconds] = useState("");
|
||||||
const [sets, setSets] = useState("");
|
const [sets, setSets] = useState("");
|
||||||
const [oldSets, setOldSets] = useState("");
|
const [oldSets, setOldSets] = useState("");
|
||||||
const navigation = useNavigation<NavigationProp<DrawerParams>>();
|
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
||||||
const setsRef = useRef<TextInput>(null);
|
const setsRef = useRef<TextInput>(null);
|
||||||
const stepsRef = useRef<TextInput>(null);
|
const stepsRef = useRef<TextInput>(null);
|
||||||
const minutesRef = useRef<TextInput>(null);
|
const minutesRef = useRef<TextInput>(null);
|
||||||
|
@ -81,12 +79,12 @@ export default function EditExercises() {
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.update()
|
.update()
|
||||||
.set({
|
.set({
|
||||||
exercises: () => `REPLACE(exercises, '${oldName}', '${name}')`,
|
workouts: () => `REPLACE(workouts, '${oldName}', '${name}')`,
|
||||||
})
|
})
|
||||||
.where("exercises LIKE :name", { name: `%${oldName}%` })
|
.where("workouts LIKE :name", { name: `%${oldName}%` })
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
navigation.navigate("Exercises");
|
navigation.navigate("WorkoutList", { clearNames: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeImage = useCallback(async () => {
|
const changeImage = useCallback(async () => {
|
||||||
|
@ -110,7 +108,7 @@ export default function EditExercises() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader title={`Edit ${params.names.length} exercises`} />
|
<StackHeader title={`Edit ${params.names.length} workouts`} />
|
||||||
<View style={{ padding: PADDING, flex: 1 }}>
|
<View style={{ padding: PADDING, flex: 1 }}>
|
||||||
<ScrollView style={{ flex: 1 }}>
|
<ScrollView style={{ flex: 1 }}>
|
||||||
<AppInput
|
<AppInput
|
||||||
|
@ -182,15 +180,15 @@ export default function EditExercises() {
|
||||||
<Button
|
<Button
|
||||||
style={{ marginBottom: MARGIN }}
|
style={{ marginBottom: MARGIN }}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
icon="image-plus"
|
icon="add-photo-alternate"
|
||||||
>
|
>
|
||||||
Image
|
Image
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<PrimaryButton disabled={!name} icon="content-save" onPress={update}>
|
<Button disabled={!name} mode="outlined" icon="save" onPress={update}>
|
||||||
Save
|
Save
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Remove image"
|
title="Remove image"
|
||||||
onOk={handleRemove}
|
onOk={handleRemove}
|
|
@ -1,55 +0,0 @@
|
||||||
import { View, useColorScheme } from "react-native";
|
|
||||||
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { Dirs, FileSystem } from "react-native-file-access";
|
|
||||||
import { Button, Text } from "react-native-paper";
|
|
||||||
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
|
||||||
import { MARGIN } from "./constants";
|
|
||||||
import { AppDataSource } from "./data-source";
|
|
||||||
import { settingsRepo } from "./db";
|
|
||||||
|
|
||||||
export default function FatalError({
|
|
||||||
message,
|
|
||||||
setAppSettings,
|
|
||||||
setError,
|
|
||||||
}: {
|
|
||||||
message: string;
|
|
||||||
setAppSettings: (settings: {
|
|
||||||
startup: any;
|
|
||||||
theme: string;
|
|
||||||
lightColor: string;
|
|
||||||
darkColor: string;
|
|
||||||
}) => void;
|
|
||||||
setError: (message: string) => void;
|
|
||||||
}) {
|
|
||||||
const systemTheme = useColorScheme();
|
|
||||||
|
|
||||||
const resetDatabase = useCallback(async () => {
|
|
||||||
await FileSystem.cp("/dev/null", Dirs.DatabaseDir + "/massive.db");
|
|
||||||
await AppDataSource.initialize();
|
|
||||||
const gotSettings = await settingsRepo.findOne({ where: {} });
|
|
||||||
setAppSettings({
|
|
||||||
startup: gotSettings.startup,
|
|
||||||
theme: gotSettings.theme,
|
|
||||||
lightColor: gotSettings.lightColor || CombinedDefaultTheme.colors.primary,
|
|
||||||
darkColor: gotSettings.darkColor || CombinedDarkTheme.colors.primary,
|
|
||||||
});
|
|
||||||
setError("");
|
|
||||||
}, [setAppSettings, setError]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: systemTheme === "dark" ? "white" : "black",
|
|
||||||
margin: MARGIN,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Database failed to initialize: {message}
|
|
||||||
</Text>
|
|
||||||
<Button mode="contained" onPress={resetDatabase}>
|
|
||||||
Reset database
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -6,47 +6,46 @@ import {
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { FlatList, Image } from "react-native";
|
import { FlatList, Image } from "react-native";
|
||||||
import { List } from "react-native-paper";
|
import { List } from "react-native-paper";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import { getBestSets } from "./best.service";
|
import { getBestSets } from "./best.service";
|
||||||
import { LIMIT } from "./constants";
|
import { LIMIT } from "./constants";
|
||||||
import { settingsRepo } from "./db";
|
import { settingsRepo } from "./db";
|
||||||
import DrawerHeader from "./DrawerHeader";
|
import DrawerHeader from "./DrawerHeader";
|
||||||
|
import { GraphsPageParams } from "./GraphsPage";
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
|
||||||
export default function GraphsList() {
|
export default function GraphsList() {
|
||||||
const [bests, setBests] = useState<GymSet[]>();
|
const [bests, setBests] = useState<GymSet[]>([]);
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const [end, setEnd] = useState(false);
|
const [end, setEnd] = useState(false);
|
||||||
const [term, setTerm] = useState("");
|
const [term, setTerm] = useState("");
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
const navigation = useNavigation<NavigationProp<GraphsPageParams>>();
|
||||||
const [settings, setSettings] = useState<Settings>();
|
const [settings, setSettings] = useState<Settings>();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
|
|
||||||
const refresh = useCallback(
|
useFocusEffect(
|
||||||
async (value: string) => {
|
useCallback(() => {
|
||||||
if (refreshing) return;
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
const result = await getBestSets({ term: value, offset: 0 });
|
}, [])
|
||||||
setBests(result);
|
|
||||||
setOffset(0);
|
|
||||||
},
|
|
||||||
[refreshing]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const refresh = useCallback(async (value: string) => {
|
||||||
|
const result = await getBestSets({ term: value, offset: 0 });
|
||||||
|
setBests(result);
|
||||||
|
setOffset(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
refresh(term);
|
refresh(term);
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
}, [refresh, term])
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [term])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const next = useCallback(async () => {
|
const next = useCallback(async () => {
|
||||||
if (end) return;
|
if (end) return;
|
||||||
const newOffset = offset + LIMIT;
|
const newOffset = offset + LIMIT;
|
||||||
console.log(`${GraphsList.name}.next:`, { offset, newOffset, term });
|
console.log(`${GraphsList.name}.next:`, { offset, newOffset, term });
|
||||||
const newBests = await getBestSets({ term, offset: newOffset });
|
const newBests = await getBestSets({ term, offset });
|
||||||
if (newBests.length === 0) return setEnd(true);
|
if (newBests.length === 0) return setEnd(true);
|
||||||
if (!bests) return;
|
if (!bests) return;
|
||||||
setBests([...bests, ...newBests]);
|
setBests([...bests, ...newBests]);
|
||||||
|
@ -67,9 +66,9 @@ export default function GraphsList() {
|
||||||
key={item.name}
|
key={item.name}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
|
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
|
||||||
onPress={() => navigation.navigate("ViewGraph", { name: item.name })}
|
onPress={() => navigation.navigate("ViewGraph", { best: item })}
|
||||||
left={() =>
|
left={() =>
|
||||||
(settings?.images && item.image && (
|
(settings.images && item.image && (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: item.image }}
|
source={{ uri: item.image }}
|
||||||
style={{ height: 75, width: 75 }}
|
style={{ height: 75, width: 75 }}
|
||||||
|
@ -94,13 +93,7 @@ export default function GraphsList() {
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
data={bests}
|
data={bests}
|
||||||
keyExtractor={(set) => set.name}
|
|
||||||
onEndReached={next}
|
onEndReached={next}
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={() => {
|
|
||||||
setRefreshing(true);
|
|
||||||
refresh(term).finally(() => setRefreshing(false));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
23
GraphsPage.tsx
Normal file
23
GraphsPage.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { createStackNavigator } from "@react-navigation/stack";
|
||||||
|
import GraphsList from "./GraphsList";
|
||||||
|
import GymSet from "./gym-set";
|
||||||
|
import ViewGraph from "./ViewGraph";
|
||||||
|
|
||||||
|
const Stack = createStackNavigator<GraphsPageParams>();
|
||||||
|
export type GraphsPageParams = {
|
||||||
|
GraphsList: {};
|
||||||
|
ViewGraph: {
|
||||||
|
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.Navigator>
|
||||||
|
);
|
||||||
|
}
|
19
HomePage.tsx
Normal file
19
HomePage.tsx
Normal file
|
@ -0,0 +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";
|
||||||
|
|
||||||
|
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.Navigator>
|
||||||
|
);
|
||||||
|
}
|
220
InsightsPage.tsx
220
InsightsPage.tsx
|
@ -1,220 +0,0 @@
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
|
||||||
import { IconButton, Text } from "react-native-paper";
|
|
||||||
import AppPieChart from "./AppPieChart";
|
|
||||||
import Chart from "./Chart";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
|
||||||
import { MARGIN, PADDING } from "./constants";
|
|
||||||
import { AppDataSource } from "./data-source";
|
|
||||||
import { DAYS } from "./days";
|
|
||||||
import DrawerHeader from "./DrawerHeader";
|
|
||||||
import { Periods } from "./periods";
|
|
||||||
import Select from "./Select";
|
|
||||||
|
|
||||||
interface WeekCount {
|
|
||||||
week: string;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HourCount {
|
|
||||||
hour: string;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InsightsPage() {
|
|
||||||
const [weekCounts, setWeekCounts] = useState<WeekCount[]>();
|
|
||||||
const [hourCounts, setHourCounts] = useState<HourCount[]>();
|
|
||||||
const [loadingWeeks, setLoadingWeeks] = useState(true);
|
|
||||||
const [loadingHours, setLoadingHours] = useState(true);
|
|
||||||
const [period, setPeriod] = useState(Periods.Monthly);
|
|
||||||
const [showWeek, setShowWeek] = useState(false);
|
|
||||||
const [showHour, setShowHour] = useState(false);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
let difference = "-1 months";
|
|
||||||
if (period === Periods.TwoMonths) difference = "-2 months";
|
|
||||||
if (period === Periods.ThreeMonths) difference = "-3 months";
|
|
||||||
if (period === Periods.SixMonths) difference = "-6 months";
|
|
||||||
|
|
||||||
const selectWeeks = `
|
|
||||||
SELECT strftime('%w', created) as week, COUNT(*) as count
|
|
||||||
FROM sets
|
|
||||||
WHERE DATE(created) >= DATE('now', 'weekday 0', '${difference}')
|
|
||||||
GROUP BY week
|
|
||||||
HAVING week IS NOT NULL
|
|
||||||
ORDER BY count DESC;
|
|
||||||
`;
|
|
||||||
const selectHours = `
|
|
||||||
SELECT strftime('%H', created) AS hour, COUNT(*) AS count
|
|
||||||
FROM sets
|
|
||||||
WHERE DATE(created) >= DATE('now', 'weekday 0', '${difference}')
|
|
||||||
GROUP BY hour
|
|
||||||
having hour is not null
|
|
||||||
ORDER BY hour
|
|
||||||
`;
|
|
||||||
|
|
||||||
setLoadingWeeks(true);
|
|
||||||
setLoadingHours(true);
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
AppDataSource.manager
|
|
||||||
.query(selectWeeks)
|
|
||||||
.then(setWeekCounts)
|
|
||||||
.then(() => setLoadingWeeks(false))
|
|
||||||
.then(() =>
|
|
||||||
AppDataSource.manager.query(selectHours).then(setHourCounts)
|
|
||||||
)
|
|
||||||
.then(() => setLoadingHours(false))
|
|
||||||
.finally(() => {
|
|
||||||
setLoadingWeeks(false);
|
|
||||||
setLoadingHours(false);
|
|
||||||
}),
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}, [period])
|
|
||||||
);
|
|
||||||
|
|
||||||
const hourLabel = (hour: string) => {
|
|
||||||
let twelveHour = Number(hour);
|
|
||||||
if (twelveHour === 0) return "12AM";
|
|
||||||
let amPm = "AM";
|
|
||||||
if (twelveHour >= 12) amPm = "PM";
|
|
||||||
if (twelveHour > 12) twelveHour -= 12;
|
|
||||||
return `${twelveHour} ${amPm}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DrawerHeader name="Insights" />
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
paddingLeft: PADDING,
|
|
||||||
paddingTop: PADDING,
|
|
||||||
paddingRight: PADDING,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
label="Period"
|
|
||||||
items={[
|
|
||||||
{ value: Periods.Monthly, label: Periods.Monthly },
|
|
||||||
{ value: Periods.TwoMonths, label: Periods.TwoMonths },
|
|
||||||
{ value: Periods.ThreeMonths, label: Periods.ThreeMonths },
|
|
||||||
{ value: Periods.SixMonths, label: Periods.SixMonths },
|
|
||||||
]}
|
|
||||||
value={period}
|
|
||||||
onChange={(value) => setPeriod(value as Periods)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<ScrollView
|
|
||||||
style={{
|
|
||||||
padding: PADDING,
|
|
||||||
flexGrow: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
alignContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
variant="titleLarge"
|
|
||||||
style={{
|
|
||||||
marginBottom: MARGIN,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Most active days of the week
|
|
||||||
</Text>
|
|
||||||
<IconButton
|
|
||||||
icon="help-circle-outline"
|
|
||||||
size={25}
|
|
||||||
style={{ padding: 0, margin: 0, paddingBottom: 10 }}
|
|
||||||
onPress={() => setShowWeek(true)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{loadingWeeks ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : (
|
|
||||||
<AppPieChart
|
|
||||||
options={weekCounts.map((weekCount) => ({
|
|
||||||
label: DAYS[weekCount.week],
|
|
||||||
value: weekCount.count,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{weekCounts?.length === 0 && (
|
|
||||||
<Text style={{ marginBottom: MARGIN }}>
|
|
||||||
No entries yet! Start recording sets to see your most active days of
|
|
||||||
the week.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
alignContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
variant="titleLarge"
|
|
||||||
style={{
|
|
||||||
marginBottom: MARGIN,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Most active hours of the day
|
|
||||||
</Text>
|
|
||||||
<IconButton
|
|
||||||
icon="help-circle-outline"
|
|
||||||
size={25}
|
|
||||||
style={{ padding: 0, margin: 0, paddingBottom: 10 }}
|
|
||||||
onPress={() => setShowHour(true)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{loadingHours ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : (
|
|
||||||
<Chart
|
|
||||||
data={hourCounts.map((hc) => hc.count)}
|
|
||||||
labels={hourCounts.map((hc) => hourLabel(hc.hour))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hourCounts?.length === 0 && (
|
|
||||||
<Text>
|
|
||||||
No entries yet! Start recording sets to see your most active hours
|
|
||||||
of the day.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<View style={{ marginBottom: MARGIN }} />
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Most active days of the week"
|
|
||||||
show={showWeek}
|
|
||||||
setShow={setShowWeek}
|
|
||||||
onOk={() => setShowWeek(false)}
|
|
||||||
>
|
|
||||||
Are mondays your weak-spot? Find out here. This counts the # of sets you
|
|
||||||
tend to do based on the day of the week.
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Most active hours of the day"
|
|
||||||
show={showHour}
|
|
||||||
setShow={setShowHour}
|
|
||||||
onOk={() => setShowHour(false)}
|
|
||||||
>
|
|
||||||
If you find yourself giving up on the gym after 5pm, consider starting
|
|
||||||
earlier! Or vice-versa. This counts the # of sets you tend to do, based
|
|
||||||
on what time of day you began your workout.
|
|
||||||
</ConfirmDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
10
ListMenu.tsx
10
ListMenu.tsx
|
@ -49,19 +49,17 @@ export default function ListMenu({
|
||||||
<Menu
|
<Menu
|
||||||
visible={showMenu}
|
visible={showMenu}
|
||||||
onDismiss={() => setShowMenu(false)}
|
onDismiss={() => setShowMenu(false)}
|
||||||
anchor={
|
anchor={<IconButton onPress={() => setShowMenu(true)} icon="more-vert" />}
|
||||||
<IconButton onPress={() => setShowMenu(true)} icon="dots-vertical" />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Menu.Item leadingIcon="check-all" title="Select all" onPress={select} />
|
<Menu.Item leadingIcon="done-all" title="Select all" onPress={select} />
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leadingIcon="close"
|
leadingIcon="clear"
|
||||||
title="Clear"
|
title="Clear"
|
||||||
onPress={clear}
|
onPress={clear}
|
||||||
disabled={ids?.length === 0}
|
disabled={ids?.length === 0}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leadingIcon="pencil"
|
leadingIcon="edit"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
onPress={edit}
|
onPress={edit}
|
||||||
disabled={ids?.length === 0}
|
disabled={ids?.length === 0}
|
||||||
|
|
4
Page.tsx
4
Page.tsx
|
@ -22,8 +22,8 @@ export default function Page({
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={term}
|
value={term}
|
||||||
onChangeText={search}
|
onChangeText={search}
|
||||||
icon="magnify"
|
icon="search"
|
||||||
clearIcon="close"
|
clearIcon="clear"
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
{onAdd && <AppFab onPress={onAdd} />}
|
{onAdd && <AppFab onPress={onAdd} />}
|
||||||
|
|
25
PlanItem.tsx
25
PlanItem.tsx
|
@ -5,13 +5,14 @@ import {
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Text } from "react-native";
|
import { Text } from "react-native";
|
||||||
import { List, useTheme } from "react-native-paper";
|
import { List } from "react-native-paper";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
||||||
import { DAYS } from "./days";
|
|
||||||
import { setRepo } from "./db";
|
import { setRepo } from "./db";
|
||||||
import GymSet, { defaultSet } from "./gym-set";
|
import { defaultSet } from "./gym-set";
|
||||||
import { Plan } from "./plan";
|
import { Plan } from "./plan";
|
||||||
|
import { PlanPageParams } from "./plan-page-params";
|
||||||
|
import { DAYS } from "./time";
|
||||||
|
import useDark from "./use-dark";
|
||||||
|
|
||||||
export default function PlanItem({
|
export default function PlanItem({
|
||||||
item,
|
item,
|
||||||
|
@ -23,9 +24,9 @@ export default function PlanItem({
|
||||||
setIds: (value: number[]) => void;
|
setIds: (value: number[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [today, setToday] = useState<string>();
|
const [today, setToday] = useState<string>();
|
||||||
const { dark } = useTheme();
|
const dark = useDark();
|
||||||
const days = useMemo(() => item.days.split(","), [item.days]);
|
const days = useMemo(() => item.days.split(","), [item.days]);
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
@ -35,12 +36,12 @@ export default function PlanItem({
|
||||||
);
|
);
|
||||||
|
|
||||||
const start = useCallback(async () => {
|
const start = useCallback(async () => {
|
||||||
const exercise = item.exercises.split(",")[0];
|
const workout = item.workouts.split(",")[0];
|
||||||
let first: Partial<GymSet> = await setRepo.findOne({
|
let first = await setRepo.findOne({
|
||||||
where: { name: exercise },
|
where: { name: workout },
|
||||||
order: { created: "desc" },
|
order: { created: "desc" },
|
||||||
});
|
});
|
||||||
if (!first) first = { ...defaultSet, name: exercise };
|
if (!first) first = { ...defaultSet, name: workout };
|
||||||
delete first.id;
|
delete first.id;
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return navigation.navigate("StartPlan", { plan: item, first });
|
return navigation.navigate("StartPlan", { plan: item, first });
|
||||||
|
@ -84,8 +85,8 @@ export default function PlanItem({
|
||||||
);
|
);
|
||||||
|
|
||||||
const description = useMemo(
|
const description = useMemo(
|
||||||
() => (item.title ? currentDays : item.exercises.replace(/,/g, ", ")),
|
() => (item.title ? currentDays : item.workouts.replace(/,/g, ", ")),
|
||||||
[item.title, currentDays, item.exercises]
|
[item.title, currentDays, item.workouts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const backgroundColor = useMemo(() => {
|
const backgroundColor = useMemo(() => {
|
||||||
|
|
20
PlanList.tsx
20
PlanList.tsx
|
@ -7,28 +7,27 @@ import { useCallback, useState } from "react";
|
||||||
import { FlatList } from "react-native";
|
import { FlatList } from "react-native";
|
||||||
import { List } from "react-native-paper";
|
import { List } from "react-native-paper";
|
||||||
import { Like } from "typeorm";
|
import { Like } from "typeorm";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import { planRepo } from "./db";
|
import { planRepo } from "./db";
|
||||||
import DrawerHeader from "./DrawerHeader";
|
import DrawerHeader from "./DrawerHeader";
|
||||||
import ListMenu from "./ListMenu";
|
import ListMenu from "./ListMenu";
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
import { defaultPlan, Plan } from "./plan";
|
import { Plan } from "./plan";
|
||||||
|
import { PlanPageParams } from "./plan-page-params";
|
||||||
import PlanItem from "./PlanItem";
|
import PlanItem from "./PlanItem";
|
||||||
|
|
||||||
export default function PlanList() {
|
export default function PlanList() {
|
||||||
const [term, setTerm] = useState("");
|
const [term, setTerm] = useState("");
|
||||||
const [plans, setPlans] = useState<Plan[]>();
|
const [plans, setPlans] = useState<Plan[]>();
|
||||||
const [ids, setIds] = useState<number[]>([]);
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||||
|
|
||||||
const refresh = useCallback(async (value: string) => {
|
const refresh = useCallback(async (value: string) => {
|
||||||
console.log(`${PlanList.name}.refresh:`, value);
|
|
||||||
planRepo
|
planRepo
|
||||||
.find({
|
.find({
|
||||||
where: [
|
where: [
|
||||||
{ title: Like(`%${value.trim()}%`) },
|
{ title: Like(`%${value.trim()}%`) },
|
||||||
{ days: Like(`%${value.trim()}%`) },
|
{ days: Like(`%${value.trim()}%`) },
|
||||||
{ exercises: Like(`%${value.trim()}%`) },
|
{ workouts: Like(`%${value.trim()}%`) },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.then(setPlans);
|
.then(setPlans);
|
||||||
|
@ -37,8 +36,7 @@ export default function PlanList() {
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
refresh(term);
|
refresh(term);
|
||||||
// eslint-disable-next-line
|
}, [refresh, term])
|
||||||
}, [term])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const search = useCallback(
|
const search = useCallback(
|
||||||
|
@ -58,7 +56,7 @@ export default function PlanList() {
|
||||||
|
|
||||||
const onAdd = () =>
|
const onAdd = () =>
|
||||||
navigation.navigate("EditPlan", {
|
navigation.navigate("EditPlan", {
|
||||||
plan: defaultPlan,
|
plan: { title: "", days: "", workouts: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const edit = useCallback(async () => {
|
const edit = useCallback(async () => {
|
||||||
|
@ -87,10 +85,8 @@ export default function PlanList() {
|
||||||
}, [ids, refresh, term]);
|
}, [ids, refresh, term]);
|
||||||
|
|
||||||
const select = useCallback(() => {
|
const select = useCallback(() => {
|
||||||
if (!plans) return;
|
|
||||||
if (ids.length === plans.length) return setIds([]);
|
|
||||||
setIds(plans.map((plan) => plan.id));
|
setIds(plans.map((plan) => plan.id));
|
||||||
}, [plans, ids.length]);
|
}, [plans]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -108,7 +104,7 @@ export default function PlanList() {
|
||||||
{plans?.length === 0 ? (
|
{plans?.length === 0 ? (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No plans yet"
|
title="No plans yet"
|
||||||
description="A plan is a list of exercises for certain days."
|
description="A plan is a list of workouts for certain days."
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
21
PlanPage.tsx
Normal file
21
PlanPage.tsx
Normal file
|
@ -0,0 +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";
|
||||||
|
|
||||||
|
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.Navigator>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
import { ComponentProps } from "react";
|
|
||||||
import { Button, useTheme } from "react-native-paper";
|
|
||||||
|
|
||||||
type PrimaryButtonProps = Omit<Partial<ComponentProps<typeof Button>>, "mode">;
|
|
||||||
|
|
||||||
export default function PrimaryButton(props: PrimaryButtonProps) {
|
|
||||||
const { colors } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button mode="contained" textColor={colors.background} {...props}>
|
|
||||||
{props.children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
22
README.md
22
README.md
|
@ -40,29 +40,43 @@ cd android
|
||||||
./gradlew assembleRelease
|
./gradlew assembleRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
The apk file can be found at `android/app/build/outputs/apk/release/app-release.apk`
|
The apk file can be found at `android/app/build/outputs/apk/release/app-*-release.apk`
|
||||||
|
The APKs are separated by architecture, for example we have:
|
||||||
|
- `app-arm64-v8a-release.apk`
|
||||||
|
- `app-armeabi-v7a-release.apk`
|
||||||
|
- `app-x86_64-release.apk`
|
||||||
|
- `app-x86-release.apk`
|
||||||
|
|
||||||
|
Your phone is probably `app-arm64-v8a-release.apk`.
|
||||||
|
|
||||||
# Running in Development
|
# Running in Development
|
||||||
|
|
||||||
First ensure Node.js dependencies are installed:
|
First ensure Node.js dependencies are installed:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
Then start the metro server:
|
Then start the metro server:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then (in a separate terminal) run the `android` script:
|
Then (in a separate terminal) run the `android` script:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run android
|
yarn android
|
||||||
```
|
```
|
||||||
|
|
||||||
# Fdroid Metadata
|
# Fdroid Metadata
|
||||||
|
|
||||||
You can find the metadata yaml file in the fdroiddata repository:
|
You can find the metadata yaml file in the fdroiddata repository:
|
||||||
https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.massive.yml
|
https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.massive.yml
|
||||||
|
|
||||||
|
# Relevant Documentation
|
||||||
|
|
||||||
|
- Android https://developer.android.com/docs
|
||||||
|
- TypeScript https://www.typescriptlang.org/docs/
|
||||||
|
- JavaScript https://developer.mozilla.org/en-US/docs/Web/JavaScript
|
||||||
|
- SQLite https://sqlite.org/docs.html
|
||||||
|
|
57
Routes.tsx
Normal file
57
Routes.tsx
Normal file
|
@ -0,0 +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";
|
||||||
|
|
||||||
|
const Drawer = createDrawerNavigator<DrawerParamList>();
|
||||||
|
|
||||||
|
export default function Routes() {
|
||||||
|
const dark = useDark();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer.Navigator
|
||||||
|
screenOptions={{
|
||||||
|
headerTintColor: dark ? "white" : "black",
|
||||||
|
swipeEdgeWidth: 1000,
|
||||||
|
headerShown: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="Home"
|
||||||
|
component={HomePage}
|
||||||
|
options={{ drawerIcon: () => <IconButton icon="home" /> }}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="Plans"
|
||||||
|
component={PlanPage}
|
||||||
|
options={{ drawerIcon: () => <IconButton icon="event" /> }}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="Graphs"
|
||||||
|
component={GraphsPage}
|
||||||
|
options={{ drawerIcon: () => <IconButton icon="insights" /> }}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="Workouts"
|
||||||
|
component={WorkoutsPage}
|
||||||
|
options={{ drawerIcon: () => <IconButton icon="fitness-center" /> }}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="Timer"
|
||||||
|
component={TimerPage}
|
||||||
|
options={{ drawerIcon: () => <IconButton icon="access-time" /> }}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="Settings"
|
||||||
|
component={SettingsPage}
|
||||||
|
options={{ drawerIcon: () => <IconButton icon="settings" /> }}
|
||||||
|
/>
|
||||||
|
</Drawer.Navigator>
|
||||||
|
);
|
||||||
|
}
|
68
Select.tsx
68
Select.tsx
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback, useMemo, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { Pressable, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { IconButton, Menu, useTheme } from "react-native-paper";
|
import { Button, Menu, Subheading, useTheme } from "react-native-paper";
|
||||||
import AppInput from "./AppInput";
|
import { ITEM_PADDING } from "./constants";
|
||||||
|
|
||||||
export interface Item {
|
export interface Item {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -22,14 +22,13 @@ function Select({
|
||||||
}) {
|
}) {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
let menuButton: React.Ref<View> = null;
|
|
||||||
|
|
||||||
const selected = useMemo(
|
const selected = useMemo(
|
||||||
() => items.find((item) => item.value === value) || items[0],
|
() => items.find((item) => item.value === value) || items[0],
|
||||||
[items, value]
|
[items, value]
|
||||||
);
|
);
|
||||||
|
|
||||||
const press = useCallback(
|
const handlePress = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
setShow(false);
|
setShow(false);
|
||||||
|
@ -38,35 +37,38 @@ function Select({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<View
|
||||||
visible={show}
|
style={{
|
||||||
onDismiss={() => setShow(false)}
|
flexDirection: "row",
|
||||||
anchor={
|
alignItems: "center",
|
||||||
<View>
|
paddingLeft: ITEM_PADDING,
|
||||||
<Pressable onPress={() => setShow(true)}>
|
}}
|
||||||
<AppInput label={label} value={selected.label} editable={false} />
|
|
||||||
</Pressable>
|
|
||||||
<View
|
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
ref={menuButton}
|
|
||||||
icon="menu-down"
|
|
||||||
onPress={() => setShow(true)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{items.map((item) => (
|
{label && <Subheading style={{ width: 100 }}>{label}</Subheading>}
|
||||||
<Menu.Item
|
<Menu
|
||||||
title={item.label}
|
visible={show}
|
||||||
key={item.value}
|
onDismiss={() => setShow(false)}
|
||||||
onPress={() => press(item.value)}
|
anchor={
|
||||||
titleStyle={{ color: item.color || colors.onSurface }}
|
<Button
|
||||||
/>
|
onPress={() => setShow(true)}
|
||||||
))}
|
style={{
|
||||||
</Menu>
|
alignSelf: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selected?.label}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Menu.Item
|
||||||
|
titleStyle={{ color: item.color || colors.onSurface }}
|
||||||
|
key={item.value}
|
||||||
|
title={item.label}
|
||||||
|
onPress={() => handlePress(item.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
141
SetItem.tsx
141
SetItem.tsx
|
@ -1,91 +1,76 @@
|
||||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Image } from "react-native";
|
import { Image } from "react-native";
|
||||||
import { List, Text, useTheme } from "react-native-paper";
|
import { List, Text } from "react-native-paper";
|
||||||
import { StackParams } from "./AppStack";
|
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
||||||
import {
|
|
||||||
DARK_RIPPLE,
|
|
||||||
DARK_SUBDUED,
|
|
||||||
LIGHT_RIPPLE,
|
|
||||||
LIGHT_SUBDUED,
|
|
||||||
} from "./constants";
|
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
|
import { HomePageParams } from "./home-page-params";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
import useDark from "./use-dark";
|
||||||
|
|
||||||
const SetItem = React.memo(
|
export default function SetItem({
|
||||||
({
|
item,
|
||||||
item,
|
settings,
|
||||||
settings,
|
ids,
|
||||||
ids,
|
setIds,
|
||||||
setIds,
|
}: {
|
||||||
disablePress,
|
item: GymSet;
|
||||||
customBg,
|
settings: Settings;
|
||||||
}: {
|
ids: number[];
|
||||||
item: GymSet;
|
setIds: (value: number[]) => void;
|
||||||
settings: Settings;
|
}) {
|
||||||
ids: number[];
|
const dark = useDark();
|
||||||
setIds: (value: number[]) => void;
|
const navigation = useNavigation<NavigationProp<HomePageParams>>();
|
||||||
disablePress?: boolean;
|
|
||||||
customBg?: string;
|
|
||||||
}) => {
|
|
||||||
const { dark } = useTheme();
|
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
|
||||||
|
|
||||||
const longPress = useCallback(() => {
|
const longPress = useCallback(() => {
|
||||||
if (ids.length > 0) return;
|
if (ids.length > 0) return;
|
||||||
setIds([item.id]);
|
setIds([item.id]);
|
||||||
}, [ids.length, item.id, setIds]);
|
}, [ids.length, item.id, setIds]);
|
||||||
|
|
||||||
const press = useCallback(() => {
|
const press = useCallback(() => {
|
||||||
if (disablePress) return;
|
if (ids.length === 0) return navigation.navigate("EditSet", { set: item });
|
||||||
if (ids.length === 0)
|
const removing = ids.find((id) => id === item.id);
|
||||||
return navigation.navigate("EditSet", { set: item });
|
if (removing) setIds(ids.filter((id) => id !== item.id));
|
||||||
const removing = ids.find((id) => id === item.id);
|
else setIds([...ids, item.id]);
|
||||||
if (removing) setIds(ids.filter((id) => id !== item.id));
|
}, [ids, item, navigation, setIds]);
|
||||||
else setIds([...ids, item.id]);
|
|
||||||
}, [ids, item, navigation, setIds, disablePress]);
|
|
||||||
|
|
||||||
const backgroundColor = useMemo(() => {
|
const backgroundColor = useMemo(() => {
|
||||||
if (!ids.includes(item.id)) return;
|
if (!ids.includes(item.id)) return;
|
||||||
if (dark) return DARK_RIPPLE;
|
if (dark) return DARK_RIPPLE;
|
||||||
return LIGHT_RIPPLE;
|
return LIGHT_RIPPLE;
|
||||||
}, [dark, ids, item.id]);
|
}, [dark, ids, item.id]);
|
||||||
|
|
||||||
const image = useCallback(() => {
|
|
||||||
if (!settings.images || !item.image) return null;
|
|
||||||
return (
|
|
||||||
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
|
|
||||||
);
|
|
||||||
}, [item.image, settings.images]);
|
|
||||||
|
|
||||||
|
const left = useCallback(() => {
|
||||||
|
if (!settings.images || !item.image) return null;
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
|
||||||
onPress={press}
|
|
||||||
title={item.name}
|
|
||||||
description={
|
|
||||||
settings.showDate ? (
|
|
||||||
<Text style={{ color: dark ? DARK_SUBDUED : LIGHT_SUBDUED }}>
|
|
||||||
{format(new Date(item.created), settings.date || "Pp")}
|
|
||||||
</Text>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onLongPress={longPress}
|
|
||||||
style={{ backgroundColor: customBg || backgroundColor }}
|
|
||||||
left={image}
|
|
||||||
right={() => (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
alignSelf: "center",
|
|
||||||
color: dark ? DARK_SUBDUED : LIGHT_SUBDUED,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${item.reps} x ${item.weight}${item.unit || "kg"}`}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}, [item.image, settings.images]);
|
||||||
);
|
|
||||||
|
|
||||||
export default SetItem;
|
const right = useCallback(() => {
|
||||||
|
if (!settings.showDate) return null;
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
alignSelf: "center",
|
||||||
|
color: dark ? "#909090ff" : "#717171ff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{format(new Date(item.created), settings.date || "P")}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}, [settings.showDate, item.created, settings.date, dark]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
onPress={press}
|
||||||
|
title={item.name}
|
||||||
|
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
|
||||||
|
onLongPress={longPress}
|
||||||
|
style={{ backgroundColor }}
|
||||||
|
left={left}
|
||||||
|
right={right}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
155
SetList.tsx
155
SetList.tsx
|
@ -1,63 +1,96 @@
|
||||||
import {
|
import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
useFocusEffect,
|
RouteProp,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
|
useRoute,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { FlatList } from "react-native";
|
import { FlatList } from "react-native";
|
||||||
import { List } from "react-native-paper";
|
import { List } from "react-native-paper";
|
||||||
import { Like } from "typeorm";
|
import { Like } from "typeorm";
|
||||||
import { StackParams } from "./AppStack";
|
import { LIMIT } from "./constants";
|
||||||
|
import { getNow, setRepo, settingsRepo } from "./db";
|
||||||
import DrawerHeader from "./DrawerHeader";
|
import DrawerHeader from "./DrawerHeader";
|
||||||
|
import GymSet, { defaultSet } from "./gym-set";
|
||||||
|
import { HomePageParams } from "./home-page-params";
|
||||||
import ListMenu from "./ListMenu";
|
import ListMenu from "./ListMenu";
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
import SetItem from "./SetItem";
|
import SetItem from "./SetItem";
|
||||||
import { LIMIT } from "./constants";
|
|
||||||
import { getNow, setRepo, settingsRepo } from "./db";
|
|
||||||
import GymSet, { defaultSet } from "./gym-set";
|
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
|
||||||
export default function SetList() {
|
export default function SetList() {
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [sets, setSets] = useState<GymSet[]>();
|
const [sets, setSets] = useState<GymSet[]>([]);
|
||||||
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 [ids, setIds] = useState<number[]>([]);
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
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 refresh = async ({
|
||||||
async (value: string) => {
|
value,
|
||||||
const newSets = await setRepo.find({
|
take,
|
||||||
|
skip,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
take: number;
|
||||||
|
skip: number;
|
||||||
|
}) => {
|
||||||
|
setRefreshing(true);
|
||||||
|
const newSets = await setRepo
|
||||||
|
.find({
|
||||||
where: { name: Like(`%${value.trim()}%`), hidden: 0 as any },
|
where: { name: Like(`%${value.trim()}%`), hidden: 0 as any },
|
||||||
take: LIMIT,
|
take,
|
||||||
skip: 0,
|
skip,
|
||||||
order: { created: "DESC" },
|
order: { created: "DESC" },
|
||||||
});
|
})
|
||||||
setSets(newSets);
|
.finally(() => setRefreshing(false));
|
||||||
console.log(`${SetList.name}.reset:`, { value, offset });
|
console.log(`${SetList.name}.refresh:`, { value, take, offset });
|
||||||
setEnd(false);
|
setSets(newSets);
|
||||||
},
|
setEnd(false);
|
||||||
[offset]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useEffect(() => {
|
||||||
useCallback(() => {
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
console.log(`${SetList.name}.focus:`, { term });
|
refresh({
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
take: LIMIT,
|
||||||
reset(term);
|
value: "",
|
||||||
// eslint-disable-next-line
|
skip: 0,
|
||||||
}, [term])
|
});
|
||||||
);
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
}, []);
|
||||||
|
|
||||||
const search = (value: string) => {
|
const search = (value: string) => {
|
||||||
console.log(`${SetList.name}.search:`, value);
|
|
||||||
setTerm(value);
|
setTerm(value);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
reset(value);
|
refresh({
|
||||||
|
skip: 0,
|
||||||
|
take: LIMIT,
|
||||||
|
value,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!params) return;
|
||||||
|
console.log({ params });
|
||||||
|
if (params.search) search(params.search);
|
||||||
|
else if (params.refresh)
|
||||||
|
refresh({
|
||||||
|
skip: 0,
|
||||||
|
take: offset,
|
||||||
|
value: term,
|
||||||
|
});
|
||||||
|
else if (params.reset)
|
||||||
|
refresh({
|
||||||
|
skip: 0,
|
||||||
|
take: LIMIT,
|
||||||
|
value: term,
|
||||||
|
});
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item }: { item: GymSet }) => (
|
({ item }: { item: GymSet }) => (
|
||||||
<SetItem
|
<SetItem
|
||||||
|
@ -72,16 +105,18 @@ export default function SetList() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
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(`${SetList.name}.next:`, { offset, newOffset, term });
|
||||||
const newSets = await setRepo.find({
|
setRefreshing(true);
|
||||||
where: { name: Like(`%${term}%`), hidden: 0 as any },
|
const newSets = await setRepo
|
||||||
take: LIMIT,
|
.find({
|
||||||
skip: newOffset,
|
where: { name: Like(`%${term}%`), hidden: 0 as any },
|
||||||
order: { created: "DESC" },
|
take: LIMIT,
|
||||||
});
|
skip: newOffset,
|
||||||
|
order: { created: "DESC" },
|
||||||
|
})
|
||||||
|
.finally(() => setRefreshing(false));
|
||||||
if (newSets.length === 0) return setEnd(true);
|
if (newSets.length === 0) return setEnd(true);
|
||||||
if (!sets) return;
|
if (!sets) return;
|
||||||
const map = new Map<number, GymSet>();
|
const map = new Map<number, GymSet>();
|
||||||
|
@ -95,7 +130,7 @@ export default function SetList() {
|
||||||
|
|
||||||
const onAdd = useCallback(async () => {
|
const onAdd = useCallback(async () => {
|
||||||
const now = await getNow();
|
const now = await getNow();
|
||||||
let set: Partial<GymSet> = { ...sets[0] };
|
let set = sets[0];
|
||||||
if (!set) set = { ...defaultSet };
|
if (!set) set = { ...defaultSet };
|
||||||
set.created = now;
|
set.created = now;
|
||||||
delete set.id;
|
delete set.id;
|
||||||
|
@ -124,18 +159,20 @@ export default function SetList() {
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
setIds([]);
|
setIds([]);
|
||||||
await setRepo.delete(ids.length > 0 ? ids : {});
|
await setRepo.delete(ids.length > 0 ? ids : {});
|
||||||
return reset(term);
|
return refresh({
|
||||||
|
skip: 0,
|
||||||
|
take: LIMIT,
|
||||||
|
value: term,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const select = useCallback(() => {
|
const select = useCallback(() => {
|
||||||
if (!sets) return;
|
|
||||||
if (ids.length === sets.length) return setIds([]);
|
|
||||||
setIds(sets.map((set) => set.id));
|
setIds(sets.map((set) => set.id));
|
||||||
}, [sets, ids]);
|
}, [sets]);
|
||||||
|
|
||||||
const getContent = () => {
|
const content = useMemo(() => {
|
||||||
if (!settings || sets === undefined) return null;
|
if (!settings) return null;
|
||||||
if (sets.length === 0)
|
if (sets?.length === 0)
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No sets yet"
|
title="No sets yet"
|
||||||
|
@ -144,27 +181,25 @@ export default function SetList() {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={sets ?? []}
|
data={sets}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
onEndReached={next}
|
onEndReached={next}
|
||||||
onEndReachedThreshold={0.5}
|
refreshing={false}
|
||||||
refreshing={refreshing}
|
onRefresh={() =>
|
||||||
keyExtractor={(set) => set.id.toString()}
|
refresh({
|
||||||
onRefresh={() => {
|
skip: 0,
|
||||||
setOffset(0);
|
take: LIMIT,
|
||||||
setRefreshing(true);
|
value: term,
|
||||||
reset(term).finally(() => setRefreshing(false));
|
})
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}, [sets, settings, term]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader
|
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Home"}>
|
||||||
name={ids.length > 0 ? `${ids.length} selected` : "History"}
|
|
||||||
>
|
|
||||||
<ListMenu
|
<ListMenu
|
||||||
onClear={clear}
|
onClear={clear}
|
||||||
onCopy={copy}
|
onCopy={copy}
|
||||||
|
@ -176,7 +211,7 @@ export default function SetList() {
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
|
|
||||||
<Page onAdd={onAdd} term={term} search={search}>
|
<Page onAdd={onAdd} term={term} search={search}>
|
||||||
{getContent()}
|
{content}
|
||||||
</Page>
|
</Page>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
33
SettingButton.tsx
Normal file
33
SettingButton.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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;
|
||||||
|
}) {
|
||||||
|
if (label) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
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}>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
747
SettingsPage.tsx
747
SettingsPage.tsx
|
@ -2,25 +2,24 @@ import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { FlatList, NativeModules } from "react-native";
|
import { NativeModules, ScrollView } from "react-native";
|
||||||
import DocumentPicker from "react-native-document-picker";
|
import DocumentPicker from "react-native-document-picker";
|
||||||
import { Dirs, FileSystem } from "react-native-file-access";
|
import { Dirs, FileSystem } from "react-native-file-access";
|
||||||
import { Button } from "react-native-paper";
|
|
||||||
import { check, PERMISSIONS, request, RESULTS } from "react-native-permissions";
|
|
||||||
import AppInput from "./AppInput";
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import { PADDING } from "./constants";
|
import { MARGIN } from "./constants";
|
||||||
import { AppDataSource } from "./data-source";
|
import { AppDataSource } from "./data-source";
|
||||||
import { setRepo, settingsRepo } from "./db";
|
import { setRepo, settingsRepo } from "./db";
|
||||||
import { DrawerParams } from "./drawer-params";
|
import { DrawerParamList } from "./drawer-param-list";
|
||||||
import DrawerHeader from "./DrawerHeader";
|
import DrawerHeader from "./DrawerHeader";
|
||||||
|
import Input from "./input";
|
||||||
import { darkOptions, lightOptions, themeOptions } from "./options";
|
import { darkOptions, lightOptions, themeOptions } from "./options";
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
import Select from "./Select";
|
import Select from "./Select";
|
||||||
|
import SettingButton from "./SettingButton";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
import Switch from "./Switch";
|
import Switch from "./Switch";
|
||||||
import { toast } from "./toast";
|
import { toast } from "./toast";
|
||||||
import { useAppTheme } from "./use-theme";
|
import { useTheme } from "./use-theme";
|
||||||
|
|
||||||
const twelveHours = [
|
const twelveHours = [
|
||||||
"dd/LL/yyyy",
|
"dd/LL/yyyy",
|
||||||
|
@ -31,7 +30,6 @@ const twelveHours = [
|
||||||
"yyyy-MM-d, p",
|
"yyyy-MM-d, p",
|
||||||
"yyyy.MM.d",
|
"yyyy.MM.d",
|
||||||
];
|
];
|
||||||
|
|
||||||
const twentyFours = [
|
const twentyFours = [
|
||||||
"dd/LL/yyyy",
|
"dd/LL/yyyy",
|
||||||
"dd/LL/yyyy, k:m",
|
"dd/LL/yyyy, k:m",
|
||||||
|
@ -42,19 +40,13 @@ const twentyFours = [
|
||||||
"yyyy.MM.d",
|
"yyyy.MM.d",
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Item {
|
|
||||||
name: string;
|
|
||||||
renderItem: (name: string) => React.JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [ignoring, setIgnoring] = useState(false);
|
const [ignoring, setIgnoring] = useState(false);
|
||||||
const [term, setTerm] = useState("");
|
const [term, setTerm] = useState("");
|
||||||
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours);
|
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours);
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const { reset } = useNavigation<NavigationProp<DrawerParamList>>();
|
||||||
const { reset } = useNavigation<NavigationProp<DrawerParams>>();
|
|
||||||
|
|
||||||
const { watch, setValue } = useForm<Settings>({
|
const { watch, setValue } = useForm<Settings>({
|
||||||
defaultValues: () => settingsRepo.findOne({ where: {} }),
|
defaultValues: () => settingsRepo.findOne({ where: {} }),
|
||||||
|
@ -68,7 +60,7 @@ export default function SettingsPage() {
|
||||||
setLightColor,
|
setLightColor,
|
||||||
darkColor,
|
darkColor,
|
||||||
setDarkColor,
|
setDarkColor,
|
||||||
} = useAppTheme();
|
} = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
NativeModules.SettingsModule.ignoringBattery(setIgnoring);
|
NativeModules.SettingsModule.ignoringBattery(setIgnoring);
|
||||||
|
@ -79,12 +71,14 @@ export default function SettingsPage() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const backupString = useMemo(() => {
|
const update = useCallback((key: keyof Settings, value: unknown) => {
|
||||||
if (!settings.backupDir) return null;
|
return settingsRepo
|
||||||
console.log(settings.backupDir);
|
.createQueryBuilder()
|
||||||
const split = decodeURIComponent(settings.backupDir).split(":");
|
.update()
|
||||||
return split.pop();
|
.set({ [key]: value })
|
||||||
}, [settings.backupDir]);
|
.printSql()
|
||||||
|
.execute();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const soundString = useMemo(() => {
|
const soundString = useMemo(() => {
|
||||||
if (!settings.sound) return null;
|
if (!settings.sound) return null;
|
||||||
|
@ -92,6 +86,181 @@ export default function SettingsPage() {
|
||||||
return split.pop();
|
return split.pop();
|
||||||
}, [settings.sound]);
|
}, [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]);
|
||||||
|
|
||||||
|
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" },
|
||||||
|
],
|
||||||
|
[settings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filter = useCallback(
|
||||||
|
({ name }) => name.toLowerCase().includes(term.toLowerCase()),
|
||||||
|
[term]
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeBoolean = useCallback(
|
||||||
|
async (key: keyof Settings, value: boolean) => {
|
||||||
|
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":
|
||||||
|
if (value) {
|
||||||
|
const result = await DocumentPicker.pickDirectory();
|
||||||
|
toast("Backup database daily.");
|
||||||
|
NativeModules.BackupModule.start(result.uri);
|
||||||
|
} else {
|
||||||
|
toast("Stopped backing up daily");
|
||||||
|
NativeModules.BackupModule.stop();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ignoring, setValue, update]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSwitch = useCallback(
|
||||||
|
(item: Input<boolean>) => (
|
||||||
|
<Switch
|
||||||
|
key={item.name}
|
||||||
|
value={item.value}
|
||||||
|
onChange={(value) => changeBoolean(item.key, value)}
|
||||||
|
title={item.name}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[changeBoolean]
|
||||||
|
);
|
||||||
|
|
||||||
|
const switchesMarkup = useMemo(
|
||||||
|
() => switches.filter(filter).map((s) => renderSwitch(s)),
|
||||||
|
[filter, switches, renderSwitch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeString = useCallback(
|
||||||
|
async (key: keyof Settings, value: string) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[update, setTheme, setDarkColor, setLightColor, setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selects: Input<string>[] = useMemo(() => {
|
||||||
|
const today = new Date();
|
||||||
|
return [
|
||||||
|
{ name: "Theme", value: theme, items: themeOptions, key: "theme" },
|
||||||
|
{
|
||||||
|
name: "Dark color",
|
||||||
|
value: darkColor,
|
||||||
|
items: lightOptions,
|
||||||
|
key: "darkColor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Light color",
|
||||||
|
value: lightColor,
|
||||||
|
items: darkOptions,
|
||||||
|
key: "lightColor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Date format",
|
||||||
|
value: settings.date,
|
||||||
|
items: formatOptions.map((option) => ({
|
||||||
|
label: format(today, option),
|
||||||
|
value: option,
|
||||||
|
})),
|
||||||
|
key: "date",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [settings, darkColor, formatOptions, theme, lightColor]);
|
||||||
|
|
||||||
|
const renderSelect = useCallback(
|
||||||
|
(input: Input<string>) => (
|
||||||
|
<Select
|
||||||
|
key={input.name}
|
||||||
|
value={input.value}
|
||||||
|
onChange={(value) => changeString(input.key, value)}
|
||||||
|
label={input.name}
|
||||||
|
items={input.items}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[changeString]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectsMarkup = useMemo(
|
||||||
|
() => selects.filter(filter).map(renderSelect),
|
||||||
|
[filter, selects, renderSelect]
|
||||||
|
);
|
||||||
|
|
||||||
const confirmDelete = useCallback(async () => {
|
const confirmDelete = useCallback(async () => {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
await AppDataSource.dropDatabase();
|
await AppDataSource.dropDatabase();
|
||||||
|
@ -102,509 +271,63 @@ export default function SettingsPage() {
|
||||||
|
|
||||||
const confirmImport = useCallback(async () => {
|
const confirmImport = useCallback(async () => {
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
await FileSystem.cp(
|
|
||||||
Dirs.DatabaseDir + "/massive.db",
|
|
||||||
Dirs.DatabaseDir + "/massive-backup.db"
|
|
||||||
);
|
|
||||||
await AppDataSource.destroy();
|
await AppDataSource.destroy();
|
||||||
const file = await DocumentPicker.pickSingle();
|
const file = await DocumentPicker.pickSingle();
|
||||||
await FileSystem.cp(file.uri, Dirs.DatabaseDir + "/massive.db");
|
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]);
|
||||||
|
|
||||||
try {
|
const exportDatabase = useCallback(async () => {
|
||||||
await AppDataSource.initialize();
|
const path = Dirs.DatabaseDir + "/massive.db";
|
||||||
} catch (e) {
|
await FileSystem.cpExternal(path, "massive.db", "downloads");
|
||||||
setError(e.toString());
|
toast("Database exported. Check downloads.");
|
||||||
await FileSystem.cp(
|
}, []);
|
||||||
Dirs.DatabaseDir + "/massive-backup.db",
|
|
||||||
Dirs.DatabaseDir + "/massive.db"
|
|
||||||
);
|
|
||||||
await AppDataSource.initialize();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await setRepo.update({}, { image: null });
|
const buttons = useMemo(
|
||||||
await settingsRepo.update({}, { sound: null, backup: false });
|
() => [
|
||||||
reset({ index: 0, routes: [{ name: "Settings" }] });
|
{
|
||||||
}, [reset]);
|
name: soundString || "Default",
|
||||||
|
onPress: changeSound,
|
||||||
|
label: "Alarm sound",
|
||||||
|
},
|
||||||
|
{ name: "Export database", onPress: exportDatabase },
|
||||||
|
{ name: "Import database", onPress: () => setImporting(true) },
|
||||||
|
{ name: "Delete database", onPress: () => setDeleting(true) },
|
||||||
|
],
|
||||||
|
[changeSound, exportDatabase, soundString]
|
||||||
|
);
|
||||||
|
|
||||||
const today = new Date();
|
const buttonsMarkup = useMemo(
|
||||||
|
() =>
|
||||||
const data: Item[] = [
|
buttons
|
||||||
{
|
.filter(filter)
|
||||||
name: "Start up page",
|
.map((button) => <SettingButton {...button} key={button.name} />),
|
||||||
renderItem: (name: string) => (
|
[buttons, filter]
|
||||||
<Select
|
);
|
||||||
label={name}
|
|
||||||
items={[
|
|
||||||
{ label: "History", value: "History" },
|
|
||||||
{ label: "Exercises", value: "Exercises" },
|
|
||||||
{ label: "Plans", value: "Plans" },
|
|
||||||
{ label: "Graphs", value: "Graphs" },
|
|
||||||
{ label: "Timer", value: "Timer" },
|
|
||||||
{ label: "Weight", value: "Weight" },
|
|
||||||
{ label: "Insights", value: "Insights" },
|
|
||||||
{ label: "Settings", value: "Settings" },
|
|
||||||
]}
|
|
||||||
value={settings.startup}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("startup", value);
|
|
||||||
await settingsRepo.update({}, { startup: value });
|
|
||||||
toast(`App will always start on ${value}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Theme",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Select
|
|
||||||
label={name}
|
|
||||||
items={themeOptions}
|
|
||||||
value={theme}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("theme", value);
|
|
||||||
setTheme(value);
|
|
||||||
await settingsRepo.update({}, { theme: value });
|
|
||||||
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.");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Dark color",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Select
|
|
||||||
label={name}
|
|
||||||
items={lightOptions}
|
|
||||||
value={darkColor}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("darkColor", value);
|
|
||||||
setDarkColor(value);
|
|
||||||
await settingsRepo.update({}, { darkColor: value });
|
|
||||||
toast("Set primary color for dark mode.");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Light color",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Select
|
|
||||||
label={name}
|
|
||||||
items={darkOptions}
|
|
||||||
value={lightColor}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("lightColor", value);
|
|
||||||
setLightColor(value);
|
|
||||||
await settingsRepo.update({}, { lightColor: value });
|
|
||||||
toast("Set primary color for light mode.");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Date format",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Select
|
|
||||||
label={name}
|
|
||||||
items={formatOptions.map((option) => ({
|
|
||||||
label: format(today, option),
|
|
||||||
value: option,
|
|
||||||
}))}
|
|
||||||
value={settings.date}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("date", value);
|
|
||||||
await settingsRepo.update({}, { date: value });
|
|
||||||
toast("Changed date format.");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Auto convert",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Select
|
|
||||||
label={name}
|
|
||||||
items={[
|
|
||||||
{ label: "Off", value: "" },
|
|
||||||
{ label: "Kilograms", value: "kg" },
|
|
||||||
{ label: "Pounds", value: "lb" },
|
|
||||||
{ label: "Stone", value: "stone" },
|
|
||||||
]}
|
|
||||||
value={settings.autoConvert}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("autoConvert", value);
|
|
||||||
await settingsRepo.update({}, { autoConvert: value });
|
|
||||||
if (value) toast(`Sets now automatically convert to ${value}`);
|
|
||||||
else toast("Stopped automatically converting sets.");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vibration duration (ms)",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<AppInput
|
|
||||||
value={settings.duration?.toString() ?? "300"}
|
|
||||||
label={name}
|
|
||||||
onChangeText={(value) => setValue("duration", Number(value))}
|
|
||||||
onSubmitEditing={async (e) => {
|
|
||||||
const value = Number(e.nativeEvent.text);
|
|
||||||
setValue("duration", value);
|
|
||||||
await settingsRepo.update({}, { duration: value });
|
|
||||||
toast("Changed duration of alarm vibrations.");
|
|
||||||
}}
|
|
||||||
keyboardType="numeric"
|
|
||||||
blurOnSubmit
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Default sets",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<AppInput
|
|
||||||
value={settings.defaultSets?.toString() ?? "3"}
|
|
||||||
label={name}
|
|
||||||
onChangeText={(value) => setValue("defaultSets", Number(value))}
|
|
||||||
onSubmitEditing={async (e) => {
|
|
||||||
const value = Number(e.nativeEvent.text);
|
|
||||||
setValue("defaultSets", value);
|
|
||||||
await settingsRepo.update({}, { defaultSets: value });
|
|
||||||
toast(`New exercises now have ${value} sets by default.`);
|
|
||||||
}}
|
|
||||||
keyboardType="numeric"
|
|
||||||
blurOnSubmit
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Default minutes",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<AppInput
|
|
||||||
value={settings.defaultMinutes?.toString() ?? "3"}
|
|
||||||
label={name}
|
|
||||||
onChangeText={(value) => setValue("defaultMinutes", Number(value))}
|
|
||||||
onSubmitEditing={async (e) => {
|
|
||||||
const value = Number(e.nativeEvent.text);
|
|
||||||
setValue("defaultMinutes", value);
|
|
||||||
await settingsRepo.update({}, { defaultMinutes: value });
|
|
||||||
toast(`New exercises now wait ${value} minutes by default.`);
|
|
||||||
}}
|
|
||||||
keyboardType="numeric"
|
|
||||||
blurOnSubmit
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Default seconds",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<AppInput
|
|
||||||
value={settings.defaultSeconds?.toString() ?? "30"}
|
|
||||||
label={name}
|
|
||||||
onChangeText={(value) => setValue("defaultSeconds", Number(value))}
|
|
||||||
onSubmitEditing={async (e) => {
|
|
||||||
const value = Number(e.nativeEvent.text);
|
|
||||||
setValue("defaultSeconds", value);
|
|
||||||
await settingsRepo.update({}, { defaultSeconds: value });
|
|
||||||
toast(`New exercises now wait ${value} seconds by default.`);
|
|
||||||
}}
|
|
||||||
keyboardType="numeric"
|
|
||||||
blurOnSubmit
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Rest timers",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.alarm}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("alarm", value);
|
|
||||||
if (value && !ignoring) {
|
|
||||||
NativeModules.SettingsModule.ignoreBattery();
|
|
||||||
}
|
|
||||||
await settingsRepo.update({}, { alarm: value });
|
|
||||||
if (value) toast("Timers will now run after each set.");
|
|
||||||
else toast("Stopped timers running after each set.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vibrate",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.vibrate}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("vibrate", value);
|
|
||||||
await settingsRepo.update({}, { vibrate: value });
|
|
||||||
if (value) toast("Timers will now run after each set.");
|
|
||||||
else toast("Stopped timers running after each set.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Disable sound",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.noSound}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("noSound", value);
|
|
||||||
const silentPath = Dirs.DocumentDir + "/silent.mp3";
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
await FileSystem.writeFile(silentPath, "");
|
|
||||||
setValue("sound", silentPath);
|
|
||||||
await settingsRepo.update(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
sound: silentPath,
|
|
||||||
noSound: value,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (!value && settings.sound === silentPath) {
|
|
||||||
setValue("sound", null);
|
|
||||||
await settingsRepo.update({}, { sound: null, noSound: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value) toast("Alarms will no longer make a sound.");
|
|
||||||
else toast("Enabled sound for alarms.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Notifications",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.notify}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("notify", value);
|
|
||||||
await settingsRepo.update({}, { notify: value });
|
|
||||||
if (value) toast("Show notifications for new records.");
|
|
||||||
else toast("Stopped notifications for new records.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Show images",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.images}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("images", value);
|
|
||||||
await settingsRepo.update({}, { images: value });
|
|
||||||
if (value) toast("Show images for sets.");
|
|
||||||
else toast("Hid images for sets.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Show unit",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.showUnit}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("showUnit", value);
|
|
||||||
await settingsRepo.update({}, { showUnit: value });
|
|
||||||
if (value) toast("Show option to select unit for sets.");
|
|
||||||
else toast("Hid unit option for sets.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Show steps",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.steps}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("steps", value);
|
|
||||||
await settingsRepo.update({}, { steps: value });
|
|
||||||
if (value) toast("Show steps for exercises.");
|
|
||||||
else toast("Hid steps for exercises.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Show date",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.showDate}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("showDate", value);
|
|
||||||
await settingsRepo.update({}, { showDate: value });
|
|
||||||
if (value) toast("Show date for sets.");
|
|
||||||
else toast("Hid date on sets.");
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Automatic backup",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Switch
|
|
||||||
value={settings.backup}
|
|
||||||
onChange={async (value) => {
|
|
||||||
setValue("backup", value);
|
|
||||||
await settingsRepo.update({}, { backup: value });
|
|
||||||
if (value) {
|
|
||||||
const result = await DocumentPicker.pickDirectory();
|
|
||||||
setValue("backupDir", result.uri);
|
|
||||||
await settingsRepo.update({}, { backupDir: result.uri });
|
|
||||||
console.log(`${SettingsPage.name}.backup:`, { result });
|
|
||||||
toast("Backup database daily.");
|
|
||||||
NativeModules.BackupModule.start(result.uri);
|
|
||||||
} else {
|
|
||||||
toast("Stopped backing up daily");
|
|
||||||
NativeModules.BackupModule.stop();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `Backup directory: ${backupString || "Downloads"}`,
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Button
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
onPress={async () => {
|
|
||||||
const result = await DocumentPicker.pickDirectory();
|
|
||||||
setValue("backupDir", result.uri);
|
|
||||||
await settingsRepo.update({}, { backupDir: result.uri });
|
|
||||||
toast("Changed backup directory.");
|
|
||||||
if (!settings.backup) return;
|
|
||||||
NativeModules.BackupModule.stop();
|
|
||||||
NativeModules.BackupModule.start(result.uri);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `Alarm sound: ${soundString || "Default"}`,
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Button
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
onPress={async () => {
|
|
||||||
const { fileCopyUri } = await DocumentPicker.pickSingle({
|
|
||||||
type: DocumentPicker.types.audio,
|
|
||||||
copyTo: "documentDirectory",
|
|
||||||
});
|
|
||||||
if (!fileCopyUri) return;
|
|
||||||
setValue("sound", fileCopyUri);
|
|
||||||
await settingsRepo.update({}, { sound: fileCopyUri });
|
|
||||||
toast("Sound will play after rest timers.");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Export database",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Button
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
onPress={async () => {
|
|
||||||
const result = await check(
|
|
||||||
PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE
|
|
||||||
);
|
|
||||||
if (result === RESULTS.DENIED || result === RESULTS.BLOCKED) {
|
|
||||||
await request(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
|
|
||||||
}
|
|
||||||
const path = Dirs.DatabaseDir + "/massive.db";
|
|
||||||
await FileSystem.cpExternal(path, "massive.db", "downloads");
|
|
||||||
toast("Database exported. Check downloads.");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Export sets as CSV",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Button
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
onPress={async () => {
|
|
||||||
const result = await check(
|
|
||||||
PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE
|
|
||||||
);
|
|
||||||
if (result === RESULTS.DENIED || result === RESULTS.BLOCKED) {
|
|
||||||
await request(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
|
|
||||||
}
|
|
||||||
await NativeModules.BackupModule.exportToCSV();
|
|
||||||
toast("Exported sets as CSV.");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Import database",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Button
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
onPress={() => setImporting(true)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Delete database",
|
|
||||||
renderItem: (name: string) => (
|
|
||||||
<Button
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
onPress={() => setDeleting(true)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Settings" />
|
<DrawerHeader name="Settings" />
|
||||||
|
|
||||||
<Page term={term} search={setTerm}>
|
<Page term={term} search={setTerm} style={{ flexGrow: 1 }}>
|
||||||
<FlatList
|
<ScrollView style={{ marginTop: MARGIN, flex: 1 }}>
|
||||||
data={data.filter((item) =>
|
{switchesMarkup}
|
||||||
item.name.toLowerCase().includes(term.toLowerCase())
|
{selectsMarkup}
|
||||||
)}
|
{buttonsMarkup}
|
||||||
renderItem={({ item }) => item.renderItem(item.name)}
|
</ScrollView>
|
||||||
style={{ flex: 1, paddingTop: PADDING }}
|
|
||||||
/>
|
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Failed to import database"
|
|
||||||
onOk={() => setError("")}
|
|
||||||
setShow={() => setError("")}
|
|
||||||
show={!!error}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Are you sure?"
|
title="Are you sure?"
|
||||||
onOk={confirmImport}
|
onOk={confirmImport}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default function StackHeader({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<IconButton icon="arrow-left" onPress={navigation.goBack} />
|
<IconButton icon="arrow-back" onPress={navigation.goBack} />
|
||||||
<Appbar.Content title={title} />
|
<Appbar.Content title={title} />
|
||||||
{children}
|
{children}
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
|
|
152
StartPlan.tsx
152
StartPlan.tsx
|
@ -7,27 +7,23 @@ import {
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { FlatList, NativeModules, TextInput, View } from "react-native";
|
import { FlatList, NativeModules, TextInput, View } from "react-native";
|
||||||
import { IconButton, ProgressBar } from "react-native-paper";
|
import { Button, IconButton, ProgressBar } from "react-native-paper";
|
||||||
import { PERMISSIONS, RESULTS, check, request } from "react-native-permissions";
|
|
||||||
import AppInput from "./AppInput";
|
import AppInput from "./AppInput";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import PrimaryButton from "./PrimaryButton";
|
|
||||||
import Select from "./Select";
|
|
||||||
import StackHeader from "./StackHeader";
|
|
||||||
import StartPlanItem from "./StartPlanItem";
|
|
||||||
import { getBestSet } from "./best.service";
|
import { getBestSet } from "./best.service";
|
||||||
import { PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import { convert } from "./conversions";
|
|
||||||
import CountMany from "./count-many";
|
import CountMany from "./count-many";
|
||||||
import { AppDataSource } from "./data-source";
|
import { AppDataSource } from "./data-source";
|
||||||
import { getNow, setRepo, settingsRepo } from "./db";
|
import { getNow, setRepo, settingsRepo } from "./db";
|
||||||
import { fixNumeric } from "./fix-numeric";
|
import { fixNumeric } from "./fix-numeric";
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
|
import { PlanPageParams } from "./plan-page-params";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
import StackHeader from "./StackHeader";
|
||||||
|
import StartPlanItem from "./StartPlanItem";
|
||||||
import { toast } from "./toast";
|
import { toast } from "./toast";
|
||||||
|
|
||||||
export default function StartPlan() {
|
export default function StartPlan() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "StartPlan">>();
|
const { params } = useRoute<RouteProp<PlanPageParams, "StartPlan">>();
|
||||||
const [reps, setReps] = useState(params.first?.reps.toString() || "0");
|
const [reps, setReps] = useState(params.first?.reps.toString() || "0");
|
||||||
const [weight, setWeight] = useState(params.first?.weight.toString() || "0");
|
const [weight, setWeight] = useState(params.first?.weight.toString() || "0");
|
||||||
const [unit, setUnit] = useState<string>(params.first?.unit || "kg");
|
const [unit, setUnit] = useState<string>(params.first?.unit || "kg");
|
||||||
|
@ -36,8 +32,9 @@ export default function StartPlan() {
|
||||||
const [counts, setCounts] = useState<CountMany[]>();
|
const [counts, setCounts] = useState<CountMany[]>();
|
||||||
const weightRef = useRef<TextInput>(null);
|
const weightRef = useRef<TextInput>(null);
|
||||||
const repsRef = useRef<TextInput>(null);
|
const repsRef = useRef<TextInput>(null);
|
||||||
const exercises = useMemo(() => params.plan.exercises.split(","), [params]);
|
const unitRef = useRef<TextInput>(null);
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
const workouts = useMemo(() => params.plan.workouts.split(","), [params]);
|
||||||
|
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||||
|
|
||||||
const [selection, setSelection] = useState({
|
const [selection, setSelection] = useState({
|
||||||
start: 0,
|
start: 0,
|
||||||
|
@ -45,33 +42,33 @@ export default function StartPlan() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
const questions = exercises
|
const questions = workouts
|
||||||
.map((exercise, index) => `('${exercise}',${index})`)
|
.map((workout, index) => `('${workout}',${index})`)
|
||||||
.join(",");
|
.join(",");
|
||||||
const select = `
|
const select = `
|
||||||
SELECT exercises.name, COUNT(sets.id) as total, sets.sets
|
SELECT workouts.name, COUNT(sets.id) as total, sets.sets
|
||||||
FROM (select 0 as name, 0 as sequence union values ${questions}) as exercises
|
FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts
|
||||||
LEFT JOIN sets ON sets.name = exercises.name
|
LEFT JOIN sets ON sets.name = workouts.name
|
||||||
AND sets.created LIKE STRFTIME('%Y-%m-%d%%', 'now', 'localtime')
|
AND sets.created LIKE STRFTIME('%Y-%m-%d%%', 'now', 'localtime')
|
||||||
AND NOT sets.hidden
|
AND NOT sets.hidden
|
||||||
GROUP BY exercises.name
|
GROUP BY workouts.name
|
||||||
ORDER BY exercises.sequence
|
ORDER BY workouts.sequence
|
||||||
LIMIT -1
|
LIMIT -1
|
||||||
OFFSET 1
|
OFFSET 1
|
||||||
`;
|
`;
|
||||||
const newCounts = await AppDataSource.manager.query(select);
|
const newCounts = await AppDataSource.manager.query(select);
|
||||||
console.log(`${StartPlan.name}.focus:`, { newCounts });
|
console.log(`${StartPlan.name}.focus:`, { newCounts });
|
||||||
setCounts(newCounts);
|
setCounts(newCounts);
|
||||||
}, [exercises]);
|
}, [workouts]);
|
||||||
|
|
||||||
const select = useCallback(
|
const select = useCallback(
|
||||||
async (index: number, newCounts?: CountMany[]) => {
|
async (index: number, newCounts?: CountMany[]) => {
|
||||||
setSelected(index);
|
setSelected(index);
|
||||||
if (!counts && !newCounts) return;
|
if (!counts && !newCounts) return;
|
||||||
const exercise = counts ? counts[index] : newCounts[index];
|
const workout = counts ? counts[index] : newCounts[index];
|
||||||
console.log(`${StartPlan.name}.next:`, { exercise });
|
console.log(`${StartPlan.name}.next:`, { workout });
|
||||||
const last = await setRepo.findOne({
|
const last = await setRepo.findOne({
|
||||||
where: { name: exercise.name },
|
where: { name: workout.name },
|
||||||
order: { created: "desc" },
|
order: { created: "desc" },
|
||||||
});
|
});
|
||||||
console.log({ last });
|
console.log({ last });
|
||||||
|
@ -89,28 +86,19 @@ export default function StartPlan() {
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
refresh();
|
refresh();
|
||||||
// eslint-disable-next-line
|
}, [refresh])
|
||||||
}, [])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const now = await getNow();
|
const now = await getNow();
|
||||||
const exercise = counts[selected];
|
const workout = counts[selected];
|
||||||
const best = await getBestSet(exercise.name);
|
const best = await getBestSet(workout.name);
|
||||||
delete best.id;
|
delete best.id;
|
||||||
|
|
||||||
let newWeight = Number(weight);
|
|
||||||
let newUnit = unit;
|
|
||||||
if (settings.autoConvert && unit !== settings.autoConvert) {
|
|
||||||
newUnit = settings.autoConvert;
|
|
||||||
newWeight = convert(newWeight, unit, settings.autoConvert);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSet: GymSet = {
|
const newSet: GymSet = {
|
||||||
...best,
|
...best,
|
||||||
weight: newWeight,
|
weight: +weight,
|
||||||
reps: Number(reps),
|
reps: +reps,
|
||||||
unit: newUnit,
|
unit,
|
||||||
created: now,
|
created: now,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
};
|
};
|
||||||
|
@ -118,35 +106,35 @@ export default function StartPlan() {
|
||||||
await refresh();
|
await refresh();
|
||||||
if (
|
if (
|
||||||
settings.notify &&
|
settings.notify &&
|
||||||
(Number(weight) > best.weight ||
|
(+weight > best.weight || (+reps > best.reps && +weight === best.weight))
|
||||||
(Number(reps) > best.reps && Number(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;
|
if (!settings.alarm) return;
|
||||||
const milliseconds =
|
const milliseconds =
|
||||||
Number(best.minutes) * 60 * 1000 + Number(best.seconds) * 1000;
|
Number(best.minutes) * 60 * 1000 + Number(best.seconds) * 1000;
|
||||||
const canNotify = await check(PERMISSIONS.ANDROID.POST_NOTIFICATIONS);
|
|
||||||
if (canNotify === RESULTS.DENIED)
|
|
||||||
await request(PERMISSIONS.ANDROID.POST_NOTIFICATIONS);
|
|
||||||
NativeModules.AlarmModule.timer(milliseconds);
|
NativeModules.AlarmModule.timer(milliseconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader
|
<StackHeader title={params.plan.days.replace(/,/g, ", ")}>
|
||||||
title={params.plan.title || params.plan.days.replace(/,/g, ", ")}
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onPress={() => navigation.navigate("EditPlan", { plan: params.plan })}
|
onPress={() => navigation.navigate("EditPlan", { plan: params.plan })}
|
||||||
icon="pencil"
|
icon="edit"
|
||||||
/>
|
/>
|
||||||
</StackHeader>
|
</StackHeader>
|
||||||
<View style={{ padding: PADDING, flex: 1, flexDirection: "column" }}>
|
<View style={{ padding: PADDING, flex: 1, flexDirection: "column" }}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<View>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: MARGIN,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AppInput
|
<AppInput
|
||||||
label="Reps"
|
label="Reps"
|
||||||
|
style={{ flex: 1 }}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={reps}
|
value={reps}
|
||||||
onChangeText={(newReps) => {
|
onChangeText={(newReps) => {
|
||||||
|
@ -160,23 +148,25 @@ export default function StartPlan() {
|
||||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
||||||
innerRef={repsRef}
|
innerRef={repsRef}
|
||||||
/>
|
/>
|
||||||
<View
|
<IconButton
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
icon="add"
|
||||||
>
|
onPress={() => setReps((Number(reps) + 1).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="plus"
|
<IconButton
|
||||||
onPress={() => setReps((Number(reps) + 1).toString())}
|
icon="remove"
|
||||||
/>
|
onPress={() => setReps((Number(reps) - 1).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="minus"
|
|
||||||
onPress={() => setReps((Number(reps) - 1).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: MARGIN,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AppInput
|
<AppInput
|
||||||
label="Weight"
|
label="Weight"
|
||||||
|
style={{ flex: 1 }}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={weight}
|
value={weight}
|
||||||
onChangeText={(newWeight) => {
|
onChangeText={(newWeight) => {
|
||||||
|
@ -189,36 +179,28 @@ export default function StartPlan() {
|
||||||
innerRef={weightRef}
|
innerRef={weightRef}
|
||||||
blurOnSubmit
|
blurOnSubmit
|
||||||
/>
|
/>
|
||||||
<View
|
<IconButton
|
||||||
style={{ position: "absolute", right: 0, flexDirection: "row" }}
|
icon="add"
|
||||||
>
|
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="plus"
|
<IconButton
|
||||||
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
icon="remove"
|
||||||
/>
|
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
||||||
<IconButton
|
/>
|
||||||
icon="minus"
|
|
||||||
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{settings?.showUnit && (
|
{settings?.showUnit && (
|
||||||
<Select
|
<AppInput
|
||||||
value={unit}
|
autoCapitalize="none"
|
||||||
onChange={setUnit}
|
|
||||||
items={[
|
|
||||||
{ label: "kg", value: "kg" },
|
|
||||||
{ label: "lb", value: "lb" },
|
|
||||||
{ label: "stone", value: "stone" },
|
|
||||||
]}
|
|
||||||
label="Unit"
|
label="Unit"
|
||||||
|
value={unit}
|
||||||
|
onChangeText={setUnit}
|
||||||
|
innerRef={unitRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{counts !== undefined && (
|
{counts && (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={counts}
|
data={counts}
|
||||||
keyExtractor={(count) => count.name}
|
|
||||||
renderItem={(props) => (
|
renderItem={(props) => (
|
||||||
<View>
|
<View>
|
||||||
<StartPlanItem
|
<StartPlanItem
|
||||||
|
@ -235,9 +217,9 @@ export default function StartPlan() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<PrimaryButton icon="content-save" onPress={handleSubmit}>
|
<Button mode="outlined" icon="save" onPress={handleSubmit}>
|
||||||
Save
|
Save
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import {
|
import { GestureResponderEvent, ListRenderItemInfo, View } from "react-native";
|
||||||
GestureResponderEvent,
|
|
||||||
ListRenderItemInfo,
|
|
||||||
NativeModules,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { List, Menu, RadioButton, useTheme } from "react-native-paper";
|
import { List, Menu, RadioButton, useTheme } from "react-native-paper";
|
||||||
import { Like } from "typeorm";
|
import { Like } from "typeorm";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import CountMany from "./count-many";
|
import CountMany from "./count-many";
|
||||||
import { getNow, setRepo } from "./db";
|
import { getNow, setRepo } from "./db";
|
||||||
|
import { HomePageParams } from "./home-page-params";
|
||||||
|
import { PlanPageParams } from "./plan-page-params";
|
||||||
import { toast } from "./toast";
|
import { toast } from "./toast";
|
||||||
|
|
||||||
interface Props extends ListRenderItemInfo<CountMany> {
|
interface Props extends ListRenderItemInfo<CountMany> {
|
||||||
|
@ -24,8 +20,9 @@ export default function StartPlanItem(props: Props) {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [anchor, setAnchor] = useState({ x: 0, y: 0 });
|
const [anchor, setAnchor] = useState({ x: 0, y: 0 });
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const { navigate: stackNavigate } =
|
const { navigate } = useNavigation<NavigationProp<PlanPageParams>>();
|
||||||
useNavigation<NavigationProp<StackParams>>();
|
const { navigate: navigateHome } =
|
||||||
|
useNavigation<NavigationProp<HomePageParams>>();
|
||||||
|
|
||||||
const undo = useCallback(async () => {
|
const undo = useCallback(async () => {
|
||||||
const now = await getNow();
|
const now = await getNow();
|
||||||
|
@ -41,7 +38,6 @@ export default function StartPlanItem(props: Props) {
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
if (!first) return toast("Nothing to undo.");
|
if (!first) return toast("Nothing to undo.");
|
||||||
await setRepo.delete(first.id);
|
await setRepo.delete(first.id);
|
||||||
NativeModules.AlarmModule.stop();
|
|
||||||
onUndo();
|
onUndo();
|
||||||
}, [setShowMenu, onUndo, item.name]);
|
}, [setShowMenu, onUndo, item.name]);
|
||||||
|
|
||||||
|
@ -66,18 +62,13 @@ export default function StartPlanItem(props: Props) {
|
||||||
});
|
});
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
if (!first) return toast("Nothing to edit.");
|
if (!first) return toast("Nothing to edit.");
|
||||||
stackNavigate("EditSet", { set: first });
|
navigate("EditSet", { set: first });
|
||||||
}, [item.name, stackNavigate]);
|
}, [item.name, navigate]);
|
||||||
|
|
||||||
const view = useCallback(() => {
|
const view = () => {
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
stackNavigate("ViewSetList", { name: item.name });
|
navigateHome("Sets", { search: item.name });
|
||||||
}, [item.name, stackNavigate]);
|
};
|
||||||
|
|
||||||
const graph = useCallback(() => {
|
|
||||||
setShowMenu(false);
|
|
||||||
stackNavigate("ViewGraph", { name: item.name });
|
|
||||||
}, [item.name, stackNavigate]);
|
|
||||||
|
|
||||||
const left = useCallback(
|
const left = useCallback(
|
||||||
() => (
|
() => (
|
||||||
|
@ -106,18 +97,13 @@ export default function StartPlanItem(props: Props) {
|
||||||
visible={showMenu}
|
visible={showMenu}
|
||||||
onDismiss={() => setShowMenu(false)}
|
onDismiss={() => setShowMenu(false)}
|
||||||
>
|
>
|
||||||
<Menu.Item leadingIcon="eye-outline" onPress={view} title="Peek" />
|
<Menu.Item leadingIcon="visibility" onPress={view} title="View" />
|
||||||
<Menu.Item
|
<Menu.Item leadingIcon="edit" onPress={edit} title="Edit" />
|
||||||
leadingIcon="chart-bell-curve-cumulative"
|
|
||||||
onPress={graph}
|
|
||||||
title="Graph"
|
|
||||||
/>
|
|
||||||
<Menu.Item leadingIcon="pencil" onPress={edit} title="Edit" />
|
|
||||||
<Menu.Item leadingIcon="undo" onPress={undo} title="Undo" />
|
<Menu.Item leadingIcon="undo" onPress={undo} title="Undo" />
|
||||||
</Menu>
|
</Menu>
|
||||||
</View>
|
</View>
|
||||||
),
|
),
|
||||||
[anchor, showMenu, edit, undo, view, graph]
|
[anchor, showMenu, edit, undo]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -29,6 +29,10 @@ function Switch({
|
||||||
style={{ marginRight: MARGIN }}
|
style={{ marginRight: MARGIN }}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
|
trackColor={{
|
||||||
|
true: colors.primary + "80",
|
||||||
|
false: colors.surfaceDisabled,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text>{title}</Text>
|
<Text>{title}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { NativeModules, View } from "react-native";
|
import { Dimensions, NativeModules, View } from "react-native";
|
||||||
import { FAB, Text, useTheme } from "react-native-paper";
|
import { Button, Text, useTheme } from "react-native-paper";
|
||||||
|
import { ProgressCircle } from "react-native-svg-charts";
|
||||||
import AppFab from "./AppFab";
|
import AppFab from "./AppFab";
|
||||||
import DrawerHeader from "./DrawerHeader";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
import { settingsRepo } from "./db";
|
import { settingsRepo } from "./db";
|
||||||
|
import DrawerHeader from "./DrawerHeader";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
import useTimer from "./use-timer";
|
import useTimer from "./use-timer";
|
||||||
|
|
||||||
|
@ -14,7 +16,7 @@ export interface TickEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TimerPage() {
|
export default function TimerPage() {
|
||||||
const { minutes, seconds, update } = useTimer();
|
const { minutes, seconds } = useTimer();
|
||||||
const [settings, setSettings] = useState<Settings>();
|
const [settings, setSettings] = useState<Settings>();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
@ -26,42 +28,47 @@ export default function TimerPage() {
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
NativeModules.AlarmModule.stop();
|
NativeModules.AlarmModule.stop();
|
||||||
update();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const add = async () => {
|
const add = async () => {
|
||||||
console.log(`${TimerPage.name}.add:`, settings);
|
console.log(`${TimerPage.name}.add:`, settings);
|
||||||
NativeModules.AlarmModule.add();
|
NativeModules.AlarmModule.add();
|
||||||
update();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const progress = useMemo(() => {
|
||||||
|
return (Number(minutes) * 60 + Number(seconds)) / 210;
|
||||||
|
}, [minutes, seconds]);
|
||||||
|
|
||||||
|
const left = useMemo(() => {
|
||||||
|
return Dimensions.get("screen").width * 0.5 - 60;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Timer" />
|
<DrawerHeader name="Timer" />
|
||||||
<View
|
<View style={{ flexGrow: 1, padding: PADDING }}>
|
||||||
style={{
|
<View
|
||||||
flex: 1,
|
style={{
|
||||||
flexGrow: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: 70, position: "absolute" }}>
|
<Text style={{ fontSize: 70, position: "absolute" }}>
|
||||||
{minutes}:{seconds}
|
{minutes}:{seconds}
|
||||||
</Text>
|
</Text>
|
||||||
|
<ProgressCircle
|
||||||
|
style={{ height: 300, width: 300, marginBottom: MARGIN }}
|
||||||
|
progress={progress}
|
||||||
|
strokeWidth={10}
|
||||||
|
progressColor={colors.primary}
|
||||||
|
backgroundColor={colors.primary + "80"}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<Button onPress={add} style={{ position: "absolute", top: "82%", left }}>
|
||||||
<FAB
|
Add 1 min
|
||||||
icon="plus"
|
</Button>
|
||||||
onPress={add}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 20,
|
|
||||||
bottom: 20,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
}}
|
|
||||||
color={colors.background}
|
|
||||||
/>
|
|
||||||
<AppFab icon="stop" onPress={stop} />
|
<AppFab icon="stop" onPress={stop} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ProgressBar } from "react-native-paper";
|
|
||||||
import { TickEvent } from "./TimerPage";
|
|
||||||
import { emitter } from "./emitter";
|
|
||||||
|
|
||||||
export default function TimerProgress() {
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const description = emitter.addListener(
|
|
||||||
"tick",
|
|
||||||
({ minutes, seconds }: TickEvent) => {
|
|
||||||
setProgress((Number(minutes) * 60 + Number(seconds)) / 210);
|
|
||||||
console.log({ minutes, seconds });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return description.remove;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (progress === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProgressBar
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
bottom: 0,
|
|
||||||
height: 5,
|
|
||||||
}}
|
|
||||||
progress={progress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ScrollView, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { FileSystem } from "react-native-file-access";
|
import { FileSystem } from "react-native-file-access";
|
||||||
import { IconButton, List } from "react-native-paper";
|
import { IconButton, List } from "react-native-paper";
|
||||||
import Share from "react-native-share";
|
import Share from "react-native-share";
|
||||||
import { captureScreen } from "react-native-view-shot";
|
import { captureScreen } from "react-native-view-shot";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import Chart from "./Chart";
|
import Chart from "./Chart";
|
||||||
|
import { GraphsPageParams } from "./GraphsPage";
|
||||||
import Select from "./Select";
|
import Select from "./Select";
|
||||||
import StackHeader from "./StackHeader";
|
import StackHeader from "./StackHeader";
|
||||||
import { PADDING } from "./constants";
|
import { PADDING } from "./constants";
|
||||||
|
@ -18,37 +18,31 @@ import { Periods } from "./periods";
|
||||||
import Volume from "./volume";
|
import Volume from "./volume";
|
||||||
|
|
||||||
export default function ViewGraph() {
|
export default function ViewGraph() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "ViewGraph">>();
|
const { params } = useRoute<RouteProp<GraphsPageParams, "ViewGraph">>();
|
||||||
const [weights, setWeights] = useState<GymSet[]>();
|
const [weights, setWeights] = useState<GymSet[]>();
|
||||||
const [volumes, setVolumes] = useState<Volume[]>();
|
const [volumes, setVolumes] = useState<Volume[]>();
|
||||||
const [metric, setMetric] = useState(Metrics.OneRepMax);
|
const [metric, setMetric] = useState(Metrics.Weight);
|
||||||
const [period, setPeriod] = useState(Periods.Monthly);
|
const [period, setPeriod] = useState(Periods.Monthly);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let difference = "-7 days";
|
let difference = "-7 days";
|
||||||
if (period === Periods.Monthly) difference = "-1 months";
|
if (period === Periods.Monthly) difference = "-1 months";
|
||||||
else if (period === Periods.Yearly) difference = "-1 years";
|
else if (period === Periods.Yearly) difference = "-1 years";
|
||||||
else if (period === Periods.TwoMonths) difference = "-2 months";
|
|
||||||
else if (period === Periods.ThreeMonths) difference = "-3 months";
|
|
||||||
else if (period === Periods.SixMonths) difference = "-6 months";
|
|
||||||
|
|
||||||
let group = "%Y-%m-%d";
|
let group = "%Y-%m-%d";
|
||||||
if (period === Periods.Yearly) group = "%Y-%m";
|
if (period === Periods.Yearly) group = "%Y-%m";
|
||||||
|
|
||||||
const builder = setRepo
|
const builder = setRepo
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.select("STRFTIME('%Y-%m-%d', created)", "created")
|
.select("STRFTIME('%Y-%m-%d', created)", "created")
|
||||||
.addSelect("unit")
|
.addSelect("unit")
|
||||||
.where("name = :name", { name: params.name })
|
.where("name = :name", { name: params.best.name })
|
||||||
.andWhere("NOT hidden")
|
.andWhere("NOT hidden")
|
||||||
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
||||||
difference,
|
difference,
|
||||||
})
|
})
|
||||||
.groupBy("name")
|
.groupBy("name")
|
||||||
.addGroupBy(`STRFTIME('${group}', created)`);
|
.addGroupBy(`STRFTIME('${group}', created)`);
|
||||||
|
|
||||||
switch (metric) {
|
switch (metric) {
|
||||||
case Metrics.Best:
|
case Metrics.Weight:
|
||||||
builder
|
builder
|
||||||
.addSelect("ROUND(MAX(weight), 2)", "weight")
|
.addSelect("ROUND(MAX(weight), 2)", "weight")
|
||||||
.getRawMany()
|
.getRawMany()
|
||||||
|
@ -73,40 +67,48 @@ export default function ViewGraph() {
|
||||||
setWeights(newWeights);
|
setWeights(newWeights);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [params.name, metric, period]);
|
}, [params.best.name, metric, period]);
|
||||||
|
|
||||||
const weightChart = useMemo(() => {
|
const charts = useMemo(() => {
|
||||||
if (weights === undefined) return null;
|
if (
|
||||||
|
(metric === Metrics.Volume && volumes?.length === 0) ||
|
||||||
if (weights.length === 0) return <List.Item title="No data yet." />;
|
(metric === Metrics.Weight && weights?.length === 0) ||
|
||||||
|
(metric === Metrics.OneRepMax && weights?.length === 0)
|
||||||
|
) {
|
||||||
|
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"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
xData={weights}
|
||||||
|
xFormat={(_value, index) =>
|
||||||
|
format(new Date(weights[index].created), "d/M")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chart
|
<Chart
|
||||||
data={weights.map((set) => set.weight)}
|
yData={weights?.map((set) => set.weight) || []}
|
||||||
labels={weights.map((set) =>
|
yFormat={(value) => `${value}${weights?.[0].unit}`}
|
||||||
format(new Date(set.created), "yyyy-MM-d")
|
xData={weights || []}
|
||||||
)}
|
xFormat={(_value, index) =>
|
||||||
|
format(new Date(weights?.[index].created), "d/M")
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [weights]);
|
}, [volumes, weights, metric]);
|
||||||
|
|
||||||
const volumeChart = useMemo(() => {
|
|
||||||
if (volumes === undefined) return null;
|
|
||||||
if (volumes.length === 0) return <List.Item title="No data yet." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
data={volumes.map((volume) => volume.value)}
|
|
||||||
labels={volumes.map((volume) =>
|
|
||||||
format(new Date(volume.created), "yyyy-MM-d")
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [volumes]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader title={params.name}>
|
<StackHeader title={params.best.name}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
captureScreen().then(async (uri) => {
|
captureScreen().then(async (uri) => {
|
||||||
|
@ -121,13 +123,16 @@ export default function ViewGraph() {
|
||||||
icon="share"
|
icon="share"
|
||||||
/>
|
/>
|
||||||
</StackHeader>
|
</StackHeader>
|
||||||
<ScrollView style={{ padding: PADDING }}>
|
<View style={{ padding: PADDING }}>
|
||||||
<Select
|
<Select
|
||||||
label="Metric"
|
label="Metric"
|
||||||
items={[
|
items={[
|
||||||
{ value: Metrics.OneRepMax, label: Metrics.OneRepMax },
|
|
||||||
{ label: Metrics.Best, value: Metrics.Best },
|
|
||||||
{ value: Metrics.Volume, label: Metrics.Volume },
|
{ value: Metrics.Volume, label: Metrics.Volume },
|
||||||
|
{ value: Metrics.OneRepMax, label: Metrics.OneRepMax },
|
||||||
|
{
|
||||||
|
label: Metrics.Weight,
|
||||||
|
value: Metrics.Weight,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setMetric(value as Metrics)}
|
onChange={(value) => setMetric(value as Metrics)}
|
||||||
value={metric}
|
value={metric}
|
||||||
|
@ -137,18 +142,13 @@ export default function ViewGraph() {
|
||||||
items={[
|
items={[
|
||||||
{ value: Periods.Weekly, label: Periods.Weekly },
|
{ value: Periods.Weekly, label: Periods.Weekly },
|
||||||
{ value: Periods.Monthly, label: Periods.Monthly },
|
{ value: Periods.Monthly, label: Periods.Monthly },
|
||||||
{ value: Periods.TwoMonths, label: Periods.TwoMonths },
|
|
||||||
{ value: Periods.ThreeMonths, label: Periods.ThreeMonths },
|
|
||||||
{ value: Periods.SixMonths, label: Periods.SixMonths },
|
|
||||||
{ value: Periods.Yearly, label: Periods.Yearly },
|
{ value: Periods.Yearly, label: Periods.Yearly },
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setPeriod(value as Periods)}
|
onChange={(value) => setPeriod(value as Periods)}
|
||||||
value={period}
|
value={period}
|
||||||
/>
|
/>
|
||||||
<View style={{ paddingTop: PADDING }}>
|
{charts}
|
||||||
{metric === Metrics.Volume ? volumeChart : weightChart}
|
</View>
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { FlatList } from "react-native";
|
|
||||||
import { List, Text, useTheme } from "react-native-paper";
|
|
||||||
import { Like } from "typeorm";
|
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import StackHeader from "./StackHeader";
|
|
||||||
import { LIMIT } from "./constants";
|
|
||||||
import { setRepo, settingsRepo } from "./db";
|
|
||||||
import GymSet from "./gym-set";
|
|
||||||
import Settings from "./settings";
|
|
||||||
|
|
||||||
interface ColorSet extends GymSet {
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ViewSetList() {
|
|
||||||
const [sets, setSets] = useState<ColorSet[]>();
|
|
||||||
const [settings, setSettings] = useState<Settings>();
|
|
||||||
const { colors } = useTheme();
|
|
||||||
const { params } = useRoute<RouteProp<StackParams, "ViewSetList">>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
|
||||||
|
|
||||||
const reset = async () => {
|
|
||||||
const newSets: ColorSet[] = await setRepo.find({
|
|
||||||
where: { name: Like(`%${params.name}%`), hidden: 0 as any },
|
|
||||||
take: LIMIT,
|
|
||||||
skip: 0,
|
|
||||||
order: { created: "DESC" },
|
|
||||||
});
|
|
||||||
|
|
||||||
let prevDate = null;
|
|
||||||
const elevate = colors.elevation.level2;
|
|
||||||
const transparent = colors.elevation.level0;
|
|
||||||
let color = elevate;
|
|
||||||
|
|
||||||
for (let i = 0; i < newSets.length; i++) {
|
|
||||||
let currDate = new Date(newSets[i].created).toDateString();
|
|
||||||
if (currDate !== prevDate)
|
|
||||||
color = color === elevate ? transparent : elevate;
|
|
||||||
newSets[i].color = color;
|
|
||||||
prevDate = currDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSets(newSets);
|
|
||||||
};
|
|
||||||
|
|
||||||
reset();
|
|
||||||
}, [params.name, colors]);
|
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: ColorSet; index: number }) => (
|
|
||||||
<List.Item
|
|
||||||
title={format(new Date(item.created), settings.date || "Pp")}
|
|
||||||
style={{ backgroundColor: item.color }}
|
|
||||||
right={() => (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
alignSelf: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${item.reps} x ${item.weight}${item.unit || "kg"}`}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getContent = () => {
|
|
||||||
if (!settings) return null;
|
|
||||||
if (sets?.length === 0)
|
|
||||||
return (
|
|
||||||
<List.Item
|
|
||||||
title="No sets yet"
|
|
||||||
description="A set is a group of repetitions. E.g. 8 reps of Squats."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={sets ?? []}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
renderItem={renderItem}
|
|
||||||
keyExtractor={(set) => set.id?.toString()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StackHeader title={params.name} />
|
|
||||||
|
|
||||||
{getContent()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { ScrollView, 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.TwoMonths);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let difference = "-7 days";
|
|
||||||
if (period === Periods.Monthly) difference = "-1 months";
|
|
||||||
else if (period === Periods.TwoMonths) difference = "-2 months";
|
|
||||||
else if (period === Periods.ThreeMonths) difference = "-3 months";
|
|
||||||
else if (period === Periods.SixMonths) difference = "-6 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("AVG(value) as value")
|
|
||||||
.addSelect("unit")
|
|
||||||
.where("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
|
||||||
difference,
|
|
||||||
})
|
|
||||||
.groupBy(`STRFTIME('${group}', created)`)
|
|
||||||
.getRawMany()
|
|
||||||
.then(setWeights);
|
|
||||||
}, [period]);
|
|
||||||
|
|
||||||
const charts = useMemo(() => {
|
|
||||||
if (!weights) return;
|
|
||||||
if (weights?.length === 0) {
|
|
||||||
return <List.Item title="No data yet." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
data={weights.map((set) => set.value)}
|
|
||||||
labels={weights.map((weight) =>
|
|
||||||
format(new Date(weight.created), "yyyy-MM-d")
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [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>
|
|
||||||
<ScrollView style={{ padding: PADDING }}>
|
|
||||||
<Select
|
|
||||||
label="Period"
|
|
||||||
items={[
|
|
||||||
{ value: Periods.Weekly, label: Periods.Weekly },
|
|
||||||
{ value: Periods.Monthly, label: Periods.Monthly },
|
|
||||||
{ value: Periods.TwoMonths, label: Periods.TwoMonths },
|
|
||||||
{ value: Periods.ThreeMonths, label: Periods.ThreeMonths },
|
|
||||||
{ value: Periods.SixMonths, label: Periods.SixMonths },
|
|
||||||
{ value: Periods.Yearly, label: Periods.Yearly },
|
|
||||||
]}
|
|
||||||
onChange={(value) => setPeriod(value as Periods)}
|
|
||||||
value={period}
|
|
||||||
/>
|
|
||||||
{charts}
|
|
||||||
</ScrollView>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useCallback, useMemo } from "react";
|
|
||||||
import { List, Text } from "react-native-paper";
|
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import Settings from "./settings";
|
|
||||||
import Weight from "./weight";
|
|
||||||
|
|
||||||
export default function WeightItem({
|
|
||||||
item,
|
|
||||||
settings,
|
|
||||||
}: {
|
|
||||||
item: Weight;
|
|
||||||
settings: Settings;
|
|
||||||
}) {
|
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
|
||||||
|
|
||||||
const press = useCallback(() => {
|
|
||||||
navigation.navigate("EditWeight", { weight: item });
|
|
||||||
}, [item, navigation]);
|
|
||||||
|
|
||||||
const today = useMemo(() => {
|
|
||||||
const now = new Date();
|
|
||||||
const created = new Date(item.created);
|
|
||||||
return (
|
|
||||||
now.getFullYear() === created.getFullYear() &&
|
|
||||||
now.getMonth() === created.getMonth() &&
|
|
||||||
now.getDate() === created.getDate()
|
|
||||||
);
|
|
||||||
}, [item.created]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<List.Item
|
|
||||||
onPress={press}
|
|
||||||
title={`${item.value}${item.unit || "kg"}`}
|
|
||||||
right={() => (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
alignSelf: "center",
|
|
||||||
textDecorationLine: today ? "underline" : "none",
|
|
||||||
fontWeight: today ? "bold" : "normal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{format(new Date(item.created), settings.date || "Pp")}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
150
WeightList.tsx
150
WeightList.tsx
|
@ -1,150 +0,0 @@
|
||||||
import {
|
|
||||||
NavigationProp,
|
|
||||||
useFocusEffect,
|
|
||||||
useNavigation,
|
|
||||||
} from "@react-navigation/native";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { FlatList } from "react-native";
|
|
||||||
import { IconButton, List } from "react-native-paper";
|
|
||||||
import { Like } from "typeorm";
|
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import { LIMIT } from "./constants";
|
|
||||||
import { getNow, settingsRepo, weightRepo } from "./db";
|
|
||||||
import DrawerHeader from "./DrawerHeader";
|
|
||||||
import Page from "./Page";
|
|
||||||
import Settings from "./settings";
|
|
||||||
import { default as Weight, defaultWeight } from "./weight";
|
|
||||||
import WeightItem from "./WeightItem";
|
|
||||||
|
|
||||||
export default function WeightList() {
|
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [weights, setWeights] = useState<Weight[]>();
|
|
||||||
const [offset, setOffset] = useState(0);
|
|
||||||
const [end, setEnd] = useState(false);
|
|
||||||
const [settings, setSettings] = useState<Settings>();
|
|
||||||
const { navigate } = useNavigation<NavigationProp<StackParams>>();
|
|
||||||
const [term, setTerm] = useState("");
|
|
||||||
|
|
||||||
const reset = useCallback(
|
|
||||||
async (value: string) => {
|
|
||||||
const newWeights = await weightRepo.find({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
value: isNaN(Number(term)) ? undefined : Number(term),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
created: Like(`%${term}%`),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
take: LIMIT,
|
|
||||||
skip: 0,
|
|
||||||
order: { created: "DESC" },
|
|
||||||
});
|
|
||||||
console.log(`${WeightList.name}.reset:`, { value, offset });
|
|
||||||
setWeights(newWeights);
|
|
||||||
setEnd(false);
|
|
||||||
},
|
|
||||||
[offset, term]
|
|
||||||
);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
|
||||||
reset(term);
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [term])
|
|
||||||
);
|
|
||||||
|
|
||||||
const search = (value: string) => {
|
|
||||||
console.log(`${WeightList.name}.search:`, value);
|
|
||||||
setTerm(value);
|
|
||||||
setOffset(0);
|
|
||||||
reset(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
|
||||||
({ item }: { item: Weight }) => (
|
|
||||||
<WeightItem settings={settings} item={item} key={item.id} />
|
|
||||||
),
|
|
||||||
[settings]
|
|
||||||
);
|
|
||||||
|
|
||||||
const next = async () => {
|
|
||||||
console.log({ end, refreshing });
|
|
||||||
if (end || refreshing) return;
|
|
||||||
const newOffset = offset + LIMIT;
|
|
||||||
console.log(`${WeightList.name}.next:`, { offset, newOffset, term });
|
|
||||||
const newWeights = await weightRepo.find({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
value: Number(term),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
created: Like(`%${term}%`),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
take: LIMIT,
|
|
||||||
skip: newOffset,
|
|
||||||
order: { created: "DESC" },
|
|
||||||
});
|
|
||||||
if (newWeights.length === 0) return setEnd(true);
|
|
||||||
if (!weights) return;
|
|
||||||
const map = new Map<number, Weight>();
|
|
||||||
for (const weight of weights) map.set(weight.id, weight);
|
|
||||||
for (const weight of newWeights) map.set(weight.id, weight);
|
|
||||||
const unique = Array.from(map.values());
|
|
||||||
setWeights(unique);
|
|
||||||
if (newWeights.length < LIMIT) return setEnd(true);
|
|
||||||
setOffset(newOffset);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAdd = useCallback(async () => {
|
|
||||||
const now = await getNow();
|
|
||||||
let weight: Partial<Weight> = { ...weights[0] };
|
|
||||||
if (!weight) weight = { ...defaultWeight };
|
|
||||||
weight.created = now;
|
|
||||||
delete weight.id;
|
|
||||||
navigate("EditWeight", { weight });
|
|
||||||
}, [navigate, weights]);
|
|
||||||
|
|
||||||
const getContent = () => {
|
|
||||||
if (!settings) return null;
|
|
||||||
if (weights?.length === 0)
|
|
||||||
return (
|
|
||||||
<List.Item
|
|
||||||
title="No sets yet"
|
|
||||||
description="A set is a group of repetitions. E.g. 8 reps of Squats."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={weights ?? []}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
renderItem={renderItem}
|
|
||||||
onEndReached={next}
|
|
||||||
refreshing={refreshing}
|
|
||||||
keyExtractor={(set) => set.id?.toString()}
|
|
||||||
onRefresh={() => {
|
|
||||||
setOffset(0);
|
|
||||||
setRefreshing(true);
|
|
||||||
reset(term).finally(() => setRefreshing(false));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DrawerHeader name="Weight">
|
|
||||||
<IconButton
|
|
||||||
onPress={() => navigate("ViewWeightGraph")}
|
|
||||||
icon="chart-bell-curve-cumulative"
|
|
||||||
/>
|
|
||||||
</DrawerHeader>
|
|
||||||
|
|
||||||
<Page onAdd={onAdd} term={term} search={search}>
|
|
||||||
{getContent()}
|
|
||||||
</Page>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,33 +1,31 @@
|
||||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Image } from "react-native";
|
import { Image } from "react-native";
|
||||||
import { List, useTheme } from "react-native-paper";
|
import { List } from "react-native-paper";
|
||||||
import { StackParams } from "./AppStack";
|
import { DARK_RIPPLE } from "./constants";
|
||||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
import { LIGHT_RIPPLE } from "./constants";
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
|
import useDark from "./use-dark";
|
||||||
|
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||||
|
|
||||||
export default function ExerciseItem({
|
export default function WorkoutItem({
|
||||||
item,
|
item,
|
||||||
setNames,
|
setNames,
|
||||||
names,
|
names,
|
||||||
images,
|
images,
|
||||||
alarm,
|
|
||||||
}: {
|
}: {
|
||||||
item: GymSet;
|
item: GymSet;
|
||||||
images: boolean;
|
images: boolean;
|
||||||
setNames: (value: string[]) => void;
|
setNames: (value: string[]) => void;
|
||||||
names: string[];
|
names: string[];
|
||||||
alarm: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
||||||
const { dark } = useTheme();
|
const dark = useDark();
|
||||||
|
|
||||||
const description = useMemo(() => {
|
const description = useMemo(() => {
|
||||||
const seconds = item.seconds?.toString().padStart(2, "0");
|
const seconds = item.seconds?.toString().padStart(2, "0");
|
||||||
const time = ` x ${item.minutes || 0}:${seconds}`;
|
return `${item.sets} x ${item.minutes || 0}:${seconds}`;
|
||||||
if (alarm) return item.sets.toString() + time;
|
}, [item.sets, item.minutes, item.seconds]);
|
||||||
return item.sets.toString();
|
|
||||||
}, [item.sets, item.minutes, item.seconds, alarm]);
|
|
||||||
|
|
||||||
const left = useCallback(() => {
|
const left = useCallback(() => {
|
||||||
if (!images || !item.image) return null;
|
if (!images || !item.image) return null;
|
||||||
|
@ -50,7 +48,7 @@ export default function ExerciseItem({
|
||||||
const press = () => {
|
const press = () => {
|
||||||
console.log({ names });
|
console.log({ names });
|
||||||
if (names.length === 0)
|
if (names.length === 0)
|
||||||
return navigation.navigate("EditExercise", { gymSet: item });
|
return navigation.navigate("EditWorkout", { gymSet: item });
|
||||||
const removing = names.find((name) => name === item.name);
|
const removing = names.find((name) => name === item.name);
|
||||||
if (removing) setNames(names.filter((name) => name !== item.name));
|
if (removing) setNames(names.filter((name) => name !== item.name));
|
||||||
else setNames([...names, item.name]);
|
else setNames([...names, item.name]);
|
|
@ -1,36 +1,37 @@
|
||||||
import {
|
import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
|
RouteProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
|
useRoute,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { useCallback, 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 { List } from "react-native-paper";
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { StackParams } from "./AppStack";
|
|
||||||
import { LIMIT } from "./constants";
|
import { LIMIT } from "./constants";
|
||||||
import { setRepo, settingsRepo } from "./db";
|
import { setRepo, settingsRepo } from "./db";
|
||||||
import DrawerHeader from "./DrawerHeader";
|
import DrawerHeader from "./DrawerHeader";
|
||||||
import ExerciseItem from "./ExerciseItem";
|
import GymSet from "./gym-set";
|
||||||
import GymSet, { defaultSet } from "./gym-set";
|
|
||||||
import ListMenu from "./ListMenu";
|
import ListMenu from "./ListMenu";
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
import SetList from "./SetList";
|
import SetList from "./SetList";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
import WorkoutItem from "./WorkoutItem";
|
||||||
|
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||||
|
|
||||||
export default function ExerciseList() {
|
export default function WorkoutList() {
|
||||||
const [exercises, setExercises] = useState<GymSet[]>();
|
const [workouts, setWorkouts] = useState<GymSet[]>();
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const [term, setTerm] = useState("");
|
const [term, setTerm] = useState("");
|
||||||
const [end, setEnd] = useState(false);
|
const [end, setEnd] = useState(false);
|
||||||
const [settings, setSettings] = useState<Settings>();
|
const [settings, setSettings] = useState<Settings>();
|
||||||
const [names, setNames] = useState<string[]>([]);
|
const [names, setNames] = useState<string[]>([]);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
||||||
const navigation = useNavigation<NavigationProp<StackParams>>();
|
const { params } = useRoute<RouteProp<WorkoutsPageParams, "WorkoutList">>();
|
||||||
|
|
||||||
const reset = async (value: string) => {
|
const refresh = useCallback(async (value: string) => {
|
||||||
console.log(`${ExerciseList.name}.reset`, value);
|
const newWorkouts = await setRepo
|
||||||
const newExercises = await setRepo
|
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.select()
|
.select()
|
||||||
.where("name LIKE :name", { name: `%${value.trim()}%` })
|
.where("name LIKE :name", { name: `%${value.trim()}%` })
|
||||||
|
@ -38,43 +39,43 @@ export default function ExerciseList() {
|
||||||
.orderBy("name")
|
.orderBy("name")
|
||||||
.limit(LIMIT)
|
.limit(LIMIT)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
console.log(`${WorkoutList.name}`, { newWorkout: newWorkouts[0] });
|
||||||
|
setWorkouts(newWorkouts);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
console.log(`${ExerciseList.name}.reset`, { length: newExercises.length });
|
setEnd(false);
|
||||||
setEnd(newExercises.length < LIMIT);
|
}, []);
|
||||||
setExercises(newExercises);
|
|
||||||
};
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
reset(term);
|
refresh(term);
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
}, [term])
|
if (params?.clearNames) setNames([]);
|
||||||
|
}, [refresh, term, params])
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item }: { item: GymSet }) => (
|
({ item }: { item: GymSet }) => (
|
||||||
<ExerciseItem
|
<WorkoutItem
|
||||||
images={settings?.images}
|
images={settings?.images}
|
||||||
alarm={settings?.alarm}
|
|
||||||
item={item}
|
item={item}
|
||||||
key={item.name}
|
key={item.name}
|
||||||
names={names}
|
names={names}
|
||||||
setNames={setNames}
|
setNames={setNames}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[settings?.images, names, settings?.alarm]
|
[settings?.images, names]
|
||||||
);
|
);
|
||||||
|
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
|
if (end) return;
|
||||||
|
const newOffset = offset + LIMIT;
|
||||||
console.log(`${SetList.name}.next:`, {
|
console.log(`${SetList.name}.next:`, {
|
||||||
offset,
|
offset,
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
|
newOffset,
|
||||||
term,
|
term,
|
||||||
end,
|
|
||||||
});
|
});
|
||||||
if (end) return;
|
const newWorkouts = await setRepo
|
||||||
const newOffset = offset + LIMIT;
|
|
||||||
const newExercises = await setRepo
|
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.select()
|
.select()
|
||||||
.where("name LIKE :name", { name: `%${term.trim()}%` })
|
.where("name LIKE :name", { name: `%${term.trim()}%` })
|
||||||
|
@ -83,23 +84,26 @@ export default function ExerciseList() {
|
||||||
.limit(LIMIT)
|
.limit(LIMIT)
|
||||||
.offset(newOffset)
|
.offset(newOffset)
|
||||||
.getMany();
|
.getMany();
|
||||||
if (newExercises.length === 0) return setEnd(true);
|
if (newWorkouts.length === 0) return setEnd(true);
|
||||||
if (!exercises) return;
|
if (!workouts) return;
|
||||||
setExercises([...exercises, ...newExercises]);
|
setWorkouts([...workouts, ...newWorkouts]);
|
||||||
if (newExercises.length < LIMIT) return setEnd(true);
|
if (newWorkouts.length < LIMIT) return setEnd(true);
|
||||||
setOffset(newOffset);
|
setOffset(newOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAdd = useCallback(async () => {
|
const onAdd = useCallback(async () => {
|
||||||
navigation.navigate("EditExercise", {
|
navigation.navigate("EditWorkout", {
|
||||||
gymSet: defaultSet,
|
gymSet: new GymSet(),
|
||||||
});
|
});
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
const search = (value: string) => {
|
const search = useCallback(
|
||||||
setTerm(value);
|
(value: string) => {
|
||||||
reset(value);
|
setTerm(value);
|
||||||
};
|
refresh(value);
|
||||||
|
},
|
||||||
|
[refresh]
|
||||||
|
);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
setNames([]);
|
setNames([]);
|
||||||
|
@ -108,23 +112,21 @@ export default function ExerciseList() {
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
setNames([]);
|
setNames([]);
|
||||||
if (names.length > 0) await setRepo.delete({ name: In(names) });
|
if (names.length > 0) await setRepo.delete({ name: In(names) });
|
||||||
await reset(term);
|
await refresh(term);
|
||||||
};
|
};
|
||||||
|
|
||||||
const select = () => {
|
const select = () => {
|
||||||
if (!exercises) return;
|
setNames(workouts.map((workout) => workout.name));
|
||||||
if (names.length === exercises.length) return setNames([]);
|
|
||||||
setNames(exercises.map((exercise) => exercise.name));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const edit = () => {
|
const edit = () => {
|
||||||
navigation.navigate("EditExercises", { names });
|
navigation.navigate("EditWorkouts", { names });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader
|
<DrawerHeader
|
||||||
name={names.length > 0 ? `${names.length} selected` : "Exercises"}
|
name={names.length > 0 ? `${names.length} selected` : "Workouts"}
|
||||||
>
|
>
|
||||||
<ListMenu
|
<ListMenu
|
||||||
onClear={clear}
|
onClear={clear}
|
||||||
|
@ -135,23 +137,18 @@ export default function ExerciseList() {
|
||||||
/>
|
/>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<Page onAdd={onAdd} term={term} search={search}>
|
<Page onAdd={onAdd} term={term} search={search}>
|
||||||
{exercises?.length === 0 ? (
|
{workouts?.length === 0 ? (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No exercises yet."
|
title="No workouts yet."
|
||||||
description="An exercise is something you do at the gym. E.g. Deadlifts"
|
description="A workout is something you do at the gym. E.g. Deadlifts"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={exercises}
|
data={workouts}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={(w) => w.name}
|
keyExtractor={(w) => w.name}
|
||||||
onEndReached={next}
|
onEndReached={next}
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={() => {
|
|
||||||
setRefreshing(true);
|
|
||||||
reset("").finally(() => setRefreshing(false));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
31
WorkoutsPage.tsx
Normal file
31
WorkoutsPage.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { createStackNavigator } from "@react-navigation/stack";
|
||||||
|
import EditWorkout from "./EditWorkout";
|
||||||
|
import EditWorkouts from "./EditWorkouts";
|
||||||
|
import GymSet from "./gym-set";
|
||||||
|
import WorkoutList from "./WorkoutList";
|
||||||
|
|
||||||
|
export type WorkoutsPageParams = {
|
||||||
|
WorkoutList: {
|
||||||
|
clearNames?: boolean;
|
||||||
|
};
|
||||||
|
EditWorkout: {
|
||||||
|
gymSet: GymSet;
|
||||||
|
};
|
||||||
|
EditWorkouts: {
|
||||||
|
names: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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="EditWorkouts" component={EditWorkouts} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,25 +3,25 @@ GEM
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.6)
|
CFPropertyList (3.0.6)
|
||||||
rexml
|
rexml
|
||||||
addressable (2.8.5)
|
addressable (2.8.4)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
artifactory (3.0.15)
|
artifactory (3.0.15)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.835.0)
|
aws-partitions (1.780.0)
|
||||||
aws-sdk-core (3.185.1)
|
aws-sdk-core (3.175.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.72.0)
|
aws-sdk-kms (1.67.0)
|
||||||
aws-sdk-core (~> 3, >= 3.184.0)
|
aws-sdk-core (~> 3, >= 3.174.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.136.0)
|
aws-sdk-s3 (1.126.0)
|
||||||
aws-sdk-core (~> 3, >= 3.181.0)
|
aws-sdk-core (~> 3, >= 3.174.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.6.0)
|
aws-sigv4 (1.5.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
|
@ -30,13 +30,13 @@ GEM
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.6.4)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.104.0)
|
excon (0.100.0)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
@ -66,7 +66,7 @@ GEM
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.7)
|
fastimage (2.2.7)
|
||||||
fastlane (2.216.0)
|
fastlane (2.213.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
|
@ -87,7 +87,6 @@ GEM
|
||||||
google-apis-playcustomapp_v1 (~> 0.1)
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
google-cloud-storage (~> 1.31)
|
google-cloud-storage (~> 1.31)
|
||||||
highline (~> 2.0)
|
highline (~> 2.0)
|
||||||
http-cookie (~> 1.0.5)
|
|
||||||
json (< 3.0.0)
|
json (< 3.0.0)
|
||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
@ -99,7 +98,7 @@ GEM
|
||||||
security (= 0.1.3)
|
security (= 0.1.3)
|
||||||
simctl (~> 1.6.3)
|
simctl (~> 1.6.3)
|
||||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
terminal-table (~> 3)
|
terminal-table (>= 1.4.5, < 2.0.0)
|
||||||
tty-screen (>= 0.6.3, < 1.0.0)
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
word_wrap (~> 1.0.0)
|
word_wrap (~> 1.0.0)
|
||||||
|
@ -107,9 +106,9 @@ GEM
|
||||||
xcpretty (~> 0.3.0)
|
xcpretty (~> 0.3.0)
|
||||||
xcpretty-travis-formatter (>= 0.0.3)
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.50.0)
|
google-apis-androidpublisher_v3 (0.43.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-core (0.11.1)
|
google-apis-core (0.11.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
@ -138,9 +137,10 @@ GEM
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.8.1)
|
googleauth (1.5.2)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
|
memoist (~> 0.16)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
|
@ -151,8 +151,9 @@ GEM
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.6.3)
|
||||||
jwt (2.7.1)
|
jwt (2.7.1)
|
||||||
|
memoist (0.16.2)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
nanaimo (0.3.0)
|
nanaimo (0.3.0)
|
||||||
|
@ -160,19 +161,19 @@ GEM
|
||||||
optparse (0.1.1)
|
optparse (0.1.1)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.0)
|
plist (3.7.0)
|
||||||
public_suffix (5.0.3)
|
public_suffix (5.0.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.5)
|
||||||
rouge (2.0.7)
|
rouge (2.0.7)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
security (0.1.3)
|
security (0.1.3)
|
||||||
signet (0.18.0)
|
signet (0.17.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
|
@ -181,8 +182,8 @@ GEM
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (3.0.2)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
tty-cursor (0.7.1)
|
tty-cursor (0.7.1)
|
||||||
tty-screen (0.8.1)
|
tty-screen (0.8.1)
|
||||||
|
@ -192,10 +193,10 @@ GEM
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (1.8.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.1)
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.23.0)
|
xcodeproj (1.22.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
|
|
@ -85,8 +85,8 @@ android {
|
||||||
applicationId "com.massive"
|
applicationId "com.massive"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 36224
|
versionCode 36174
|
||||||
versionName "2.9"
|
versionName "1.148"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {
|
release {
|
||||||
|
@ -125,7 +125,6 @@ dependencies {
|
||||||
implementation("com.facebook.react:react-android")
|
implementation("com.facebook.react:react-android")
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'com.opencsv:opencsv:5.5.2'
|
|
||||||
implementation project(':react-native-sqlite-storage')
|
implementation project(':react-native-sqlite-storage')
|
||||||
implementation project(':react-native-vector-icons')
|
implementation project(':react-native-vector-icons')
|
||||||
|
|
||||||
|
@ -144,6 +143,6 @@ dependencies {
|
||||||
|
|
||||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||||
project.ext.vectoricons = [
|
project.ext.vectoricons = [
|
||||||
iconFontNames: ['MaterialCommunityIcons.ttf']
|
iconFontNames: ['MaterialIcons.ttf']
|
||||||
]
|
]
|
||||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||||
|
|
|
@ -68,14 +68,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
||||||
reactApplicationContext.stopService(intent)
|
reactApplicationContext.stopService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
||||||
fun getCurrent(): Int {
|
|
||||||
Log.d("AlarmModule", "currentMs=$currentMs")
|
|
||||||
if (running)
|
|
||||||
return currentMs.toInt();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
@ -141,10 +133,10 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
val context = reactApplicationContext
|
val context = reactApplicationContext
|
||||||
context.startService(Intent(context, AlarmService::class.java))
|
context.startForegroundService(Intent(context, AlarmService::class.java))
|
||||||
context
|
context
|
||||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||||
.emit("tick", Arguments.createMap().apply {
|
.emit("finish", Arguments.createMap().apply {
|
||||||
putString("minutes", "00")
|
putString("minutes", "00")
|
||||||
putString("seconds", "00")
|
putString("seconds", "00")
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,7 @@ import android.os.*
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
|
||||||
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long)
|
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
class AlarmService : Service(), OnPreparedListener {
|
class AlarmService : Service(), OnPreparedListener {
|
||||||
|
@ -35,7 +35,6 @@ class AlarmService : Service(), OnPreparedListener {
|
||||||
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
|
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
|
||||||
return NotificationCompat.Builder(context, AlarmModule.CHANNEL_ID_PENDING)
|
return NotificationCompat.Builder(context, AlarmModule.CHANNEL_ID_PENDING)
|
||||||
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
|
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
|
||||||
.setSound(null)
|
|
||||||
.setContentIntent(pendingContent)
|
.setContentIntent(pendingContent)
|
||||||
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
|
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
|
||||||
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
|
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
|
||||||
|
@ -46,24 +45,21 @@ class AlarmService : Service(), OnPreparedListener {
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
private fun getSettings(): Settings {
|
private fun getSettings(): Settings {
|
||||||
val db = DatabaseHelper(applicationContext).readableDatabase
|
val db = DatabaseHelper(applicationContext).readableDatabase
|
||||||
val cursor = db.rawQuery("SELECT sound, noSound, vibrate, duration FROM settings", null)
|
val cursor = db.rawQuery("SELECT sound, noSound, vibrate FROM settings", null)
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
val sound = cursor.getString(cursor.getColumnIndex("sound"))
|
val sound = cursor.getString(cursor.getColumnIndex("sound"))
|
||||||
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
|
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
|
||||||
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
|
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
|
||||||
var duration = cursor.getLong(cursor.getColumnIndex("duration"))
|
|
||||||
if (duration.toInt() == 0) duration = 300
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
return Settings(sound, noSound, vibrate, duration)
|
return Settings(sound, noSound, vibrate)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playSound(settings: Settings) {
|
private fun playSound(settings: Settings) {
|
||||||
if (settings.noSound) return;
|
if (settings.sound == null && !settings.noSound) {
|
||||||
if (settings.sound == null) {
|
|
||||||
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
|
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
|
||||||
mediaPlayer?.start()
|
mediaPlayer?.start()
|
||||||
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
|
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
|
||||||
} else {
|
} else if (settings.sound != null && !settings.noSound) {
|
||||||
mediaPlayer = MediaPlayer().apply {
|
mediaPlayer = MediaPlayer().apply {
|
||||||
setAudioAttributes(
|
setAudioAttributes(
|
||||||
AudioAttributes.Builder()
|
AudioAttributes.Builder()
|
||||||
|
@ -121,7 +117,7 @@ class AlarmService : Service(), OnPreparedListener {
|
||||||
val settings = getSettings()
|
val settings = getSettings()
|
||||||
playSound(settings)
|
playSound(settings)
|
||||||
if (!settings.vibrate) return START_STICKY
|
if (!settings.vibrate) return START_STICKY
|
||||||
val pattern = longArrayOf(0, settings.duration, 1300, settings.duration, 1300, settings.duration / 2)
|
val pattern = longArrayOf(0, 300, 1300, 300, 1300, 300)
|
||||||
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
val vibratorManager =
|
val vibratorManager =
|
||||||
getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||||
|
|
|
@ -5,10 +5,8 @@ import android.app.PendingIntent
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.facebook.react.bridge.Promise
|
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
import com.facebook.react.bridge.ReactMethod
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
@ -18,12 +16,11 @@ import java.util.*
|
||||||
class BackupModule constructor(context: ReactApplicationContext?) :
|
class BackupModule constructor(context: ReactApplicationContext?) :
|
||||||
ReactContextBaseJavaModule(context) {
|
ReactContextBaseJavaModule(context) {
|
||||||
val context: ReactApplicationContext = reactApplicationContext
|
val context: ReactApplicationContext = reactApplicationContext
|
||||||
|
private var targetDir: String? = null
|
||||||
|
|
||||||
private val copyReceiver = object : BroadcastReceiver() {
|
private val copyReceiver = object : BroadcastReceiver() {
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
val targetDir = intent?.getStringExtra("targetDir");
|
|
||||||
Log.d("BackupModule", "onReceive $targetDir")
|
|
||||||
val treeUri: Uri = Uri.parse(targetDir)
|
val treeUri: Uri = Uri.parse(targetDir)
|
||||||
val documentFile = context?.let { DocumentFile.fromTreeUri(it, treeUri) }
|
val documentFile = context?.let { DocumentFile.fromTreeUri(it, treeUri) }
|
||||||
val file = documentFile?.createFile("application/octet-stream", "massive.db")
|
val file = documentFile?.createFile("application/octet-stream", "massive.db")
|
||||||
|
@ -41,12 +38,11 @@ class BackupModule constructor(context: ReactApplicationContext?) :
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun start(baseUri: String) {
|
fun start(baseUri: String) {
|
||||||
Log.d("BackupModule", "start $baseUri")
|
targetDir = baseUri
|
||||||
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
val intent = Intent(COPY_BROADCAST)
|
val intent = Intent(COPY_BROADCAST)
|
||||||
intent.putExtra("targetDir", baseUri)
|
|
||||||
val pendingIntent =
|
val pendingIntent =
|
||||||
PendingIntent.getBroadcast(context, baseUri.hashCode(), intent, PendingIntent.FLAG_IMMUTABLE)
|
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
pendingIntent.send()
|
pendingIntent.send()
|
||||||
|
|
||||||
val calendar = Calendar.getInstance().apply {
|
val calendar = Calendar.getInstance().apply {
|
||||||
|
@ -65,7 +61,7 @@ class BackupModule constructor(context: ReactApplicationContext?) :
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
@ReactMethod
|
||||||
fun stop() {
|
fun stop() {
|
||||||
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
val intent = Intent(COPY_BROADCAST)
|
val intent = Intent(COPY_BROADCAST)
|
||||||
|
@ -74,18 +70,6 @@ class BackupModule constructor(context: ReactApplicationContext?) :
|
||||||
alarmMgr.cancel(pendingIntent)
|
alarmMgr.cancel(pendingIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod()
|
|
||||||
fun exportToCSV(promise: Promise) {
|
|
||||||
try {
|
|
||||||
val db = DatabaseHelper(reactApplicationContext)
|
|
||||||
db.exportToCSV()
|
|
||||||
promise.resolve("Export successful!")
|
|
||||||
}
|
|
||||||
catch (e: Exception) {
|
|
||||||
promise.reject("ERROR", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
reactApplicationContext.registerReceiver(copyReceiver, IntentFilter(COPY_BROADCAST))
|
reactApplicationContext.registerReceiver(copyReceiver, IntentFilter(COPY_BROADCAST))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,6 @@ package com.massive
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import android.os.Environment
|
|
||||||
import com.opencsv.CSVWriter
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileWriter
|
|
||||||
|
|
||||||
class DatabaseHelper(context: Context) :
|
class DatabaseHelper(context: Context) :
|
||||||
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||||
|
@ -15,32 +11,6 @@ class DatabaseHelper(context: Context) :
|
||||||
private const val DATABASE_VERSION = 1
|
private const val DATABASE_VERSION = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportToCSV() {
|
|
||||||
val exportDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
if (!exportDir.exists()) {
|
|
||||||
exportDir.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = File(exportDir, "gym_sets.csv")
|
|
||||||
file.createNewFile()
|
|
||||||
|
|
||||||
val csvWrite = CSVWriter(FileWriter(file))
|
|
||||||
val db = this.readableDatabase
|
|
||||||
val cursor = db.rawQuery("SELECT * FROM sets", null)
|
|
||||||
csvWrite.writeNext(cursor.columnNames)
|
|
||||||
|
|
||||||
while(cursor.moveToNext()) {
|
|
||||||
val arrStr = arrayOfNulls<String>(cursor.columnCount)
|
|
||||||
for(i in 0 until cursor.columnCount) {
|
|
||||||
arrStr[i] = cursor.getString(i)
|
|
||||||
}
|
|
||||||
csvWrite.writeNext(arrStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
csvWrite.close()
|
|
||||||
cursor.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.massive
|
package com.massive
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.facebook.react.ReactActivity
|
import com.facebook.react.ReactActivity
|
||||||
import com.facebook.react.ReactActivityDelegate
|
import com.facebook.react.ReactActivityDelegate
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||||
|
@ -26,8 +25,4 @@ class MainActivity : ReactActivity() {
|
||||||
fabricEnabled
|
fabricEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
39
colors.ts
39
colors.ts
|
@ -1,28 +1,41 @@
|
||||||
import { DefaultTheme, MD3DarkTheme } from "react-native-paper";
|
import { DefaultTheme, MD3DarkTheme } from "react-native-paper";
|
||||||
|
|
||||||
export const LIGHT_COLORS = [
|
export const lightColors = [
|
||||||
{ hex: MD3DarkTheme.colors.primary, name: "Purple" },
|
{ hex: MD3DarkTheme.colors.primary, name: "Purple" },
|
||||||
{ hex: "#B3E5FC", name: "Blue" },
|
{ hex: "#B3E5FC", name: "Blue" },
|
||||||
{ hex: "#FA8072", name: "Salmon" },
|
{ hex: "#FA8072", name: "Salmon" },
|
||||||
{ hex: "#FFC0CB", name: "Pink" },
|
{ hex: "#FFC0CB", name: "Pink" },
|
||||||
{ hex: "#E9DCC9", name: "Linen" },
|
{ hex: "#E9DCC9", name: "Linen" },
|
||||||
{ hex: "#9ACD32", name: "Yellow Green" },
|
|
||||||
{ hex: "#FFD700", name: "Gold" },
|
|
||||||
{ hex: "#00CED1", name: "Dark Turquoise" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DARK_COLORS = [
|
export const darkColors = [
|
||||||
{ hex: DefaultTheme.colors.primary, name: "Purple" },
|
{ hex: DefaultTheme.colors.primary, name: "Purple" },
|
||||||
{ hex: "#0051a9", name: "Blue" },
|
{ hex: "#0051a9", name: "Blue" },
|
||||||
{ hex: "#000000", name: "Black" },
|
{ hex: "#000000", name: "Black" },
|
||||||
{ hex: "#863c3c", name: "Brandy" },
|
{ hex: "#863c3c", name: "Red" },
|
||||||
{ hex: "#1c6000", name: "Kermit" },
|
{ hex: "#1c6000", name: "Kermit" },
|
||||||
{ hex: "#8A2BE2", name: "Blue Violet" },
|
|
||||||
{ hex: "#6A5ACD", name: "Slate Blue" },
|
|
||||||
{ hex: "#FF8C00", name: "Dark Orange" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function darkenRgba(rgba: string, amount: number) {
|
export const colorShade = (color: any, amount: number) => {
|
||||||
let [r, g, b, a] = rgba.match(/\d+/g).map(Number);
|
color = color.replace(/^#/, "");
|
||||||
return `rgba(${r}, ${g}, ${b}, ${Math.max(0, a - amount)})`;
|
if (color.length === 3) {
|
||||||
}
|
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
};
|
||||||
|
|
|
@ -3,6 +3,4 @@ export const PADDING = 10;
|
||||||
export const ITEM_PADDING = 8;
|
export const ITEM_PADDING = 8;
|
||||||
export const DARK_RIPPLE = "#444444";
|
export const DARK_RIPPLE = "#444444";
|
||||||
export const LIGHT_RIPPLE = "#c2c2c2";
|
export const LIGHT_RIPPLE = "#c2c2c2";
|
||||||
export const DARK_SUBDUED = "#909090ff";
|
|
||||||
export const LIGHT_SUBDUED = "#717171ff";
|
|
||||||
export const LIMIT = 15;
|
export const LIMIT = 15;
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
export function convert(weight: number, fromUnit: string, toUnit: string) {
|
|
||||||
let result = Number(weight);
|
|
||||||
if (fromUnit === "lb" && toUnit === "kg") result /= 2.2;
|
|
||||||
else if (fromUnit === "kg" && toUnit === "lb") result *= 2.2;
|
|
||||||
else if (fromUnit === "stone" && toUnit === "kg") result *= 6.35;
|
|
||||||
else if (fromUnit === "kg" && toUnit === "stone") result /= 6.35;
|
|
||||||
else if (fromUnit === "stone" && toUnit === "lb") result *= 14;
|
|
||||||
else if (fromUnit === "lb" && toUnit === "stone") result /= 14;
|
|
||||||
result = Math.round((result + Number.EPSILON) * 100) / 100;
|
|
||||||
return result;
|
|
||||||
}
|
|
|
@ -26,25 +26,14 @@ 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 { exercises1699508495726 } from "./migrations/1699508495726-exercises";
|
|
||||||
import { exercisesFix1699613077628 } from "./migrations/1699613077628-exercises-fix";
|
|
||||||
import { settingsDuration1699743753975 } from "./migrations/1699743753975-settings-duration";
|
|
||||||
import { settingsStartup1699783784680 } from "./migrations/1699783784680-settings-startup";
|
|
||||||
import { settingsBackupDir1699839054226 } from "./migrations/1699839054226-settings-backup-dir";
|
|
||||||
import { homeHistoryStartup1699853245534 } from "./migrations/1699853245534-home-history-startup";
|
|
||||||
import { autoConvert1699948105001 } from "./migrations/1699948105001-auto-convert";
|
|
||||||
import { Plan } from "./plan";
|
import { Plan } from "./plan";
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
import Weight from "./weight";
|
|
||||||
import { settingsDefaultSets1700009253976 } from "./migrations/1700009253976-settings-default-sets";
|
|
||||||
import { settingsDefaults1700009729468 } from "./migrations/1700009729468-settings-defaults";
|
|
||||||
|
|
||||||
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, Weight],
|
entities: [GymSet, Plan, Settings],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTableName: "typeorm_migrations",
|
migrationsTableName: "typeorm_migrations",
|
||||||
migrations: [
|
migrations: [
|
||||||
|
@ -74,15 +63,5 @@ export const AppDataSource = new DataSource({
|
||||||
splitColor1669420187764,
|
splitColor1669420187764,
|
||||||
addBackup1678334268359,
|
addBackup1678334268359,
|
||||||
planTitle1692654882408,
|
planTitle1692654882408,
|
||||||
weight1697766633971,
|
|
||||||
exercises1699508495726,
|
|
||||||
exercisesFix1699613077628,
|
|
||||||
settingsDuration1699743753975,
|
|
||||||
settingsStartup1699783784680,
|
|
||||||
settingsBackupDir1699839054226,
|
|
||||||
homeHistoryStartup1699853245534,
|
|
||||||
autoConvert1699948105001,
|
|
||||||
settingsDefaultSets1700009253976,
|
|
||||||
settingsDefaults1700009729468,
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
2
db.ts
2
db.ts
|
@ -2,12 +2,10 @@ 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(
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
npx tsc
|
|
||||||
|
|
||||||
cd android || exit 1
|
cd android || exit 1
|
||||||
|
|
||||||
build=app/build.gradle
|
build=app/build.gradle
|
||||||
|
@ -26,6 +24,8 @@ sed -i "s/\(^\s*\)versionName \"[0-9]*.[0-9]*\"$/\1versionName \"$major.$minor\"
|
||||||
sed -i "s/\"version\": \"[0-9]*.[0-9]*\"/\"version\": \"$major.$minor\"/" ../package.json
|
sed -i "s/\"version\": \"[0-9]*.[0-9]*\"/\"version\": \"$major.$minor\"/" ../package.json
|
||||||
|
|
||||||
if [ "$1" != "-n" ]; then
|
if [ "$1" != "-n" ]; then
|
||||||
|
yarn tsc
|
||||||
|
yarn lint
|
||||||
./gradlew bundleRelease
|
./gradlew bundleRelease
|
||||||
bundle install
|
bundle install
|
||||||
bundle exec fastlane supply --aab app/build/outputs/bundle/release/app-release.aab
|
bundle exec fastlane supply --aab app/build/outputs/bundle/release/app-release.aab
|
||||||
|
@ -33,7 +33,7 @@ fi
|
||||||
|
|
||||||
git add app/build.gradle ../package.json
|
git add app/build.gradle ../package.json
|
||||||
git commit --amend --message \
|
git commit --amend --message \
|
||||||
"$(git log -1 --pretty=%B | sed " 1 s/.*/& - $major.$minor 🚀/")"
|
"$(git log -1 --pretty=%B | sed " 1 s/.*/& - $major.$minor/")"
|
||||||
git tag "$versionCode"
|
git tag "$versionCode"
|
||||||
git push origin HEAD
|
git push origin HEAD
|
||||||
git push --tags
|
git push --tags
|
||||||
|
|
8
drawer-param-list.ts
Normal file
8
drawer-param-list.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export type DrawerParamList = {
|
||||||
|
Home: {};
|
||||||
|
Settings: {};
|
||||||
|
Graphs: {};
|
||||||
|
Plans: {};
|
||||||
|
Workouts: {};
|
||||||
|
Timer: {};
|
||||||
|
};
|
|
@ -1,10 +0,0 @@
|
||||||
export type DrawerParams = {
|
|
||||||
History: {};
|
|
||||||
Exercises: {};
|
|
||||||
Plans: {};
|
|
||||||
Graphs: {};
|
|
||||||
Timer: {};
|
|
||||||
Weight: {};
|
|
||||||
Insights: {};
|
|
||||||
Settings: {};
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { NativeEventEmitter, NativeModules } from "react-native";
|
|
||||||
|
|
||||||
export const emitter = new NativeEventEmitter(NativeModules.Emitter);
|
|
16
gym-set.ts
16
gym-set.ts
|
@ -3,7 +3,7 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
@Entity("sets")
|
@Entity("sets")
|
||||||
export default class GymSet {
|
export default class GymSet {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id?: number;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -14,9 +14,6 @@ export default class GymSet {
|
||||||
@Column("int")
|
@Column("int")
|
||||||
weight: number;
|
weight: number;
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
created: string;
|
|
||||||
|
|
||||||
@Column("int")
|
@Column("int")
|
||||||
sets = 3;
|
sets = 3;
|
||||||
|
|
||||||
|
@ -30,16 +27,19 @@ export default class GymSet {
|
||||||
hidden = false;
|
hidden = false;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
unit = "kg";
|
created: string;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
image: string | null;
|
unit: string;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
steps: string | null;
|
image: string;
|
||||||
|
|
||||||
|
@Column("text")
|
||||||
|
steps?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSet: Partial<GymSet> = {
|
export const defaultSet: GymSet = {
|
||||||
created: "",
|
created: "",
|
||||||
name: "",
|
name: "",
|
||||||
image: "",
|
image: "",
|
||||||
|
|
23
home-page-params.ts
Normal file
23
home-page-params.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import GymSet from "./gym-set";
|
||||||
|
|
||||||
|
export type HomePageParams = {
|
||||||
|
Sets: {
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the current list with limit = offset
|
||||||
|
*/
|
||||||
|
refresh?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the list with limit = 0
|
||||||
|
*/
|
||||||
|
reset?: number;
|
||||||
|
};
|
||||||
|
EditSet: {
|
||||||
|
set: GymSet;
|
||||||
|
};
|
||||||
|
EditSets: {
|
||||||
|
ids: number[];
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
export enum Metrics {
|
export enum Metrics {
|
||||||
Best = "Best weight",
|
Weight = "Best weight",
|
||||||
Volume = "Volume",
|
Volume = "Volume",
|
||||||
OneRepMax = "One rep max",
|
OneRepMax = "One rep max",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
export class addSteps1667186211251 implements MigrationInterface {
|
export class addSteps1667186211251 implements MigrationInterface {
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner
|
await queryRunner
|
||||||
.query("ALTER TABLE settings ADD COLUMN steps BOOLEAN DEFAULT false")
|
.query('ALTER TABLE settings ADD COLUMN steps BOOLEAN DEFAULT true')
|
||||||
.catch(() => null);
|
.catch(() => null)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.dropColumn("settings", "steps");
|
await queryRunner.dropColumn('settings', 'steps')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
export class addShowDate1667186435051 implements MigrationInterface {
|
export class addShowDate1667186435051 implements MigrationInterface {
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner
|
await queryRunner
|
||||||
.query("ALTER TABLE settings ADD COLUMN showDate BOOLEAN DEFAULT true")
|
.query('ALTER TABLE settings ADD COLUMN showDate BOOLEAN DEFAULT false')
|
||||||
.catch(() => null);
|
.catch(() => null)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.dropColumn("settings", "showDate");
|
await queryRunner.dropColumn('settings', 'showDate')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
migrations/1668215159730-update-date.ts
Normal file
12
migrations/1668215159730-update-date.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
import {settingsRepo} from '../db'
|
||||||
|
|
||||||
|
export class updateDate1668215159730 implements MigrationInterface {
|
||||||
|
public async up(_queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const settings = await settingsRepo.findOne({where: {}})
|
||||||
|
settings.date = 'P'
|
||||||
|
await settingsRepo.save(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(_queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class exercises1699508495726 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE plans ADD COLUMN exercises TEXT`);
|
|
||||||
await queryRunner.query(`UPDATE plans SET exercises = workouts`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class exercisesFix1699613077628 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE plans_temp (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
days TEXT NOT NULL,
|
|
||||||
workouts TEXT NOT NULL,
|
|
||||||
title TEXT
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryRunner.query(`
|
|
||||||
INSERT INTO plans_temp (id,days,workouts,title)
|
|
||||||
SELECT id,days,workouts,title
|
|
||||||
FROM plans
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryRunner.query(`
|
|
||||||
DROP TABLE plans
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE plans (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
days TEXT NOT NULL,
|
|
||||||
exercises TEXT NOT NULL,
|
|
||||||
title TEXT
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryRunner.query(`
|
|
||||||
INSERT INTO plans (id,days,exercises,title)
|
|
||||||
SELECT id,days,workouts,title
|
|
||||||
FROM plans_temp
|
|
||||||
`);
|
|
||||||
|
|
||||||
await queryRunner.query(`DROP TABLE plans_temp`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class settingsDuration1699743753975 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner
|
|
||||||
.query("ALTER TABLE settings ADD COLUMN duration INTEGER")
|
|
||||||
.catch(() => null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class settingsStartup1699783784680 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query("ALTER TABLE settings ADD COLUMN startup TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class settingsBackupDir1699839054226 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query("ALTER TABLE settings ADD COLUMN backupDir TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
import { settingsRepo } from "../db";
|
|
||||||
|
|
||||||
export class homeHistoryStartup1699853245534 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await settingsRepo.update({ startup: "Home" }, { startup: "History" });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class autoConvert1699948105001 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query("ALTER TABLE settings ADD COLUMN autoConvert TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class settingsDefaultSets1700009253976 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
"ALTER TABLE settings ADD COLUMN defaultSets INTEGER"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class settingsDefaults1700009729468 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
"ALTER TABLE settings ADD COLUMN defaultMinutes INTEGER"
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
"ALTER TABLE settings ADD COLUMN defaultSeconds INTEGER"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
|
||||||
}
|
|
30
mock-providers.tsx
Normal file
30
mock-providers.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const MockProviders = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: JSX.Element | JSX.Element[];
|
||||||
|
}) => (
|
||||||
|
<PaperProvider settings={{ icon: (props) => <MaterialIcon {...props} /> }}>
|
||||||
|
<ThemeContext.Provider
|
||||||
|
value={{
|
||||||
|
theme: "system",
|
||||||
|
setTheme: jest.fn(),
|
||||||
|
lightColor: DefaultTheme.colors.primary,
|
||||||
|
darkColor: MD3DarkTheme.colors.primary,
|
||||||
|
setLightColor: jest.fn(),
|
||||||
|
setDarkColor: jest.fn(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NavigationContainer>{children}</NavigationContainer>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
</PaperProvider>
|
||||||
|
);
|
|
@ -1,42 +0,0 @@
|
||||||
export const MUSCLE_GROUPS = ["chest", "arms", "abdominal", "leg", "shoulder"];
|
|
||||||
|
|
||||||
export const EXERCISES = [
|
|
||||||
{ name: "Bench press", group: "chest" },
|
|
||||||
{ name: "Push-up", group: "chest" },
|
|
||||||
{ name: "Push up", group: "chest" },
|
|
||||||
{ name: "Pull-up", group: "arm" },
|
|
||||||
{ name: "Pull up", group: "arm" },
|
|
||||||
{ name: "Biceps curl", group: "arm" },
|
|
||||||
{ name: "Triceps dip", group: "arm" },
|
|
||||||
{ name: "Crunch", group: "abdominal" },
|
|
||||||
{ name: "Leg raise", group: "abdominal" },
|
|
||||||
{ name: "Squat", group: "leg" },
|
|
||||||
{ name: "Lunge", group: "leg" },
|
|
||||||
{ name: "Deadlift", group: "leg" },
|
|
||||||
{ name: "Shoulder press", group: "shoulder" },
|
|
||||||
{ name: "Lateral raise", group: "shoulder" },
|
|
||||||
{ name: "Front raise", group: "shoulder" },
|
|
||||||
{ name: "Dead hang", group: "arm" },
|
|
||||||
{ name: "Russian twist", group: "abdominal" },
|
|
||||||
{ name: "Leg press", group: "leg" },
|
|
||||||
{ name: "Calf raise", group: "leg" },
|
|
||||||
{ name: "Arnold press", group: "shoulder" },
|
|
||||||
{ name: "Chin-up", group: "arm" },
|
|
||||||
{ name: "Chin up", group: "arm" },
|
|
||||||
{ name: "Plank", group: "abdominal" },
|
|
||||||
{ name: "Hip thrust", group: "leg" },
|
|
||||||
{ name: "Reverse fly", group: "shoulder" },
|
|
||||||
{ name: "Side raise", group: "shoulder" },
|
|
||||||
{ name: "Incline bench press", group: "chest" },
|
|
||||||
{ name: "Hammer curl", group: "arm" },
|
|
||||||
{ name: "Skull crusher", group: "arm" },
|
|
||||||
{ name: "Mountain climber", group: "abdominal" },
|
|
||||||
{ name: "Step-up", group: "leg" },
|
|
||||||
{ name: "Box jump", group: "leg" },
|
|
||||||
{ name: "Clean and press", group: "shoulder" },
|
|
||||||
{ name: "Dip", group: "arm" },
|
|
||||||
{ name: "Flutter kick", group: "abdominal" },
|
|
||||||
{ name: "Romanian deadlift", group: "leg" },
|
|
||||||
{ name: "Upright row", group: "shoulder" },
|
|
||||||
// Add more exercises and their muscle group associations here
|
|
||||||
];
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { DARK_COLORS, LIGHT_COLORS } from "./colors";
|
import { darkColors, lightColors } from "./colors";
|
||||||
|
|
||||||
export const themeOptions = [
|
export const themeOptions = [
|
||||||
{ label: "System", value: "system" },
|
{ label: "System", value: "system" },
|
||||||
|
@ -6,13 +6,13 @@ export const themeOptions = [
|
||||||
{ label: "Light", value: "light" },
|
{ label: "Light", value: "light" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const lightOptions = LIGHT_COLORS.map((color) => ({
|
export const lightOptions = lightColors.map((color) => ({
|
||||||
label: color.name,
|
label: color.name,
|
||||||
value: color.hex,
|
value: color.hex,
|
||||||
color: color.hex,
|
color: color.hex,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const darkOptions = DARK_COLORS.map((color) => ({
|
export const darkOptions = darkColors.map((color) => ({
|
||||||
label: color.name,
|
label: color.name,
|
||||||
value: color.hex,
|
value: color.hex,
|
||||||
color: color.hex,
|
color: color.hex,
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
organize-imports-cli *.ts* && prettier --write *.ts*
|
organize-imports-cli *.ts* tests/*.ts* && deno fmt *.ts* tests/*.ts*
|
||||||
|
|
11470
package-lock.json
generated
11470
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "massive",
|
"name": "massive",
|
||||||
"version": "2.9",
|
"version": "1.148",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -24,6 +24,7 @@
|
||||||
"@testing-library/react-native": "^12.1.2",
|
"@testing-library/react-native": "^12.1.2",
|
||||||
"@types/d3-shape": "^3.1.1",
|
"@types/d3-shape": "^3.1.1",
|
||||||
"@types/react-native-sqlite-storage": "^6.0.0",
|
"@types/react-native-sqlite-storage": "^6.0.0",
|
||||||
|
"@types/react-native-svg-charts": "^5.0.12",
|
||||||
"@types/react-native-vector-icons": "^6.4.13",
|
"@types/react-native-vector-icons": "^6.4.13",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"babel-preset-react-native": "^4.0.1",
|
"babel-preset-react-native": "^4.0.1",
|
||||||
|
@ -32,20 +33,19 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-hook-form": "^7.45.1",
|
"react-hook-form": "^7.45.1",
|
||||||
"react-native": "^0.72.3",
|
"react-native": "^0.72.3",
|
||||||
"react-native-chart-kit": "^6.12.0",
|
|
||||||
"react-native-document-picker": "^9.0.1",
|
"react-native-document-picker": "^9.0.1",
|
||||||
"react-native-file-access": "^3.0.4",
|
"react-native-file-access": "^3.0.4",
|
||||||
"react-native-gesture-handler": "^2.12.0",
|
"react-native-gesture-handler": "^2.12.0",
|
||||||
"react-native-linear-gradient": "^2.7.3",
|
"react-native-linear-gradient": "^2.7.3",
|
||||||
"react-native-pager-view": "^6.2.0",
|
"react-native-pager-view": "^6.2.0",
|
||||||
"react-native-paper": "^5.9.1",
|
"react-native-paper": "^5.9.1",
|
||||||
"react-native-permissions": "^3.10.1",
|
|
||||||
"react-native-reanimated": "^3.3.0",
|
"react-native-reanimated": "^3.3.0",
|
||||||
"react-native-safe-area-context": "^4.7.1",
|
"react-native-safe-area-context": "^4.7.1",
|
||||||
"react-native-screens": "^3.22.1",
|
"react-native-screens": "^3.22.1",
|
||||||
"react-native-share": "^9.2.3",
|
"react-native-share": "^9.2.3",
|
||||||
"react-native-sqlite-storage": "^6.0.1",
|
"react-native-sqlite-storage": "^6.0.1",
|
||||||
"react-native-svg": "^13.10.10",
|
"react-native-svg": "^13.10.0",
|
||||||
|
"react-native-svg-charts": "^5.4.0",
|
||||||
"react-native-vector-icons": "^9.2.0",
|
"react-native-vector-icons": "^9.2.0",
|
||||||
"react-native-view-shot": "^3.7.0",
|
"react-native-view-shot": "^3.7.0",
|
||||||
"typeorm": "^0.3.17"
|
"typeorm": "^0.3.17"
|
||||||
|
|
|
@ -2,7 +2,4 @@ export enum Periods {
|
||||||
Weekly = "This week",
|
Weekly = "This week",
|
||||||
Monthly = "This month",
|
Monthly = "This month",
|
||||||
Yearly = "This year",
|
Yearly = "This year",
|
||||||
TwoMonths = "2 months",
|
|
||||||
ThreeMonths = "3 months",
|
|
||||||
SixMonths = "6 months",
|
|
||||||
}
|
}
|
||||||
|
|
16
plan-page-params.ts
Normal file
16
plan-page-params.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import GymSet from "./gym-set";
|
||||||
|
import { Plan } from "./plan";
|
||||||
|
|
||||||
|
export type PlanPageParams = {
|
||||||
|
PlanList: {};
|
||||||
|
EditPlan: {
|
||||||
|
plan: Plan;
|
||||||
|
};
|
||||||
|
StartPlan: {
|
||||||
|
plan: Plan;
|
||||||
|
first?: GymSet;
|
||||||
|
};
|
||||||
|
EditSet: {
|
||||||
|
set: GymSet;
|
||||||
|
};
|
||||||
|
};
|
16
plan.ts
16
plan.ts
|
@ -3,20 +3,14 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
@Entity("plans")
|
@Entity("plans")
|
||||||
export class Plan {
|
export class Plan {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id?: number;
|
||||||
|
|
||||||
|
@Column("text")
|
||||||
|
title?: string;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
days: string;
|
days: string;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
exercises: string;
|
workouts: string;
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
title: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPlan: Partial<Plan> = {
|
|
||||||
days: "",
|
|
||||||
exercises: "",
|
|
||||||
title: "",
|
|
||||||
};
|
|
||||||
|
|
4
route.ts
4
route.ts
|
@ -1,7 +1,7 @@
|
||||||
import { DrawerParams } from "./drawer-params";
|
import { DrawerParamList } from "./drawer-param-list";
|
||||||
|
|
||||||
export default interface Route {
|
export default interface Route {
|
||||||
name: keyof DrawerParams;
|
name: keyof DrawerParamList;
|
||||||
component: React.ComponentType<any>;
|
component: React.ComponentType<any>;
|
||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
50
settings.ts
50
settings.ts
|
@ -3,65 +3,47 @@ import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||||
@Entity()
|
@Entity()
|
||||||
export default class Settings {
|
export default class Settings {
|
||||||
@PrimaryColumn("boolean")
|
@PrimaryColumn("boolean")
|
||||||
alarm = false;
|
alarm: boolean;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
vibrate = true;
|
vibrate: boolean;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
sound: string | null;
|
sound: string;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
notify = false;
|
notify: boolean;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
images = true;
|
images: boolean;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
showUnit = true;
|
showUnit: boolean;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
lightColor: string | null;
|
lightColor?: string;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
darkColor: string | null;
|
darkColor?: string;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
steps = false;
|
steps: boolean;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
date: string | null;
|
date: string;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
showDate = true;
|
showDate: boolean;
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
theme: string | null;
|
theme: string;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
noSound = false;
|
showSets: boolean;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
backup = false;
|
noSound: boolean;
|
||||||
|
|
||||||
@Column("text")
|
@Column("boolean")
|
||||||
backupDir: string | null;
|
backup: boolean;
|
||||||
|
|
||||||
@Column("int")
|
|
||||||
duration: number | null;
|
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
startup: string | null;
|
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
autoConvert: string | null;
|
|
||||||
|
|
||||||
@Column("int")
|
|
||||||
defaultSets: number | null;
|
|
||||||
|
|
||||||
@Column("int")
|
|
||||||
defaultMinutes: number | null;
|
|
||||||
|
|
||||||
@Column("int")
|
|
||||||
defaultSeconds: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
24
tests/App.test.tsx
Normal file
24
tests/App.test.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { render, waitFor } from '@testing-library/react-native'
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import App from '../App'
|
||||||
|
import Settings from '../settings'
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
settingsRepo: {
|
||||||
|
findOne: () => Promise.resolve({} as Settings),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../data-source.ts', () => ({
|
||||||
|
AppDataSource: {
|
||||||
|
isInitialized: false,
|
||||||
|
initialize: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const { getAllByText } = render(<App />)
|
||||||
|
const title = await waitFor(() => getAllByText('Home'))
|
||||||
|
expect(title.length).toBeGreaterThan(0)
|
||||||
|
})
|
53
tests/EditPlan.test.tsx
Normal file
53
tests/EditPlan.test.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack'
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import { render, waitFor } from '@testing-library/react-native'
|
||||||
|
import EditPlan from '../EditPlan'
|
||||||
|
import { MockProviders } from '../mock-providers'
|
||||||
|
import { Plan } from '../plan'
|
||||||
|
import { PlanPageParams } from '../plan-page-params'
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
setRepo: {
|
||||||
|
createQueryBuilder: () => ({
|
||||||
|
select: jest.fn().mockReturnThis(),
|
||||||
|
distinct: jest.fn().mockReturnThis(),
|
||||||
|
orderBy: jest.fn().mockReturnThis(),
|
||||||
|
getRawMany: jest.fn(() =>
|
||||||
|
Promise.resolve([
|
||||||
|
{ name: 'Bench press' },
|
||||||
|
{ name: 'Bicep curls' },
|
||||||
|
{ name: 'Rows' },
|
||||||
|
])
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const Stack = createStackNavigator<PlanPageParams>()
|
||||||
|
const { getByText, getAllByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen
|
||||||
|
initialParams={{
|
||||||
|
plan: {
|
||||||
|
workouts: 'Bench,Rows,Curls',
|
||||||
|
days: 'Monday,Tuesday,Thursday',
|
||||||
|
id: 1,
|
||||||
|
} as Plan,
|
||||||
|
}}
|
||||||
|
name='EditPlan'
|
||||||
|
component={EditPlan}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const title = await waitFor(() => getByText(/Edit plan/i))
|
||||||
|
expect(title).toBeDefined()
|
||||||
|
expect(getAllByText('Days').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Monday').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Workouts').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Bench press').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Save').length).toBeGreaterThan(0)
|
||||||
|
})
|
89
tests/EditSet.test.tsx
Normal file
89
tests/EditSet.test.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack'
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import { fireEvent, render, waitFor } from '@testing-library/react-native'
|
||||||
|
import EditSet from '../EditSet'
|
||||||
|
import GymSet from '../gym-set'
|
||||||
|
import { HomePageParams } from '../home-page-params'
|
||||||
|
import { MockProviders } from '../mock-providers'
|
||||||
|
import SetList from '../SetList'
|
||||||
|
import Settings from '../settings'
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
getNow: () => Promise.resolve(new Date().toISOString()),
|
||||||
|
setRepo: {
|
||||||
|
findOne: () => Promise.resolve({}),
|
||||||
|
save: jest.fn(() => Promise.resolve({})),
|
||||||
|
},
|
||||||
|
settingsRepo: {
|
||||||
|
findOne: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
showUnit: true,
|
||||||
|
showDate: true,
|
||||||
|
images: true,
|
||||||
|
} as Settings),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const Stack = createStackNavigator<HomePageParams>()
|
||||||
|
const { getByText, getAllByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen
|
||||||
|
initialParams={{
|
||||||
|
set: {
|
||||||
|
created: '2023-01-01T01:45:13.238Z',
|
||||||
|
id: 1,
|
||||||
|
} as GymSet,
|
||||||
|
}}
|
||||||
|
name='EditSet'
|
||||||
|
component={EditSet}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const title = await waitFor(() => getByText('Edit set'))
|
||||||
|
expect(title).toBeDefined()
|
||||||
|
expect(getAllByText('Name').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Reps').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Weight').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Unit').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Created').length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText('Image').length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('saves', async () => {
|
||||||
|
const Stack = createStackNavigator<HomePageParams>()
|
||||||
|
const { getByText, getAllByText, getByTestId } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen name='Sets' component={SetList} />
|
||||||
|
<Stack.Screen
|
||||||
|
initialParams={{
|
||||||
|
set: {
|
||||||
|
created: '2023-01-01T01:45:13.238Z',
|
||||||
|
id: 1,
|
||||||
|
} as GymSet,
|
||||||
|
}}
|
||||||
|
name='EditSet'
|
||||||
|
component={EditSet}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const add = await waitFor(() => getByTestId('add'))
|
||||||
|
fireEvent.press(add)
|
||||||
|
const names = await waitFor(() => getAllByText('Name'))
|
||||||
|
fireEvent.changeText(names[0], 'Bench Press')
|
||||||
|
const reps = await waitFor(() => getAllByText('Reps'))
|
||||||
|
fireEvent.changeText(reps[0], '10')
|
||||||
|
const weights = await waitFor(() => getAllByText('Weight'))
|
||||||
|
fireEvent.changeText(weights[0], '60')
|
||||||
|
const units = await waitFor(() => getAllByText('Unit'))
|
||||||
|
fireEvent.changeText(units[0], 'lb')
|
||||||
|
const save = getByText('Save')
|
||||||
|
fireEvent.press(save)
|
||||||
|
const home = await waitFor(() => getByText('Home'))
|
||||||
|
expect(home).toBeDefined()
|
||||||
|
})
|
86
tests/EditSets.test.tsx
Normal file
86
tests/EditSets.test.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack'
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import { fireEvent, render, waitFor } from '@testing-library/react-native'
|
||||||
|
import EditSets from '../EditSets'
|
||||||
|
import { HomePageParams } from '../home-page-params'
|
||||||
|
import { MockProviders } from '../mock-providers'
|
||||||
|
|
||||||
|
const mockGoBack = jest.fn()
|
||||||
|
|
||||||
|
jest.mock('@react-navigation/native', () => ({
|
||||||
|
...jest.requireActual('@react-navigation/native'),
|
||||||
|
useNavigation: () => ({
|
||||||
|
goBack: mockGoBack,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
getNow: () => Promise.resolve(new Date().toISOString()),
|
||||||
|
setRepo: {
|
||||||
|
find: () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{ name: 'Bench press', reps: 8, weight: 60, id: 1 },
|
||||||
|
{ name: 'Bench press', reps: 6, weight: 70, id: 2 },
|
||||||
|
{ name: 'Bench press', reps: 4, weight: 85, id: 3 },
|
||||||
|
]),
|
||||||
|
update: jest.fn(() => Promise.resolve()),
|
||||||
|
},
|
||||||
|
settingsRepo: {
|
||||||
|
findOne: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
showUnit: true,
|
||||||
|
showDate: true,
|
||||||
|
images: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const Stack = createStackNavigator<HomePageParams>()
|
||||||
|
const { getByText, getAllByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen
|
||||||
|
initialParams={{ ids: [1, 2, 3] }}
|
||||||
|
name='EditSets'
|
||||||
|
component={EditSets}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const title = await waitFor(() => getByText('Edit 3 sets'))
|
||||||
|
expect(title).toBeDefined()
|
||||||
|
expect(getAllByText(/Names/i).length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText(/Reps/i).length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText(/Weights/i).length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText(/Units/i).length).toBeGreaterThan(0)
|
||||||
|
expect(getAllByText(/Image/i).length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('saves', async () => {
|
||||||
|
const Stack = createStackNavigator<HomePageParams>()
|
||||||
|
const { getByText, getAllByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen
|
||||||
|
initialParams={{ ids: [1, 2, 3] }}
|
||||||
|
name='EditSets'
|
||||||
|
component={EditSets}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const items = await waitFor(() => getAllByText(/Bench press/i))
|
||||||
|
fireEvent.changeText(items[0], 'Shoulder press')
|
||||||
|
const reps = await waitFor(() => getAllByText(/Reps/i))
|
||||||
|
fireEvent.changeText(reps[0], '10')
|
||||||
|
const weights = await waitFor(() => getAllByText(/Weights/i))
|
||||||
|
fireEvent.changeText(weights[0], '60')
|
||||||
|
const units = await waitFor(() => getAllByText(/Units/i))
|
||||||
|
fireEvent.changeText(units[0], 'lb')
|
||||||
|
const save = getByText('Save')
|
||||||
|
fireEvent.press(save)
|
||||||
|
await waitFor(() => getByText('Save'))
|
||||||
|
expect(mockGoBack).toHaveBeenCalled()
|
||||||
|
})
|
43
tests/EditWorkout.test.tsx
Normal file
43
tests/EditWorkout.test.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { createStackNavigator } from "@react-navigation/stack";
|
||||||
|
import React from "react";
|
||||||
|
import "react-native";
|
||||||
|
import { render, waitFor } from "@testing-library/react-native";
|
||||||
|
import EditWorkout from "../EditWorkout";
|
||||||
|
import GymSet from "../gym-set";
|
||||||
|
import { MockProviders } from "../mock-providers";
|
||||||
|
import Settings from "../settings";
|
||||||
|
import { WorkoutsPageParams } from "../WorkoutsPage";
|
||||||
|
|
||||||
|
jest.mock("../db.ts", () => ({
|
||||||
|
settingsRepo: {
|
||||||
|
findOne: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
showSets: true,
|
||||||
|
alarm: true,
|
||||||
|
} as Settings),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("renders correctly", async () => {
|
||||||
|
const Stack = createStackNavigator<WorkoutsPageParams>();
|
||||||
|
const { getByText, getAllByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen
|
||||||
|
initialParams={{
|
||||||
|
gymSet: { name: "Bench press" } as GymSet,
|
||||||
|
}}
|
||||||
|
name="EditWorkout"
|
||||||
|
component={EditWorkout}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</MockProviders>
|
||||||
|
);
|
||||||
|
const title = await waitFor(() => getByText(/Edit workout/i));
|
||||||
|
expect(title).toBeDefined();
|
||||||
|
expect(getAllByText(/Name/i).length).toBeGreaterThan(0);
|
||||||
|
expect(getAllByText(/Sets/i).length).toBeGreaterThan(0);
|
||||||
|
expect(getAllByText(/Minutes/i).length).toBeGreaterThan(0);
|
||||||
|
expect(getAllByText(/Seconds/i).length).toBeGreaterThan(0);
|
||||||
|
expect(getAllByText(/Save/i).length).toBeGreaterThan(0);
|
||||||
|
});
|
67
tests/GraphsPage.test.tsx
Normal file
67
tests/GraphsPage.test.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import { fireEvent, render, waitFor } from '@testing-library/react-native'
|
||||||
|
import GraphsPage from '../GraphsPage'
|
||||||
|
import { MockProviders } from '../mock-providers'
|
||||||
|
import Settings from '../settings'
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
setRepo: {
|
||||||
|
createQueryBuilder: () => ({
|
||||||
|
select: jest.fn().mockReturnThis(),
|
||||||
|
addSelect: jest.fn().mockReturnThis(),
|
||||||
|
where: jest.fn().mockReturnThis(),
|
||||||
|
andWhere: jest.fn().mockReturnThis(),
|
||||||
|
groupBy: jest.fn().mockReturnThis(),
|
||||||
|
distinct: jest.fn().mockReturnThis(),
|
||||||
|
getMany: jest.fn(() =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
name: 'Bench press',
|
||||||
|
weight: 60,
|
||||||
|
reps: 8,
|
||||||
|
image: 'https://picsum.photos/id/10/1000/600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Bicep curls',
|
||||||
|
weight: 20,
|
||||||
|
reps: 10,
|
||||||
|
image: 'https://picsum.photos/id/0/1000/600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rows',
|
||||||
|
weight: 100,
|
||||||
|
reps: 10,
|
||||||
|
image: 'https://picsum.photos/id/1/1000/600',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
settingsRepo: {
|
||||||
|
findOne: () => Promise.resolve({ images: true } as Settings),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<GraphsPage />
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const title = await waitFor(() => getByText('Graphs'))
|
||||||
|
expect(title).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('searches', async () => {
|
||||||
|
const { getByDisplayValue, getByPlaceholderText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<GraphsPage />
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const search = await waitFor(() => getByPlaceholderText('Search'))
|
||||||
|
expect(search).toBeDefined()
|
||||||
|
fireEvent.changeText(search, 'SearchValue')
|
||||||
|
const value = await waitFor(() => getByDisplayValue('SearchValue'))
|
||||||
|
expect(value).toBeDefined()
|
||||||
|
})
|
25
tests/HomePage.test.tsx
Normal file
25
tests/HomePage.test.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import { render, waitFor } from '@testing-library/react-native'
|
||||||
|
import { Repository } from 'typeorm'
|
||||||
|
import GymSet from '../gym-set'
|
||||||
|
import HomePage from '../HomePage'
|
||||||
|
import { MockProviders } from '../mock-providers'
|
||||||
|
import Settings from '../settings'
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
setRepo: { find: () => Promise.resolve([]) } as Repository<GymSet>,
|
||||||
|
settingsRepo: {
|
||||||
|
findOne: () => Promise.resolve({} as Settings),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<HomePage />
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const title = await waitFor(() => getByText('Home'))
|
||||||
|
expect(title).toBeDefined()
|
||||||
|
})
|
68
tests/PlanList.test.tsx
Normal file
68
tests/PlanList.test.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import { fireEvent, render, waitFor } from '@testing-library/react-native'
|
||||||
|
import { MockProviders } from '../mock-providers'
|
||||||
|
import { Plan } from '../plan'
|
||||||
|
import PlanPage from '../PlanPage'
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
setRepo: {
|
||||||
|
createQueryBuilder: () => ({
|
||||||
|
select: jest.fn().mockReturnThis(),
|
||||||
|
addSelect: jest.fn().mockReturnThis(),
|
||||||
|
where: jest.fn().mockReturnThis(),
|
||||||
|
andWhere: jest.fn().mockReturnThis(),
|
||||||
|
orderBy: jest.fn().mockReturnThis(),
|
||||||
|
groupBy: jest.fn().mockReturnThis(),
|
||||||
|
distinct: jest.fn().mockReturnThis(),
|
||||||
|
getRawMany: jest.fn(() =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
name: 'Bench press',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Bicep curls',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rows',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
planRepo: {
|
||||||
|
find: () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
days: 'Monday,Tuesday,Wednesday',
|
||||||
|
workouts: 'Bench press,Side raises, Bicep curls',
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
days: 'Thursday,Friday,Saturday',
|
||||||
|
workouts: 'Deadlifts,Barbell rows,Pull ups',
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
] as Plan[]),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<PlanPage />
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const title = await waitFor(() => getByText('Plans'))
|
||||||
|
expect(title).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('adds', async () => {
|
||||||
|
const { getByTestId, getByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<PlanPage />
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
fireEvent.press(await waitFor(() => getByTestId('add')))
|
||||||
|
expect(await waitFor(() => getByText('Add plan'))).toBeDefined()
|
||||||
|
})
|
25
tests/PlanPage.test.tsx
Normal file
25
tests/PlanPage.test.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import 'react-native'
|
||||||
|
import { render, waitFor } from '@testing-library/react-native'
|
||||||
|
import { Repository } from 'typeorm'
|
||||||
|
import GymSet from '../gym-set'
|
||||||
|
import { MockProviders } from '../mock-providers'
|
||||||
|
import PlanPage from '../PlanPage'
|
||||||
|
import Settings from '../settings'
|
||||||
|
|
||||||
|
jest.mock('../db.ts', () => ({
|
||||||
|
setRepo: { find: () => Promise.resolve([]) } as Repository<GymSet>,
|
||||||
|
settingsRepo: {
|
||||||
|
findOne: () => Promise.resolve({} as Settings),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MockProviders>
|
||||||
|
<PlanPage />
|
||||||
|
</MockProviders>,
|
||||||
|
)
|
||||||
|
const title = await waitFor(() => getByText('Plans'))
|
||||||
|
expect(title).toBeDefined()
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user