Compare commits

..

1 Commits

Author SHA1 Message Date
Brandon Presley ebe9a392ca Replace raw SQL in StartPlan with query builder code
Running this code results in the following error:

TypeError: subQueryBuilder.getParameters is not a function (it is undefined)
    at createFromAlias (http://localhost:8081/index.bundle//&platform=android&dev=t
y=false&app=com.massive&modulesOnly=false&runModule=true:205802:59)
    at from (http://localhost:8081/index.bundle//&platform=android&dev=true&minify=
=com.massive&modulesOnly=false&runModule=true:201576:45)
    at ?anon_0_ (http://localhost:8081/index.bundle//&platform=android&dev=true&min
&app=com.massive&modulesOnly=false&runModule=true:316375:209)
    at next (native)
    at asyncGeneratorStep (http://localhost:8081/index.bundle//&platform=android&de
nify=false&app=com.massive&modulesOnly=false&runModule=true:28380:26)
    at _next (http://localhost:8081/index.bundle//&platform=android&dev=true&minify
p=com.massive&modulesOnly=false&runModule=true:28399:29)
    at anonymous (http://localhost:8081/index.bundle//&platform=android&dev=true&mi
e&app=com.massive&modulesOnly=false&runModule=true:28404:14)
    at tryCallTwo (/root/react-native/packages/react-native/ReactAndroid/hermes-eng
Release/4i495j47/arm64-v8a/lib/InternalBytecode/InternalBytecode.js:61:9)
    at doResolve (/root/react-native/packages/react-native/ReactAndroid/hermes-engi
elease/4i495j47/arm64-v8a/lib/InternalBytecode/InternalBytecode.js:216:25)
    at Promise (/root/react-native/packages/react-native/ReactAndroid/hermes-engine
ease/4i495j47/arm64-v8a/lib/InternalBytecode/InternalBytecode.js:82:14)
    at anonymous (http://localhost:8081/index.bundle//&platform=android&dev=true&mi
e&app=com.massive&modulesOnly=false&runModule=true:28396:25)
    at anonymous (http://localhost:8081/index.bundle//&platform=android&dev=true&mi
e&app=com.massive&modulesOnly=false&runModule=true:316420:14)
    at callback (http://localhost:8081/index.bundle//&platform=android&dev=true&min
&app=com.massive&modulesOnly=false&runModule=true:145400:29)
    at anonymous (http://localhost:8081/index.bundle//&platform=android&dev=true&mi
e&app=com.massive&modulesOnly=false&runModule=true:145419:27)
    at commitHookEffectListMount (http://localhost:8081/index.bundle//&platform=and
true&minify=false&app=com.massive&modulesOnly=false&runModule=true:94929:38)
    at commitPassiveMountOnFiber (http://localhost:8081/index.bundle//&platform=and
true&minify=false&app=com.massive&modulesOnly=false&runModule=true:96130:44)
    at commitPassiveMountEffects_complete (http://localhost:8081/index.bundle//&pla
roid&dev=true&minify=false&app=com.massive&modulesOnly=false&runModule=true:96102:4
    at commitPassiveMountEffects_begin (http://localhost:8081/index.bundle//&platfo
d&dev=true&minify=false&app=com.massive&modulesOnly=false&runModule=true:96092:47)
    at commitPassiveMountEffects (http://localhost:8081/index.bundle//&platform=and
true&minify=false&app=com.massive&modulesOnly=false&runModule=true:96082:40)
    at flushPassiveEffectsImpl (http://localhost:8081/index.bundle//&platform=andro
ue&minify=false&app=com.massive&modulesOnly=false&runModule=true:97758:34)
    at flushPassiveEffects (http://localhost:8081/index.bundle//&platform=android&d
inify=false&app=com.massive&modulesOnly=false&runModule=true:97713:43)
    at performSyncWorkOnRoot (http://localhost:8081/index.bundle//&platform=android
&minify=false&app=com.massive&modulesOnly=false&runModule=true:97003:28)
    at flushSyncCallbacks (http://localhost:8081/index.bundle//&platform=android&de
nify=false&app=com.massive&modulesOnly=false&runModule=true:86202:36)
    at flushSyncCallbacksOnlyInLegacyMode (http://localhost:8081/index.bundle//&pla
2023-08-13 20:59:59 +12:00
161 changed files with 14993 additions and 16834 deletions

1
.gitignore vendored
View File

@ -74,4 +74,3 @@ massive-build
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
coverage coverage
profiles

131
App.tsx
View File

@ -1,21 +1,21 @@
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";
export const CombinedDefaultTheme = { export const CombinedDefaultTheme = {
@ -37,54 +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: {} });
setAppTheme(settings.theme);
const gotSettings = await settingsRepo.findOne({ where: {} }); if (settings.lightColor) setLightColor(settings.lightColor);
console.log(`${App.name}.mount`, { gotSettings }); if (settings.darkColor) setDarkColor(settings.darkColor);
setAppSettings({ setInitialized(true);
startup: gotSettings.startup,
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,32 +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
duration={3000}
onDismiss={() => setSnackbar("")}
visible={!!snackbar}
action={{
label: "Close",
onPress: () => setSnackbar(""),
textColor: paperTheme.colors.background,
}}
>
{snackbar}
</Snackbar>
</PaperProvider> </PaperProvider>
); );
}; };

View File

@ -1,82 +0,0 @@
import { createDrawerNavigator } from "@react-navigation/drawer";
import { StackScreenProps } from "@react-navigation/stack";
import { IconButton, useTheme, Banner } from "react-native-paper";
import { DrawerParams } from "./drawer-params";
import ExerciseList from "./ExerciseList";
import GraphsList from "./GraphList";
import InsightsPage from "./InsightsPage";
import PlanList from "./PlanList";
import SetList from "./SetList";
import SettingsPage from "./SettingsPage";
import WeightList from "./WeightList";
import Daily from "./Daily";
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="Daily"
component={Daily}
options={{ drawerIcon: () => <IconButton icon="calendar-outline" /> }}
/>
<Drawer.Screen
name="Plans"
component={PlanList}
options={{ drawerIcon: () => <IconButton icon="checkbox-multiple-marked-outline" /> }}
/>
<Drawer.Screen
name="Graphs"
component={GraphsList}
options={{
drawerIcon: () => <IconButton icon="chart-bell-curve-cumulative" />,
}}
/>
<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>
);
}

View File

@ -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,

View File

@ -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
@ -17,7 +18,6 @@ function AppInput(
selectTextOnFocus selectTextOnFocus
ref={props.innerRef} ref={props.innerRef}
blurOnSubmit={false} blurOnSubmit={false}
mode="outlined"
{...props} {...props}
/> />
); );

View File

@ -1,50 +0,0 @@
import { useMemo } from "react";
import { useWindowDimensions } from "react-native";
import { LineChart } from "react-native-chart-kit";
import { AbstractChartConfig } from "react-native-chart-kit/dist/AbstractChart";
import { useTheme } from "react-native-paper";
interface ChartProps {
labels: string[];
data: number[];
}
export default function AppLineChart({ labels, data }: ChartProps) {
const { width } = useWindowDimensions();
const { colors } = useTheme();
const config: AbstractChartConfig = {
backgroundGradientFrom: colors.background,
backgroundGradientTo: colors.elevation.level1,
color: () => colors.primary,
};
const pruned = useMemo(() => {
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 (
<LineChart
height={400}
width={width - 20}
data={{
labels: pruned,
datasets: [
{
data: data.map(d => isNaN(d) ? 0 : d),
},
],
}}
bezier
chartConfig={config}
/>
);
}

View File

@ -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: isNaN(option.value) ? 0 : 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"}
/>
);
}

View File

@ -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>
);
}

View File

@ -1,101 +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";
import { View, Text, StyleSheet } from "react-native";
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 (
<>
{__DEV__ && (
<View style={styles.debugBanner}>
<Text style={styles.debugText}>DEBUG</Text>
</View>
)}
<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>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
debugBanner: {
position: 'absolute',
top: 20,
right: 100,
backgroundColor: 'red',
zIndex: 1000,
borderRadius: 5,
},
debugText: {
color: 'white',
padding: 5,
fontSize: 10,
},
});

70
Chart.tsx Normal file
View File

@ -0,0 +1,70 @@
import { useTheme } from "@react-navigation/native";
import * as shape from "d3-shape";
import { View } from "react-native";
import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
import { MARGIN, PADDING } from "./constants";
import GymSet from "./gym-set";
import useDark from "./use-dark";
export default function Chart({
yData,
xFormat,
xData,
yFormat,
}: {
yData: number[];
xData: GymSet[];
xFormat: (value: any, index: number) => string;
yFormat: (value: any) => string;
}) {
const { colors } = useTheme();
const dark = useDark();
const axesSvg = {
fontSize: 10,
fill: dark
? CombinedDarkTheme.colors.text
: CombinedDefaultTheme.colors.text,
};
const verticalContentInset = { top: 10, bottom: 10 };
const xAxisHeight = 30;
return (
<>
<View
style={{
height: 300,
padding: PADDING,
flexDirection: "row",
}}
>
<YAxis
data={yData}
style={{ marginBottom: xAxisHeight }}
contentInset={verticalContentInset}
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>
</>
);
}

109
Daily.tsx
View File

@ -1,109 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { FlatList, View } from "react-native";
import { Button, IconButton, List } from "react-native-paper";
import AppFab from "./AppFab";
import DrawerHeader from "./DrawerHeader";
import { LIMIT, PADDING } from "./constants";
import GymSet, { defaultSet } from "./gym-set";
import { getNow, setRepo, settingsRepo } from "./db";
import { NavigationProp, useFocusEffect, useNavigation } from "@react-navigation/native";
import { Like } from "typeorm";
import Settings from "./settings";
import { format } from "date-fns";
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
import SetItem from "./SetItem";
import { StackParams } from "./AppStack";
export default function Daily() {
const [sets, setSets] = useState<GymSet[]>();
const [day, setDay] = useState<Date>()
const [settings, setSettings] = useState<Settings>();
const navigation = useNavigation<NavigationProp<StackParams>>();
const mounted = async () => {
const now = await getNow();
let created = now.split('T')[0];
setDay(new Date(created));
}
useEffect(() => {
mounted();
}, [])
const refresh = async () => {
if (!day) return;
const created = day.toISOString().split('T')[0]
const newSets = await setRepo.find({
where: { hidden: 0 as any, created: Like(`${created}%`) },
take: LIMIT,
skip: 0,
order: { created: "DESC" },
});
setSets(newSets);
settingsRepo.findOne({ where: {} }).then(setSettings)
}
useEffect(() => {
refresh();
}, [day])
useFocusEffect(useCallback(() => {
refresh();
}, [day]))
const onAdd = async () => {
const now = await getNow();
let set: Partial<GymSet> = { ...sets[0] };
if (!set) set = { ...defaultSet };
set.created = now;
delete set.id;
navigation.navigate("EditSet", { set });
}
const onRight = () => {
const newDay = new Date(day)
newDay.setDate(newDay.getDate() + 1)
setDay(newDay)
}
const onLeft = () => {
const newDay = new Date(day)
newDay.setDate(newDay.getDate() - 1)
setDay(newDay)
}
const onDate = () => {
DateTimePickerAndroid.open({
value: new Date(day),
onChange: (event, date) => {
if (event.type === 'dismissed') return;
setDay(date)
},
mode: 'date',
})
}
return (
<>
<DrawerHeader name="Daily" />
<View style={{ padding: PADDING, flexGrow: 1 }}>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
<IconButton style={{ marginRight: 'auto' }} icon="chevron-double-left" onPress={onLeft} />
<Button onPress={onDate}>{format(day ? new Date(day) : new Date(), "PPPP")}</Button>
<IconButton style={{ marginLeft: 'auto' }} icon="chevron-double-right" onPress={onRight} />
</View>
{settings && (
<FlatList ListEmptyComponent={<List.Item title="No sets yet" />} style={{ flex: 1 }} data={sets} renderItem={({ item }) => <SetItem ids={[]} setIds={() => { }} item={item} settings={settings} />} />
)}
<AppFab onPress={onAdd} />
</View>
</>
)
}

View File

@ -1,26 +1,20 @@
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,
children, children,
ids,
unSelect,
}: { }: {
name: string; name: string;
children?: JSX.Element | JSX.Element[]; children?: JSX.Element | JSX.Element[];
ids?: unknown[],
unSelect?: () => void,
}) { }) {
const navigation = useNavigation<DrawerNavigationProp<DrawerParams>>(); const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>();
return ( return (
<Appbar.Header> <Appbar.Header>
{ids && ids.length > 0 ? (<IconButton icon="arrow-left" onPress={unSelect} />) : ( <IconButton icon="menu" onPress={navigation.openDrawer} />
<IconButton icon="menu" onPress={navigation.openDrawer} />
)}
<Appbar.Content title={name} /> <Appbar.Content title={name} />
{children} {children}
</Appbar.Header> </Appbar.Header>

View File

@ -1,231 +0,0 @@
import {
NavigationProp,
RouteProp,
useFocusEffect,
useNavigation,
useRoute,
} from "@react-navigation/native";
import { useCallback, useRef, useState } from "react";
import { ScrollView, TextInput, View } from "react-native";
import DocumentPicker from "react-native-document-picker";
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
import AppInput from "./AppInput";
import { StackParams } from "./AppStack";
import ConfirmDialog from "./ConfirmDialog";
import PrimaryButton from "./PrimaryButton";
import StackHeader from "./StackHeader";
import { MARGIN, PADDING } from "./constants";
import { getNow, planRepo, setRepo, settingsRepo } from "./db";
import { DrawerParams } from "./drawer-params";
import { fixNumeric } from "./fix-numeric";
import GymSet, { defaultSet } from "./gym-set";
import Settings from "./settings";
import { toast } from "./toast";
export default function EditExercise() {
const { params } = useRoute<RouteProp<StackParams, "EditExercise">>();
const [removeImage, setRemoveImage] = useState(false);
const [showRemoveImage, setShowRemoveImage] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [name, setName] = useState(params.gymSet.name);
const [steps, setSteps] = useState(params.gymSet.steps);
const [uri, setUri] = useState(params.gymSet.image);
const [minutes, setMinutes] = useState(
params.gymSet.minutes?.toString() ?? "3"
);
const [seconds, setSeconds] = useState(
params.gymSet.seconds?.toString() ?? "30"
);
const [sets, setSets] = useState(params.gymSet.sets?.toString() ?? "3");
const { navigate } = useNavigation<NavigationProp<DrawerParams>>();
const setsRef = useRef<TextInput>(null);
const stepsRef = useRef<TextInput>(null);
const minutesRef = useRef<TextInput>(null);
const secondsRef = useRef<TextInput>(null);
const [settings, setSettings] = useState<Settings>();
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({ where: {} }).then((gotSettings) => {
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 newExercise = {
name: name || params.gymSet.name,
sets: Number(sets),
minutes: Number(minutes),
seconds: Number(seconds),
steps,
image: removeImage ? "" : uri,
} as GymSet;
await setRepo.update({ name: params.gymSet.name }, newExercise);
await planRepo.query(
`UPDATE plans
SET exercises = REPLACE(exercises, $1, $2)
WHERE exercises LIKE $3`,
[params.gymSet.name, name, `%${params.gymSet.name}%`]
);
navigate("Exercises", { update: newExercise });
};
const add = async () => {
const now = await getNow();
await setRepo.save({
...defaultSet,
name,
hidden: true,
image: uri,
minutes: minutes ? Number(minutes) : 3,
seconds: seconds ? Number(seconds) : 30,
sets: sets ? Number(sets) : 3,
steps,
created: now,
});
navigate("Exercises");
};
const remove = async () => {
await setRepo.delete({ name: params.gymSet.name });
navigate("Exercises");
};
const save = async () => {
if (params.gymSet.name) return update();
return add();
};
const changeImage = useCallback(async () => {
const { fileCopyUri } = await DocumentPicker.pickSingle({
type: DocumentPicker.types.images,
copyTo: "documentDirectory",
});
if (fileCopyUri) setUri(fileCopyUri);
}, []);
const handleRemove = useCallback(async () => {
setUri("");
setRemoveImage(true);
setShowRemoveImage(false);
}, []);
const submitName = () => {
if (settings.steps) stepsRef.current?.focus();
else setsRef.current?.focus();
};
return (
<>
<StackHeader
title={params.gymSet.name ? "Edit exercise" : "Add exercise"}
>
{typeof params.gymSet.id === "number" ? (
<IconButton onPress={() => setShowDelete(true)} icon="delete" />
) : null}
</StackHeader>
<View style={{ padding: PADDING, flex: 1 }}>
<ScrollView style={{ flex: 1 }}>
<AppInput
autoFocus
label="Name"
value={name}
onChangeText={setName}
onSubmitEditing={submitName}
/>
<AppInput
innerRef={stepsRef}
selectTextOnFocus={false}
value={steps}
onChangeText={setSteps}
label="Steps"
multiline
onSubmitEditing={() => setsRef.current?.focus()}
/>
<AppInput
innerRef={setsRef}
value={sets}
onChangeText={(newSets) => {
const fixed = fixNumeric(newSets);
setSets(fixed);
if (fixed.length !== newSets.length)
toast("Sets must be a number");
}}
label="Sets per exercise"
keyboardType="numeric"
onSubmitEditing={() => minutesRef.current?.focus()}
/>
{settings?.alarm && (
<>
<AppInput
innerRef={minutesRef}
onSubmitEditing={() => secondsRef.current?.focus()}
value={minutes}
onChangeText={(newMinutes) => {
const fixed = fixNumeric(newMinutes);
setMinutes(fixed);
if (fixed.length !== newMinutes.length)
toast("Reps must be a number");
}}
label="Rest minutes"
keyboardType="numeric"
/>
<AppInput
innerRef={secondsRef}
value={seconds}
onChangeText={setSeconds}
label="Rest seconds"
keyboardType="numeric"
blurOnSubmit
/>
</>
)}
{settings?.images && uri && (
<TouchableRipple
style={{ marginBottom: MARGIN }}
onPress={changeImage}
onLongPress={() => setShowRemoveImage(true)}
>
<Card.Cover source={{ uri }} />
</TouchableRipple>
)}
{settings?.images && !uri && (
<Button
style={{ marginBottom: MARGIN }}
onPress={changeImage}
icon="image-plus"
>
Image
</Button>
)}
</ScrollView>
<PrimaryButton disabled={!name} icon="content-save" onPress={save}>
Save
</PrimaryButton>
<ConfirmDialog
title="Remove image"
onOk={handleRemove}
show={showRemoveImage}
setShow={setShowRemoveImage}
>
Are you sure you want to remove the image?
</ConfirmDialog>
<ConfirmDialog
title="Delete set"
show={showDelete}
onOk={remove}
setShow={setShowDelete}
>
<>Are you sure you want to delete {name}</>
</ConfirmDialog>
</View>
</>
);
}

View File

@ -4,50 +4,28 @@ 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 { 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 [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 +35,28 @@ 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({ days: newDays, workouts: newWorkouts, id: plan.id });
title: title, }, [days, workouts, plan]);
days: newDays,
exercises: newExercises,
id: plan.id,
});
if (saved.id === 1) toast("Tap your plan again to begin using it");
}, [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 +70,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 +82,59 @@ 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" <Text style={styles.title}>Days</Text>
value={title} {DAYS.map((day) => (
placeholder={days.join(", ")} <Switch
onChangeText={(value) => setTitle(value)} 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 +144,5 @@ const styles = StyleSheet.create({
fontSize: 20, fontSize: 20,
marginBottom: MARGIN, marginBottom: MARGIN,
}, },
button: {},
}); });

View File

@ -1,6 +1,5 @@
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker"; import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
import { import {
NavigationProp,
RouteProp, RouteProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
@ -10,50 +9,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, 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 navigation = useNavigation();
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,
@ -62,10 +48,7 @@ export default function EditSet() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
settingsRepo.findOne({ where: {} }).then(gotSettings => { settingsRepo.findOne({ where: {} }).then(setSettings);
setSettings(gotSettings);
console.log(`${EditSet.name}.focus:`, { gotSettings })
});
}, []) }, [])
); );
@ -75,46 +58,35 @@ 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 }); if (milliseconds) NativeModules.AlarmModule.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, `${first.name}`);
}, },
[settings] [settings]
); );
const notify = (value: Partial<GymSet>) => { const added = useCallback(
if (!settings.notify) return navigate("History"); async (value: GymSet) => {
if ( startTimer(value.name);
value.weight > set.weight || console.log(`${EditSet.name}.add`, { set: value });
(value.reps > set.reps && value.weight === set.weight) if (!settings.notify) return;
) { if (
toast("Great work King! That's a new record."); value.weight > set.weight ||
} (value.reps > set.reps && value.weight === set.weight)
}; ) {
toast("Great work King! That's a new record.");
const added = async (value: GymSet) => { }
console.log(`${EditSet.name}.added:`, value); },
startTimer(value.name); [startTimer, set, settings]
}; );
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,
@ -132,9 +104,8 @@ 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") added(saved); if (typeof set.id !== "number") added(saved);
navigate("History"); navigation.goBack();
}; };
const changeImage = useCallback(async () => { const changeImage = useCallback(async () => {
@ -148,14 +119,13 @@ 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(() => {
DateTimePickerAndroid.open({ DateTimePickerAndroid.open({
value: created, value: created,
onChange: (event, date) => { onChange: (_, date) => {
if (event.type === 'dismissed') return;
if (date === created) return; if (date === created) return;
setCreated(date); setCreated(date);
setCreatedDirty(true); setCreatedDirty(true);
@ -169,151 +139,65 @@ 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> <AppInput
<AppInput label="Reps"
label="Reps" keyboardType="numeric"
keyboardType="numeric" value={reps}
value={reps} onChangeText={(newReps) => {
onChangeText={(newReps) => { const fixed = fixNumeric(newReps);
const fixed = fixNumeric(newReps); setReps(fixed);
setReps(fixed.replace(/-/g, '')) if (fixed.length !== newReps.length) toast("Reps must be a number");
if (fixed.length !== newReps.length) }}
toast("Reps must be a number"); onSubmitEditing={() => weightRef.current?.focus()}
else if (fixed.includes('-')) selection={selection}
toast("Reps must be a positive value") onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
}} innerRef={repsRef}
onSubmitEditing={() => weightRef.current?.focus()} />
selection={selection}
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
innerRef={repsRef}
/>
<View
style={{ position: "absolute", right: 0, flexDirection: "row" }}
>
<IconButton
icon="plus"
onPress={() => setReps((Number(reps) + 1).toString())}
/>
<IconButton
icon="minus"
onPress={() => setReps((Number(reps) - 1).toString())}
/>
</View>
</View>
<View> <AppInput
<AppInput label="Weight"
label="Weight" keyboardType="numeric"
keyboardType="numeric" value={weight}
value={weight} onChangeText={(newWeight) => {
onChangeText={(newWeight) => { const fixed = fixNumeric(newWeight);
const fixed = fixNumeric(newWeight); setWeight(fixed);
setWeight(fixed); if (fixed.length !== newWeight.length)
if (fixed.length !== newWeight.length) toast("Weight must be a number");
toast("Weight must be a number"); }}
}} onSubmitEditing={handleSubmit}
onSubmitEditing={handleSubmit} innerRef={weightRef}
innerRef={weightRef} />
/>
<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>
{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}
/> />
)} )}
@ -322,7 +206,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>
@ -332,39 +216,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>
</> </>
); );
} }

View File

@ -1,5 +1,4 @@
import { import {
NavigationProp,
RouteProp, RouteProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
@ -8,26 +7,21 @@ import {
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { View } from "react-native"; import { 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 { 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";
import { fixNumeric } from "./fix-numeric";
import { toast } from "./toast";
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("");
@ -57,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);
@ -66,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 () => {
@ -95,70 +89,30 @@ export default function EditSets() {
autoFocus={!name} autoFocus={!name}
/> />
<View> <AppInput
<AppInput label={`Reps: ${oldReps}`}
label={`Reps: ${oldReps}`} keyboardType="numeric"
keyboardType="numeric" value={reps}
value={reps} onChangeText={setReps}
onChangeText={(newReps) => { selection={selection}
const fixed = fixNumeric(newReps); onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
setReps(fixed.replace(/-/g, '')) autoFocus={!!name}
if (fixed.length !== newReps.length) />
toast("Reps must be a number");
else if (fixed.includes('-'))
toast("Reps must be a positive value")
}}
selection={selection}
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
autoFocus={!!name}
/>
<View
style={{ position: "absolute", right: 0, flexDirection: "row" }}
>
<IconButton
icon="plus"
onPress={() => setReps((Number(reps) + 1).toString())}
/>
<IconButton
icon="minus"
onPress={() => setReps((Number(reps) - 1).toString())}
/>
</View>
</View>
<View> <AppInput
<AppInput label={`Weights: ${weights}`}
label={`Weights: ${weights}`} keyboardType="numeric"
keyboardType="numeric" value={weight}
value={weight} onChangeText={setWeight}
onChangeText={setWeight} onSubmitEditing={handleSubmit}
onSubmitEditing={save} />
/>
<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>
{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}
/> />
)} )}
@ -184,20 +138,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>
</> </>
); );
} }

View File

@ -1,165 +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 { navigate: stackNavigate, goBack } = useNavigation<NavigationProp<StackParams>>();
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();
goBack();
stackNavigate("ViewWeightGraph");
};
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>
</>
);
}

View File

@ -1,5 +1,4 @@
import { import {
NavigationProp,
RouteProp, RouteProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
@ -9,35 +8,32 @@ 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, TouchableRipple } from "react-native-paper"; import { Button, Card, TouchableRipple } from "react-native-paper";
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 { getNow, planRepo, setRepo, settingsRepo } from "./db";
import { DrawerParams } from "./drawer-params";
import { fixNumeric } from "./fix-numeric"; import { fixNumeric } from "./fix-numeric";
import { defaultSet } from "./gym-set";
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 EditWorkout() {
const { params } = useRoute<RouteProp<StackParams, "EditExercises">>(); const { params } = useRoute<RouteProp<WorkoutsPageParams, "EditWorkout">>();
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(params.value.name);
const [oldNames, setOldNames] = useState(params.names.join(", ")); const [steps, setSteps] = useState(params.value.steps);
const [steps, setSteps] = useState(""); const [uri, setUri] = useState(params.value.image);
const [oldSteps, setOldSteps] = useState(""); const [minutes, setMinutes] = useState(
const [uri, setUri] = useState(""); params.value.minutes?.toString() ?? "3"
const [minutes, setMinutes] = useState(""); );
const [oldMinutes, setOldMinutes] = useState(""); const [seconds, setSeconds] = useState(
const [seconds, setSeconds] = useState(""); params.value.seconds?.toString() ?? "30"
const [oldSeconds, setOldSeconds] = useState(""); );
const [sets, setSets] = useState(""); const [sets, setSets] = useState(params.value.sets?.toString() ?? "3");
const [oldSets, setOldSets] = useState(""); const navigation = useNavigation();
const navigation = useNavigation<NavigationProp<DrawerParams>>();
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);
@ -47,46 +43,49 @@ export default function EditExercises() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings); settingsRepo.findOne({ where: {} }).then(setSettings);
setRepo }, [])
.createQueryBuilder()
.select()
.where("name IN (:...names)", { names: params.names })
.groupBy("name")
.getMany()
.then((gymSets) => {
console.log(`${EditExercises.name}.focus:`, { gymSets });
setOldNames(gymSets.map((set) => set.name).join(", "));
setOldSteps(gymSets.map((set) => set.steps).join(", "));
setOldMinutes(gymSets.map((set) => set.minutes).join(", "));
setOldSeconds(gymSets.map((set) => set.seconds).join(", "));
setOldSets(gymSets.map((set) => set.sets).join(", "));
});
}, [params.names])
); );
const update = async () => { const update = async () => {
await setRepo.update( await setRepo.update(
{ name: In(params.names) }, { name: params.value.name },
{ {
name: name || undefined, name: name || params.value.name,
sets: sets ? Number(sets) : undefined, sets: Number(sets),
minutes: minutes ? Number(minutes) : undefined, minutes: +minutes,
seconds: seconds ? Number(seconds) : undefined, seconds: +seconds,
steps: steps || undefined, steps,
image: removeImage ? "" : uri, image: removeImage ? "" : uri,
} }
); );
for (const oldName of params.names) { await planRepo.query(
await planRepo `UPDATE plans
.createQueryBuilder() SET workouts = REPLACE(workouts, $1, $2)
.update() WHERE workouts LIKE $3`,
.set({ [params.value.name, name, `%${params.value.name}%`]
exercises: () => `REPLACE(exercises, '${oldName}', '${name}')`, );
}) navigation.goBack();
.where("exercises LIKE :name", { name: `%${oldName}%` }) };
.execute();
} const add = async () => {
navigation.navigate("Exercises"); const now = await getNow();
await setRepo.save({
...defaultSet,
name,
hidden: true,
image: uri,
minutes: minutes ? +minutes : 3,
seconds: seconds ? +seconds : 30,
sets: sets ? +sets : 3,
steps,
created: now,
});
navigation.goBack();
};
const save = async () => {
if (params.value.name) return update();
return add();
}; };
const changeImage = useCallback(async () => { const changeImage = useCallback(async () => {
@ -110,25 +109,27 @@ export default function EditExercises() {
return ( return (
<> <>
<StackHeader title={`Edit ${params.names.length} exercises`} /> <StackHeader title={params.value.name ? "Edit workout" : "Add workout"} />
<View style={{ padding: PADDING, flex: 1 }}> <View style={{ padding: PADDING, flex: 1 }}>
<ScrollView style={{ flex: 1 }}> <ScrollView style={{ flex: 1 }}>
<AppInput <AppInput
autoFocus autoFocus
label={`Names: ${oldNames}`} label="Name"
value={name} value={name}
onChangeText={setName} onChangeText={setName}
onSubmitEditing={submitName} onSubmitEditing={submitName}
/> />
<AppInput {settings?.steps && (
innerRef={stepsRef} <AppInput
selectTextOnFocus={false} innerRef={stepsRef}
value={steps} selectTextOnFocus={false}
onChangeText={setSteps} value={steps}
label={`Steps: ${oldSteps}`} onChangeText={setSteps}
multiline label="Steps"
onSubmitEditing={() => setsRef.current?.focus()} multiline
/> onSubmitEditing={() => setsRef.current?.focus()}
/>
)}
<AppInput <AppInput
innerRef={setsRef} innerRef={setsRef}
value={sets} value={sets}
@ -138,7 +139,7 @@ export default function EditExercises() {
if (fixed.length !== newSets.length) if (fixed.length !== newSets.length)
toast("Sets must be a number"); toast("Sets must be a number");
}} }}
label={`Sets: ${oldSets}`} label="Sets per workout"
keyboardType="numeric" keyboardType="numeric"
onSubmitEditing={() => minutesRef.current?.focus()} onSubmitEditing={() => minutesRef.current?.focus()}
/> />
@ -154,14 +155,14 @@ export default function EditExercises() {
if (fixed.length !== newMinutes.length) if (fixed.length !== newMinutes.length)
toast("Reps must be a number"); toast("Reps must be a number");
}} }}
label={`Rest minutes: ${oldMinutes}`} label="Rest minutes"
keyboardType="numeric" keyboardType="numeric"
/> />
<AppInput <AppInput
innerRef={secondsRef} innerRef={secondsRef}
value={seconds} value={seconds}
onChangeText={setSeconds} onChangeText={setSeconds}
label={`Rest seconds: ${oldSeconds}`} label="Rest seconds"
keyboardType="numeric" keyboardType="numeric"
blurOnSubmit blurOnSubmit
/> />
@ -180,15 +181,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={save}>
Save Save
</PrimaryButton> </Button>
<ConfirmDialog <ConfirmDialog
title="Remove image" title="Remove image"
onOk={handleRemove} onOk={handleRemove}

View File

@ -1,69 +0,0 @@
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { useCallback, useMemo } from "react";
import { Image } from "react-native";
import { List, useTheme } from "react-native-paper";
import { StackParams } from "./AppStack";
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
import GymSet from "./gym-set";
export default function ExerciseItem({
item,
setNames,
names,
images,
alarm,
}: {
item: GymSet;
images: boolean;
setNames: (value: string[]) => void;
names: string[];
alarm: boolean;
}) {
const navigation = useNavigation<NavigationProp<StackParams>>();
const { dark } = useTheme();
const description = useMemo(() => {
const seconds = item.seconds?.toString().padStart(2, "0");
const time = ` x ${item.minutes || 0}:${seconds}`;
if (alarm) return item.sets.toString() + time;
return item.sets.toString();
}, [item.sets, item.minutes, item.seconds, alarm]);
const left = useCallback(() => {
if (!images || !item.image) return null;
return (
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
);
}, [item.image, images]);
const long = () => {
if (names.length > 0) return;
setNames([item.name]);
};
const backgroundColor = useMemo(() => {
if (!names.includes(item.name)) return;
if (dark) return DARK_RIPPLE;
return LIGHT_RIPPLE;
}, [dark, names, item.name]);
const press = () => {
console.log(`${ExerciseItem.name}.press:`, { names });
if (names.length === 0)
return navigation.navigate("EditExercise", { gymSet: item });
const removing = names.find((name) => name === item.name);
if (removing) setNames(names.filter((name) => name !== item.name));
else setNames([...names, item.name]);
};
return (
<List.Item
onPress={press}
title={item.name}
description={description}
onLongPress={long}
left={left}
style={{ backgroundColor }}
/>
);
}

View File

@ -1,162 +0,0 @@
import {
NavigationProp,
useFocusEffect,
useNavigation,
} from "@react-navigation/native";
import { useCallback, useState } from "react";
import { FlatList } from "react-native";
import { List } from "react-native-paper";
import { In } from "typeorm";
import { StackParams } from "./AppStack";
import { LIMIT } from "./constants";
import { setRepo, settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader";
import ExerciseItem from "./ExerciseItem";
import GymSet, { defaultSet } from "./gym-set";
import ListMenu from "./ListMenu";
import Page from "./Page";
import SetList from "./SetList";
import Settings from "./settings";
export default function ExerciseList() {
const [exercises, setExercises] = useState<GymSet[]>();
const [offset, setOffset] = useState(0);
const [term, setTerm] = useState("");
const [end, setEnd] = useState(false);
const [settings, setSettings] = useState<Settings>();
const [names, setNames] = useState<string[]>([]);
const [refreshing, setRefreshing] = useState(false);
const navigation = useNavigation<NavigationProp<StackParams>>();
const reset = async (value: string) => {
console.log(`${ExerciseList.name}.reset`, value);
const newExercises = await setRepo
.createQueryBuilder()
.select()
.where("name LIKE :name", { name: `%${value.trim()}%` })
.groupBy("name")
.orderBy("name")
.limit(LIMIT)
.getMany();
setOffset(0);
console.log(`${ExerciseList.name}.reset`, { length: newExercises.length });
setEnd(newExercises.length < LIMIT);
setExercises(newExercises);
};
useFocusEffect(
useCallback(() => {
reset(term);
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [term])
);
const renderItem = useCallback(
({ item }: { item: GymSet }) => (
<ExerciseItem
images={settings?.images}
alarm={settings?.alarm}
item={item}
key={item.name}
names={names}
setNames={setNames}
/>
),
[settings?.images, names, settings?.alarm]
);
const next = async () => {
console.log(`${SetList.name}.next:`, {
offset,
limit: LIMIT,
term,
end,
});
if (end) return;
const newOffset = offset + LIMIT;
const newExercises = await setRepo
.createQueryBuilder()
.select()
.where("name LIKE :name", { name: `%${term.trim()}%` })
.groupBy("name")
.orderBy("name")
.limit(LIMIT)
.offset(newOffset)
.getMany();
if (newExercises.length === 0) return setEnd(true);
if (!exercises) return;
setExercises([...exercises, ...newExercises]);
if (newExercises.length < LIMIT) return setEnd(true);
setOffset(newOffset);
};
const onAdd = useCallback(async () => {
navigation.navigate("EditExercise", {
gymSet: defaultSet,
});
}, [navigation]);
const search = (value: string) => {
setTerm(value);
reset(value);
};
const clear = useCallback(() => {
setNames([]);
}, []);
const remove = async () => {
setNames([]);
if (names.length > 0) await setRepo.delete({ name: In(names) });
await reset(term);
};
const select = () => {
if (!exercises) return;
if (names.length === exercises.length) return setNames([]);
setNames(exercises.map((exercise) => exercise.name));
};
const edit = () => {
navigation.navigate("EditExercises", { names });
};
return (
<>
<DrawerHeader
name={names.length > 0 ? `${names.length} selected` : "Exercises"}
ids={names}
unSelect={() => setNames([])}
>
<ListMenu
onClear={clear}
onDelete={remove}
onEdit={edit}
ids={names}
onSelect={select}
/>
</DrawerHeader>
<Page onAdd={onAdd} term={term} search={search}>
{exercises?.length === 0 ? (
<List.Item
title="No exercises yet."
description="An exercise is something you do at the gym. E.g. Deadlifts"
/>
) : (
<FlatList
data={exercises}
style={{ flex: 1 }}
renderItem={renderItem}
keyExtractor={(w) => w.name}
onEndReached={next}
refreshing={refreshing}
onRefresh={() => {
setRefreshing(true);
reset("").finally(() => setRefreshing(false));
}}
/>
)}
</Page>
</>
);
}

View File

@ -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>
);
}

View File

@ -3,7 +3,4 @@ source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10" ruby ">= 2.6.10"
# Cocoapods 1.15 introduced a bug which break the build. We will remove the upper gem 'cocoapods', '~> 1.12'
# bound in the template on Cocoapods with next React Native release.
gem 'cocoapods', '>= 1.13', '< 1.15'
gem 'activesupport', '>= 6.1.7.5', '< 7.1.0'

View File

@ -1,109 +0,0 @@
import {
NavigationProp,
useFocusEffect,
useNavigation,
} from "@react-navigation/native";
import { useCallback, useState } from "react";
import { FlatList, Image } from "react-native";
import { List } from "react-native-paper";
import { StackParams } from "./AppStack";
import { getBestSets } from "./best.service";
import { LIMIT } from "./constants";
import { settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader";
import GymSet from "./gym-set";
import Page from "./Page";
import Settings from "./settings";
export default function GraphsList() {
const [bests, setBests] = useState<GymSet[]>();
const [offset, setOffset] = useState(0);
const [end, setEnd] = useState(false);
const [term, setTerm] = useState("");
const navigation = useNavigation<NavigationProp<StackParams>>();
const [settings, setSettings] = useState<Settings>();
const [refreshing, setRefreshing] = useState(false);
const refresh = useCallback(
async (value: string) => {
if (refreshing) return;
const result = await getBestSets({ term: value, offset: 0 });
setBests(result);
setOffset(0);
},
[refreshing]
);
useFocusEffect(
useCallback(() => {
refresh(term);
settingsRepo.findOne({ where: {} }).then(setSettings);
// eslint-disable-next-line
}, [term])
);
const next = useCallback(async () => {
if (end) return;
const newOffset = offset + LIMIT;
console.log(`${GraphsList.name}.next:`, { offset, newOffset, term });
const newBests = await getBestSets({ term, offset: newOffset });
if (newBests.length === 0) return setEnd(true);
if (!bests) return;
setBests([...bests, ...newBests]);
if (newBests.length < LIMIT) return setEnd(true);
setOffset(newOffset);
}, [term, end, offset, bests]);
const search = useCallback(
(value: string) => {
setTerm(value);
refresh(value);
},
[refresh]
);
const renderItem = ({ item }: { item: GymSet }) => (
<List.Item
key={item.name}
title={item.name}
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
onPress={() => navigation.navigate("ViewGraph", { name: item.name })}
left={() =>
(settings?.images && item.image && (
<Image
source={{ uri: item.image }}
style={{ height: 75, width: 75 }}
/>
)) ||
null
}
/>
);
return (
<>
<DrawerHeader name="Graphs" />
<Page term={term} search={search}>
{bests?.length === 0 ? (
<List.Item
title="No exercises yet"
description="Once sets have been added, this will highlight your personal bests."
/>
) : (
<FlatList
style={{ flex: 1 }}
renderItem={renderItem}
data={bests}
keyExtractor={(set) => set.name}
onEndReached={next}
refreshing={refreshing}
onRefresh={() => {
setRefreshing(true);
refresh(term).finally(() => setRefreshing(false));
}}
/>
)}
</Page>
</>
);
}

94
GraphsList.tsx Normal file
View File

@ -0,0 +1,94 @@
import {
NavigationProp,
useFocusEffect,
useNavigation,
} from "@react-navigation/native";
import { useCallback, useState } from "react";
import { FlatList, Image } from "react-native";
import { List } from "react-native-paper";
import { GraphsPageParams } from "./GraphsPage";
import { setRepo, settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader";
import GymSet from "./gym-set";
import Page from "./Page";
import Settings from "./settings";
export default function GraphsList() {
const [bests, setBests] = useState<GymSet[]>();
const [term, setTerm] = useState("");
const navigation = useNavigation<NavigationProp<GraphsPageParams>>();
const [settings, setSettings] = useState<Settings>();
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [])
);
const refresh = useCallback(async (value: string) => {
const result = await setRepo
.createQueryBuilder("gym_set")
.select(["gym_set.name", "gym_set.reps", "gym_set.weight"])
.groupBy("gym_set.name")
.innerJoin(
(qb) =>
qb
.select(["gym_set2.name", "MAX(gym_set2.weight) AS max_weight"])
.from(GymSet, "gym_set2")
.where("gym_set2.name LIKE (:name)", { name: `%${value.trim()}%` })
.groupBy("gym_set2.name"),
"subquery",
"gym_set.name = subquery.gym_set2_name AND gym_set.weight = subquery.max_weight"
)
.getMany();
setBests(result);
}, []);
useFocusEffect(
useCallback(() => {
refresh(term);
}, [refresh, term])
);
const search = useCallback(
(value: string) => {
setTerm(value);
refresh(value);
},
[refresh]
);
const renderItem = ({ item }: { item: GymSet }) => (
<List.Item
key={item.name}
title={item.name}
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
onPress={() => navigation.navigate("ViewGraph", { best: item })}
left={() =>
(settings.images && item.image && (
<Image
source={{ uri: item.image }}
style={{ height: 75, width: 75 }}
/>
)) ||
null
}
/>
);
return (
<>
<DrawerHeader name="Graphs" />
<Page term={term} search={search}>
{bests?.length === 0 ? (
<List.Item
title="No exercises yet"
description="Once sets have been added, this will highlight your personal bests."
/>
) : (
<FlatList style={{ flex: 1 }} renderItem={renderItem} data={bests} />
)}
</Page>
</>
);
}

23
GraphsPage.tsx Normal file
View 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
View 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>
);
}

View File

@ -1,216 +0,0 @@
import { useFocusEffect } from "@react-navigation/native";
import { useCallback, useMemo, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { IconButton, Text } from "react-native-paper";
import AppPieChart from "./AppPieChart";
import AppLineChart from "./AppLineChart";
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)
)
.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}`;
};
const hourCharts = useMemo(() => {
if (loadingHours) return <ActivityIndicator />
if (hourCounts?.length === 0) return (<Text style={{ marginBottom: MARGIN }}>
No entries yet! Start recording sets to see your most active days of
the week.
</Text>)
return <AppLineChart
data={hourCounts.map((hc) => hc.count)}
labels={hourCounts.map((hc) => hourLabel(hc.hour))}
/>
}, [hourCounts, loadingHours])
const weekCharts = useMemo(() => {
if (loadingWeeks) return <ActivityIndicator />
if (weekCounts?.length === 0) return (<Text style={{ marginBottom: MARGIN }}>
No entries yet! Start recording sets to see your most active days of
the week.
</Text>)
return <AppPieChart
options={weekCounts.map((weekCount) => ({
label: DAYS[weekCount.week],
value: weekCount.count,
}))}
/>
}, [weekCounts, loadingWeeks])
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>
{weekCharts}
<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>
{hourCharts}
<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>
</>
);
}

View File

@ -11,11 +11,11 @@ export default function ListMenu({
ids, ids,
}: { }: {
onEdit: () => void; onEdit: () => void;
onCopy?: () => void; onCopy: () => void;
onClear: () => void; onClear: () => void;
onDelete: () => void; onDelete: () => void;
onSelect: () => void; onSelect: () => void;
ids?: unknown[]; ids?: number[];
}) { }) {
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [showRemove, setShowRemove] = useState(false); const [showRemove, setShowRemove] = useState(false);
@ -42,50 +42,40 @@ export default function ListMenu({
}; };
const select = () => { const select = () => {
setShowMenu(false);
onSelect(); onSelect();
}; };
return ( return (
<> <Menu
{ids.length > 0 && ( visible={showMenu}
<IconButton icon="delete" onPress={() => setShowRemove(true)} /> onDismiss={() => setShowMenu(false)}
)} anchor={<IconButton onPress={() => setShowMenu(true)} icon="more-vert" />}
<Menu >
visible={showMenu} <Menu.Item leadingIcon="done-all" title="Select all" onPress={select} />
onDismiss={() => setShowMenu(false)} <Menu.Item
anchor={ leadingIcon="clear"
<IconButton onPress={() => setShowMenu(true)} icon="dots-vertical" /> title="Clear"
} onPress={clear}
> disabled={ids?.length === 0}
<Menu.Item leadingIcon="check-all" title="Select all" onPress={select} /> />
<Menu.Item <Menu.Item
leadingIcon="close" leadingIcon="edit"
title="Clear" title="Edit"
onPress={clear} onPress={edit}
disabled={ids?.length === 0} disabled={ids?.length === 0}
/> />
<Menu.Item <Menu.Item
leadingIcon="pencil" leadingIcon="content-copy"
title="Edit" title="Copy"
onPress={edit} onPress={copy}
disabled={ids?.length === 0} disabled={ids?.length === 0}
/> />
{onCopy && ( <Divider />
<Menu.Item <Menu.Item
leadingIcon="content-copy" leadingIcon="delete"
title="Copy" onPress={() => setShowRemove(true)}
onPress={copy} title="Delete"
disabled={ids?.length === 0} />
/>
)}
<Divider />
<Menu.Item
leadingIcon="delete"
onPress={() => setShowRemove(true)}
title="Delete"
/>
</Menu>
<ConfirmDialog <ConfirmDialog
title={ids?.length === 0 ? "Delete all" : "Delete selected"} title={ids?.length === 0 ? "Delete all" : "Delete selected"}
show={showRemove} show={showRemove}
@ -96,9 +86,9 @@ export default function ListMenu({
{ids?.length === 0 ? ( {ids?.length === 0 ? (
<>This irreversibly deletes records from the app. Are you sure?</> <>This irreversibly deletes records from the app. Are you sure?</>
) : ( ) : (
<>This will delete {ids.length} {ids?.length > 1 ? "records" : "record"}. Are you sure?</> <>This will delete {ids?.length} record(s). Are you sure?</>
)} )}
</ConfirmDialog> </ConfirmDialog>
</> </Menu>
); );
} }

View File

@ -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} />}

View File

@ -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 });
@ -55,37 +56,28 @@ export default function PlanItem({
setIds([item.id]); setIds([item.id]);
}, [ids.length, item.id, setIds]); }, [ids.length, item.id, setIds]);
const currentDays = days.map((day, index) => (
<Text key={day}>
{day === today ? (
<Text
style={{
fontWeight: "bold",
textDecorationLine: "underline",
}}
>
{day}
</Text>
) : (
day
)}
{index === days.length - 1 ? "" : ", "}
</Text>
));
const title = useMemo( const title = useMemo(
() => () =>
item.title ? ( days.map((day, index) => (
<Text style={{ fontWeight: "bold" }}>{item.title}</Text> <Text key={day}>
) : ( {day === today ? (
currentDays <Text
), style={{ fontWeight: "bold", textDecorationLine: "underline" }}
[item.title, currentDays] >
{day}
</Text>
) : (
day
)}
{index === days.length - 1 ? "" : ", "}
</Text>
)),
[days, today]
); );
const description = useMemo( const description = useMemo(
() => (item.title ? currentDays : item.exercises.replace(/,/g, ", ")), () => item.workouts.replace(/,/g, ", "),
[item.title, currentDays, item.exercises] [item.workouts]
); );
const backgroundColor = useMemo(() => { const backgroundColor = useMemo(() => {

View File

@ -7,28 +7,26 @@ 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()}%`) },
{ days: Like(`%${value.trim()}%`) }, { days: Like(`%${value.trim()}%`) },
{ exercises: Like(`%${value.trim()}%`) }, { workouts: Like(`%${value.trim()}%`) },
], ],
}) })
.then(setPlans); .then(setPlans);
@ -37,8 +35,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(
@ -57,9 +54,7 @@ export default function PlanList() {
); );
const onAdd = () => const onAdd = () =>
navigation.navigate("EditPlan", { navigation.navigate("EditPlan", { plan: { days: "", workouts: "" } });
plan: defaultPlan,
});
const edit = useCallback(async () => { const edit = useCallback(async () => {
const plan = await planRepo.findOne({ where: { id: ids.pop() } }); const plan = await planRepo.findOne({ where: { id: ids.pop() } });
@ -87,17 +82,12 @@ 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 (
<> <>
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Plans"} <DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Plans"}>
ids={ids}
unSelect={() => setIds([])}
>
<ListMenu <ListMenu
onClear={clear} onClear={clear}
onCopy={copy} onCopy={copy}
@ -111,7 +101,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
View 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>
);
}

View File

@ -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>
);
}

View File

@ -23,14 +23,13 @@ Massive tracks your reps and sets at the gym. No internet connectivity or high s
<img src="metadata/en-US/images/phoneScreenshots/home.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/home.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/edit.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/edit.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/timer.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/plans.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/plans.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/plan-edit.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/plan-edit.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/plan-start.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/plan-start.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/best-view.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/best-view.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/settings.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/settings.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/drawer.png" width="318"/> <img src="metadata/en-US/images/phoneScreenshots/drawer.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/exercises.png" width="318"/>
<img src="metadata/en-US/images/phoneScreenshots/exercise-edit.png" width="318"/>
# Building from Source # Building from Source
@ -41,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
View 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>
);
}

View File

@ -1,13 +1,12 @@
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 } from "react-native-paper";
import AppInput from "./AppInput"; import { ITEM_PADDING } from "./constants";
export interface Item { export interface Item {
value: string; value: string;
label: string; label: string;
color?: string; color?: string;
icon?: string;
} }
function Select({ function Select({
@ -22,15 +21,13 @@ function Select({
label?: string; label?: string;
}) { }) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
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);
@ -39,36 +36,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
leadingIcon={item.icon} onPress={() => setShow(true)}
/> style={{
))} alignSelf: "flex-start",
</Menu> }}
>
{selected?.label}
</Button>
}
>
{items.map((item) => (
<Menu.Item
titleStyle={{ color: item.color }}
key={item.value}
title={item.label}
onPress={() => handlePress(item.value)}
/>
))}
</Menu>
</View>
); );
} }

View File

@ -1,91 +1,77 @@
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, onRemove: () => void;
}: { settings: Settings;
item: GymSet; ids: number[];
settings: Settings; setIds: (value: number[]) => void;
ids: number[]; }) {
setIds: (value: number[]) => void; const dark = useDark();
disablePress?: boolean; const navigation = useNavigation<NavigationProp<HomePageParams>>();
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}
/>
);
}

View File

@ -7,101 +7,94 @@ 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 { 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";
const limit = 15;
export default function SetList() { export default function SetList() {
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 [term, setTerm] = useState("");
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 reset = useCallback( const refresh = useCallback(async (value: string) => {
async (value: string) => { const newSets = await setRepo.find({
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: LIMIT, skip: 0,
skip: 0, order: { created: "DESC" },
order: { created: "DESC" }, });
}); console.log(`${SetList.name}.refresh:`, { value, limit });
setSets(newSets); setSets(newSets);
console.log(`${SetList.name}.reset:`, { value, offset }); setOffset(0);
setEnd(false); setEnd(false);
}, }, []);
[offset]
);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
console.log(`${SetList.name}.focus:`, { term }); refresh(term);
settingsRepo.findOne({ where: {} }).then(setSettings); settingsRepo.findOne({ where: {} }).then(setSettings);
reset(term); }, [refresh, term])
// eslint-disable-next-line
}, [term])
); );
const search = (value: string) => {
console.log(`${SetList.name}.search:`, value);
setTerm(value);
setOffset(0);
reset(value);
};
const renderItem = useCallback( const renderItem = useCallback(
({ item }: { item: GymSet }) => ( ({ item }: { item: GymSet }) => (
<SetItem <SetItem
settings={settings} settings={settings}
item={item} item={item}
key={item.id} key={item.id}
onRemove={() => refresh(term)}
ids={ids} ids={ids}
setIds={setIds} setIds={setIds}
/> />
), ),
[settings, ids] [refresh, term, settings, ids]
); );
const next = async () => { const next = useCallback(async () => {
console.log(`${SetList.name}.next:`, { end, refreshing }); if (end) 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({ const newSets = await setRepo.find({
where: { name: Like(`%${term}%`), hidden: 0 as any }, where: { name: Like(`%${term}%`), hidden: 0 as any },
take: LIMIT, take: limit,
skip: newOffset, skip: newOffset,
order: { created: "DESC" }, order: { created: "DESC" },
}); });
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>(); setSets([...sets, ...newSets]);
for (const set of sets) map.set(set.id, set); if (newSets.length < limit) return setEnd(true);
for (const set of newSets) map.set(set.id, set);
const unique = Array.from(map.values());
setSets(unique);
if (newSets.length < LIMIT) return setEnd(true);
setOffset(newOffset); setOffset(newOffset);
}; }, [term, end, offset, sets]);
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;
navigation.navigate("EditSet", { set }); navigation.navigate("EditSet", { set });
}, [navigation, sets]); }, [navigation, sets]);
const search = useCallback(
(value: string) => {
setTerm(value);
refresh(value);
},
[refresh]
);
const edit = useCallback(() => { const edit = useCallback(() => {
navigation.navigate("EditSets", { ids }); navigation.navigate("EditSets", { ids });
setIds([]); setIds([]);
@ -121,52 +114,19 @@ export default function SetList() {
setIds([]); setIds([]);
}, []); }, []);
const remove = async () => { const remove = useCallback(async () => {
setIds([]); setIds([]);
await setRepo.delete(ids.length > 0 ? ids : {}); await setRepo.delete(ids.length > 0 ? ids : {});
return reset(term); await refresh(term);
}; }, [ids, refresh, 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 = () => {
if (!settings || sets === undefined) 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}
onEndReached={next}
onEndReachedThreshold={0.5}
refreshing={refreshing}
keyExtractor={(set) => set.id.toString()}
onRefresh={() => {
setOffset(0);
setRefreshing(true);
reset(term).finally(() => setRefreshing(false));
}}
/>
);
};
return ( return (
<> <>
<DrawerHeader <DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Home"}>
name={ids.length > 0 ? `${ids.length} selected` : "History"}
ids={ids}
unSelect={() => setIds([])}
>
<ListMenu <ListMenu
onClear={clear} onClear={clear}
onCopy={copy} onCopy={copy}
@ -178,7 +138,21 @@ export default function SetList() {
</DrawerHeader> </DrawerHeader>
<Page onAdd={onAdd} term={term} search={search}> <Page onAdd={onAdd} term={term} search={search}>
{getContent()} {sets?.length === 0 ? (
<List.Item
title="No sets yet"
description="A set is a group of repetitions. E.g. 8 reps of Squats."
/>
) : (
settings && (
<FlatList
data={sets}
style={{ flex: 1 }}
renderItem={renderItem}
onEndReached={next}
/>
)
)}
</Page> </Page>
</> </>
); );

33
SettingButton.tsx Normal file
View 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>
);
}

View File

@ -2,58 +2,51 @@ 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 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",
"dd/LL/yyyy, p", "dd/LL/yyyy, p",
"ccc p", "ccc p",
"p", "p",
"yyyy-MM-dd", "yyyy-MM-d",
"yyyy-MM-dd, p", "yyyy-MM-d, p",
"yyyy.MM.dd", "yyyy.MM.d",
]; ];
const twentyFours = [ const twentyFours = [
"dd/LL/yyyy", "dd/LL/yyyy",
"dd/LL/yyyy, k:mm", "dd/LL/yyyy, k:m",
"ccc k:mm", "ccc k:m",
"k:mm", "k:m",
"yyyy-MM-dd", "yyyy-MM-d",
"yyyy-MM-dd, k:mm", "yyyy-MM-d, k:m",
"yyyy.MM.dd", "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: {} }),
@ -67,10 +60,10 @@ export default function SettingsPage() {
setLightColor, setLightColor,
darkColor, darkColor,
setDarkColor, setDarkColor,
} = useAppTheme(); } = useTheme();
useEffect(() => { useEffect(() => {
NativeModules.SettingsModule.ignoringBattery().then(setIgnoring); NativeModules.SettingsModule.ignoringBattery(setIgnoring);
NativeModules.SettingsModule.is24().then((is24: boolean) => { NativeModules.SettingsModule.is24().then((is24: boolean) => {
console.log(`${SettingsPage.name}.focus:`, { is24 }); console.log(`${SettingsPage.name}.focus:`, { is24 });
if (is24) setFormatOptions(twentyFours); if (is24) setFormatOptions(twentyFours);
@ -78,11 +71,14 @@ export default function SettingsPage() {
}); });
}, []); }, []);
const backupString = useMemo(() => { const update = useCallback((key: keyof Settings, value: unknown) => {
if (!settings.backupDir) return null; return settingsRepo
const split = decodeURIComponent(settings.backupDir).split(":"); .createQueryBuilder()
return split.pop(); .update()
}, [settings.backupDir]); .set({ [key]: value })
.printSql()
.execute();
}, []);
const soundString = useMemo(() => { const soundString = useMemo(() => {
if (!settings.sound) return null; if (!settings.sound) return null;
@ -90,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(
(item: Input<string>) => (
<Select
key={item.name}
value={item.value}
onChange={(value) => changeString(item.key, value)}
label={item.name}
items={item.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();
@ -100,487 +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();
if (!file.uri.endsWith('.db'))
return toast("File name must end with .db")
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" }] }); {
toast("Imported database successfully.") name: soundString || "Default",
}, [reset]); 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", icon: 'history' },
{ label: "Exercises", value: "Exercises", icon: 'dumbbell' },
{ label: "Daily", value: "Daily", icon: 'calendar-outline' },
{ label: "Plans", value: "Plans", icon: 'checkbox-multiple-marked-outline' },
{ label: "Graphs", value: "Graphs", icon: 'chart-bell-curve-cumulative' },
{ label: "Timer", value: "Timer", icon: 'timer-outline' },
{ label: "Weight", value: "Weight", icon: 'scale-bathroom' },
{ label: "Insights", value: "Insights", icon: 'lightbulb-on-outline' },
{ label: "Settings", value: "Settings", icon: 'cog-outline' },
]}
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: "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: "", icon: 'scale-off' },
{ label: "Kilograms", value: "kg", icon: 'weight-kilogram' },
{ label: "Pounds", value: "lb", icon: 'weight-pound' },
{ label: "Stone", value: "stone", icon: 'weight' },
]}
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: "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: "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("Alarms will vibrate.");
else toast("Stopped alarms from vibrating.");
}}
title={name}
/>
),
},
{
name: "Sound",
renderItem: (name: string) => (
<Switch
value={!settings.noSound}
onChange={async (value) => {
setValue("noSound", !value);
await settingsRepo.update({}, { 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 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 || "Not set yet!"}`,
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 DocumentPicker.pickDirectory();
const error = await NativeModules.BackupModule.once(result.uri);
if (error) toast(error);
else toast("Database exported.");
}}
>
{name}
</Button>
),
},
{
name: "Export sets as CSV",
renderItem: (name: string) => (
<Button
style={{ alignSelf: "flex-start" }}
onPress={async () => {
const result = await DocumentPicker.pickDirectory();
await NativeModules.BackupModule.exportSets(result.uri);
toast("Exported sets as CSV.");
}}
>
{name}
</Button>
),
},
{
name: "Export plans as CSV",
renderItem: (name: string) => (
<Button
style={{ alignSelf: "flex-start" }}
onPress={async () => {
const result = await DocumentPicker.pickDirectory();
await NativeModules.BackupModule.exportPlans(result.uri);
toast("Exported plans 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}

View File

@ -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>

View File

@ -7,37 +7,34 @@ 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 { 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");
const [selected, setSelected] = useState<number>(0); const [selected, setSelected] = useState(0);
const [settings, setSettings] = useState<Settings>(); const [settings, setSettings] = useState<Settings>();
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,35 +42,48 @@ 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 newCounts = await AppDataSource.manager
SELECT exercises.name, COUNT(sets.id) as total, sets.sets .createQueryBuilder()
FROM (select 0 as name, 0 as sequence union values ${questions}) as exercises .select("workouts.name")
LEFT JOIN sets ON sets.name = exercises.name .addSelect("COUNT(sets.id)", "total")
AND sets.created LIKE STRFTIME('%Y-%m-%d%%', 'now', 'localtime') .addSelect("sets.sets")
AND NOT sets.hidden .from((qb) => {
GROUP BY exercises.name const subQuery = qb
ORDER BY exercises.sequence .subQuery()
LIMIT -1 .select("0", "name")
OFFSET 1 .addSelect("0", "sequence")
`; .from("workouts", "workouts")
const newCounts = await AppDataSource.manager.query(select); .getQuery();
return `(${subQuery} UNION ALL values ${questions})`;
}, "workouts")
.leftJoin(
"sets",
"sets",
"sets.name = workouts.name AND sets.created LIKE STRFTIME('%Y-%m-%d%%', 'now', 'localtime') AND NOT sets.hidden"
)
.groupBy("workouts.name")
.orderBy("workouts.sequence")
.limit(-1)
.offset(1)
.getRawMany();
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 });
if (!last) return; if (!last) return;
delete last.id; delete last.id;
console.log(`${StartPlan.name}.select:`, { last }); console.log(`${StartPlan.name}.select:`, { last });
@ -88,28 +98,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,
}; };
@ -117,111 +118,67 @@ 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); NativeModules.AlarmModule.timer(milliseconds);
if (canNotify === RESULTS.DENIED)
await request(PERMISSIONS.ANDROID.POST_NOTIFICATIONS);
if (isNaN(exercise.total) ? 0 : exercise.total === best.sets - 1 && selected === exercises.length - 1)
return
NativeModules.AlarmModule.timer(milliseconds, `${exercise.name} (${exercise.total + 1}/${best.sets})`);
}; };
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> <AppInput
<AppInput label="Reps"
label="Reps" keyboardType="numeric"
keyboardType="numeric" value={reps}
value={reps} onChangeText={(newReps) => {
onChangeText={(newReps) => { const fixed = fixNumeric(newReps);
const fixed = fixNumeric(newReps); setReps(fixed);
setReps(fixed.replace(/-/g, '')) if (fixed.length !== newReps.length)
if (fixed.length !== newReps.length) toast("Reps must be a number");
toast("Reps must be a number"); }}
else if (fixed.includes('-')) onSubmitEditing={() => weightRef.current?.focus()}
toast("Reps must be a positive value") selection={selection}
}} onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
onSubmitEditing={() => weightRef.current?.focus()} innerRef={repsRef}
selection={selection} />
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)} <AppInput
innerRef={repsRef} label="Weight"
/> keyboardType="numeric"
<View value={weight}
style={{ position: "absolute", right: 0, flexDirection: "row" }} onChangeText={(newWeight) => {
> const fixed = fixNumeric(newWeight);
<IconButton setWeight(fixed);
icon="plus" if (fixed.length !== newWeight.length)
onPress={() => setReps((Number(reps) + 1).toString())} toast("Weight must be a number");
/> }}
<IconButton onSubmitEditing={handleSubmit}
icon="minus" innerRef={weightRef}
onPress={() => setReps((Number(reps) - 1).toString())} blurOnSubmit
/> />
</View>
</View>
<View>
<AppInput
label="Weight"
keyboardType="numeric"
value={weight}
onChangeText={(newWeight) => {
const fixed = fixNumeric(newWeight);
setWeight(fixed);
if (fixed.length !== newWeight.length)
toast("Weight must be a number");
}}
onSubmitEditing={handleSubmit}
innerRef={weightRef}
blurOnSubmit
/>
<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>
{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
@ -238,9 +195,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>
</> </>
); );

View File

@ -1,16 +1,11 @@
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 { 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 +19,7 @@ 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 undo = useCallback(async () => { const undo = useCallback(async () => {
const now = await getNow(); const now = await getNow();
@ -65,18 +59,8 @@ 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(() => {
setShowMenu(false);
stackNavigate("ViewSetList", { name: item.name });
}, [item.name, stackNavigate]);
const graph = useCallback(() => {
setShowMenu(false);
stackNavigate("ViewGraph", { name: item.name });
}, [item.name, stackNavigate]);
const left = useCallback( const left = useCallback(
() => ( () => (
@ -105,18 +89,12 @@ 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="edit" onPress={edit} title="Edit" />
<Menu.Item
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 (

View File

@ -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>

75
TimerPage.tsx Normal file
View File

@ -0,0 +1,75 @@
import { useFocusEffect } from "@react-navigation/native";
import React, { useCallback, useMemo, useState } from "react";
import { Dimensions, NativeModules, View } from "react-native";
import { Button, Text, useTheme } from "react-native-paper";
import { ProgressCircle } from "react-native-svg-charts";
import AppFab from "./AppFab";
import { MARGIN, PADDING } from "./constants";
import { settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader";
import Settings from "./settings";
import useTimer from "./use-timer";
export interface TickEvent {
minutes: string;
seconds: string;
}
export default function TimerPage() {
const { minutes, seconds } = useTimer();
const [settings, setSettings] = useState<Settings>();
const { colors } = useTheme();
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [])
);
const stop = () => {
NativeModules.AlarmModule.stop();
};
const add = async () => {
console.log(`${TimerPage.name}.add:`, settings);
NativeModules.AlarmModule.add();
};
const progress = useMemo(() => {
return (Number(minutes) * 60 + Number(seconds)) / 210;
}, [minutes, seconds]);
const left = useMemo(() => {
return Dimensions.get("screen").width * 0.5 - 60;
}, []);
return (
<>
<DrawerHeader name="Timer" />
<View style={{ flexGrow: 1, padding: PADDING }}>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 70, position: "absolute" }}>
{minutes}:{seconds}
</Text>
<ProgressCircle
style={{ height: 300, width: 300, marginBottom: MARGIN }}
progress={progress}
strokeWidth={10}
progressColor={colors.primary}
backgroundColor={colors.primary + "80"}
/>
</View>
</View>
<Button onPress={add} style={{ position: "absolute", top: "82%", left }}>
Add 1 min
</Button>
<AppFab icon="stop" onPress={stop} />
</>
);
}

View File

@ -1,104 +1,57 @@
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker"; import { RouteProp, useRoute } from "@react-navigation/native";
import { RouteProp, useFocusEffect, useRoute } from "@react-navigation/native";
import { format } from "date-fns"; import { format } from "date-fns";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Keyboard, 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 AppInput from "./AppInput"; import Chart from "./Chart";
import AppLineChart from "./AppLineChart"; import { GraphsPageParams } from "./GraphsPage";
import { StackParams } from "./AppStack";
import Select from "./Select"; import Select from "./Select";
import StackHeader from "./StackHeader"; import StackHeader from "./StackHeader";
import { MARGIN, PADDING } from "./constants"; import { PADDING } from "./constants";
import { setRepo, settingsRepo } from "./db"; import { setRepo } from "./db";
import GymSet from "./gym-set"; import GymSet from "./gym-set";
import { Metrics } from "./metrics"; import { Metrics } from "./metrics";
import { Periods } from "./periods"; import { Periods } from "./periods";
import Settings from "./settings";
import Volume from "./volume"; import Volume from "./volume";
import { convert } from "./conversions";
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);
const [unit, setUnit] = useState("kg");
const [start, setStart] = useState<Date | null>(null);
const [end, setEnd] = useState<Date | null>(null);
const [settings, setSettings] = useState<Settings>({} as Settings);
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [])
);
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";
else if (period === Periods.AllTime) difference = null;
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)", {
if (start) builder.andWhere("DATE(created) >= :start", { start }); difference,
if (end) builder.andWhere("DATE(created) <= :end", { end }); })
if (difference) .groupBy("name")
builder.andWhere( .addGroupBy(`STRFTIME('${group}', created)`);
"DATE(created) >= DATE('now', 'weekday 0', :difference)",
{
difference,
}
);
builder.groupBy("name").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()
.then((newWeights) =>
newWeights.map((set) => {
let weight = convert(set.weight, set.unit, unit);
if (isNaN(weight)) weight = 0;
return ({
...set,
weight: weight
});
})
)
.then(setWeights); .then(setWeights);
break; break;
case Metrics.Volume: case Metrics.Volume:
builder builder
.addSelect("ROUND(SUM(weight * reps), 2)", "value") .addSelect("ROUND(SUM(weight * reps), 2)", "value")
.getRawMany() .getRawMany()
.then((newWeights) =>
newWeights.map((set) => {
let weight = convert(set.value, set.unit, unit);
if (isNaN(weight)) weight = 0;
return ({
...set,
value: weight
});
})
)
.then(setVolumes); .then(setVolumes);
break; break;
default: default:
@ -109,85 +62,53 @@ export default function ViewGraph() {
"weight" "weight"
) )
.getRawMany() .getRawMany()
.then((newWeights) =>
newWeights.map((set) => {
let weight = convert(set.weight, set.unit, unit);
if (isNaN(weight)) weight = 0;
return ({
...set,
weight: weight,
});
})
)
.then((newWeights) => { .then((newWeights) => {
console.log(`${ViewGraph.name}.oneRepMax:`, { console.log({ weights: newWeights });
weights: newWeights,
});
setWeights(newWeights); setWeights(newWeights);
}); });
} }
}, [params.name, metric, period, unit, start, end]); }, [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 (
<AppLineChart <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 (
<AppLineChart
data={volumes.map((volume) => volume.value)}
labels={volumes.map((volume) =>
format(new Date(volume.created), "yyyy-MM-d")
)}
/>
);
}, [volumes]);
const pickStart = useCallback(() => {
DateTimePickerAndroid.open({
value: start || new Date(),
onChange: (event, date) => {
if (event.type === "dismissed") return;
if (date === start) return;
setStart(date);
setPeriod(Periods.AllTime);
Keyboard.dismiss();
},
mode: "date",
});
}, [start]);
const pickEnd = useCallback(() => {
DateTimePickerAndroid.open({
value: end || new Date(),
onChange: (event, date) => {
if (event.type === "dismissed") return;
if (date === end) return;
setEnd(date);
setPeriod(Periods.AllTime);
Keyboard.dismiss();
},
mode: "date",
});
}, [end]);
return ( return (
<> <>
<StackHeader title={params.name}> <StackHeader title={params.best.name}>
<IconButton <IconButton
onPress={() => onPress={() =>
captureScreen().then(async (uri) => { captureScreen().then(async (uri) => {
@ -202,66 +123,32 @@ 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}
/> />
<Select <Select
label="Period" label="Period"
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 },
{ value: Periods.AllTime, label: Periods.AllTime },
]} ]}
onChange={(value) => { onChange={(value) => setPeriod(value as Periods)}
setPeriod(value as Periods);
setStart(null);
setEnd(null);
}}
value={period} value={period}
/> />
{charts}
<View style={{ flexDirection: "row", marginBottom: MARGIN }}> </View>
<AppInput
label="Start date"
value={start ? format(start, settings.date || "Pp") : null}
onPressOut={pickStart}
style={{ flex: 1, marginRight: MARGIN }}
/>
<AppInput
label="End date"
value={end ? format(end, settings.date || "Pp") : null}
onPressOut={pickEnd}
style={{ flex: 1 }}
/>
</View>
<Select
label="Unit"
value={unit}
onChange={setUnit}
items={[
{ label: "Pounds (lb)", value: "lb" },
{ label: "Kilograms (kg)", value: "kg" },
{ label: "Stone", value: "stone" },
]}
/>
<View style={{ paddingTop: PADDING }}>
{metric === Metrics.Volume ? volumeChart : weightChart}
</View>
</ScrollView>
</> </>
); );
} }

View File

@ -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()}
</>
);
}

View File

@ -1,167 +0,0 @@
import { format } from "date-fns";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Keyboard, 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 AppLineChart from "./AppLineChart";
import { MARGIN, PADDING } from "./constants";
import { settingsRepo, weightRepo } from "./db";
import { Periods } from "./periods";
import Select from "./Select";
import StackHeader from "./StackHeader";
import Weight from "./weight";
import { useFocusEffect } from "@react-navigation/native";
import Settings from "./settings";
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
import AppInput from "./AppInput";
export default function ViewWeightGraph() {
const [weights, setWeights] = useState<Weight[]>();
const [period, setPeriod] = useState(Periods.TwoMonths);
const [start, setStart] = useState<Date | null>(null)
const [end, setEnd] = useState<Date | null>(null)
const [settings, setSettings] = useState<Settings>({} as Settings);
useFocusEffect(useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings)
}, []))
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";
else if (period === Periods.AllTime) difference = null;
let group = "%Y-%m-%d";
if (period === Periods.Yearly) group = "%Y-%m";
const builder = weightRepo
.createQueryBuilder()
.select("STRFTIME('%Y-%m-%d', created)", "created")
.addSelect("AVG(value) as value")
.addSelect("unit")
.groupBy(`STRFTIME('${group}', created)`)
if (difference)
builder.where("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
difference,
})
if (start)
builder.andWhere("DATE(created) >= :start", { start });
if (end)
builder.andWhere("DATE(created) <= :end", { end });
builder
.getRawMany()
.then(setWeights);
}, [period, start, end]);
const pickStart = useCallback(() => {
DateTimePickerAndroid.open({
value: start || new Date(),
onChange: (event, date) => {
if (event.type === 'dismissed') return;
if (date === start) return;
setStart(date);
setPeriod(Periods.AllTime);
Keyboard.dismiss();
},
mode: "date",
});
}, [start]);
const pickEnd = useCallback(() => {
DateTimePickerAndroid.open({
value: end || new Date(),
onChange: (event, date) => {
if (event.type === 'dismissed') return;
if (date === end) return;
setEnd(date);
setPeriod(Periods.AllTime);
Keyboard.dismiss();
},
mode: "date",
});
}, [end]);
const charts = useMemo(() => {
if (!weights) return;
if (weights?.length === 0) {
return <List.Item title="No data yet." />;
}
return (
<AppLineChart
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 },
{ value: Periods.AllTime, label: Periods.AllTime },
]}
onChange={(value) => {
setPeriod(value as Periods);
if (value === Periods.AllTime) return;
setStart(null);
setEnd(null);
}}
value={period}
/>
<View style={{ flexDirection: 'row', marginBottom: MARGIN }}>
<AppInput
label="Start date"
value={start ? format(start, settings.date || "Pp") : null}
onPressOut={pickStart}
style={{ flex: 1, marginRight: MARGIN }}
/>
<AppInput
label="End date"
value={end ? format(end, settings.date || "Pp") : null}
onPressOut={pickEnd}
style={{ flex: 1 }}
/>
</View>
{charts}
</ScrollView>
</>
);
}

View File

@ -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>
)}
/>
);
}

View File

@ -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(`${WeightList.name}.next:`, { 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>
</>
);
}

96
WorkoutItem.tsx Normal file
View File

@ -0,0 +1,96 @@
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { useCallback, useMemo, useState } from "react";
import { GestureResponderEvent, Image } from "react-native";
import { List, Menu, Text } from "react-native-paper";
import ConfirmDialog from "./ConfirmDialog";
import { setRepo } from "./db";
import GymSet from "./gym-set";
import { WorkoutsPageParams } from "./WorkoutsPage";
export default function WorkoutItem({
item,
onRemove,
images,
}: {
item: GymSet;
onRemove: () => void;
images: boolean;
}) {
const [showMenu, setShowMenu] = useState(false);
const [anchor, setAnchor] = useState({ x: 0, y: 0 });
const [showRemove, setShowRemove] = useState("");
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
const remove = useCallback(async () => {
await setRepo.delete({ name: item.name });
setShowMenu(false);
onRemove();
}, [setShowMenu, onRemove, item.name]);
const longPress = useCallback(
(e: GestureResponderEvent) => {
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY });
setShowMenu(true);
},
[setShowMenu, setAnchor]
);
const description = useMemo(() => {
const seconds = item.seconds?.toString().padStart(2, "0");
return `${item.sets} x ${item.minutes || 0}:${seconds}`;
}, [item]);
const left = useCallback(() => {
if (!images || !item.image) return null;
return (
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
);
}, [item.image, images]);
const right = useCallback(() => {
return (
<Text
style={{
alignSelf: "center",
}}
>
<Menu
anchor={anchor}
visible={showMenu}
onDismiss={() => setShowMenu(false)}
>
<Menu.Item
leadingIcon="delete"
onPress={() => {
setShowRemove(item.name);
setShowMenu(false);
}}
title="Delete"
/>
</Menu>
</Text>
);
}, [anchor, showMenu, item.name]);
return (
<>
<List.Item
onPress={() => navigation.navigate("EditWorkout", { value: item })}
title={item.name}
description={description}
onLongPress={longPress}
left={left}
right={right}
/>
<ConfirmDialog
title={`Delete ${showRemove}`}
show={!!showRemove}
setShow={(show) => (show ? null : setShowRemove(""))}
onOk={remove}
>
This irreversibly deletes ALL sets related to this workout. Are you
sure?
</ConfirmDialog>
</>
);
}

122
WorkoutList.tsx Normal file
View File

@ -0,0 +1,122 @@
import {
NavigationProp,
useFocusEffect,
useNavigation,
} from "@react-navigation/native";
import { useCallback, useState } from "react";
import { FlatList } from "react-native";
import { List } from "react-native-paper";
import { setRepo, settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader";
import GymSet from "./gym-set";
import Page from "./Page";
import SetList from "./SetList";
import Settings from "./settings";
import WorkoutItem from "./WorkoutItem";
import { WorkoutsPageParams } from "./WorkoutsPage";
const limit = 15;
export default function WorkoutList() {
const [workouts, setWorkouts] = useState<GymSet[]>();
const [offset, setOffset] = useState(0);
const [term, setTerm] = useState("");
const [end, setEnd] = useState(false);
const [settings, setSettings] = useState<Settings>();
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
const refresh = useCallback(async (value: string) => {
const newWorkouts = await setRepo
.createQueryBuilder()
.select()
.where("name LIKE :name", { name: `%${value.trim()}%` })
.groupBy("name")
.orderBy("name")
.limit(limit)
.getMany();
console.log(`${WorkoutList.name}`, { newWorkout: newWorkouts[0] });
setWorkouts(newWorkouts);
setOffset(0);
setEnd(false);
}, []);
useFocusEffect(
useCallback(() => {
refresh(term);
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [refresh, term])
);
const renderItem = useCallback(
({ item }: { item: GymSet }) => (
<WorkoutItem
images={settings?.images}
item={item}
key={item.name}
onRemove={() => refresh(term)}
/>
),
[refresh, term, settings?.images]
);
const next = useCallback(async () => {
if (end) return;
const newOffset = offset + limit;
console.log(`${SetList.name}.next:`, {
offset,
limit,
newOffset,
term,
});
const newWorkouts = await setRepo
.createQueryBuilder()
.select()
.where("name LIKE :name", { name: `%${term.trim()}%` })
.groupBy("name")
.orderBy("name")
.limit(limit)
.offset(newOffset)
.getMany();
if (newWorkouts.length === 0) return setEnd(true);
if (!workouts) return;
setWorkouts([...workouts, ...newWorkouts]);
if (newWorkouts.length < limit) return setEnd(true);
setOffset(newOffset);
}, [term, end, offset, workouts]);
const onAdd = useCallback(async () => {
navigation.navigate("EditWorkout", {
value: new GymSet(),
});
}, [navigation]);
const search = useCallback(
(value: string) => {
setTerm(value);
refresh(value);
},
[refresh]
);
return (
<>
<DrawerHeader name="Workouts" />
<Page onAdd={onAdd} term={term} search={search}>
{workouts?.length === 0 ? (
<List.Item
title="No workouts yet."
description="A workout is something you do at the gym. For example Deadlifts are a workout."
/>
) : (
<FlatList
data={workouts}
style={{ flex: 1 }}
renderItem={renderItem}
keyExtractor={(w) => w.name}
onEndReached={next}
/>
)}
</Page>
</>
);
}

24
WorkoutsPage.tsx Normal file
View File

@ -0,0 +1,24 @@
import { createStackNavigator } from "@react-navigation/stack";
import EditWorkout from "./EditWorkout";
import GymSet from "./gym-set";
import WorkoutList from "./WorkoutList";
export type WorkoutsPageParams = {
WorkoutList: {};
EditWorkout: {
value: GymSet;
};
};
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.Navigator>
);
}

View File

@ -3,25 +3,25 @@ GEM
specs: specs:
CFPropertyList (3.0.6) CFPropertyList (3.0.6)
rexml rexml
addressable (2.8.6) 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.3.0) aws-eventstream (1.2.0)
aws-partitions (1.888.0) aws-partitions (1.780.0)
aws-sdk-core (3.191.1) aws-sdk-core (3.175.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.77.0) aws-sdk-kms (1.67.0)
aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-core (~> 3, >= 3.174.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.143.0) aws-sdk-s3 (1.126.0)
aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.4)
aws-sigv4 (1.8.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,12 +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.6.20240107) domain_name (0.5.20190701)
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.109.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)
@ -64,8 +65,8 @@ GEM
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.3.0) fastimage (2.2.7)
fastlane (2.219.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)
@ -84,22 +85,20 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
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)
multipart-post (>= 2.0.0, < 3.0.0) multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2) naturally (~> 2.2)
optparse (>= 0.1.1) optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 2.0.0, < 3.0.0)
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.54.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.3) 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)
@ -117,29 +116,31 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick
google-apis-iamcredentials_v1 (0.17.0) google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0) google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0) google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.1) google-cloud-core (1.6.0)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1) google-cloud-errors (1.3.1)
google-cloud-storage (1.47.0) google-cloud-storage (1.44.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0) google-apis-storage_v1 (~> 0.19.0)
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)
@ -148,30 +149,31 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.1) 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.4.0) multipart-post (2.3.0)
nanaimo (0.3.0) nanaimo (0.3.0)
naturally (2.2.1) naturally (2.2.1)
optparse (0.4.0) optparse (0.1.1)
os (1.1.4) os (1.1.4)
plist (3.7.1) plist (3.7.0)
public_suffix (5.0.4) public_suffix (5.0.1)
rake (13.1.0) 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)
@ -180,17 +182,21 @@ 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.2) tty-screen (0.8.1)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.5.0) unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.24.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)
@ -204,7 +210,6 @@ GEM
PLATFORMS PLATFORMS
ruby ruby
x64-mingw-ucrt
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES

View File

@ -1,7 +1,6 @@
apply plugin: "com.android.application" apply plugin: "com.android.application"
apply plugin: "com.facebook.react" apply plugin: "com.facebook.react"
apply plugin: "kotlin-android" apply plugin: "kotlin-android"
apply plugin: "org.jetbrains.kotlin.android"
/** /**
* This is the configuration block to customize your React Native Android app. * This is the configuration block to customize your React Native Android app.
@ -73,8 +72,7 @@ def jscFlavor = 'org.webkit:android-jsc:+'
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion compileSdkVersion rootProject.ext.compileSdkVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "com.massive" namespace "com.massive"
@ -87,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 36251 versionCode 36173
versionName "2.36" versionName "1.147"
} }
signingConfigs { signingConfigs {
release { release {
@ -126,12 +124,16 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin // The version of react-native is set by the React Native Gradle Plugin
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.4' 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')
implementation("com.facebook.react:flipper-integration")
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
if (hermesEnabled.toBoolean()) { if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android") implementation("com.facebook.react:hermes-android")
} else { } else {
@ -141,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"

View File

@ -7,5 +7,7 @@
<application <application
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="28" tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning"/> tools:ignore="GoogleAppIndexingWarning">
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false" />
</application>
</manifest> </manifest>

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.massive;
import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.react.ReactInstanceEventListener;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
import okhttp3.OkHttpClient;
/**
* Class responsible of loading Flipper inside your React Native application. This is the debug
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
if (FlipperUtils.shouldEnableFlipper(context)) {
final FlipperClient client = AndroidFlipperClient.getInstance(context);
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(CrashReporterPlugin.getInstance());
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
NetworkingModule.setCustomClientBuilder(
new NetworkingModule.CustomClientBuilder() {
@Override
public void apply(OkHttpClient.Builder builder) {
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
}
});
client.addPlugin(networkFlipperPlugin);
client.start();
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
// Hence we run if after all native modules have been initialized
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext == null) {
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext reactContext) {
reactInstanceManager.removeReactInstanceEventListener(this);
reactContext.runOnNativeModulesQueueThread(
new Runnable() {
@Override
public void run() {
client.addPlugin(new FrescoFlipperPlugin());
}
});
}
});
} else {
client.addPlugin(new FrescoFlipperPlugin());
}
}
}
}

View File

@ -1,60 +1,56 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission <uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE" android:name="android.permission.ACCESS_NETWORK_STATE"
tools:node="remove" /> tools:node="remove" />
<uses-permission <uses-permission android:name="com.google.android.gms.permission.AD_ID"
android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>
tools:node="remove" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:icon="@mipmap/ic_launcher" android:label="@string/app_name"
android:label="@string/app_name" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme"> android:allowBackup="false"
<activity android:theme="@style/AppTheme">
android:name=".MainActivity" <activity
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:name=".MainActivity"
android:exported="true" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize"
<intent-filter> android:exported="true">
<action android:name="android.intent.action.MAIN" /> <intent-filter>
<category android:name="android.intent.category.LAUNCHER" /> <action android:name="android.intent.action.MAIN" />
</intent-filter> <category android:name="android.intent.category.LAUNCHER" />
</activity> </intent-filter>
</activity>
<activity <activity
android:name=".TimerDone" android:name=".TimerDone"
android:exported="false"> android:exported="false">
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="" /> android:value="" />
</activity> </activity>
<activity <activity
android:name=".StopAlarm" android:name=".StopAlarm"
android:exported="true" android:exported="true"
android:process=":remote" /> android:process=":remote" />
<service <service
android:name=".TimerService" android:name=".AlarmService"
android:exported="false" android:exported="false" />
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="App does not require SCHEDULE_EXACT_ALARM or USE_EXACT_ALARM, but needs foreground service for foreground timer."/>
</service>
</application> </application>
</manifest> </manifest>

View File

@ -1,25 +1,191 @@
package com.massive package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.CountDownTimer
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlin.math.floor
@RequiresApi(Build.VERSION_CODES.O)
class AlarmModule(context: ReactApplicationContext?) : class AlarmModule constructor(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context) { ReactContextBaseJavaModule(context) {
private var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0
var running = false
override fun getName(): String { override fun getName(): String {
return "AlarmModule" return "AlarmModule"
} }
private val stopReceiver = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("AlarmModule", "Received stop broadcast intent")
stop()
}
}
private val addReceiver = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) {
add()
}
}
init {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
}
override fun onCatalystInstanceDestroy() {
reactApplicationContext.unregisterReceiver(stopReceiver)
reactApplicationContext.unregisterReceiver(addReceiver)
super.onCatalystInstanceDestroy()
}
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun timer(milliseconds: Int, description: String) { fun add() {
Log.d("AlarmModule", "Add 1 min to alarm.")
countdownTimer?.cancel()
val newMs = if (running) currentMs.toInt().plus(60000) else 60000
countdownTimer = getTimer(newMs)
countdownTimer?.start()
running = true
val manager = getManager()
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext.stopService(intent)
}
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod
fun stop() {
Log.d("AlarmModule", "Stop alarm.")
countdownTimer?.cancel()
running = false
val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext?.stopService(intent)
val manager = getManager()
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
manager.cancel(NOTIFICATION_ID_PENDING)
val params = Arguments.createMap().apply {
putString("minutes", "00")
putString("seconds", "00")
}
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("tick", params)
}
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod
fun timer(milliseconds: Int) {
Log.d("AlarmModule", "Queue alarm for $milliseconds delay") Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
val intent = Intent(reactApplicationContext, TimerService::class.java) val manager = getManager()
intent.putExtra("milliseconds", milliseconds) manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
intent.putExtra("description", description) val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext.startForegroundService(intent) reactApplicationContext.stopService(intent)
countdownTimer?.cancel()
countdownTimer = getTimer(milliseconds)
countdownTimer?.start()
running = true
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getTimer(
endMs: Int,
): CountDownTimer {
val builder = getBuilder()
return object : CountDownTimer(endMs.toLong(), 1000) {
@RequiresApi(Build.VERSION_CODES.O)
override fun onTick(current: Long) {
currentMs = current
val seconds =
floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0')
val minutes =
floor((current / 1000).toDouble() / 60).toInt().toString().padStart(2, '0')
builder.setContentText("$minutes:$seconds").setAutoCancel(false).setDefaults(0)
.setProgress(endMs, current.toInt(), false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS).priority =
NotificationCompat.PRIORITY_LOW
val manager = getManager()
manager.notify(NOTIFICATION_ID_PENDING, builder.build())
val params = Arguments.createMap().apply {
putString("minutes", minutes)
putString("seconds", seconds)
}
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("tick", params)
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onFinish() {
val context = reactApplicationContext
context.startForegroundService(Intent(context, AlarmService::class.java))
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("finish", Arguments.createMap().apply {
putString("minutes", "00")
putString("seconds", "00")
})
}
}
}
@SuppressLint("UnspecifiedImmutableFlag")
@RequiresApi(Build.VERSION_CODES.M)
private fun getBuilder(): NotificationCompat.Builder {
val context = reactApplicationContext
val contentIntent = Intent(context, MainActivity::class.java)
val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(ADD_BROADCAST).apply {
setPackage(context.packageName)
}
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(context.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, CHANNEL_ID_PENDING)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getManager(): NotificationManager {
val notificationManager = reactApplicationContext.getSystemService(
NotificationManager::class.java
)
val timersChannel = NotificationChannel(
CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW
)
timersChannel.setSound(null, null)
timersChannel.description = "Progress on rest timers."
notificationManager.createNotificationChannel(timersChannel)
return notificationManager
}
companion object {
const val STOP_BROADCAST = "stop-timer-event"
const val ADD_BROADCAST = "add-timer-event"
const val CHANNEL_ID_PENDING = "Timer"
const val NOTIFICATION_ID_PENDING = 1
} }
} }

View File

@ -0,0 +1,156 @@
package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.media.MediaPlayer.OnPreparedListener
import android.net.Uri
import android.os.*
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean)
@RequiresApi(Build.VERSION_CODES.O)
class AlarmService : Service(), OnPreparedListener {
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private fun getBuilder(): NotificationCompat.Builder {
val context = applicationContext
val contentIntent = Intent(context, MainActivity::class.java)
val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(AlarmModule.ADD_BROADCAST).apply {
setPackage(context.packageName)
}
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(AlarmModule.STOP_BROADCAST)
stopBroadcast.setPackage(context.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, AlarmModule.CHANNEL_ID_PENDING)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
@SuppressLint("Range")
private fun getSettings(): Settings {
val db = DatabaseHelper(applicationContext).readableDatabase
val cursor = db.rawQuery("SELECT sound, noSound, vibrate FROM settings", null)
cursor.moveToFirst()
val sound = cursor.getString(cursor.getColumnIndex("sound"))
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
cursor.close()
return Settings(sound, noSound, vibrate)
}
private fun playSound(settings: Settings) {
if (settings.sound == null && !settings.noSound) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
} else if (settings.sound != null && !settings.noSound) {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(applicationContext, Uri.parse(settings.sound))
prepare()
start()
setOnCompletionListener { vibrator?.cancel() }
}
}
}
private fun doNotify(): Notification {
val alarmsChannel = NotificationChannel(
CHANNEL_ID_DONE,
CHANNEL_ID_DONE,
NotificationManager.IMPORTANCE_HIGH
)
alarmsChannel.description = "Alarms for rest timers."
alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
alarmsChannel.setSound(null, null)
val manager = applicationContext.getSystemService(
NotificationManager::class.java
)
manager.createNotificationChannel(alarmsChannel)
val builder = getBuilder()
val context = applicationContext
val finishIntent = Intent(context, StopAlarm::class.java)
val finishPending = PendingIntent.getActivity(
context, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE
)
val fullIntent = Intent(context, TimerDone::class.java)
val fullPending = PendingIntent.getActivity(
context, 0, fullIntent, PendingIntent.FLAG_IMMUTABLE
)
builder.setContentText("Timer finished.").setProgress(0, 0, false)
.setAutoCancel(true).setOngoing(true).setFullScreenIntent(fullPending, true)
.setContentIntent(finishPending).setChannelId(CHANNEL_ID_DONE)
.setCategory(NotificationCompat.CATEGORY_ALARM).priority =
NotificationCompat.PRIORITY_HIGH
val notification = builder.build()
manager.notify(NOTIFICATION_ID_DONE, notification)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
return notification
}
@SuppressLint("Recycle")
@RequiresApi(api = Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val notification = doNotify()
startForeground(NOTIFICATION_ID_DONE, notification)
val settings = getSettings()
playSound(settings)
if (!settings.vibrate) return START_STICKY
val pattern = longArrayOf(0, 300, 1300, 300, 1300, 300)
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
getSystemService(VIBRATOR_SERVICE) as Vibrator
}
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ALARM)
.build()
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, 1), audioAttributes)
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onPrepared(player: MediaPlayer) {
player.start()
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer?.stop()
mediaPlayer?.release()
vibrator?.cancel()
}
companion object {
const val CHANNEL_ID_DONE = "Alarm"
const val NOTIFICATION_ID_DONE = 2
}
}

View File

@ -1,29 +1,26 @@
package com.massive package com.massive
import android.annotation.SuppressLint
import android.app.AlarmManager import android.app.AlarmManager
import android.app.PendingIntent 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.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
import java.io.* import java.io.*
import java.util.* import java.util.*
@SuppressLint("UnspecifiedRegisterReceiverFlag") class BackupModule constructor(context: ReactApplicationContext?) :
class BackupModule(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)
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")
@ -38,36 +35,14 @@ class BackupModule(context: ReactApplicationContext?) :
} }
} }
@ReactMethod @RequiresApi(Build.VERSION_CODES.M)
fun once(target: String, promise: Promise) {
Log.d("BackupModule", "once $target")
try {
val treeUri: Uri = Uri.parse(target)
val documentFile = context.let { DocumentFile.fromTreeUri(it, treeUri) }
val file = documentFile?.createFile("application/octet-stream", "massive.db")
val output = context.contentResolver?.openOutputStream(file!!.uri)
val sourceFile = File(context.getDatabasePath("massive.db")!!.path)
val input = FileInputStream(sourceFile)
if (output != null) {
input.copyTo(output)
}
output?.flush()
output?.close()
promise.resolve(0)
}
catch (error: Exception) {
promise.reject("ERROR", error)
}
}
@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 {
@ -85,7 +60,8 @@ class BackupModule(context: ReactApplicationContext?) :
) )
} }
@ReactMethod(isBlockingSynchronousMethod = true) @RequiresApi(Build.VERSION_CODES.M)
@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)
@ -94,38 +70,8 @@ class BackupModule(context: ReactApplicationContext?) :
alarmMgr.cancel(pendingIntent) alarmMgr.cancel(pendingIntent)
} }
@ReactMethod
fun exportPlans(target: String, promise: Promise) {
try {
val db = DatabaseHelper(reactApplicationContext)
db.exportPlans(target, reactApplicationContext)
promise.resolve("Export successful!")
}
catch (e: Exception) {
promise.reject("ERROR", e)
}
}
@ReactMethod
fun exportSets(target: String, promise: Promise) {
try {
val db = DatabaseHelper(reactApplicationContext)
db.exportSets(target, reactApplicationContext)
promise.resolve("Export successful!")
}
catch (e: Exception) {
promise.reject("ERROR", e)
}
}
init { init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { reactApplicationContext.registerReceiver(copyReceiver, IntentFilter(COPY_BROADCAST))
reactApplicationContext.registerReceiver(copyReceiver, IntentFilter(COPY_BROADCAST),
Context.RECEIVER_NOT_EXPORTED)
}
else {
reactApplicationContext.registerReceiver(copyReceiver, IntentFilter(COPY_BROADCAST))
}
} }
companion object { companion object {

View File

@ -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.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.opencsv.CSVWriter
class DatabaseHelper(context: Context) : class DatabaseHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
@ -15,74 +11,6 @@ class DatabaseHelper(context: Context) :
private const val DATABASE_VERSION = 1 private const val DATABASE_VERSION = 1
} }
fun exportSets(target: String, context: Context) {
Log.d("DatabaseHelper", "exportSets $target")
val treeUri: Uri = Uri.parse(target)
val documentFile = context.let { DocumentFile.fromTreeUri(it, treeUri) }
val file = documentFile?.createFile("application/octet-stream", "sets.csv") ?: return
context.contentResolver.openOutputStream(file.uri).use { outputStream ->
val csvWrite = CSVWriter(outputStream?.writer())
val db = this.readableDatabase
val setCursor = db.rawQuery("SELECT * FROM sets", null)
csvWrite.writeNext(setCursor.columnNames)
var lastId = 0
while(setCursor.moveToNext()) {
val arrStr = arrayOfNulls<String>(setCursor.columnCount)
for(i in 0 until setCursor.columnCount) {
arrStr[i] = setCursor.getString(i)
}
val id = arrStr[0]?.toInt()
if (id != null && id > lastId) lastId = id
csvWrite.writeNext(arrStr)
}
val weightCursor = db.rawQuery("SELECT * FROM weights", null)
while (weightCursor.moveToNext()) {
val arrStr = arrayOfNulls<String>(setCursor.columnCount)
arrStr[0] = lastId++.toString()
arrStr[1] = "Weight"
arrStr[2] = "1"
arrStr[3] = weightCursor.getString(1)
arrStr[4] = weightCursor.getString(2)
arrStr[5] = "kg"
csvWrite.writeNext(arrStr)
}
csvWrite.close()
setCursor.close()
weightCursor.close()
}
}
fun exportPlans(target: String, context: Context) {
Log.d("DatabaseHelper", "exportPlans $target")
val treeUri: Uri = Uri.parse(target)
val documentFile = context.let { DocumentFile.fromTreeUri(it, treeUri) }
val file = documentFile?.createFile("application/octet-stream", "plans.csv") ?: return
context.contentResolver.openOutputStream(file.uri).use { outputStream ->
val csvWrite = CSVWriter(outputStream?.writer())
val db = this.readableDatabase
val cursor = db.rawQuery("SELECT * FROM plans", 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) {
} }

View File

@ -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
@ -11,7 +10,9 @@ class MainActivity : ReactActivity() {
* Returns the name of the main component registered from JavaScript. This is used to schedule * Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component. * rendering of the component.
*/ */
override fun getMainComponentName(): String = "massive" override fun getMainComponentName(): String? {
return "massive"
}
/** /**
* Returns the instance of the [ReactActivityDelegate]. Here we use a util class [ ] which allows you to easily enable Fabric and Concurrent React * Returns the instance of the [ReactActivityDelegate]. Here we use a util class [ ] which allows you to easily enable Fabric and Concurrent React
@ -20,12 +21,8 @@ class MainActivity : ReactActivity() {
override fun createReactActivityDelegate(): ReactActivityDelegate { override fun createReactActivityDelegate(): ReactActivityDelegate {
return DefaultReactActivityDelegate( return DefaultReactActivityDelegate(
this, this,
mainComponentName, // If you opted-in for the New Architecture, we enable the Fabric Renderer. mainComponentName!!, // If you opted-in for the New Architecture, we enable the Fabric Renderer.
fabricEnabled fabricEnabled
) )
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}
} }

View File

@ -3,39 +3,41 @@ package com.massive
import android.app.Application import android.app.Application
import com.facebook.react.PackageList import com.facebook.react.PackageList
import com.facebook.react.ReactApplication import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication { class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = private val mReactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) {
object : DefaultReactNativeHost(this) { override fun getUseDeveloperSupport(): Boolean {
override fun getPackages(): List<ReactPackage> = return BuildConfig.DEBUG
PackageList(this).packages.apply {
add(MassivePackage())
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
} }
override val reactHost: ReactHost override fun getPackages(): List<ReactPackage> {
get() = getDefaultReactHost(this.applicationContext, reactNativeHost) val packages: MutableList<ReactPackage> = PackageList(this).packages
packages.add(MassivePackage())
return packages
}
override fun getJSMainModuleName(): String {
return "index"
}
override val isNewArchEnabled: Boolean
protected get() = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean
protected get() = BuildConfig.IS_HERMES_ENABLED
}
override fun getReactNativeHost(): ReactNativeHost {
return mReactNativeHost
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
SoLoader.init(this, false) SoLoader.init(this, /* native exopackage */false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app. // If you opted-in for the New Architecture, we load the native entry point for this app.
load() load()

View File

@ -21,13 +21,13 @@ class SettingsModule constructor(context: ReactApplicationContext?) :
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
@ReactMethod @ReactMethod
fun ignoringBattery(promise: Promise) { fun ignoringBattery(callback: Callback) {
val packageName = reactApplicationContext.packageName val packageName = reactApplicationContext.packageName
val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
promise.resolve(pm.isIgnoringBatteryOptimizations(packageName)) callback.invoke(pm.isIgnoringBatteryOptimizations(packageName))
} else { } else {
promise.resolve(true) callback.invoke(true)
} }
} }

View File

@ -1,11 +1,17 @@
package com.massive package com.massive
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.View
import android.view.WindowManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.massive.AlarmService
import com.massive.MainActivity
class StopAlarm : Activity() { class StopAlarm : Activity() {
@RequiresApi(Build.VERSION_CODES.O_MR1) @RequiresApi(Build.VERSION_CODES.O_MR1)
@ -13,7 +19,7 @@ class StopAlarm : Activity() {
Log.d("AlarmActivity", "Call to AlarmActivity") Log.d("AlarmActivity", "Call to AlarmActivity")
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val context = applicationContext val context = applicationContext
context.stopService(Intent(context, TimerService::class.java)) context.stopService(Intent(context, AlarmService::class.java))
savedInstanceState.apply { setShowWhenLocked(true) } savedInstanceState.apply { setShowWhenLocked(true) }
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

View File

@ -1,171 +0,0 @@
package com.massive
import android.app.AlarmManager
import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.SystemClock
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
import android.widget.Toast
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.O)
class Timer(private var msTimerDuration: Long) {
enum class State {
Running,
Paused,
Expired
}
fun start(context: Context) {
if (state != State.Paused) return
endTime = SystemClock.elapsedRealtime() + msTimerDuration
registerPendingIntent(context)
state = State.Running
}
fun stop(context: Context) {
if (state != State.Running) return
msTimerDuration = endTime - SystemClock.elapsedRealtime()
unregisterPendingIntent(context)
state = State.Paused
}
fun expire() {
state = State.Expired
msTimerDuration = 0
totalTimerDuration = 0
}
fun getRemainingSeconds(): Int {
return (getRemainingMillis() / 1000).toInt()
}
fun increaseDuration(context: Context, milli: Long) {
val wasRunning = isRunning()
if (wasRunning) stop(context)
msTimerDuration += milli
totalTimerDuration += milli
if (wasRunning) start(context)
}
fun isExpired(): Boolean {
return state == State.Expired
}
fun getDurationSeconds(): Int {
return (totalTimerDuration / 1000).toInt()
}
fun getRemainingMillis(): Long {
return if (state == State.Running) endTime - SystemClock.elapsedRealtime()
else
msTimerDuration
}
private fun isRunning(): Boolean {
return state == State.Running
}
private fun requestPermission(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
intent.data = Uri.parse("package:" + context.packageName)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return try {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context2: Context?, intent: Intent?) {
context.unregisterReceiver(this)
registerPendingIntent(context)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(
receiver,
IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED),
Context.RECEIVER_NOT_EXPORTED
)
} else {
context.registerReceiver(
receiver,
IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED)
)
}
context.startActivity(intent)
false
} catch (e: ActivityNotFoundException) {
Toast.makeText(
context,
"Request for SCHEDULE_EXACT_ALARM rejected on your device",
Toast.LENGTH_LONG
).show()
false
}
}
private fun incorrectPermissions(context: Context, alarmManager: AlarmManager): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& !alarmManager.canScheduleExactAlarms()
&& !requestPermission(context)
}
private fun getAlarmManager(context: Context): AlarmManager {
return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
}
private fun unregisterPendingIntent(context: Context) {
val intent = Intent(context, TimerService::class.java)
.setAction(TimerService.TIMER_EXPIRED)
val pendingIntent = PendingIntent.getService(
context,
0,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = getAlarmManager(context)
if (incorrectPermissions(context, alarmManager)) return
alarmManager.cancel(pendingIntent)
pendingIntent.cancel()
}
private fun registerPendingIntent(context: Context) {
val intent = Intent(context, TimerService::class.java)
.setAction(TimerService.TIMER_EXPIRED)
val pendingIntent = PendingIntent.getService(
context,
0,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = getAlarmManager(context)
if (incorrectPermissions(context, alarmManager)) return
alarmManager.setExactAndAllowWhileIdle(
ELAPSED_REALTIME_WAKEUP,
endTime,
pendingIntent
)
}
private var endTime: Long = 0
private var totalTimerDuration: Long = msTimerDuration
private var state: State = State.Paused
companion object {
fun emptyTimer(): Timer {
return Timer(0)
}
const val ONE_MINUTE_MILLI: Long = 60000
}
}

View File

@ -1,5 +1,8 @@
package com.massive package com.massive
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -7,24 +10,49 @@ import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
class TimerDone : AppCompatActivity() { class TimerDone : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_timer_done) setContentView(R.layout.activity_timer_done)
Log.d("TimerDone", "Rendered.")
} }
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun stop(view: View) { fun stop(view: View) {
Log.d("TimerDone", "Stopping...") Log.d("TimerDone", "Stopping...")
applicationContext.stopService(Intent(applicationContext, TimerService::class.java)) applicationContext.stopService(Intent(applicationContext, AlarmService::class.java))
val manager = NotificationManagerCompat.from(this) val manager = getManager()
manager.cancel(TimerService.ONGOING_ID) manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
val intent = Intent(applicationContext, MainActivity::class.java) val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext.startActivity(intent) applicationContext.startActivity(intent)
} }
@RequiresApi(Build.VERSION_CODES.O)
fun getManager(): NotificationManager {
val alarmsChannel = NotificationChannel(
AlarmService.CHANNEL_ID_DONE,
AlarmService.CHANNEL_ID_DONE,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Alarms for rest timers."
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
val timersChannel = NotificationChannel(
AlarmModule.CHANNEL_ID_PENDING,
AlarmModule.CHANNEL_ID_PENDING,
NotificationManager.IMPORTANCE_LOW
).apply {
setSound(null, null)
description = "Progress on rest timers."
}
val notificationManager = applicationContext.getSystemService(
NotificationManager::class.java
)
notificationManager.createNotificationChannel(alarmsChannel)
notificationManager.createNotificationChannel(timersChannel)
return notificationManager
}
} }

View File

@ -1,358 +0,0 @@
package com.massive
import android.Manifest
import android.annotation.SuppressLint
import android.app.*
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.net.Uri
import android.os.*
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long)
@RequiresApi(Build.VERSION_CODES.O)
class TimerService : Service() {
private lateinit var timerHandler: Handler
private var timerRunnable: Runnable? = null
private var timer: Timer = Timer.emptyTimer()
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private var currentDescription = ""
private val stopReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("TimerService", "Received stop broadcast intent")
timer.stop(applicationContext)
timer.expire()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
private val addReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
timer.increaseDuration(applicationContext, Timer.ONE_MINUTE_MILLI)
updateNotification(timer.getRemainingSeconds())
mediaPlayer?.stop()
vibrator?.cancel()
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onCreate() {
super.onCreate()
timerHandler = Handler(Looper.getMainLooper())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
applicationContext.registerReceiver(
stopReceiver, IntentFilter(STOP_BROADCAST),
Context.RECEIVER_NOT_EXPORTED
)
applicationContext.registerReceiver(
addReceiver, IntentFilter(ADD_BROADCAST),
Context.RECEIVER_NOT_EXPORTED
)
} else {
applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
applicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
}
}
private fun onTimerStart(intent: Intent?) {
timerRunnable?.let { timerHandler.removeCallbacks(it) }
currentDescription = intent?.getStringExtra("description").toString()
timer.stop(applicationContext)
timer = Timer((intent?.getIntExtra("milliseconds", 0) ?: 0).toLong())
timer.start(applicationContext)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
ONGOING_ID,
getProgress(timer.getRemainingSeconds()).build(),
FOREGROUND_SERVICE_TYPE_SPECIAL_USE
)
} else {
startForeground(ONGOING_ID, getProgress(timer.getRemainingSeconds()).build())
}
battery()
Log.d("TimerService", "onTimerStart seconds=${timer.getDurationSeconds()}")
timerRunnable = object : Runnable {
override fun run() {
val startTime = SystemClock.elapsedRealtime()
if (timer.isExpired()) return
updateNotification(timer.getRemainingSeconds())
val delay = timer.getRemainingMillis() % 1000
timerHandler.postDelayed(this, if (SystemClock.elapsedRealtime() - startTime + delay > 980) 20 else delay)
}
}
timerHandler.postDelayed(timerRunnable!!, 20)
}
private fun onTimerExpired() {
Log.d("TimerService", "onTimerExpired duration=${timer.getDurationSeconds()}")
timer.expire()
val settings = getSettings()
vibrate(settings)
playSound(settings)
notifyFinished()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null && intent.action == TIMER_EXPIRED) onTimerExpired()
else onTimerStart(intent)
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
timerRunnable?.let { timerHandler.removeCallbacks(it) }
applicationContext.unregisterReceiver(stopReceiver)
applicationContext.unregisterReceiver(addReceiver)
mediaPlayer?.stop()
mediaPlayer?.release()
vibrator?.cancel()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
@SuppressLint("BatteryLife")
fun battery() {
val powerManager =
applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
val ignoring =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
powerManager.isIgnoringBatteryOptimizations(
applicationContext.packageName
)
else true
if (ignoring) return
val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:" + applicationContext.packageName)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
try {
applicationContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
applicationContext,
"Requests to ignore battery optimizations are disabled on your device.",
Toast.LENGTH_LONG
).show()
}
}
@SuppressLint("Range")
private fun getSettings(): Settings {
val db = DatabaseHelper(applicationContext).readableDatabase
val cursor = db.rawQuery("SELECT sound, noSound, vibrate, duration FROM settings", null)
cursor.moveToFirst()
val sound = cursor.getString(cursor.getColumnIndex("sound"))
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
var duration = cursor.getLong(cursor.getColumnIndex("duration"))
if (duration.toInt() == 0) duration = 300
cursor.close()
return Settings(sound, noSound, vibrate, duration)
}
private fun playSound(settings: Settings) {
if (settings.noSound) return
if (settings.sound == null) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
} else {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(applicationContext, Uri.parse(settings.sound))
prepare()
start()
setOnCompletionListener { vibrator?.cancel() }
}
}
}
private fun getProgress(timeLeftInSeconds: Int): NotificationCompat.Builder {
val notificationText = formatTime(timeLeftInSeconds)
val notificationChannelId = "timer_channel"
val notificationIntent = Intent(this, MainActivity::class.java)
val contentPending = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(applicationContext.packageName)
val stopPending =
PendingIntent.getBroadcast(
applicationContext,
0,
stopBroadcast,
PendingIntent.FLAG_IMMUTABLE
)
val addBroadcast =
Intent(ADD_BROADCAST).apply { setPackage(applicationContext.packageName) }
val addPending =
PendingIntent.getBroadcast(
applicationContext,
0,
addBroadcast,
PendingIntent.FLAG_MUTABLE
)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setContentTitle(currentDescription)
.setContentText(notificationText)
.setSmallIcon(R.drawable.ic_baseline_timer_24)
.setProgress(timer.getDurationSeconds(), timeLeftInSeconds, false)
.setContentIntent(contentPending)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setDeleteIntent(stopPending)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", stopPending)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", addPending)
val notificationManager = NotificationManagerCompat.from(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
notificationChannelId,
"Timer Channel",
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
return notificationBuilder
}
private fun vibrate(settings: Settings) {
if (!settings.vibrate) return
val pattern =
longArrayOf(0, settings.duration, 1000, settings.duration, 1000, settings.duration)
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
getSystemService(VIBRATOR_SERVICE) as Vibrator
}
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, 2))
}
private fun notifyFinished() {
val channelId = "finished_channel"
val notificationManager = NotificationManagerCompat.from(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
channelId,
"Timer Finished Channel",
NotificationManager.IMPORTANCE_HIGH
)
channel.setSound(null, null)
channel.setBypassDnd(true)
channel.enableVibration(false)
channel.description = "Plays an alarm when a rest timer completes."
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
notificationManager.createNotificationChannel(channel)
}
val fullIntent = Intent(applicationContext, TimerDone::class.java)
val fullPending = PendingIntent.getActivity(
applicationContext, 0, fullIntent, PendingIntent.FLAG_MUTABLE
)
val finishIntent = Intent(applicationContext, StopAlarm::class.java)
val finishPending = PendingIntent.getActivity(
applicationContext, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE
)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(applicationContext.packageName)
val pendingStop =
PendingIntent.getBroadcast(
applicationContext,
0,
stopBroadcast,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(this, channelId)
.setContentTitle("Timer finished")
.setContentText(currentDescription)
.setSmallIcon(R.drawable.ic_baseline_timer_24)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(finishPending)
.setFullScreenIntent(fullPending, true)
.setAutoCancel(true)
.setDeleteIntent(pendingStop)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(FINISHED_ID, builder.build())
}
private fun updateNotification(seconds: Int) {
val notificationManager = NotificationManagerCompat.from(this)
val notification = getProgress(seconds)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(ONGOING_ID, notification.build())
}
private fun formatTime(timeInSeconds: Int): String {
val minutes = timeInSeconds / 60
val seconds = timeInSeconds % 60
return String.format("%02d:%02d", minutes, seconds)
}
companion object {
const val STOP_BROADCAST = "stop-timer-event"
const val ADD_BROADCAST = "add-timer-event"
const val TIMER_EXPIRED = "timer-expired-event"
const val ONGOING_ID = 1
const val FINISHED_ID = 1
}
}

View File

@ -5,6 +5,70 @@
android:viewportHeight="108" android:viewportHeight="108"
android:viewportWidth="108" android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#1d1f21" <path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/> android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector> </vector>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.massive;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
/**
* Class responsible of loading Flipper inside your React Native application. This is the release
* flavor of it so it's empty as we don't want to load Flipper.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
// Do nothing as we don't want to initialize Flipper on Release.
}
}

View File

@ -2,14 +2,13 @@
buildscript { buildscript {
ext { ext {
buildToolsVersion = "34.0.0" buildToolsVersion = "33.0.0"
minSdkVersion = 21 minSdkVersion = 21
compileSdkVersion = 34 compileSdkVersion = 33
targetSdkVersion = 34 targetSdkVersion = 33
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "25.1.8937393" ndkVersion = "23.1.7779620"
kotlinVersion = "1.8.0"
} }
repositories { repositories {
google() google()
@ -18,8 +17,6 @@ buildscript {
dependencies { dependencies {
classpath("com.android.tools.build:gradle") classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin") classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
} }
} }
apply plugin: "com.facebook.react.rootproject"

View File

@ -24,6 +24,9 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.182.0
# Use this property to specify which architecture you want to build. # Use this property to specify which architecture you want to build.
# You can also override it from the CLI using # You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64 # ./gradlew <task> -PreactNativeArchitectures=x86_64

View File

@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
validateDistributionUrl=true
networkTimeout=10000 networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

19
android/gradlew vendored
View File

@ -83,8 +83,10 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -131,13 +133,10 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@ -145,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -153,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -198,10 +197,6 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in # shell script including quotes and variable substitutions, so put them in

View File

@ -1,9 +1,9 @@
module.exports = { module.exports = {
presets: ['module:@react-native/babel-preset'], presets: ['module:metro-react-native-babel-preset'],
plugins: [ plugins: [
'@babel/plugin-transform-flow-strip-types', '@babel/plugin-transform-flow-strip-types',
['@babel/plugin-proposal-decorators', { legacy: true }], ['@babel/plugin-proposal-decorators', {legacy: true}],
['@babel/plugin-proposal-class-properties', { loose: true }], ['@babel/plugin-proposal-class-properties', {loose: true}],
'react-native-paper/babel', 'react-native-paper/babel',
'react-native-reanimated/plugin', 'react-native-reanimated/plugin',
], ],

View File

@ -1,4 +1,3 @@
import { LIMIT } from "./constants";
import { setRepo } from "./db"; import { setRepo } from "./db";
import GymSet from "./gym-set"; import GymSet from "./gym-set";
@ -14,29 +13,3 @@ export const getBestSet = async (name: string): Promise<GymSet> => {
.addOrderBy("reps", "DESC") .addOrderBy("reps", "DESC")
.getOne(); .getOne();
}; };
export const getBestSets = ({
term: term,
offset,
}: {
term: string;
offset?: number;
}) => {
return setRepo
.createQueryBuilder("gym_set")
.select(["gym_set.name", "gym_set.reps", "gym_set.weight"])
.groupBy("gym_set.name")
.innerJoin(
(qb) =>
qb
.select(["gym_set2.name", "MAX(gym_set2.weight) AS max_weight"])
.from(GymSet, "gym_set2")
.where("gym_set2.name LIKE (:name)", { name: `%${term.trim()}%` })
.groupBy("gym_set2.name"),
"subquery",
"gym_set.name = subquery.gym_set2_name AND gym_set.weight = subquery.max_weight"
)
.limit(LIMIT)
.offset(offset || 0)
.getMany();
};

View File

@ -1,27 +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: "Green" },
{ hex: "#FFD700", name: "Gold" },
{ hex: "#00CED1", name: "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: "#990000", name: "Red" },
{ hex: "#660066", name: "Magenta" },
]; ];
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}`;
};

View File

@ -3,6 +3,3 @@ 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;

View File

@ -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;
}

View File

@ -25,27 +25,14 @@ import { addNoSound1667186456118 } from "./migrations/1667186456118-add-no-sound
import { dropMigrations1667190214743 } from "./migrations/1667190214743-drop-migrations"; import { dropMigrations1667190214743 } from "./migrations/1667190214743-drop-migrations";
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 { 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";
import { leadingZeros1707094662099 } from "./migrations/1707094662099-leading-zeros";
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,17 +61,5 @@ export const AppDataSource = new DataSource({
dropMigrations1667190214743, dropMigrations1667190214743,
splitColor1669420187764, splitColor1669420187764,
addBackup1678334268359, addBackup1678334268359,
planTitle1692654882408,
weight1697766633971,
exercises1699508495726,
exercisesFix1699613077628,
settingsDuration1699743753975,
settingsStartup1699783784680,
settingsBackupDir1699839054226,
homeHistoryStartup1699853245534,
autoConvert1699948105001,
settingsDefaultSets1700009253976,
settingsDefaults1700009729468,
leadingZeros1707094662099,
], ],
}); });

2
db.ts
View File

@ -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(

View File

@ -1,47 +0,0 @@
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import simpleGit from 'simple-git';
import os from 'os';
execSync('npx tsc', { stdio: 'inherit' });
let build = readFileSync('android/app/build.gradle', 'utf8');
const codeMatch = build.match(/versionCode (\d+)/);
if (!codeMatch) throw new Error('versionCode not found in build.gradle');
const versionCode = parseInt(codeMatch[1], 10) + 1;
build = build.replace(/versionCode \d+/, `versionCode ${versionCode}`);
const nameMatch = build.match(/versionName "(\d+\.\d+)"/);
if (!nameMatch) throw new Error('versionName not found in build.gradle');
const versionParts = nameMatch[1].split('.');
versionParts[1] = (parseInt(versionParts[1], 10) + 1).toString();
const versionName = versionParts.join('.');
build = build.replace(/versionName "\d+\.\d+"/, `versionName "${versionName}"`);
writeFileSync('android/app/build.gradle', build);
let packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
packageJson.version = versionName;
writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
const git = simpleGit();
await git.add(['package.json', 'android/app/build.gradle']);
await git.log(['-1']).then(log => {
const newTitle = `${log.latest.message} - ${versionName} 🚀`;
console.log(newTitle);
const message = [newTitle, log.latest.body].join('\n');
return git.commit(message, [], ['--amend']);
}).then(() => {
return git.addTag(versionCode.toString());
}).then(() => {
return git.push('origin', 'HEAD', ['--tags', '--force']);
}).catch(err => {
console.error('Error amending commit:', err);
});
process.chdir('android')
const isWindows = os.platform() === 'win32';
execSync(isWindows ? '.\\gradlew.bat bundleRelease -q' : './gradlew bundleRelease -q', { stdio: 'inherit' });
execSync('bundle install --quiet', { stdio: 'inherit' });
execSync('bundle exec fastlane supply --aab app/build/outputs/bundle/release/app-release.aab', { stdio: 'inherit' });

39
deploy.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
set -ex
cd android || exit 1
build=app/build.gradle
versionCode=$(
grep '^\s*versionCode [0-9]*$' "$build" | awk '{print $2+1}'
)
major=$(
grep '^\s*versionName "[0-9]*\.[0-9]*"' "$build" |
sed 's/"//g' | cut -d '.' -f 1 | awk '{print $2}'
)
minor=$(
grep '^\s*versionName "[0-9]*\.[0-9]*"' "$build" |
sed 's/"//g' | cut -d '.' -f 2
)
minor=$((minor + 1))
sed -i "s/\(^\s*\)versionCode [0-9]*$/\1versionCode $versionCode/" \
"$build"
sed -i "s/\(^\s*\)versionName \"[0-9]*.[0-9]*\"$/\1versionName \"$major.$minor\"/" "$build"
sed -i "s/\"version\": \"[0-9]*.[0-9]*\"/\"version\": \"$major.$minor\"/" ../package.json
if [ "$1" != "-n" ]; then
yarn tsc
yarn lint
./gradlew bundleRelease
bundle install
bundle exec fastlane supply --aab app/build/outputs/bundle/release/app-release.aab
fi
git add app/build.gradle ../package.json
git commit --amend --message \
"$(git log -1 --pretty=%B | sed " 1 s/.*/& - $major.$minor/")"
git tag "$versionCode"
git push origin HEAD
git push --tags

8
drawer-param-list.ts Normal file
View File

@ -0,0 +1,8 @@
export type DrawerParamList = {
Home: {};
Settings: {};
Graphs: {};
Plans: {};
Workouts: {};
Timer: {};
};

View File

@ -1,11 +0,0 @@
export type DrawerParams = {
History: {};
Exercises: {};
Plans: {};
Graphs: {};
Timer: {};
Weight: {};
Insights: {};
Settings: {};
Daily: {};
};

View File

@ -1,3 +0,0 @@
import { NativeEventEmitter, NativeModules } from "react-native";
export const emitter = new NativeEventEmitter(NativeModules.Emitter);

View File

@ -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: "",

11
home-page-params.ts Normal file
View File

@ -0,0 +1,11 @@
import GymSet from "./gym-set";
export type HomePageParams = {
Sets: {};
EditSet: {
set: GymSet;
};
EditSets: {
ids: number[];
};
};

View File

@ -25,11 +25,17 @@ if linkage != nil
use_frameworks! :linkage => linkage.to_sym use_frameworks! :linkage => linkage.to_sym
end end
target 'RnDiffApp' do target 'massive' do
config = use_native_modules! config = use_native_modules!
# Flags change depending on the env values.
flags = get_default_flags()
use_react_native!( use_react_native!(
:path => config[:reactNativePath], :path => config[:reactNativePath],
# Hermes is now enabled by default. Disable by setting this flag to false.
:hermes_enabled => flags[:hermes_enabled],
:fabric_enabled => flags[:fabric_enabled],
# Enables Flipper. # Enables Flipper.
# #
# Note that if you have use_frameworks! enabled, Flipper will not work and # Note that if you have use_frameworks! enabled, Flipper will not work and
@ -39,7 +45,7 @@ target 'RnDiffApp' do
:app_path => "#{Pod::Config.instance.installation_root}/.." :app_path => "#{Pod::Config.instance.installation_root}/.."
) )
target 'RnDiffAppTests' do target 'massiveTests' do
inherit! :complete inherit! :complete
# Pods for testing # Pods for testing
end end
@ -51,5 +57,6 @@ target 'RnDiffApp' do
config[:reactNativePath], config[:reactNativePath],
:mac_catalyst_enabled => false :mac_catalyst_enabled => false
) )
__apply_Xcode_12_5_M1_post_install_workaround(installer)
end end
end end

View File

@ -7,12 +7,12 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* RnDiffAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* RnDiffAppTests.m */; }; 00E356F31AD99517003FC87E /* massiveTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* massiveTests.m */; };
0C80B921A6F3F58F76C31292 /* libPods-RnDiffApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-RnDiffApp.a */; }; 0C80B921A6F3F58F76C31292 /* libPods-massive.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-massive.a */; };
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
7699B88040F8A987B510C191 /* libPods-RnDiffApp-RnDiffAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-RnDiffApp-RnDiffAppTests.a */; }; 7699B88040F8A987B510C191 /* libPods-massive-massiveTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-massive-massiveTests.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -22,27 +22,27 @@
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1; proxyType = 1;
remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
remoteInfo = RnDiffApp; remoteInfo = massive;
}; };
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
00E356EE1AD99517003FC87E /* RnDiffAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RnDiffAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356EE1AD99517003FC87E /* massiveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = massiveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* RnDiffAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RnDiffAppTests.m; sourceTree = "<group>"; }; 00E356F21AD99517003FC87E /* massiveTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = massiveTests.m; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* RnDiffApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RnDiffApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* massive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = massive.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = RnDiffApp/AppDelegate.h; sourceTree = "<group>"; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = massive/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RnDiffApp/AppDelegate.mm; sourceTree = "<group>"; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = massive/AppDelegate.mm; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RnDiffApp/Images.xcassets; sourceTree = "<group>"; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = massive/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RnDiffApp/Info.plist; sourceTree = "<group>"; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = massive/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = RnDiffApp/main.m; sourceTree = "<group>"; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = massive/main.m; sourceTree = "<group>"; };
19F6CBCC0A4E27FBF8BF4A61 /* libPods-RnDiffApp-RnDiffAppTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RnDiffApp-RnDiffAppTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 19F6CBCC0A4E27FBF8BF4A61 /* libPods-massive-massiveTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-massive-massiveTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
3B4392A12AC88292D35C810B /* Pods-RnDiffApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RnDiffApp.debug.xcconfig"; path = "Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp.debug.xcconfig"; sourceTree = "<group>"; }; 3B4392A12AC88292D35C810B /* Pods-massive.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-massive.debug.xcconfig"; path = "Target Support Files/Pods-massive/Pods-massive.debug.xcconfig"; sourceTree = "<group>"; };
5709B34CF0A7D63546082F79 /* Pods-RnDiffApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RnDiffApp.release.xcconfig"; path = "Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp.release.xcconfig"; sourceTree = "<group>"; }; 5709B34CF0A7D63546082F79 /* Pods-massive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-massive.release.xcconfig"; path = "Target Support Files/Pods-massive/Pods-massive.release.xcconfig"; sourceTree = "<group>"; };
5B7EB9410499542E8C5724F5 /* Pods-RnDiffApp-RnDiffAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RnDiffApp-RnDiffAppTests.debug.xcconfig"; path = "Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests.debug.xcconfig"; sourceTree = "<group>"; }; 5B7EB9410499542E8C5724F5 /* Pods-massive-massiveTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-massive-massiveTests.debug.xcconfig"; path = "Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests.debug.xcconfig"; sourceTree = "<group>"; };
5DCACB8F33CDC322A6C60F78 /* libPods-RnDiffApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RnDiffApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 5DCACB8F33CDC322A6C60F78 /* libPods-massive.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-massive.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RnDiffApp/LaunchScreen.storyboard; sourceTree = "<group>"; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = massive/LaunchScreen.storyboard; sourceTree = "<group>"; };
89C6BE57DB24E9ADA2F236DE /* Pods-RnDiffApp-RnDiffAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RnDiffApp-RnDiffAppTests.release.xcconfig"; path = "Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests.release.xcconfig"; sourceTree = "<group>"; }; 89C6BE57DB24E9ADA2F236DE /* Pods-massive-massiveTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-massive-massiveTests.release.xcconfig"; path = "Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests.release.xcconfig"; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -51,7 +51,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7699B88040F8A987B510C191 /* libPods-RnDiffApp-RnDiffAppTests.a in Frameworks */, 7699B88040F8A987B510C191 /* libPods-massive-massiveTests.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -59,20 +59,20 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0C80B921A6F3F58F76C31292 /* libPods-RnDiffApp.a in Frameworks */, 0C80B921A6F3F58F76C31292 /* libPods-massive.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
00E356EF1AD99517003FC87E /* RnDiffAppTests */ = { 00E356EF1AD99517003FC87E /* massiveTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
00E356F21AD99517003FC87E /* RnDiffAppTests.m */, 00E356F21AD99517003FC87E /* massiveTests.m */,
00E356F01AD99517003FC87E /* Supporting Files */, 00E356F01AD99517003FC87E /* Supporting Files */,
); );
path = RnDiffAppTests; path = massiveTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
00E356F01AD99517003FC87E /* Supporting Files */ = { 00E356F01AD99517003FC87E /* Supporting Files */ = {
@ -83,7 +83,7 @@
name = "Supporting Files"; name = "Supporting Files";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
13B07FAE1A68108700A75B9A /* RnDiffApp */ = { 13B07FAE1A68108700A75B9A /* massive */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */,
@ -93,15 +93,15 @@
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB71A68108700A75B9A /* main.m */, 13B07FB71A68108700A75B9A /* main.m */,
); );
name = RnDiffApp; name = massive;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
2D16E6871FA4F8E400B85C8A /* Frameworks */ = { 2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
5DCACB8F33CDC322A6C60F78 /* libPods-RnDiffApp.a */, 5DCACB8F33CDC322A6C60F78 /* libPods-massive.a */,
19F6CBCC0A4E27FBF8BF4A61 /* libPods-RnDiffApp-RnDiffAppTests.a */, 19F6CBCC0A4E27FBF8BF4A61 /* libPods-massive-massiveTests.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -116,9 +116,9 @@
83CBB9F61A601CBA00E9B192 = { 83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
13B07FAE1A68108700A75B9A /* RnDiffApp */, 13B07FAE1A68108700A75B9A /* massive */,
832341AE1AAA6A7D00B99B32 /* Libraries */, 832341AE1AAA6A7D00B99B32 /* Libraries */,
00E356EF1AD99517003FC87E /* RnDiffAppTests */, 00E356EF1AD99517003FC87E /* massiveTests */,
83CBBA001A601CBA00E9B192 /* Products */, 83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */, 2D16E6871FA4F8E400B85C8A /* Frameworks */,
BBD78D7AC51CEA395F1C20DB /* Pods */, BBD78D7AC51CEA395F1C20DB /* Pods */,
@ -131,8 +131,8 @@
83CBBA001A601CBA00E9B192 /* Products */ = { 83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
13B07F961A680F5B00A75B9A /* RnDiffApp.app */, 13B07F961A680F5B00A75B9A /* massive.app */,
00E356EE1AD99517003FC87E /* RnDiffAppTests.xctest */, 00E356EE1AD99517003FC87E /* massiveTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -140,10 +140,10 @@
BBD78D7AC51CEA395F1C20DB /* Pods */ = { BBD78D7AC51CEA395F1C20DB /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3B4392A12AC88292D35C810B /* Pods-RnDiffApp.debug.xcconfig */, 3B4392A12AC88292D35C810B /* Pods-massive.debug.xcconfig */,
5709B34CF0A7D63546082F79 /* Pods-RnDiffApp.release.xcconfig */, 5709B34CF0A7D63546082F79 /* Pods-massive.release.xcconfig */,
5B7EB9410499542E8C5724F5 /* Pods-RnDiffApp-RnDiffAppTests.debug.xcconfig */, 5B7EB9410499542E8C5724F5 /* Pods-massive-massiveTests.debug.xcconfig */,
89C6BE57DB24E9ADA2F236DE /* Pods-RnDiffApp-RnDiffAppTests.release.xcconfig */, 89C6BE57DB24E9ADA2F236DE /* Pods-massive-massiveTests.release.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@ -151,9 +151,9 @@
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
00E356ED1AD99517003FC87E /* RnDiffAppTests */ = { 00E356ED1AD99517003FC87E /* massiveTests */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "RnDiffAppTests" */; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "massiveTests" */;
buildPhases = ( buildPhases = (
A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */, A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */,
00E356EA1AD99517003FC87E /* Sources */, 00E356EA1AD99517003FC87E /* Sources */,
@ -167,16 +167,17 @@
dependencies = ( dependencies = (
00E356F51AD99517003FC87E /* PBXTargetDependency */, 00E356F51AD99517003FC87E /* PBXTargetDependency */,
); );
name = RnDiffAppTests; name = massiveTests;
productName = RnDiffAppTests; productName = massiveTests;
productReference = 00E356EE1AD99517003FC87E /* RnDiffAppTests.xctest */; productReference = 00E356EE1AD99517003FC87E /* massiveTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
13B07F861A680F5B00A75B9A /* RnDiffApp */ = { 13B07F861A680F5B00A75B9A /* massive */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RnDiffApp" */; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "massive" */;
buildPhases = ( buildPhases = (
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
FD10A7F022414F080027D42C /* Start Packager */,
13B07F871A680F5B00A75B9A /* Sources */, 13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */, 13B07F8E1A680F5B00A75B9A /* Resources */,
@ -188,9 +189,9 @@
); );
dependencies = ( dependencies = (
); );
name = RnDiffApp; name = massive;
productName = RnDiffApp; productName = massive;
productReference = 13B07F961A680F5B00A75B9A /* RnDiffApp.app */; productReference = 13B07F961A680F5B00A75B9A /* massive.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@ -210,7 +211,7 @@
}; };
}; };
}; };
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "RnDiffApp" */; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "massive" */;
compatibilityVersion = "Xcode 12.0"; compatibilityVersion = "Xcode 12.0";
developmentRegion = en; developmentRegion = en;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
@ -223,8 +224,8 @@
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
13B07F861A680F5B00A75B9A /* RnDiffApp */, 13B07F861A680F5B00A75B9A /* massive */,
00E356ED1AD99517003FC87E /* RnDiffAppTests */, 00E356ED1AD99517003FC87E /* massiveTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -271,15 +272,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive/Pods-massive-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive/Pods-massive-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-massive/Pods-massive-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = {
@ -297,7 +298,7 @@
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RnDiffApp-RnDiffAppTests-checkManifestLockResult.txt", "$(DERIVED_FILE_DIR)/Pods-massive-massiveTests-checkManifestLockResult.txt",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@ -319,7 +320,7 @@
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RnDiffApp-checkManifestLockResult.txt", "$(DERIVED_FILE_DIR)/Pods-massive-checkManifestLockResult.txt",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@ -332,15 +333,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
@ -349,15 +350,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive/Pods-massive-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive/Pods-massive-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp/Pods-RnDiffApp-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-massive/Pods-massive-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = { F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = {
@ -366,15 +367,34 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RnDiffApp-RnDiffAppTests/Pods-RnDiffApp-RnDiffAppTests-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-massive-massiveTests/Pods-massive-massiveTests-resources.sh\"\n";
showEnvVarsInLog = 0;
};
FD10A7F022414F080027D42C /* Start Packager */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Start Packager";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@ -384,7 +404,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
00E356F31AD99517003FC87E /* RnDiffAppTests.m in Sources */, 00E356F31AD99517003FC87E /* massiveTests.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -402,7 +422,7 @@
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
00E356F51AD99517003FC87E /* PBXTargetDependency */ = { 00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 13B07F861A680F5B00A75B9A /* RnDiffApp */; target = 13B07F861A680F5B00A75B9A /* massive */;
targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
}; };
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
@ -410,15 +430,15 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* Debug */ = { 00E356F61AD99517003FC87E /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-RnDiffApp-RnDiffAppTests.debug.xcconfig */; baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-massive-massiveTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1", "DEBUG=1",
"$(inherited)", "$(inherited)",
); );
INFOPLIST_FILE = RnDiffAppTests/Info.plist; INFOPLIST_FILE = massiveTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 12.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -431,18 +451,18 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RnDiffApp.app/RnDiffApp"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/massive.app/massive";
}; };
name = Debug; name = Debug;
}; };
00E356F71AD99517003FC87E /* Release */ = { 00E356F71AD99517003FC87E /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-RnDiffApp-RnDiffAppTests.release.xcconfig */; baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-massive-massiveTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
INFOPLIST_FILE = RnDiffAppTests/Info.plist; INFOPLIST_FILE = massiveTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 12.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -455,19 +475,19 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RnDiffApp.app/RnDiffApp"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/massive.app/massive";
}; };
name = Release; name = Release;
}; };
13B07F941A680F5B00A75B9A /* Debug */ = { 13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-RnDiffApp.debug.xcconfig */; baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-massive.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = RnDiffApp/Info.plist; INFOPLIST_FILE = massive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -479,7 +499,7 @@
"-lc++", "-lc++",
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = RnDiffApp; PRODUCT_NAME = massive;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@ -488,12 +508,12 @@
}; };
13B07F951A680F5B00A75B9A /* Release */ = { 13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-RnDiffApp.release.xcconfig */; baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-massive.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = RnDiffApp/Info.plist; INFOPLIST_FILE = massive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -505,7 +525,7 @@
"-lc++", "-lc++",
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = RnDiffApp; PRODUCT_NAME = massive;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
@ -516,7 +536,7 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_ARC = YES;
@ -560,7 +580,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 12.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift, /usr/lib/swift,
"$(inherited)", "$(inherited)",
@ -577,7 +597,6 @@
"-DFOLLY_NO_CONFIG", "-DFOLLY_NO_CONFIG",
"-DFOLLY_MOBILE=1", "-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1", "-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
); );
SDKROOT = iphoneos; SDKROOT = iphoneos;
}; };
@ -588,7 +607,7 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_ARC = YES;
@ -625,7 +644,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 12.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift, /usr/lib/swift,
"$(inherited)", "$(inherited)",
@ -641,7 +660,6 @@
"-DFOLLY_NO_CONFIG", "-DFOLLY_NO_CONFIG",
"-DFOLLY_MOBILE=1", "-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1", "-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
); );
SDKROOT = iphoneos; SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
@ -651,7 +669,7 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "RnDiffAppTests" */ = { 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "massiveTests" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
00E356F61AD99517003FC87E /* Debug */, 00E356F61AD99517003FC87E /* Debug */,
@ -660,7 +678,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RnDiffApp" */ = { 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "massive" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */, 13B07F941A680F5B00A75B9A /* Debug */,
@ -669,7 +687,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "RnDiffApp" */ = { 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "massive" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */, 83CBBA201A601CBA00E9B192 /* Debug */,
@ -681,4 +699,4 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
} }

View File

@ -6,7 +6,7 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{ {
self.moduleName = @"RnDiffApp"; self.moduleName = @"massive";
// You can add your custom initial props in the dictionary below. // You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native. // They will be passed down to the ViewController used by React Native.
self.initialProps = @{}; self.initialProps = @{};
@ -15,11 +15,6 @@
} }
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self getBundleURL];
}
- (NSURL *)getBundleURL
{ {
#if DEBUG #if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
@ -28,4 +23,4 @@
#endif #endif
} }
@end @end

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>RnDiffApp</string> <string>massive</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -26,11 +26,14 @@
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! --> <key>NSExceptionDomains</key>
<key>NSAllowsArbitraryLoads</key> <dict>
<false/> <key>localhost</key>
<key>NSAllowsLocalNetworking</key> <dict>
<true/> <key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string></string>
@ -49,4 +52,4 @@
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
</dict> </dict>
</plist> </plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Some files were not shown because too many files have changed in this diff Show More