Compare commits
107 Commits
unit-tests
...
master
Author | SHA1 | Date | |
---|---|---|---|
314b09017b | |||
331597e3ee | |||
79cde3a219 | |||
63e1db7349 | |||
da17f8899c | |||
8648cf166e | |||
af96ec8507 | |||
f51284e4ea | |||
f778426aba | |||
44283fc990 | |||
c97ba1151e | |||
95681c0b3d | |||
158dd61668 | |||
e628d345ca | |||
85915b9aa0 | |||
9833752bab | |||
556347e632 | |||
9dc188e6ec | |||
82da62f699 | |||
36d3de401b | |||
040d588b5a | |||
47d4532169 | |||
3e41c3bbd8 | |||
b776d88327 | |||
adc2d05b2c | |||
c3a3e33e25 | |||
89606b9d21 | |||
6dabb7049f | |||
4b42ab5f21 | |||
a7da93583d | |||
1b2cbab370 | |||
09354829a8 | |||
514efc6467 | |||
1603496424 | |||
0beb1397a6 | |||
a5b6673e9a | |||
6a7bd632e5 | |||
4303fe2cc4 | |||
23ed95dcdb | |||
8f1f9f6e7d | |||
bdd5e23f32 | |||
9c9a5fdd63 | |||
90db607190 | |||
457134df6b | |||
db5cc566ea | |||
76e5aeacfd | |||
2fb46e1dcc | |||
d1342c0efa | |||
288ae1ae0c | |||
0e7920bde9 | |||
d2a1c432bb | |||
5dd569ef72 | |||
dfc4f73ca4 | |||
79a48b1e47 | |||
13b340f5be | |||
4db820f10a | |||
7b401388b5 | |||
a1643c349d | |||
640a25a0f4 | |||
c9b1ab1f9d | |||
00d4edcfc3 | |||
8dd8f786ef | |||
a84cab6bbf | |||
f4db61aeec | |||
3af3e1faf2 | |||
7bc9c00a63 | |||
a03731c6ff | |||
f1e0911488 | |||
1a75d8897d | |||
9f7cbba80a | |||
de2aa67e6e | |||
28ec021258 | |||
04ef72e48b | |||
467df629b0 | |||
e7f85a9954 | |||
5e6896eaba | |||
6438a9c48a | |||
8e8961419c | |||
b0696d1d58 | |||
73d9b1c617 | |||
a6130b3a10 | |||
7bee8ae732 | |||
6c8731c17a | |||
6fa2bbb506 | |||
069f770c96 | |||
b41c30d886 | |||
495b89fba3 | |||
42912040ff | |||
c7952738b5 | |||
cffc458338 | |||
05237fc293 | |||
5fd7e75908 | |||
705052f1b4 | |||
efc97bdf47 | |||
d0702b7675 | |||
24e230e8b9 | |||
67689f4af8 | |||
a2721e9f12 | |||
bafdecd3e3 | |||
e432c1b711 | |||
08f91bf531 | |||
80f2dfdff5 | |||
3c9b93f0bc | |||
f221ebb8df | |||
a68d4d6a69 | |||
5d9df37778 | |||
8246155c13 |
|
@ -1,12 +1,12 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native-community',
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: '@react-native',
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.js'],
|
||||
rules: {
|
||||
'jsx-quotes': 0,
|
||||
'prettier/prettier': 0,
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
'no-shadow': 'off',
|
||||
'no-undef': 'off',
|
||||
|
@ -18,4 +18,5 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
],
|
||||
ignorePatterns: ['coverage/', 'mock-providers.tsx'],
|
||||
}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -73,3 +73,4 @@ massive-build
|
|||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
coverage
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
bracketSameLine: true,
|
||||
bracketSpacing: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
semi: false,
|
||||
};
|
127
App.tsx
127
App.tsx
|
@ -2,22 +2,21 @@ import {
|
|||
DarkTheme as NavigationDarkTheme,
|
||||
DefaultTheme as NavigationDefaultTheme,
|
||||
NavigationContainer,
|
||||
} from '@react-navigation/native'
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import {DeviceEventEmitter, useColorScheme} from 'react-native'
|
||||
import React from 'react'
|
||||
} from "@react-navigation/native";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { DeviceEventEmitter, useColorScheme } from "react-native";
|
||||
import {
|
||||
DarkTheme as PaperDarkTheme,
|
||||
DefaultTheme as PaperDefaultTheme,
|
||||
MD3DarkTheme as PaperDarkTheme,
|
||||
MD3LightTheme as PaperDefaultTheme,
|
||||
Provider as PaperProvider,
|
||||
Snackbar,
|
||||
} from 'react-native-paper'
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
|
||||
import {AppDataSource} from './data-source'
|
||||
import {settingsRepo} from './db'
|
||||
import Routes from './Routes'
|
||||
import {TOAST} from './toast'
|
||||
import {ThemeContext} from './use-theme'
|
||||
} from "react-native-paper";
|
||||
import MaterialIcon from "react-native-vector-icons/MaterialIcons";
|
||||
import { AppDataSource } from "./data-source";
|
||||
import { settingsRepo } from "./db";
|
||||
import Routes from "./Routes";
|
||||
import { TOAST } from "./toast";
|
||||
import { ThemeContext } from "./use-theme";
|
||||
|
||||
export const CombinedDefaultTheme = {
|
||||
...NavigationDefaultTheme,
|
||||
|
@ -26,7 +25,7 @@ export const CombinedDefaultTheme = {
|
|||
...NavigationDefaultTheme.colors,
|
||||
...PaperDefaultTheme.colors,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const CombinedDarkTheme = {
|
||||
...NavigationDarkTheme,
|
||||
|
@ -35,85 +34,76 @@ export const CombinedDarkTheme = {
|
|||
...NavigationDarkTheme.colors,
|
||||
...PaperDarkTheme.colors,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const isDark = useColorScheme() === 'dark'
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [snackbar, setSnackbar] = useState('')
|
||||
const [theme, setTheme] = useState('system')
|
||||
const phoneTheme = useColorScheme();
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [snackbar, setSnackbar] = useState("");
|
||||
const [appTheme, setAppTheme] = useState("system");
|
||||
|
||||
const [lightColor, setLightColor] = useState<string>(
|
||||
CombinedDefaultTheme.colors.primary,
|
||||
)
|
||||
CombinedDefaultTheme.colors.primary
|
||||
);
|
||||
|
||||
const [darkColor, setDarkColor] = useState<string>(
|
||||
CombinedDarkTheme.colors.primary,
|
||||
)
|
||||
CombinedDarkTheme.colors.primary
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize()
|
||||
const settings = await settingsRepo.findOne({where: {}})
|
||||
console.log(`${App.name}.useEffect:`, {gotSettings: settings})
|
||||
setTheme(settings.theme)
|
||||
if (settings.lightColor) setLightColor(settings.lightColor)
|
||||
if (settings.darkColor) setDarkColor(settings.darkColor)
|
||||
setInitialized(true)
|
||||
}
|
||||
init()
|
||||
(async () => {
|
||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||
const settings = await settingsRepo.findOne({ where: {} });
|
||||
setAppTheme(settings.theme);
|
||||
if (settings.lightColor) setLightColor(settings.lightColor);
|
||||
if (settings.darkColor) setDarkColor(settings.darkColor);
|
||||
setInitialized(true);
|
||||
})();
|
||||
const description = DeviceEventEmitter.addListener(
|
||||
TOAST,
|
||||
({value}: {value: string}) => {
|
||||
setSnackbar(value)
|
||||
},
|
||||
)
|
||||
return description.remove
|
||||
}, [])
|
||||
({ value }: { value: string }) => {
|
||||
setSnackbar(value);
|
||||
}
|
||||
);
|
||||
return description.remove;
|
||||
}, []);
|
||||
|
||||
const paperTheme = useMemo(() => {
|
||||
const darkTheme = lightColor
|
||||
? {
|
||||
...CombinedDarkTheme,
|
||||
colors: {...CombinedDarkTheme.colors, primary: darkColor},
|
||||
colors: { ...CombinedDarkTheme.colors, primary: darkColor },
|
||||
}
|
||||
: CombinedDarkTheme
|
||||
: CombinedDarkTheme;
|
||||
const lightTheme = lightColor
|
||||
? {
|
||||
...CombinedDefaultTheme,
|
||||
colors: {...CombinedDefaultTheme.colors, primary: lightColor},
|
||||
colors: { ...CombinedDefaultTheme.colors, primary: lightColor },
|
||||
}
|
||||
: CombinedDefaultTheme
|
||||
let value = isDark ? darkTheme : lightTheme
|
||||
if (theme === 'dark') value = darkTheme
|
||||
else if (theme === 'light') value = lightTheme
|
||||
return value
|
||||
}, [isDark, theme, lightColor, darkColor])
|
||||
|
||||
const action = useMemo(
|
||||
() => ({
|
||||
label: 'Close',
|
||||
onPress: () => setSnackbar(''),
|
||||
color: paperTheme.colors.background,
|
||||
}),
|
||||
[paperTheme.colors.background],
|
||||
)
|
||||
: CombinedDefaultTheme;
|
||||
let value = phoneTheme === "dark" ? darkTheme : lightTheme;
|
||||
if (appTheme === "dark") value = darkTheme;
|
||||
else if (appTheme === "light") value = lightTheme;
|
||||
return value;
|
||||
}, [phoneTheme, appTheme, lightColor, darkColor]);
|
||||
|
||||
return (
|
||||
<PaperProvider
|
||||
theme={paperTheme}
|
||||
settings={{icon: props => <MaterialIcon {...props} />}}>
|
||||
settings={{ icon: (props) => <MaterialIcon {...props} /> }}
|
||||
>
|
||||
<NavigationContainer theme={paperTheme}>
|
||||
{initialized && (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
setTheme,
|
||||
theme: appTheme,
|
||||
setTheme: setAppTheme,
|
||||
lightColor,
|
||||
setLightColor,
|
||||
darkColor,
|
||||
setDarkColor,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Routes />
|
||||
</ThemeContext.Provider>
|
||||
)}
|
||||
|
@ -121,13 +111,18 @@ const App = () => {
|
|||
|
||||
<Snackbar
|
||||
duration={3000}
|
||||
onDismiss={() => setSnackbar('')}
|
||||
onDismiss={() => setSnackbar("")}
|
||||
visible={!!snackbar}
|
||||
action={action}>
|
||||
action={{
|
||||
label: "Close",
|
||||
onPress: () => setSnackbar(""),
|
||||
textColor: paperTheme.colors.background,
|
||||
}}
|
||||
>
|
||||
{snackbar}
|
||||
</Snackbar>
|
||||
</PaperProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
|
21
AppFab.tsx
21
AppFab.tsx
|
@ -1,30 +1,31 @@
|
|||
import {ComponentProps, useMemo} from 'react'
|
||||
import {FAB, useTheme} from 'react-native-paper'
|
||||
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
|
||||
import {lightColors} from './colors'
|
||||
import { ComponentProps, useMemo } from "react";
|
||||
import { FAB, useTheme } from "react-native-paper";
|
||||
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
||||
import { lightColors } from "./colors";
|
||||
|
||||
export default function AppFab(props: Partial<ComponentProps<typeof FAB>>) {
|
||||
const {colors} = useTheme()
|
||||
const { colors } = useTheme();
|
||||
|
||||
const fabColor = useMemo(
|
||||
() =>
|
||||
lightColors.map(color => color.hex).includes(colors.primary)
|
||||
lightColors.map((color) => color.hex).includes(colors.primary)
|
||||
? CombinedDarkTheme.colors.background
|
||||
: CombinedDefaultTheme.colors.background,
|
||||
[colors.primary],
|
||||
)
|
||||
[colors.primary]
|
||||
);
|
||||
|
||||
return (
|
||||
<FAB
|
||||
icon="add"
|
||||
testID="add"
|
||||
color={fabColor}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
backgroundColor: colors.primary,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
24
AppInput.tsx
24
AppInput.tsx
|
@ -1,26 +1,26 @@
|
|||
import React, {ComponentProps, Ref} from 'react'
|
||||
import {TextInput} from 'react-native-paper'
|
||||
import {CombinedDefaultTheme} from './App'
|
||||
import {MARGIN} from './constants'
|
||||
import useDark from './use-dark'
|
||||
import React, { ComponentProps, Ref } from "react";
|
||||
import { TextInput } from "react-native-paper";
|
||||
import { CombinedDefaultTheme } from "./App";
|
||||
import { MARGIN } from "./constants";
|
||||
import useDark from "./use-dark";
|
||||
|
||||
function AppInput(
|
||||
props: Partial<ComponentProps<typeof TextInput>> & {
|
||||
innerRef?: Ref<any>
|
||||
},
|
||||
innerRef?: Ref<any>;
|
||||
}
|
||||
) {
|
||||
const dark = useDark()
|
||||
const dark = useDark();
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
selectionColor={dark ? '#2A2A2A' : CombinedDefaultTheme.colors.border}
|
||||
style={{marginBottom: MARGIN, minWidth: 100}}
|
||||
selectionColor={dark ? "#2A2A2A" : CombinedDefaultTheme.colors.border}
|
||||
style={{ marginBottom: MARGIN, minWidth: 100 }}
|
||||
selectTextOnFocus
|
||||
ref={props.innerRef}
|
||||
blurOnSubmit={false}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(AppInput)
|
||||
export default React.memo(AppInput);
|
||||
|
|
98
BestList.tsx
98
BestList.tsx
|
@ -1,98 +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 {BestPageParams} from './BestPage'
|
||||
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 BestList() {
|
||||
const [bests, setBests] = useState<GymSet[]>()
|
||||
const [term, setTerm] = useState('')
|
||||
const navigation = useNavigation<NavigationProp<BestPageParams>>()
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||
}, []),
|
||||
)
|
||||
|
||||
const refresh = useCallback(async (value: string) => {
|
||||
const weights = await setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addSelect('MAX(weight)', 'weight')
|
||||
.where('name LIKE :name', {name: `%${value}%`})
|
||||
.andWhere('NOT hidden')
|
||||
.groupBy('name')
|
||||
.getMany()
|
||||
console.log(`${BestList.name}.refresh:`, {length: weights.length})
|
||||
let newBest: GymSet[] = []
|
||||
for (const set of weights) {
|
||||
const reps = await setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addSelect('MAX(reps)', 'reps')
|
||||
.where('name = :name', {name: set.name})
|
||||
.andWhere('weight = :weight', {weight: set.weight})
|
||||
.andWhere('NOT hidden')
|
||||
.groupBy('name')
|
||||
.getMany()
|
||||
newBest.push(...reps)
|
||||
}
|
||||
setBests(newBest)
|
||||
}, [])
|
||||
|
||||
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('ViewBest', {best: item})}
|
||||
left={() =>
|
||||
(settings.images && item.image && (
|
||||
<Image source={{uri: item.image}} style={{height: 75, width: 75}} />
|
||||
)) ||
|
||||
null
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name="Best" />
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
22
BestPage.tsx
22
BestPage.tsx
|
@ -1,22 +0,0 @@
|
|||
import {createStackNavigator} from '@react-navigation/stack'
|
||||
import BestList from './BestList'
|
||||
import GymSet from './gym-set'
|
||||
import ViewBest from './ViewBest'
|
||||
|
||||
const Stack = createStackNavigator<BestPageParams>()
|
||||
export type BestPageParams = {
|
||||
BestList: {}
|
||||
ViewBest: {
|
||||
best: GymSet
|
||||
}
|
||||
}
|
||||
|
||||
export default function BestPage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{headerShown: false, animationEnabled: false}}>
|
||||
<Stack.Screen name="BestList" component={BestList} />
|
||||
<Stack.Screen name="ViewBest" component={ViewBest} />
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
52
Chart.tsx
52
Chart.tsx
|
@ -1,11 +1,11 @@
|
|||
import {useTheme} from '@react-navigation/native'
|
||||
import * as shape from 'd3-shape'
|
||||
import {View} from 'react-native'
|
||||
import {Grid, LineChart, XAxis, YAxis} from 'react-native-svg-charts'
|
||||
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
|
||||
import {MARGIN, PADDING} from './constants'
|
||||
import GymSet from './gym-set'
|
||||
import useDark from './use-dark'
|
||||
import { useTheme } from "@react-navigation/native";
|
||||
import * as shape from "d3-shape";
|
||||
import { View } from "react-native";
|
||||
import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
|
||||
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import GymSet from "./gym-set";
|
||||
import useDark from "./use-dark";
|
||||
|
||||
export default function Chart({
|
||||
yData,
|
||||
|
@ -13,21 +13,21 @@ export default function Chart({
|
|||
xData,
|
||||
yFormat,
|
||||
}: {
|
||||
yData: number[]
|
||||
xData: GymSet[]
|
||||
xFormat: (value: any, index: number) => string
|
||||
yFormat: (value: any) => string
|
||||
yData: number[];
|
||||
xData: GymSet[];
|
||||
xFormat: (value: any, index: number) => string;
|
||||
yFormat: (value: any) => string;
|
||||
}) {
|
||||
const {colors} = useTheme()
|
||||
const dark = useDark()
|
||||
const { colors } = useTheme();
|
||||
const dark = useDark();
|
||||
const axesSvg = {
|
||||
fontSize: 10,
|
||||
fill: dark
|
||||
? CombinedDarkTheme.colors.text
|
||||
: CombinedDefaultTheme.colors.text,
|
||||
}
|
||||
const verticalContentInset = {top: 10, bottom: 10}
|
||||
const xAxisHeight = 30
|
||||
};
|
||||
const verticalContentInset = { top: 10, bottom: 10 };
|
||||
const xAxisHeight = 30;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -35,34 +35,36 @@ export default function Chart({
|
|||
style={{
|
||||
height: 300,
|
||||
padding: PADDING,
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<YAxis
|
||||
data={yData}
|
||||
style={{marginBottom: xAxisHeight}}
|
||||
style={{ marginBottom: xAxisHeight }}
|
||||
contentInset={verticalContentInset}
|
||||
svg={axesSvg}
|
||||
formatLabel={yFormat}
|
||||
/>
|
||||
<View style={{flex: 1, marginLeft: MARGIN}}>
|
||||
<View style={{ flex: 1, marginLeft: MARGIN }}>
|
||||
<LineChart
|
||||
style={{flex: 1}}
|
||||
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}}
|
||||
contentInset={{ left: 15, right: 16 }}
|
||||
svg={axesSvg}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Button, Dialog, Portal, Text} from 'react-native-paper'
|
||||
import { Button, Dialog, Portal, Text } from "react-native-paper";
|
||||
|
||||
export default function ConfirmDialog({
|
||||
title,
|
||||
|
@ -8,17 +8,17 @@ export default function ConfirmDialog({
|
|||
setShow,
|
||||
onCancel,
|
||||
}: {
|
||||
title: string
|
||||
children: JSX.Element | JSX.Element[] | string
|
||||
onOk: () => void
|
||||
show: boolean
|
||||
setShow: (show: boolean) => void
|
||||
onCancel?: () => void
|
||||
title: string;
|
||||
children: JSX.Element | JSX.Element[] | string;
|
||||
onOk: () => void;
|
||||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
onCancel?: () => void;
|
||||
}) {
|
||||
const cancel = () => {
|
||||
setShow(false)
|
||||
onCancel && onCancel()
|
||||
}
|
||||
setShow(false);
|
||||
onCancel && onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
|
@ -33,5 +33,5 @@ export default function ConfirmDialog({
|
|||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,22 @@
|
|||
import {DrawerNavigationProp} from '@react-navigation/drawer'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {Appbar, IconButton} from 'react-native-paper'
|
||||
import {DrawerParamList} from './drawer-param-list'
|
||||
import useDark from './use-dark'
|
||||
import { DrawerNavigationProp } from "@react-navigation/drawer";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { Appbar, IconButton } from "react-native-paper";
|
||||
import { DrawerParamList } from "./drawer-param-list";
|
||||
|
||||
export default function DrawerHeader({
|
||||
name,
|
||||
children,
|
||||
}: {
|
||||
name: keyof DrawerParamList
|
||||
children?: JSX.Element | JSX.Element[]
|
||||
name: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>()
|
||||
const dark = useDark()
|
||||
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>();
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
<IconButton
|
||||
color={dark ? 'white' : 'white'}
|
||||
icon="menu"
|
||||
onPress={navigation.openDrawer}
|
||||
/>
|
||||
<IconButton icon="menu" onPress={navigation.openDrawer} />
|
||||
<Appbar.Content title={name} />
|
||||
{children}
|
||||
</Appbar.Header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
141
EditPlan.tsx
141
EditPlan.tsx
|
@ -3,97 +3,130 @@ import {
|
|||
RouteProp,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {ScrollView, StyleSheet, View} from 'react-native'
|
||||
import {Button, Text} from 'react-native-paper'
|
||||
import {MARGIN, PADDING} from './constants'
|
||||
import {planRepo, setRepo} from './db'
|
||||
import {DrawerParamList} from './drawer-param-list'
|
||||
import {PlanPageParams} from './plan-page-params'
|
||||
import StackHeader from './StackHeader'
|
||||
import Switch from './Switch'
|
||||
import {DAYS} from './time'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ScrollView, StyleSheet, View } from "react-native";
|
||||
import { Button, IconButton, Text } from "react-native-paper";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { planRepo, setRepo } from "./db";
|
||||
import { defaultSet } from "./gym-set";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import StackHeader from "./StackHeader";
|
||||
import Switch from "./Switch";
|
||||
import { DAYS } from "./time";
|
||||
import AppInput from "./AppInput";
|
||||
|
||||
export default function EditPlan() {
|
||||
const {params} = useRoute<RouteProp<PlanPageParams, 'EditPlan'>>()
|
||||
const {plan} = params
|
||||
const { params } = useRoute<RouteProp<PlanPageParams, "EditPlan">>();
|
||||
const { plan } = params;
|
||||
const [title, setTitle] = useState<string>(plan?.title);
|
||||
const [days, setDays] = useState<string[]>(
|
||||
plan.days ? plan.days.split(',') : [],
|
||||
)
|
||||
plan.days ? plan.days.split(",") : []
|
||||
);
|
||||
const [workouts, setWorkouts] = useState<string[]>(
|
||||
plan.workouts ? plan.workouts.split(',') : [],
|
||||
)
|
||||
const [names, setNames] = useState<string[]>([])
|
||||
const navigation = useNavigation<NavigationProp<DrawerParamList>>()
|
||||
plan.workouts ? plan.workouts.split(",") : []
|
||||
);
|
||||
const [names, setNames] = useState<string[]>([]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
useEffect(() => {
|
||||
setRepo
|
||||
.createQueryBuilder()
|
||||
.select('name')
|
||||
.select("name")
|
||||
.distinct(true)
|
||||
.orderBy("name")
|
||||
.getRawMany()
|
||||
.then(values => {
|
||||
console.log(EditPlan.name, {values})
|
||||
setNames(values.map(value => value.name))
|
||||
})
|
||||
}, [])
|
||||
.then((values) => {
|
||||
console.log(EditPlan.name, { values });
|
||||
setNames(values.map((value) => value.name));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const save = useCallback(async () => {
|
||||
console.log(`${EditPlan.name}.save`, {days, workouts, plan})
|
||||
if (!days || !workouts) return
|
||||
const newWorkouts = workouts.filter(workout => workout).join(',')
|
||||
const newDays = days.filter(day => day).join(',')
|
||||
await planRepo.save({days: newDays, workouts: newWorkouts, id: plan.id})
|
||||
navigation.goBack()
|
||||
}, [days, workouts, plan, navigation])
|
||||
console.log(`${EditPlan.name}.save`, { days, workouts, plan });
|
||||
if (!days || !workouts) return;
|
||||
const newWorkouts = workouts.filter((workout) => workout).join(",");
|
||||
const newDays = days.filter((day) => day).join(",");
|
||||
await planRepo.save({
|
||||
title: title,
|
||||
days: newDays,
|
||||
workouts: newWorkouts,
|
||||
id: plan.id,
|
||||
});
|
||||
}, [title, days, workouts, plan]);
|
||||
|
||||
const toggleWorkout = useCallback(
|
||||
(on: boolean, name: string) => {
|
||||
if (on) {
|
||||
setWorkouts([...workouts, name])
|
||||
setWorkouts([...workouts, name]);
|
||||
} else {
|
||||
setWorkouts(workouts.filter(workout => workout !== name))
|
||||
setWorkouts(workouts.filter((workout) => workout !== name));
|
||||
}
|
||||
},
|
||||
[setWorkouts, workouts],
|
||||
)
|
||||
[setWorkouts, workouts]
|
||||
);
|
||||
|
||||
const toggleDay = useCallback(
|
||||
(on: boolean, day: string) => {
|
||||
if (on) {
|
||||
setDays([...days, day])
|
||||
setDays([...days, day]);
|
||||
} else {
|
||||
setDays(days.filter(d => d !== day))
|
||||
setDays(days.filter((d) => d !== day));
|
||||
}
|
||||
},
|
||||
[setDays, days],
|
||||
)
|
||||
[setDays, days]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title="Edit plan" />
|
||||
<View style={{padding: PADDING, flex: 1}}>
|
||||
<ScrollView style={{flex: 1}}>
|
||||
<StackHeader
|
||||
title={typeof plan.id === "number" ? "Edit plan" : "Add plan"}
|
||||
>
|
||||
{typeof plan.id === "number" && (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await save();
|
||||
const newPlan = await planRepo.findOne({
|
||||
where: { id: plan.id },
|
||||
});
|
||||
let first = await setRepo.findOne({
|
||||
where: { name: workouts[0] },
|
||||
order: { created: "desc" },
|
||||
});
|
||||
if (!first) first = { ...defaultSet, name: workouts[0] };
|
||||
delete first.id;
|
||||
navigation.navigate("StartPlan", { plan: newPlan, first });
|
||||
}}
|
||||
icon="play-arrow"
|
||||
/>
|
||||
)}
|
||||
</StackHeader>
|
||||
<View style={{ padding: PADDING, flex: 1 }}>
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<AppInput
|
||||
label="Title"
|
||||
value={title}
|
||||
onChangeText={(value) => setTitle(value)}
|
||||
/>
|
||||
<Text style={styles.title}>Days</Text>
|
||||
{DAYS.map(day => (
|
||||
{DAYS.map((day) => (
|
||||
<Switch
|
||||
key={day}
|
||||
onChange={value => toggleDay(value, day)}
|
||||
onChange={(value) => toggleDay(value, day)}
|
||||
value={days.includes(day)}
|
||||
title={day}
|
||||
/>
|
||||
))}
|
||||
<Text style={[styles.title, {marginTop: MARGIN}]}>Workouts</Text>
|
||||
<Text style={[styles.title, { marginTop: MARGIN }]}>Workouts</Text>
|
||||
{names.length === 0 ? (
|
||||
<View>
|
||||
<Text>No workouts found.</Text>
|
||||
</View>
|
||||
) : (
|
||||
names.map(name => (
|
||||
names.map((name) => (
|
||||
<Switch
|
||||
key={name}
|
||||
onChange={value => toggleWorkout(value, name)}
|
||||
onChange={(value) => toggleWorkout(value, name)}
|
||||
value={workouts.includes(name)}
|
||||
title={name}
|
||||
/>
|
||||
|
@ -104,14 +137,18 @@ export default function EditPlan() {
|
|||
<Button
|
||||
disabled={workouts.length === 0 && days.length === 0}
|
||||
style={styles.button}
|
||||
mode="contained"
|
||||
mode="outlined"
|
||||
icon="save"
|
||||
onPress={save}>
|
||||
onPress={async () => {
|
||||
await save();
|
||||
navigation.navigate("PlanList");
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -120,4 +157,4 @@ const styles = StyleSheet.create({
|
|||
marginBottom: MARGIN,
|
||||
},
|
||||
button: {},
|
||||
})
|
||||
});
|
||||
|
|
281
EditSet.tsx
281
EditSet.tsx
|
@ -1,123 +1,151 @@
|
|||
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
|
||||
import {
|
||||
RouteProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import {format} from 'date-fns'
|
||||
import {useCallback, useRef, useState} from 'react'
|
||||
import {NativeModules, TextInput, View} from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import {Button, Card, TouchableRipple} from 'react-native-paper'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import {MARGIN, PADDING} from './constants'
|
||||
import {getNow, setRepo, settingsRepo} from './db'
|
||||
import GymSet from './gym-set'
|
||||
import {HomePageParams} from './home-page-params'
|
||||
import AppInput from './AppInput'
|
||||
import Settings from './settings'
|
||||
import StackHeader from './StackHeader'
|
||||
import {toast} from './toast'
|
||||
} from "@react-navigation/native";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { NativeModules, 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 ConfirmDialog from "./ConfirmDialog";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { getNow, setRepo, settingsRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import Settings from "./settings";
|
||||
import StackHeader from "./StackHeader";
|
||||
import { toast } from "./toast";
|
||||
import { fixNumeric } from "./fix-numeric";
|
||||
|
||||
export default function EditSet() {
|
||||
const {params} = useRoute<RouteProp<HomePageParams, 'EditSet'>>()
|
||||
const {set} = params
|
||||
const navigation = useNavigation()
|
||||
const [settings, setSettings] = useState<Settings>({} as Settings)
|
||||
const [name, setName] = useState(set.name)
|
||||
const [reps, setReps] = useState(set.reps?.toString())
|
||||
const [weight, setWeight] = useState(set.weight?.toString())
|
||||
const [newImage, setNewImage] = useState(set.image)
|
||||
const [unit, setUnit] = useState(set.unit)
|
||||
const [showRemove, setShowRemove] = useState(false)
|
||||
const [removeImage, setRemoveImage] = useState(false)
|
||||
const weightRef = useRef<TextInput>(null)
|
||||
const repsRef = useRef<TextInput>(null)
|
||||
const unitRef = useRef<TextInput>(null)
|
||||
const { params } = useRoute<RouteProp<HomePageParams, "EditSet">>();
|
||||
const { set } = params;
|
||||
const navigation = useNavigation();
|
||||
const [settings, setSettings] = useState<Settings>({} as Settings);
|
||||
const [name, setName] = useState(set.name);
|
||||
const [reps, setReps] = useState(set.reps?.toString());
|
||||
const [weight, setWeight] = useState(set.weight?.toString());
|
||||
const [newImage, setNewImage] = useState(set.image);
|
||||
const [unit, setUnit] = useState(set.unit);
|
||||
const [created, setCreated] = useState<Date>(
|
||||
set.created ? new Date(set.created) : new Date()
|
||||
);
|
||||
const [createdDirty, setCreatedDirty] = useState(false);
|
||||
const [showRemove, setShowRemove] = useState(false);
|
||||
const [removeImage, setRemoveImage] = useState(false);
|
||||
const weightRef = useRef<TextInput>(null);
|
||||
const repsRef = useRef<TextInput>(null);
|
||||
const unitRef = useRef<TextInput>(null);
|
||||
|
||||
const [selection, setSelection] = useState({
|
||||
start: 0,
|
||||
end: set.reps?.toString().length,
|
||||
})
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||
}, []),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const startTimer = useCallback(
|
||||
async (value: string) => {
|
||||
if (!settings.alarm) return
|
||||
const first = await setRepo.findOne({where: {name: value}})
|
||||
if (!settings.alarm) return;
|
||||
const first = await setRepo.findOne({ where: { name: value } });
|
||||
const milliseconds =
|
||||
(first?.minutes ?? 3) * 60 * 1000 + (first?.seconds ?? 0) * 1000
|
||||
const {vibrate, sound, noSound} = settings
|
||||
const args = [milliseconds, vibrate, sound, noSound]
|
||||
NativeModules.AlarmModule.timer(...args)
|
||||
(first?.minutes ?? 3) * 60 * 1000 + (first?.seconds ?? 0) * 1000;
|
||||
if (milliseconds) NativeModules.AlarmModule.timer(milliseconds);
|
||||
},
|
||||
[settings],
|
||||
)
|
||||
[settings]
|
||||
);
|
||||
|
||||
const added = useCallback(
|
||||
async (value: GymSet) => {
|
||||
startTimer(value.name)
|
||||
console.log(`${EditSet.name}.add`, {set: value})
|
||||
if (!settings.notify) return
|
||||
startTimer(value.name);
|
||||
console.log(`${EditSet.name}.add`, { set: value });
|
||||
if (!settings.notify) return;
|
||||
if (
|
||||
value.weight > set.weight ||
|
||||
(value.reps > set.reps && value.weight === set.weight)
|
||||
)
|
||||
toast("Great work King! That's a new record.")
|
||||
) {
|
||||
toast("Great work King! That's a new record.");
|
||||
}
|
||||
},
|
||||
[startTimer, set, settings],
|
||||
)
|
||||
[startTimer, set, settings]
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
console.log(`${EditSet.name}.handleSubmit:`, {set, uri: newImage, name})
|
||||
if (!name) return
|
||||
let image = newImage
|
||||
if (!newImage && !removeImage)
|
||||
image = await setRepo.findOne({where: {name}}).then(s => s?.image)
|
||||
if (!name) return;
|
||||
|
||||
console.log(`${EditSet.name}.handleSubmit:`, {image})
|
||||
const [{now}] = await getNow()
|
||||
const saved = await setRepo.save({
|
||||
const newSet: Partial<GymSet> = {
|
||||
id: set.id,
|
||||
name,
|
||||
created: set.created || now,
|
||||
reps: Number(reps),
|
||||
weight: Number(weight),
|
||||
unit,
|
||||
image,
|
||||
minutes: Number(set.minutes ?? 3),
|
||||
seconds: Number(set.seconds ?? 30),
|
||||
sets: set.sets ?? 3,
|
||||
hidden: false,
|
||||
})
|
||||
if (typeof set.id !== 'number') added(saved)
|
||||
navigation.goBack()
|
||||
}
|
||||
};
|
||||
|
||||
newSet.image = newImage;
|
||||
if (!newImage && !removeImage) {
|
||||
newSet.image = await setRepo
|
||||
.findOne({ where: { name } })
|
||||
.then((s) => s?.image);
|
||||
}
|
||||
|
||||
if (createdDirty) newSet.created = created.toISOString();
|
||||
if (typeof set.id !== "number") newSet.created = await getNow();
|
||||
|
||||
const saved = await setRepo.save(newSet);
|
||||
if (typeof set.id !== "number") added(saved);
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const changeImage = useCallback(async () => {
|
||||
const {fileCopyUri} = await DocumentPicker.pickSingle({
|
||||
const { fileCopyUri } = await DocumentPicker.pickSingle({
|
||||
type: DocumentPicker.types.images,
|
||||
copyTo: 'documentDirectory',
|
||||
})
|
||||
if (fileCopyUri) setNewImage(fileCopyUri)
|
||||
}, [])
|
||||
copyTo: "documentDirectory",
|
||||
});
|
||||
if (fileCopyUri) setNewImage(fileCopyUri);
|
||||
}, []);
|
||||
|
||||
const handleRemove = useCallback(async () => {
|
||||
setNewImage('')
|
||||
setRemoveImage(true)
|
||||
setShowRemove(false)
|
||||
}, [])
|
||||
setNewImage("");
|
||||
setRemoveImage(true);
|
||||
setShowRemove(false);
|
||||
}, []);
|
||||
|
||||
const pickDate = useCallback(() => {
|
||||
DateTimePickerAndroid.open({
|
||||
value: created,
|
||||
onChange: (_, date) => {
|
||||
if (date === created) return;
|
||||
setCreated(date);
|
||||
setCreatedDirty(true);
|
||||
DateTimePickerAndroid.open({
|
||||
value: date,
|
||||
onChange: (__, time) => setCreated(time),
|
||||
mode: "time",
|
||||
});
|
||||
},
|
||||
mode: "date",
|
||||
});
|
||||
}, [created]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title="Edit set" />
|
||||
<StackHeader
|
||||
title={typeof set.id === "number" ? "Edit set" : "Add set"}
|
||||
/>
|
||||
|
||||
<View style={{padding: PADDING, flex: 1}}>
|
||||
<View style={{ padding: PADDING, flex: 1 }}>
|
||||
<AppInput
|
||||
label="Name"
|
||||
value={name}
|
||||
|
@ -127,26 +155,65 @@ export default function EditSet() {
|
|||
onSubmitEditing={() => repsRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<AppInput
|
||||
label="Reps"
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={setReps}
|
||||
onSubmitEditing={() => weightRef.current?.focus()}
|
||||
selection={selection}
|
||||
onSelectionChange={e => setSelection(e.nativeEvent.selection)}
|
||||
autoFocus={!!name}
|
||||
innerRef={repsRef}
|
||||
/>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<AppInput
|
||||
style={{
|
||||
flex: 1,
|
||||
marginBottom: MARGIN,
|
||||
}}
|
||||
label="Reps"
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={(newReps) => {
|
||||
const fixed = fixNumeric(newReps);
|
||||
setReps(fixed);
|
||||
if (fixed.length !== newReps.length)
|
||||
toast("Reps must be a number");
|
||||
}}
|
||||
onSubmitEditing={() => weightRef.current?.focus()}
|
||||
selection={selection}
|
||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
||||
innerRef={repsRef}
|
||||
/>
|
||||
<IconButton
|
||||
icon="add"
|
||||
onPress={() => setReps((Number(reps) + 1).toString())}
|
||||
/>
|
||||
<IconButton
|
||||
icon="remove"
|
||||
onPress={() => setReps((Number(reps) - 1).toString())}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<AppInput
|
||||
label="Weight"
|
||||
keyboardType="numeric"
|
||||
value={weight}
|
||||
onChangeText={setWeight}
|
||||
onSubmitEditing={handleSubmit}
|
||||
innerRef={weightRef}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: MARGIN,
|
||||
}}
|
||||
>
|
||||
<AppInput
|
||||
style={{ flex: 1 }}
|
||||
label="Weight"
|
||||
keyboardType="numeric"
|
||||
value={weight}
|
||||
onChangeText={(newWeight) => {
|
||||
const fixed = fixNumeric(newWeight);
|
||||
setWeight(fixed);
|
||||
if (fixed.length !== newWeight.length)
|
||||
toast("Weight must be a number");
|
||||
}}
|
||||
onSubmitEditing={handleSubmit}
|
||||
innerRef={weightRef}
|
||||
/>
|
||||
<IconButton
|
||||
icon="add"
|
||||
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
||||
/>
|
||||
<IconButton
|
||||
icon="remove"
|
||||
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{settings.showUnit && (
|
||||
<AppInput
|
||||
|
@ -158,28 +225,30 @@ export default function EditSet() {
|
|||
/>
|
||||
)}
|
||||
|
||||
{typeof set.id === 'number' && settings.showDate && (
|
||||
{settings.showDate && (
|
||||
<AppInput
|
||||
label="Created"
|
||||
disabled
|
||||
value={format(new Date(set.created), settings.date || 'P')}
|
||||
value={format(created, settings.date || "P")}
|
||||
onPressOut={pickDate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{settings.images && newImage && (
|
||||
<TouchableRipple
|
||||
style={{marginBottom: MARGIN}}
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
onLongPress={() => setShowRemove(true)}>
|
||||
<Card.Cover source={{uri: newImage}} />
|
||||
onLongPress={() => setShowRemove(true)}
|
||||
>
|
||||
<Card.Cover source={{ uri: newImage }} />
|
||||
</TouchableRipple>
|
||||
)}
|
||||
|
||||
{settings.images && !newImage && (
|
||||
<Button
|
||||
style={{marginBottom: MARGIN}}
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
icon="add-photo-alternate">
|
||||
icon="add-photo-alternate"
|
||||
>
|
||||
Image
|
||||
</Button>
|
||||
)}
|
||||
|
@ -187,10 +256,11 @@ export default function EditSet() {
|
|||
|
||||
<Button
|
||||
disabled={!name}
|
||||
mode="contained"
|
||||
mode="outlined"
|
||||
icon="save"
|
||||
style={{margin: MARGIN}}
|
||||
onPress={handleSubmit}>
|
||||
style={{ margin: MARGIN }}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
|
@ -198,9 +268,10 @@ export default function EditSet() {
|
|||
title="Remove image"
|
||||
onOk={handleRemove}
|
||||
show={showRemove}
|
||||
setShow={setShowRemove}>
|
||||
setShow={setShowRemove}
|
||||
>
|
||||
Are you sure you want to remove the image?
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
204
EditSets.tsx
204
EditSets.tsx
|
@ -3,84 +3,84 @@ import {
|
|||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import {useCallback, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import {Button, Card, TouchableRipple} from 'react-native-paper'
|
||||
import {In} from 'typeorm'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import {MARGIN, PADDING} from './constants'
|
||||
import {setRepo, settingsRepo} from './db'
|
||||
import GymSet from './gym-set'
|
||||
import {HomePageParams} from './home-page-params'
|
||||
import AppInput from './AppInput'
|
||||
import Settings from './settings'
|
||||
import StackHeader from './StackHeader'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { Button, Card, IconButton, TouchableRipple } from "react-native-paper";
|
||||
import { In } from "typeorm";
|
||||
import AppInput from "./AppInput";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { setRepo, settingsRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import Settings from "./settings";
|
||||
import StackHeader from "./StackHeader";
|
||||
|
||||
export default function EditSets() {
|
||||
const {params} = useRoute<RouteProp<HomePageParams, 'EditSets'>>()
|
||||
const {ids} = params
|
||||
const navigation = useNavigation()
|
||||
const [settings, setSettings] = useState<Settings>({} as Settings)
|
||||
const [name, setName] = useState('')
|
||||
const [reps, setReps] = useState('')
|
||||
const [weight, setWeight] = useState('')
|
||||
const [newImage, setNewImage] = useState('')
|
||||
const [unit, setUnit] = useState('')
|
||||
const [showRemove, setShowRemove] = useState(false)
|
||||
const [names, setNames] = useState('')
|
||||
const [oldReps, setOldReps] = useState('')
|
||||
const [weights, setWeights] = useState('')
|
||||
const [units, setUnits] = useState('')
|
||||
const { params } = useRoute<RouteProp<HomePageParams, "EditSets">>();
|
||||
const { ids } = params;
|
||||
const navigation = useNavigation();
|
||||
const [settings, setSettings] = useState<Settings>({} as Settings);
|
||||
const [name, setName] = useState("");
|
||||
const [reps, setReps] = useState("");
|
||||
const [weight, setWeight] = useState("");
|
||||
const [newImage, setNewImage] = useState("");
|
||||
const [unit, setUnit] = useState("");
|
||||
const [showRemove, setShowRemove] = useState(false);
|
||||
const [names, setNames] = useState("");
|
||||
const [oldReps, setOldReps] = useState("");
|
||||
const [weights, setWeights] = useState("");
|
||||
const [units, setUnits] = useState("");
|
||||
|
||||
const [selection, setSelection] = useState({
|
||||
start: 0,
|
||||
end: 1,
|
||||
})
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||
setRepo.find({where: {id: In(ids)}}).then(sets => {
|
||||
setNames(sets.map(set => set.name).join(', '))
|
||||
setOldReps(sets.map(set => set.reps).join(', '))
|
||||
setWeights(sets.map(set => set.weight).join(', '))
|
||||
setUnits(sets.map(set => set.unit).join(', '))
|
||||
})
|
||||
}, [ids]),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
setRepo.find({ where: { id: In(ids) } }).then((sets) => {
|
||||
setNames(sets.map((set) => set.name).join(", "));
|
||||
setOldReps(sets.map((set) => set.reps).join(", "));
|
||||
setWeights(sets.map((set) => set.weight).join(", "));
|
||||
setUnits(sets.map((set) => set.unit).join(", "));
|
||||
});
|
||||
}, [ids])
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
console.log(`${EditSets.name}.handleSubmit:`, {uri: newImage, name})
|
||||
const update: Partial<GymSet> = {}
|
||||
if (name) update.name = name
|
||||
if (reps) update.reps = Number(reps)
|
||||
if (weight) update.weight = Number(weight)
|
||||
if (unit) update.unit = unit
|
||||
if (newImage) update.image = newImage
|
||||
if (Object.keys(update).length > 0) await setRepo.update(ids, update)
|
||||
navigation.goBack()
|
||||
}
|
||||
console.log(`${EditSets.name}.handleSubmit:`, { uri: newImage, name });
|
||||
const update: Partial<GymSet> = {};
|
||||
if (name) update.name = name;
|
||||
if (reps) update.reps = Number(reps);
|
||||
if (weight) update.weight = Number(weight);
|
||||
if (unit) update.unit = unit;
|
||||
if (newImage) update.image = newImage;
|
||||
if (Object.keys(update).length > 0) await setRepo.update(ids, update);
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const changeImage = useCallback(async () => {
|
||||
const {fileCopyUri} = await DocumentPicker.pickSingle({
|
||||
const { fileCopyUri } = await DocumentPicker.pickSingle({
|
||||
type: DocumentPicker.types.images,
|
||||
copyTo: 'documentDirectory',
|
||||
})
|
||||
if (fileCopyUri) setNewImage(fileCopyUri)
|
||||
}, [])
|
||||
copyTo: "documentDirectory",
|
||||
});
|
||||
if (fileCopyUri) setNewImage(fileCopyUri);
|
||||
}, []);
|
||||
|
||||
const handleRemove = useCallback(async () => {
|
||||
setNewImage('')
|
||||
setShowRemove(false)
|
||||
}, [])
|
||||
setNewImage("");
|
||||
setShowRemove(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title={`Edit ${ids.length} sets`} />
|
||||
|
||||
<View style={{padding: PADDING, flex: 1}}>
|
||||
<View style={{ padding: PADDING, flex: 1 }}>
|
||||
<AppInput
|
||||
label={`Names: ${names}`}
|
||||
value={name}
|
||||
|
@ -89,23 +89,57 @@ export default function EditSets() {
|
|||
autoFocus={!name}
|
||||
/>
|
||||
|
||||
<AppInput
|
||||
label={`Reps: ${oldReps}`}
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={setReps}
|
||||
selection={selection}
|
||||
onSelectionChange={e => setSelection(e.nativeEvent.selection)}
|
||||
autoFocus={!!name}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: MARGIN,
|
||||
}}
|
||||
>
|
||||
<AppInput
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
label={`Reps: ${oldReps}`}
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={setReps}
|
||||
selection={selection}
|
||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
||||
autoFocus={!!name}
|
||||
/>
|
||||
<IconButton
|
||||
icon="add"
|
||||
onPress={() => setReps((Number(reps) + 1).toString())}
|
||||
/>
|
||||
<IconButton
|
||||
icon="remove"
|
||||
onPress={() => setReps((Number(reps) - 1).toString())}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<AppInput
|
||||
label={`Weights: ${weights}`}
|
||||
keyboardType="numeric"
|
||||
value={weight}
|
||||
onChangeText={setWeight}
|
||||
onSubmitEditing={handleSubmit}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: MARGIN,
|
||||
}}
|
||||
>
|
||||
<AppInput
|
||||
style={{ flex: 1 }}
|
||||
label={`Weights: ${weights}`}
|
||||
keyboardType="numeric"
|
||||
value={weight}
|
||||
onChangeText={setWeight}
|
||||
onSubmitEditing={handleSubmit}
|
||||
/>
|
||||
<IconButton
|
||||
icon="add"
|
||||
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
||||
/>
|
||||
<IconButton
|
||||
icon="remove"
|
||||
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{settings.showUnit && (
|
||||
<AppInput
|
||||
|
@ -118,37 +152,41 @@ export default function EditSets() {
|
|||
|
||||
{settings.images && newImage && (
|
||||
<TouchableRipple
|
||||
style={{marginBottom: MARGIN}}
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
onLongPress={() => setShowRemove(true)}>
|
||||
<Card.Cover source={{uri: newImage}} />
|
||||
onLongPress={() => setShowRemove(true)}
|
||||
>
|
||||
<Card.Cover source={{ uri: newImage }} />
|
||||
</TouchableRipple>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
title="Remove image"
|
||||
onOk={handleRemove}
|
||||
show={showRemove}
|
||||
setShow={setShowRemove}>
|
||||
setShow={setShowRemove}
|
||||
>
|
||||
Are you sure you want to remove the image?
|
||||
</ConfirmDialog>
|
||||
|
||||
{settings.images && !newImage && (
|
||||
<Button
|
||||
style={{marginBottom: MARGIN}}
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
icon="add-photo-alternate">
|
||||
icon="add-photo-alternate"
|
||||
>
|
||||
Image
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
mode="contained"
|
||||
mode="outlined"
|
||||
icon="save"
|
||||
style={{margin: MARGIN}}
|
||||
onPress={handleSubmit}>
|
||||
style={{ margin: MARGIN }}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
161
EditWorkout.tsx
161
EditWorkout.tsx
|
@ -3,70 +3,72 @@ import {
|
|||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import {useCallback, useRef, useState} from 'react'
|
||||
import {ScrollView, TextInput, View} from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import {Button, Card, TouchableRipple} from 'react-native-paper'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import {MARGIN, PADDING} from './constants'
|
||||
import {getNow, planRepo, setRepo, settingsRepo} from './db'
|
||||
import {defaultSet} from './gym-set'
|
||||
import AppInput from './AppInput'
|
||||
import Settings from './settings'
|
||||
import StackHeader from './StackHeader'
|
||||
import {WorkoutsPageParams} from './WorkoutsPage'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { ScrollView, TextInput, View } from "react-native";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { Button, Card, TouchableRipple } from "react-native-paper";
|
||||
import AppInput from "./AppInput";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { getNow, planRepo, setRepo, settingsRepo } from "./db";
|
||||
import { fixNumeric } from "./fix-numeric";
|
||||
import { defaultSet } from "./gym-set";
|
||||
import Settings from "./settings";
|
||||
import StackHeader from "./StackHeader";
|
||||
import { toast } from "./toast";
|
||||
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||
|
||||
export default function EditWorkout() {
|
||||
const {params} = useRoute<RouteProp<WorkoutsPageParams, 'EditWorkout'>>()
|
||||
const [removeImage, setRemoveImage] = useState(false)
|
||||
const [showRemove, setShowRemove] = useState(false)
|
||||
const [name, setName] = useState(params.value.name)
|
||||
const [steps, setSteps] = useState(params.value.steps)
|
||||
const [uri, setUri] = useState(params.value.image)
|
||||
const { params } = useRoute<RouteProp<WorkoutsPageParams, "EditWorkout">>();
|
||||
const [removeImage, setRemoveImage] = useState(false);
|
||||
const [showRemove, setShowRemove] = useState(false);
|
||||
const [name, setName] = useState(params.value.name);
|
||||
const [steps, setSteps] = useState(params.value.steps);
|
||||
const [uri, setUri] = useState(params.value.image);
|
||||
const [minutes, setMinutes] = useState(
|
||||
params.value.minutes?.toString() ?? '3',
|
||||
)
|
||||
params.value.minutes?.toString() ?? "3"
|
||||
);
|
||||
const [seconds, setSeconds] = useState(
|
||||
params.value.seconds?.toString() ?? '30',
|
||||
)
|
||||
const [sets, setSets] = useState(params.value.sets?.toString() ?? '3')
|
||||
const navigation = useNavigation()
|
||||
const setsRef = useRef<TextInput>(null)
|
||||
const stepsRef = useRef<TextInput>(null)
|
||||
const minutesRef = useRef<TextInput>(null)
|
||||
const secondsRef = useRef<TextInput>(null)
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
params.value.seconds?.toString() ?? "30"
|
||||
);
|
||||
const [sets, setSets] = useState(params.value.sets?.toString() ?? "3");
|
||||
const navigation = useNavigation();
|
||||
const setsRef = useRef<TextInput>(null);
|
||||
const stepsRef = useRef<TextInput>(null);
|
||||
const minutesRef = useRef<TextInput>(null);
|
||||
const secondsRef = useRef<TextInput>(null);
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||
}, []),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const update = async () => {
|
||||
await setRepo.update(
|
||||
{name: params.value.name},
|
||||
{ name: params.value.name },
|
||||
{
|
||||
name: name || params.value.name,
|
||||
sets: Number(sets),
|
||||
minutes: +minutes,
|
||||
seconds: +seconds,
|
||||
steps,
|
||||
image: removeImage ? '' : uri,
|
||||
},
|
||||
)
|
||||
image: removeImage ? "" : uri,
|
||||
}
|
||||
);
|
||||
await planRepo.query(
|
||||
`UPDATE plans
|
||||
SET workouts = REPLACE(workouts, $1, $2)
|
||||
WHERE workouts LIKE $3`,
|
||||
[params.value.name, name, `%${params.value.name}%`],
|
||||
)
|
||||
navigation.goBack()
|
||||
}
|
||||
[params.value.name, name, `%${params.value.name}%`]
|
||||
);
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
const [{now}] = await getNow()
|
||||
const now = await getNow();
|
||||
await setRepo.save({
|
||||
...defaultSet,
|
||||
name,
|
||||
|
@ -77,39 +79,39 @@ export default function EditWorkout() {
|
|||
sets: sets ? +sets : 3,
|
||||
steps,
|
||||
created: now,
|
||||
})
|
||||
navigation.goBack()
|
||||
}
|
||||
});
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (params.value.name) return update()
|
||||
return add()
|
||||
}
|
||||
if (params.value.name) return update();
|
||||
return add();
|
||||
};
|
||||
|
||||
const changeImage = useCallback(async () => {
|
||||
const {fileCopyUri} = await DocumentPicker.pickSingle({
|
||||
const { fileCopyUri } = await DocumentPicker.pickSingle({
|
||||
type: DocumentPicker.types.images,
|
||||
copyTo: 'documentDirectory',
|
||||
})
|
||||
if (fileCopyUri) setUri(fileCopyUri)
|
||||
}, [])
|
||||
copyTo: "documentDirectory",
|
||||
});
|
||||
if (fileCopyUri) setUri(fileCopyUri);
|
||||
}, []);
|
||||
|
||||
const handleRemove = useCallback(async () => {
|
||||
setUri('')
|
||||
setRemoveImage(true)
|
||||
setShowRemove(false)
|
||||
}, [])
|
||||
setUri("");
|
||||
setRemoveImage(true);
|
||||
setShowRemove(false);
|
||||
}, []);
|
||||
|
||||
const submitName = () => {
|
||||
if (settings.steps) stepsRef.current?.focus()
|
||||
else setsRef.current?.focus()
|
||||
}
|
||||
if (settings.steps) stepsRef.current?.focus();
|
||||
else setsRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title="Edit workout" />
|
||||
<View style={{padding: PADDING, flex: 1}}>
|
||||
<ScrollView style={{flex: 1}}>
|
||||
<StackHeader title={params.value.name ? "Edit workout" : "Add workout"} />
|
||||
<View style={{ padding: PADDING, flex: 1 }}>
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<AppInput
|
||||
autoFocus
|
||||
label="Name"
|
||||
|
@ -131,7 +133,12 @@ export default function EditWorkout() {
|
|||
<AppInput
|
||||
innerRef={setsRef}
|
||||
value={sets}
|
||||
onChangeText={setSets}
|
||||
onChangeText={(newSets) => {
|
||||
const fixed = fixNumeric(newSets);
|
||||
setSets(fixed);
|
||||
if (fixed.length !== newSets.length)
|
||||
toast("Sets must be a number");
|
||||
}}
|
||||
label="Sets per workout"
|
||||
keyboardType="numeric"
|
||||
onSubmitEditing={() => minutesRef.current?.focus()}
|
||||
|
@ -142,7 +149,12 @@ export default function EditWorkout() {
|
|||
innerRef={minutesRef}
|
||||
onSubmitEditing={() => secondsRef.current?.focus()}
|
||||
value={minutes}
|
||||
onChangeText={setMinutes}
|
||||
onChangeText={(newMinutes) => {
|
||||
const fixed = fixNumeric(newMinutes);
|
||||
setMinutes(fixed);
|
||||
if (fixed.length !== newMinutes.length)
|
||||
toast("Reps must be a number");
|
||||
}}
|
||||
label="Rest minutes"
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
|
@ -158,32 +170,35 @@ export default function EditWorkout() {
|
|||
)}
|
||||
{settings?.images && uri && (
|
||||
<TouchableRipple
|
||||
style={{marginBottom: MARGIN}}
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
onLongPress={() => setShowRemove(true)}>
|
||||
<Card.Cover source={{uri}} />
|
||||
onLongPress={() => setShowRemove(true)}
|
||||
>
|
||||
<Card.Cover source={{ uri }} />
|
||||
</TouchableRipple>
|
||||
)}
|
||||
{settings?.images && !uri && (
|
||||
<Button
|
||||
style={{marginBottom: MARGIN}}
|
||||
style={{ marginBottom: MARGIN }}
|
||||
onPress={changeImage}
|
||||
icon="add-photo-alternate">
|
||||
icon="add-photo-alternate"
|
||||
>
|
||||
Image
|
||||
</Button>
|
||||
)}
|
||||
</ScrollView>
|
||||
<Button disabled={!name} mode="contained" icon="save" onPress={save}>
|
||||
<Button disabled={!name} mode="outlined" icon="save" onPress={save}>
|
||||
Save
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
title="Remove image"
|
||||
onOk={handleRemove}
|
||||
show={showRemove}
|
||||
setShow={setShowRemove}>
|
||||
setShow={setShowRemove}
|
||||
>
|
||||
Are you sure you want to remove the image?
|
||||
</ConfirmDialog>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -1,6 +1,6 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby '2.7.5'
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
gem 'cocoapods', '~> 1.11', '>= 1.11.2'
|
||||
gem 'cocoapods', '~> 1.12'
|
||||
|
|
102
GraphsList.tsx
Normal file
102
GraphsList.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
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 { getBestSets } from "./best.service";
|
||||
import { LIMIT } from "./constants";
|
||||
import { settingsRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import { GraphsPageParams } from "./GraphsPage";
|
||||
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<GraphsPageParams>>();
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const refresh = useCallback(async (value: string) => {
|
||||
const result = await getBestSets({ term: value, offset: 0 });
|
||||
setBests(result);
|
||||
setOffset(0);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refresh(term);
|
||||
}, [refresh, 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 });
|
||||
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", { 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}
|
||||
onEndReached={next}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
}
|
23
GraphsPage.tsx
Normal file
23
GraphsPage.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import GraphsList from "./GraphsList";
|
||||
import GymSet from "./gym-set";
|
||||
import ViewGraph from "./ViewGraph";
|
||||
|
||||
const Stack = createStackNavigator<GraphsPageParams>();
|
||||
export type GraphsPageParams = {
|
||||
GraphsList: {};
|
||||
ViewGraph: {
|
||||
best: GymSet;
|
||||
};
|
||||
};
|
||||
|
||||
export default function GraphsPage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
||||
>
|
||||
<Stack.Screen name="GraphsList" component={GraphsList} />
|
||||
<Stack.Screen name="ViewGraph" component={ViewGraph} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
17
HomePage.tsx
17
HomePage.tsx
|
@ -1,18 +1,19 @@
|
|||
import {createStackNavigator} from '@react-navigation/stack'
|
||||
import EditSet from './EditSet'
|
||||
import EditSets from './EditSets'
|
||||
import {HomePageParams} from './home-page-params'
|
||||
import SetList from './SetList'
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import EditSet from "./EditSet";
|
||||
import EditSets from "./EditSets";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import SetList from "./SetList";
|
||||
|
||||
const Stack = createStackNavigator<HomePageParams>()
|
||||
const Stack = createStackNavigator<HomePageParams>();
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{headerShown: false, animationEnabled: false}}>
|
||||
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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
80
ListMenu.tsx
80
ListMenu.tsx
|
@ -1,7 +1,6 @@
|
|||
import {useState} from 'react'
|
||||
import {Divider, IconButton, Menu} from 'react-native-paper'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import useDark from './use-dark'
|
||||
import { useState } from "react";
|
||||
import { Divider, IconButton, Menu } from "react-native-paper";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
|
||||
export default function ListMenu({
|
||||
onEdit,
|
||||
|
@ -11,84 +10,79 @@ export default function ListMenu({
|
|||
onSelect,
|
||||
ids,
|
||||
}: {
|
||||
onEdit: () => void
|
||||
onCopy: () => void
|
||||
onClear: () => void
|
||||
onDelete: () => void
|
||||
onSelect: () => void
|
||||
ids?: number[]
|
||||
onEdit: () => void;
|
||||
onCopy: () => void;
|
||||
onClear: () => void;
|
||||
onDelete: () => void;
|
||||
onSelect: () => void;
|
||||
ids?: number[];
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const [showRemove, setShowRemove] = useState(false)
|
||||
const dark = useDark()
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [showRemove, setShowRemove] = useState(false);
|
||||
|
||||
const edit = () => {
|
||||
setShowMenu(false)
|
||||
onEdit()
|
||||
}
|
||||
setShowMenu(false);
|
||||
onEdit();
|
||||
};
|
||||
|
||||
const copy = () => {
|
||||
setShowMenu(false)
|
||||
onCopy()
|
||||
}
|
||||
setShowMenu(false);
|
||||
onCopy();
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
setShowMenu(false)
|
||||
onClear()
|
||||
}
|
||||
setShowMenu(false);
|
||||
onClear();
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
setShowMenu(false)
|
||||
setShowRemove(false)
|
||||
onDelete()
|
||||
}
|
||||
setShowMenu(false);
|
||||
setShowRemove(false);
|
||||
onDelete();
|
||||
};
|
||||
|
||||
const select = () => {
|
||||
onSelect()
|
||||
}
|
||||
onSelect();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
visible={showMenu}
|
||||
onDismiss={() => setShowMenu(false)}
|
||||
anchor={
|
||||
<IconButton
|
||||
color={dark ? 'white' : 'white'}
|
||||
onPress={() => setShowMenu(true)}
|
||||
icon="more-vert"
|
||||
/>
|
||||
}>
|
||||
<Menu.Item icon="done-all" title="Select all" onPress={select} />
|
||||
anchor={<IconButton onPress={() => setShowMenu(true)} icon="more-vert" />}
|
||||
>
|
||||
<Menu.Item leadingIcon="done-all" title="Select all" onPress={select} />
|
||||
<Menu.Item
|
||||
icon="clear"
|
||||
leadingIcon="clear"
|
||||
title="Clear"
|
||||
onPress={clear}
|
||||
disabled={ids?.length === 0}
|
||||
/>
|
||||
<Menu.Item
|
||||
icon="edit"
|
||||
leadingIcon="edit"
|
||||
title="Edit"
|
||||
onPress={edit}
|
||||
disabled={ids?.length === 0}
|
||||
/>
|
||||
<Menu.Item
|
||||
icon="content-copy"
|
||||
leadingIcon="content-copy"
|
||||
title="Copy"
|
||||
onPress={copy}
|
||||
disabled={ids?.length === 0}
|
||||
/>
|
||||
<Divider />
|
||||
<Menu.Item
|
||||
icon="delete"
|
||||
leadingIcon="delete"
|
||||
onPress={() => setShowRemove(true)}
|
||||
title="Delete"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title={ids?.length === 0 ? 'Delete all' : 'Delete selected'}
|
||||
title={ids?.length === 0 ? "Delete all" : "Delete selected"}
|
||||
show={showRemove}
|
||||
setShow={setShowRemove}
|
||||
onOk={remove}
|
||||
onCancel={() => setShowMenu(false)}>
|
||||
onCancel={() => setShowMenu(false)}
|
||||
>
|
||||
{ids?.length === 0 ? (
|
||||
<>This irreversibly deletes records from the app. Are you sure?</>
|
||||
) : (
|
||||
|
@ -96,5 +90,5 @@ export default function ListMenu({
|
|||
)}
|
||||
</ConfirmDialog>
|
||||
</Menu>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
22
Page.tsx
22
Page.tsx
|
@ -1,7 +1,7 @@
|
|||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {Searchbar} from 'react-native-paper'
|
||||
import AppFab from './AppFab'
|
||||
import {PADDING} from './constants'
|
||||
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { Searchbar } from "react-native-paper";
|
||||
import AppFab from "./AppFab";
|
||||
import { PADDING } from "./constants";
|
||||
|
||||
export default function Page({
|
||||
onAdd,
|
||||
|
@ -10,11 +10,11 @@ export default function Page({
|
|||
search,
|
||||
style,
|
||||
}: {
|
||||
children: JSX.Element | JSX.Element[]
|
||||
onAdd?: () => void
|
||||
term: string
|
||||
search: (value: string) => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
children: JSX.Element | JSX.Element[];
|
||||
onAdd?: () => void;
|
||||
term: string;
|
||||
search: (value: string) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}) {
|
||||
return (
|
||||
<View style={[styles.view, style]}>
|
||||
|
@ -28,7 +28,7 @@ export default function Page({
|
|||
{children}
|
||||
{onAdd && <AppFab onPress={onAdd} />}
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -36,4 +36,4 @@ const styles = StyleSheet.create({
|
|||
padding: PADDING,
|
||||
flexGrow: 1,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
131
PlanItem.tsx
131
PlanItem.tsx
|
@ -2,83 +2,98 @@ import {
|
|||
NavigationProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {Text} from 'react-native'
|
||||
import {List} from 'react-native-paper'
|
||||
import {getBestSet} from './best.service'
|
||||
import {DARK_RIPPLE, LIGHT_RIPPLE} from './constants'
|
||||
import {defaultSet} from './gym-set'
|
||||
import {Plan} from './plan'
|
||||
import {PlanPageParams} from './plan-page-params'
|
||||
import {DAYS} from './time'
|
||||
import useDark from './use-dark'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Text } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
||||
import { setRepo } from "./db";
|
||||
import { defaultSet } from "./gym-set";
|
||||
import { Plan } from "./plan";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import { DAYS } from "./time";
|
||||
import useDark from "./use-dark";
|
||||
|
||||
export default function PlanItem({
|
||||
item,
|
||||
setIds,
|
||||
ids,
|
||||
}: {
|
||||
item: Plan
|
||||
ids: number[]
|
||||
setIds: (value: number[]) => void
|
||||
item: Plan;
|
||||
ids: number[];
|
||||
setIds: (value: number[]) => void;
|
||||
}) {
|
||||
const [today, setToday] = useState<string>()
|
||||
const dark = useDark()
|
||||
const days = useMemo(() => item.days.split(','), [item.days])
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>()
|
||||
const [today, setToday] = useState<string>();
|
||||
const dark = useDark();
|
||||
const days = useMemo(() => item.days.split(","), [item.days]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const newToday = DAYS[new Date().getDay()]
|
||||
setToday(newToday)
|
||||
}, []),
|
||||
)
|
||||
const newToday = DAYS[new Date().getDay()];
|
||||
setToday(newToday);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
const workout = item.workouts.split(',')[0]
|
||||
let first = await getBestSet(workout)
|
||||
if (!first) first = {...defaultSet, name: workout}
|
||||
delete first.id
|
||||
if (ids.length === 0)
|
||||
return navigation.navigate('StartPlan', {plan: item, first})
|
||||
const removing = ids.find(id => id === item.id)
|
||||
if (removing) setIds(ids.filter(id => id !== item.id))
|
||||
else setIds([...ids, item.id])
|
||||
}, [ids, setIds, item, navigation])
|
||||
const workout = item.workouts.split(",")[0];
|
||||
let first = await setRepo.findOne({
|
||||
where: { name: workout },
|
||||
order: { created: "desc" },
|
||||
});
|
||||
if (!first) first = { ...defaultSet, name: workout };
|
||||
delete first.id;
|
||||
if (ids.length === 0) {
|
||||
return navigation.navigate("StartPlan", { plan: item, first });
|
||||
}
|
||||
const removing = ids.find((id) => id === item.id);
|
||||
if (removing) setIds(ids.filter((id) => id !== item.id));
|
||||
else setIds([...ids, item.id]);
|
||||
}, [ids, setIds, item, navigation]);
|
||||
|
||||
const longPress = useCallback(() => {
|
||||
if (ids.length > 0) return
|
||||
setIds([item.id])
|
||||
}, [ids.length, item.id, setIds])
|
||||
if (ids.length > 0) return;
|
||||
setIds([item.id]);
|
||||
}, [ids.length, item.id, setIds]);
|
||||
|
||||
const 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(
|
||||
() =>
|
||||
days.map((day, index) => (
|
||||
<Text key={day}>
|
||||
{day === today ? (
|
||||
<Text style={{fontWeight: 'bold', textDecorationLine: 'underline'}}>
|
||||
{day}
|
||||
</Text>
|
||||
) : (
|
||||
day
|
||||
)}
|
||||
{index === days.length - 1 ? '' : ', '}
|
||||
</Text>
|
||||
)),
|
||||
[days, today],
|
||||
)
|
||||
item.title ? (
|
||||
<Text style={{ fontWeight: "bold" }}>{item.title}</Text>
|
||||
) : (
|
||||
currentDays
|
||||
),
|
||||
[item.title, currentDays]
|
||||
);
|
||||
|
||||
const description = useMemo(
|
||||
() => item.workouts.replace(/,/g, ', '),
|
||||
[item.workouts],
|
||||
)
|
||||
() => (item.title ? currentDays : item.workouts.replace(/,/g, ", ")),
|
||||
[item.title, currentDays, item.workouts]
|
||||
);
|
||||
|
||||
const backgroundColor = useMemo(() => {
|
||||
if (!ids.includes(item.id)) return
|
||||
if (dark) return DARK_RIPPLE
|
||||
return LIGHT_RIPPLE
|
||||
}, [dark, ids, item.id])
|
||||
if (!ids.includes(item.id)) return;
|
||||
if (dark) return DARK_RIPPLE;
|
||||
return LIGHT_RIPPLE;
|
||||
}, [dark, ids, item.id]);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
|
@ -86,7 +101,7 @@ export default function PlanItem({
|
|||
title={title}
|
||||
description={description}
|
||||
onLongPress={longPress}
|
||||
style={{backgroundColor}}
|
||||
style={{ backgroundColor }}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
110
PlanList.tsx
110
PlanList.tsx
|
@ -2,89 +2,95 @@ import {
|
|||
NavigationProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
import {useCallback, useState} from 'react'
|
||||
import {FlatList} from 'react-native'
|
||||
import {List} from 'react-native-paper'
|
||||
import {Like} from 'typeorm'
|
||||
import {planRepo} from './db'
|
||||
import DrawerHeader from './DrawerHeader'
|
||||
import ListMenu from './ListMenu'
|
||||
import Page from './Page'
|
||||
import {Plan} from './plan'
|
||||
import {PlanPageParams} from './plan-page-params'
|
||||
import PlanItem from './PlanItem'
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { Like } from "typeorm";
|
||||
import { planRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import ListMenu from "./ListMenu";
|
||||
import Page from "./Page";
|
||||
import { Plan } from "./plan";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import PlanItem from "./PlanItem";
|
||||
|
||||
export default function PlanList() {
|
||||
const [term, setTerm] = useState('')
|
||||
const [plans, setPlans] = useState<Plan[]>()
|
||||
const [ids, setIds] = useState<number[]>([])
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>()
|
||||
const [term, setTerm] = useState("");
|
||||
const [plans, setPlans] = useState<Plan[]>();
|
||||
const [ids, setIds] = useState<number[]>([]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
const refresh = useCallback(async (value: string) => {
|
||||
planRepo
|
||||
.find({
|
||||
where: [{days: Like(`%${value}%`)}, {workouts: Like(`%${value}%`)}],
|
||||
where: [
|
||||
{ title: Like(`%${value.trim()}%`) },
|
||||
{ days: Like(`%${value.trim()}%`) },
|
||||
{ workouts: Like(`%${value.trim()}%`) },
|
||||
],
|
||||
})
|
||||
.then(setPlans)
|
||||
}, [])
|
||||
.then(setPlans);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refresh(term)
|
||||
}, [refresh, term]),
|
||||
)
|
||||
refresh(term);
|
||||
}, [refresh, term])
|
||||
);
|
||||
|
||||
const search = useCallback(
|
||||
(value: string) => {
|
||||
setTerm(value)
|
||||
refresh(value)
|
||||
setTerm(value);
|
||||
refresh(value);
|
||||
},
|
||||
[refresh],
|
||||
)
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: Plan}) => (
|
||||
({ item }: { item: Plan }) => (
|
||||
<PlanItem ids={ids} setIds={setIds} item={item} key={item.id} />
|
||||
),
|
||||
[ids],
|
||||
)
|
||||
[ids]
|
||||
);
|
||||
|
||||
const onAdd = () =>
|
||||
navigation.navigate('EditPlan', {plan: {days: '', workouts: ''}})
|
||||
navigation.navigate("EditPlan", {
|
||||
plan: { title: "", days: "", workouts: "" },
|
||||
});
|
||||
|
||||
const edit = useCallback(async () => {
|
||||
const plan = await planRepo.findOne({where: {id: ids.pop()}})
|
||||
navigation.navigate('EditPlan', {plan})
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
const plan = await planRepo.findOne({ where: { id: ids.pop() } });
|
||||
navigation.navigate("EditPlan", { plan });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const copy = useCallback(async () => {
|
||||
const plan = await planRepo.findOne({
|
||||
where: {id: ids.pop()},
|
||||
})
|
||||
delete plan.id
|
||||
navigation.navigate('EditPlan', {plan})
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
where: { id: ids.pop() },
|
||||
});
|
||||
delete plan.id;
|
||||
navigation.navigate("EditPlan", { plan });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setIds([])
|
||||
}, [])
|
||||
setIds([]);
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
await planRepo.delete(ids.length > 0 ? ids : {})
|
||||
await refresh(term)
|
||||
setIds([])
|
||||
}, [ids, refresh, term])
|
||||
await planRepo.delete(ids.length > 0 ? ids : {});
|
||||
await refresh(term);
|
||||
setIds([]);
|
||||
}, [ids, refresh, term]);
|
||||
|
||||
const select = useCallback(() => {
|
||||
setIds(plans.map(plan => plan.id))
|
||||
}, [plans])
|
||||
setIds(plans.map((plan) => plan.id));
|
||||
}, [plans]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name="Plans">
|
||||
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Plans"}>
|
||||
<ListMenu
|
||||
onClear={clear}
|
||||
onCopy={copy}
|
||||
|
@ -102,13 +108,13 @@ export default function PlanList() {
|
|||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
style={{flex: 1}}
|
||||
style={{ flex: 1 }}
|
||||
data={plans}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={set => set.id?.toString() || ''}
|
||||
keyExtractor={(set) => set.id?.toString() || ""}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
19
PlanPage.tsx
19
PlanPage.tsx
|
@ -1,20 +1,21 @@
|
|||
import {createStackNavigator} from '@react-navigation/stack'
|
||||
import EditPlan from './EditPlan'
|
||||
import EditSet from './EditSet'
|
||||
import {PlanPageParams} from './plan-page-params'
|
||||
import PlanList from './PlanList'
|
||||
import StartPlan from './StartPlan'
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import EditPlan from "./EditPlan";
|
||||
import EditSet from "./EditSet";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import PlanList from "./PlanList";
|
||||
import StartPlan from "./StartPlan";
|
||||
|
||||
const Stack = createStackNavigator<PlanPageParams>()
|
||||
const Stack = createStackNavigator<PlanPageParams>();
|
||||
|
||||
export default function PlanPage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{headerShown: false, animationEnabled: false}}>
|
||||
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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
85
Routes.tsx
85
Routes.tsx
|
@ -1,50 +1,57 @@
|
|||
import {createDrawerNavigator} from '@react-navigation/drawer'
|
||||
import {useMemo} from 'react'
|
||||
import {IconButton} from 'react-native-paper'
|
||||
import BestPage from './BestPage'
|
||||
import {DrawerParamList} from './drawer-param-list'
|
||||
import HomePage from './HomePage'
|
||||
import PlanPage from './PlanPage'
|
||||
import Route from './route'
|
||||
import SettingsPage from './SettingsPage'
|
||||
import TimerPage from './TimerPage'
|
||||
import useDark from './use-dark'
|
||||
import WorkoutsPage from './WorkoutsPage'
|
||||
import { createDrawerNavigator } from "@react-navigation/drawer";
|
||||
import { IconButton } from "react-native-paper";
|
||||
import GraphsPage from "./GraphsPage";
|
||||
import { DrawerParamList } from "./drawer-param-list";
|
||||
import HomePage from "./HomePage";
|
||||
import PlanPage from "./PlanPage";
|
||||
import SettingsPage from "./SettingsPage";
|
||||
import TimerPage from "./TimerPage";
|
||||
import useDark from "./use-dark";
|
||||
import WorkoutsPage from "./WorkoutsPage";
|
||||
|
||||
const Drawer = createDrawerNavigator<DrawerParamList>()
|
||||
const Drawer = createDrawerNavigator<DrawerParamList>();
|
||||
|
||||
export default function Routes() {
|
||||
const dark = useDark()
|
||||
|
||||
const routes: Route[] = useMemo(
|
||||
() => [
|
||||
{name: 'Home', component: HomePage, icon: 'home'},
|
||||
{name: 'Plans', component: PlanPage, icon: 'event'},
|
||||
{name: 'Best', component: BestPage, icon: 'insights'},
|
||||
{name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'},
|
||||
{name: 'Timer', component: TimerPage, icon: 'access-time'},
|
||||
{name: 'Settings', component: SettingsPage, icon: 'settings'},
|
||||
],
|
||||
[],
|
||||
)
|
||||
const dark = useDark();
|
||||
|
||||
return (
|
||||
<Drawer.Navigator
|
||||
screenOptions={{
|
||||
headerTintColor: dark ? 'white' : 'black',
|
||||
headerTintColor: dark ? "white" : "black",
|
||||
swipeEdgeWidth: 1000,
|
||||
headerShown: false,
|
||||
}}>
|
||||
{routes.map(route => (
|
||||
<Drawer.Screen
|
||||
key={route.name}
|
||||
name={route.name}
|
||||
component={route.component}
|
||||
options={{
|
||||
drawerIcon: () => <IconButton icon={route.icon} />,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
65
Select.tsx
65
Select.tsx
|
@ -1,12 +1,12 @@
|
|||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Button, Menu, Subheading, useTheme} from 'react-native-paper'
|
||||
import {ITEM_PADDING} from './constants'
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Button, Menu, Subheading, useTheme } from "react-native-paper";
|
||||
import { ITEM_PADDING } from "./constants";
|
||||
|
||||
export interface Item {
|
||||
value: string
|
||||
label: string
|
||||
color?: string
|
||||
value: string;
|
||||
label: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function Select({
|
||||
|
@ -15,35 +15,36 @@ function Select({
|
|||
items,
|
||||
label,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
items: Item[]
|
||||
label?: string
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
items: Item[];
|
||||
label?: string;
|
||||
}) {
|
||||
const [show, setShow] = useState(false)
|
||||
const {colors} = useTheme()
|
||||
const [show, setShow] = useState(false);
|
||||
const { colors } = useTheme();
|
||||
|
||||
const selected = useMemo(
|
||||
() => items.find(item => item.value === value) || items[0],
|
||||
[items, value],
|
||||
)
|
||||
() => items.find((item) => item.value === value) || items[0],
|
||||
[items, value]
|
||||
);
|
||||
|
||||
const handlePress = useCallback(
|
||||
(newValue: string) => {
|
||||
onChange(newValue)
|
||||
setShow(false)
|
||||
onChange(newValue);
|
||||
setShow(false);
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: ITEM_PADDING,
|
||||
}}>
|
||||
{label && <Subheading style={{width: 100}}>{label}</Subheading>}
|
||||
}}
|
||||
>
|
||||
{label && <Subheading style={{ width: 100 }}>{label}</Subheading>}
|
||||
<Menu
|
||||
visible={show}
|
||||
onDismiss={() => setShow(false)}
|
||||
|
@ -51,22 +52,24 @@ function Select({
|
|||
<Button
|
||||
onPress={() => setShow(true)}
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
}}>
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
>
|
||||
{selected?.label}
|
||||
</Button>
|
||||
}>
|
||||
{items.map(item => (
|
||||
}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<Menu.Item
|
||||
titleStyle={{ color: item.color || colors.onSurface }}
|
||||
key={item.value}
|
||||
titleStyle={{color: item.color || colors.text}}
|
||||
title={item.label}
|
||||
onPress={() => handlePress(item.value)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Select)
|
||||
export default React.memo(Select);
|
||||
|
|
118
SetItem.tsx
118
SetItem.tsx
|
@ -1,13 +1,13 @@
|
|||
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||
import {format} from 'date-fns'
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {Image} from 'react-native'
|
||||
import {List, Text} from 'react-native-paper'
|
||||
import {DARK_RIPPLE, LIGHT_RIPPLE} from './constants'
|
||||
import GymSet from './gym-set'
|
||||
import {HomePageParams} from './home-page-params'
|
||||
import Settings from './settings'
|
||||
import useDark from './use-dark'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Image } from "react-native";
|
||||
import { List, Text } from "react-native-paper";
|
||||
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
|
||||
import GymSet from "./gym-set";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import Settings from "./settings";
|
||||
import useDark from "./use-dark";
|
||||
|
||||
export default function SetItem({
|
||||
item,
|
||||
|
@ -15,61 +15,63 @@ export default function SetItem({
|
|||
ids,
|
||||
setIds,
|
||||
}: {
|
||||
item: GymSet
|
||||
onRemove: () => void
|
||||
settings: Settings
|
||||
ids: number[]
|
||||
setIds: (value: number[]) => void
|
||||
item: GymSet;
|
||||
onRemove: () => void;
|
||||
settings: Settings;
|
||||
ids: number[];
|
||||
setIds: (value: number[]) => void;
|
||||
}) {
|
||||
const dark = useDark()
|
||||
const navigation = useNavigation<NavigationProp<HomePageParams>>()
|
||||
const dark = useDark();
|
||||
const navigation = useNavigation<NavigationProp<HomePageParams>>();
|
||||
|
||||
const longPress = useCallback(() => {
|
||||
if (ids.length > 0) return
|
||||
setIds([item.id])
|
||||
}, [ids.length, item.id, setIds])
|
||||
if (ids.length > 0) return;
|
||||
setIds([item.id]);
|
||||
}, [ids.length, item.id, setIds]);
|
||||
|
||||
const press = useCallback(() => {
|
||||
if (ids.length === 0) return navigation.navigate('EditSet', {set: item})
|
||||
const removing = ids.find(id => id === item.id)
|
||||
if (removing) setIds(ids.filter(id => id !== item.id))
|
||||
else setIds([...ids, item.id])
|
||||
}, [ids, item, navigation, setIds])
|
||||
if (ids.length === 0) return navigation.navigate("EditSet", { set: item });
|
||||
const removing = ids.find((id) => id === item.id);
|
||||
if (removing) setIds(ids.filter((id) => id !== item.id));
|
||||
else setIds([...ids, item.id]);
|
||||
}, [ids, item, navigation, setIds]);
|
||||
|
||||
const backgroundColor = useMemo(() => {
|
||||
if (!ids.includes(item.id)) return
|
||||
if (dark) return DARK_RIPPLE
|
||||
return LIGHT_RIPPLE
|
||||
}, [dark, ids, item.id])
|
||||
if (!ids.includes(item.id)) return;
|
||||
if (dark) return DARK_RIPPLE;
|
||||
return LIGHT_RIPPLE;
|
||||
}, [dark, ids, item.id]);
|
||||
|
||||
const left = useCallback(() => {
|
||||
if (!settings.images || !item.image) return null;
|
||||
return (
|
||||
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
|
||||
);
|
||||
}, [item.image, settings.images]);
|
||||
|
||||
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={() =>
|
||||
settings.images &&
|
||||
item.image && (
|
||||
<Image source={{uri: item.image}} style={{height: 75, width: 75}} />
|
||||
)
|
||||
}
|
||||
right={() => (
|
||||
<>
|
||||
{settings.showDate && (
|
||||
<Text
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
color: dark ? '#909090ff' : '#717171ff',
|
||||
}}>
|
||||
{format(new Date(item.created), settings.date || 'P')}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
<List.Item
|
||||
onPress={press}
|
||||
title={item.name}
|
||||
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
|
||||
onLongPress={longPress}
|
||||
style={{ backgroundColor }}
|
||||
left={left}
|
||||
right={right}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
165
SetList.tsx
165
SetList.tsx
|
@ -2,53 +2,52 @@ import {
|
|||
NavigationProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
import {useCallback, useState} from 'react'
|
||||
import {FlatList} from 'react-native'
|
||||
import {List} from 'react-native-paper'
|
||||
import {Like} from 'typeorm'
|
||||
import {getNow, setRepo, settingsRepo} from './db'
|
||||
import DrawerHeader from './DrawerHeader'
|
||||
import GymSet, {defaultSet} from './gym-set'
|
||||
import {HomePageParams} from './home-page-params'
|
||||
import ListMenu from './ListMenu'
|
||||
import Page from './Page'
|
||||
import SetItem from './SetItem'
|
||||
import Settings from './settings'
|
||||
|
||||
const limit = 15
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { Like } from "typeorm";
|
||||
import { LIMIT } from "./constants";
|
||||
import { getNow, setRepo, settingsRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import GymSet, { defaultSet } from "./gym-set";
|
||||
import { HomePageParams } from "./home-page-params";
|
||||
import ListMenu from "./ListMenu";
|
||||
import Page from "./Page";
|
||||
import SetItem from "./SetItem";
|
||||
import Settings from "./settings";
|
||||
|
||||
export default function SetList() {
|
||||
const [sets, setSets] = useState<GymSet[]>([])
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [term, setTerm] = useState('')
|
||||
const [end, setEnd] = useState(false)
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
const [ids, setIds] = useState<number[]>([])
|
||||
const navigation = useNavigation<NavigationProp<HomePageParams>>()
|
||||
const [sets, setSets] = useState<GymSet[]>([]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [term, setTerm] = useState("");
|
||||
const [end, setEnd] = useState(false);
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
const [ids, setIds] = useState<number[]>([]);
|
||||
const navigation = useNavigation<NavigationProp<HomePageParams>>();
|
||||
|
||||
const refresh = useCallback(async (value: string) => {
|
||||
const newSets = await setRepo.find({
|
||||
where: {name: Like(`%${value}%`), hidden: 0 as any},
|
||||
take: limit,
|
||||
where: { name: Like(`%${value.trim()}%`), hidden: 0 as any },
|
||||
take: LIMIT,
|
||||
skip: 0,
|
||||
order: {created: 'DESC'},
|
||||
})
|
||||
console.log(`${SetList.name}.refresh:`, {value, limit})
|
||||
setSets(newSets)
|
||||
setOffset(0)
|
||||
setEnd(false)
|
||||
}, [])
|
||||
order: { created: "DESC" },
|
||||
});
|
||||
console.log(`${SetList.name}.refresh:`, { value });
|
||||
setSets(newSets);
|
||||
setOffset(0);
|
||||
setEnd(false);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refresh(term)
|
||||
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||
}, [refresh, term]),
|
||||
)
|
||||
refresh(term);
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [refresh, term])
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: GymSet}) => (
|
||||
({ item }: { item: GymSet }) => (
|
||||
<SetItem
|
||||
settings={settings}
|
||||
item={item}
|
||||
|
@ -58,75 +57,75 @@ export default function SetList() {
|
|||
setIds={setIds}
|
||||
/>
|
||||
),
|
||||
[refresh, term, settings, ids],
|
||||
)
|
||||
[refresh, term, settings, ids]
|
||||
);
|
||||
|
||||
const next = useCallback(async () => {
|
||||
if (end) return
|
||||
const newOffset = offset + limit
|
||||
console.log(`${SetList.name}.next:`, {offset, newOffset, term})
|
||||
if (end) return;
|
||||
const newOffset = offset + LIMIT;
|
||||
console.log(`${SetList.name}.next:`, { offset, newOffset, term });
|
||||
const newSets = await setRepo.find({
|
||||
where: {name: Like(`%${term}%`), hidden: 0 as any},
|
||||
take: limit,
|
||||
where: { name: Like(`%${term}%`), hidden: 0 as any },
|
||||
take: LIMIT,
|
||||
skip: newOffset,
|
||||
order: {created: 'DESC'},
|
||||
})
|
||||
if (newSets.length === 0) return setEnd(true)
|
||||
if (!sets) return
|
||||
setSets([...sets, ...newSets])
|
||||
if (newSets.length < limit) return setEnd(true)
|
||||
setOffset(newOffset)
|
||||
}, [term, end, offset, sets])
|
||||
order: { created: "DESC" },
|
||||
});
|
||||
if (newSets.length === 0) return setEnd(true);
|
||||
if (!sets) return;
|
||||
setSets([...sets, ...newSets]);
|
||||
if (newSets.length < LIMIT) return setEnd(true);
|
||||
setOffset(newOffset);
|
||||
}, [term, end, offset, sets]);
|
||||
|
||||
const onAdd = useCallback(async () => {
|
||||
const [{now}] = await getNow()
|
||||
let set = sets[0]
|
||||
if (!set) set = {...defaultSet}
|
||||
set.created = now
|
||||
delete set.id
|
||||
navigation.navigate('EditSet', {set})
|
||||
}, [navigation, sets])
|
||||
const now = await getNow();
|
||||
let set = sets[0];
|
||||
if (!set) set = { ...defaultSet };
|
||||
set.created = now;
|
||||
delete set.id;
|
||||
navigation.navigate("EditSet", { set });
|
||||
}, [navigation, sets]);
|
||||
|
||||
const search = useCallback(
|
||||
(value: string) => {
|
||||
setTerm(value)
|
||||
refresh(value)
|
||||
setTerm(value);
|
||||
refresh(value);
|
||||
},
|
||||
[refresh],
|
||||
)
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const edit = useCallback(() => {
|
||||
navigation.navigate('EditSets', {ids})
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
navigation.navigate("EditSets", { ids });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const copy = useCallback(async () => {
|
||||
const set = await setRepo.findOne({
|
||||
where: {id: ids.pop()},
|
||||
})
|
||||
delete set.id
|
||||
delete set.created
|
||||
navigation.navigate('EditSet', {set})
|
||||
setIds([])
|
||||
}, [ids, navigation])
|
||||
where: { id: ids.pop() },
|
||||
});
|
||||
delete set.id;
|
||||
delete set.created;
|
||||
navigation.navigate("EditSet", { set });
|
||||
setIds([]);
|
||||
}, [ids, navigation]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setIds([])
|
||||
}, [])
|
||||
setIds([]);
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
setIds([])
|
||||
await setRepo.delete(ids.length > 0 ? ids : {})
|
||||
await refresh(term)
|
||||
}, [ids, refresh, term])
|
||||
setIds([]);
|
||||
await setRepo.delete(ids.length > 0 ? ids : {});
|
||||
await refresh(term);
|
||||
}, [ids, refresh, term]);
|
||||
|
||||
const select = useCallback(() => {
|
||||
setIds(sets.map(set => set.id))
|
||||
}, [sets])
|
||||
setIds(sets.map((set) => set.id));
|
||||
}, [sets]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name="Home">
|
||||
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Home"}>
|
||||
<ListMenu
|
||||
onClear={clear}
|
||||
onCopy={copy}
|
||||
|
@ -147,7 +146,7 @@ export default function SetList() {
|
|||
settings && (
|
||||
<FlatList
|
||||
data={sets}
|
||||
style={{flex: 1}}
|
||||
style={{ flex: 1 }}
|
||||
renderItem={renderItem}
|
||||
onEndReached={next}
|
||||
/>
|
||||
|
@ -155,5 +154,5 @@ export default function SetList() {
|
|||
)}
|
||||
</Page>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
33
SettingButton.tsx
Normal file
33
SettingButton.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { View } from "react-native";
|
||||
import { Button, Subheading } from "react-native-paper";
|
||||
import { ITEM_PADDING } from "./constants";
|
||||
|
||||
export default function SettingButton({
|
||||
name: text,
|
||||
label,
|
||||
onPress,
|
||||
}: {
|
||||
name: string;
|
||||
label?: string;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
if (label) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: ITEM_PADDING,
|
||||
}}
|
||||
>
|
||||
<Subheading style={{ width: 100 }}>{label}</Subheading>
|
||||
<Button onPress={onPress}>{text}</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button style={{ alignSelf: "flex-start" }} onPress={onPress}>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
470
SettingsPage.tsx
470
SettingsPage.tsx
|
@ -1,312 +1,352 @@
|
|||
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||
import {format} from 'date-fns'
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {NativeModules, View} from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import {Dirs, FileSystem} from 'react-native-file-access'
|
||||
import {Button, Subheading} from 'react-native-paper'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import {ITEM_PADDING, MARGIN} from './constants'
|
||||
import {AppDataSource} from './data-source'
|
||||
import {setRepo, settingsRepo} from './db'
|
||||
import {DrawerParamList} from './drawer-param-list'
|
||||
import DrawerHeader from './DrawerHeader'
|
||||
import Input from './input'
|
||||
import {darkOptions, lightOptions, themeOptions} from './options'
|
||||
import Page from './Page'
|
||||
import Select from './Select'
|
||||
import Settings from './settings'
|
||||
import Switch from './Switch'
|
||||
import {toast} from './toast'
|
||||
import {useTheme} from './use-theme'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { NativeModules, ScrollView } from "react-native";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { Dirs, FileSystem } from "react-native-file-access";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { MARGIN } from "./constants";
|
||||
import { AppDataSource } from "./data-source";
|
||||
import { setRepo, settingsRepo } from "./db";
|
||||
import { DrawerParamList } from "./drawer-param-list";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import Input from "./input";
|
||||
import { darkOptions, lightOptions, themeOptions } from "./options";
|
||||
import Page from "./Page";
|
||||
import Select from "./Select";
|
||||
import SettingButton from "./SettingButton";
|
||||
import Settings from "./settings";
|
||||
import Switch from "./Switch";
|
||||
import { toast } from "./toast";
|
||||
import { useTheme } from "./use-theme";
|
||||
|
||||
const twelveHours = ['P', 'Pp', 'ccc p', 'p', 'yyyy-MM-d', 'yyyy.MM.d']
|
||||
const twentyFours = ['P', 'P, k:m', 'ccc k:m', 'k:m', 'yyyy-MM-d', 'yyyy.MM.d']
|
||||
const twelveHours = [
|
||||
"dd/LL/yyyy",
|
||||
"dd/LL/yyyy, p",
|
||||
"ccc p",
|
||||
"p",
|
||||
"yyyy-MM-d",
|
||||
"yyyy-MM-d, p",
|
||||
"yyyy.MM.d",
|
||||
];
|
||||
const twentyFours = [
|
||||
"dd/LL/yyyy",
|
||||
"dd/LL/yyyy, k:m",
|
||||
"ccc k:m",
|
||||
"k:m",
|
||||
"yyyy-MM-d",
|
||||
"yyyy-MM-d, k:m",
|
||||
"yyyy.MM.d",
|
||||
];
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [ignoring, setIgnoring] = useState(false)
|
||||
const [term, setTerm] = useState('')
|
||||
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
|
||||
const [ignoring, setIgnoring] = useState(false);
|
||||
const [term, setTerm] = useState("");
|
||||
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const { reset } = useNavigation<NavigationProp<DrawerParamList>>();
|
||||
|
||||
const {watch, setValue} = useForm<Settings>({
|
||||
defaultValues: () => settingsRepo.findOne({where: {}}),
|
||||
})
|
||||
const settings = watch()
|
||||
const { watch, setValue } = useForm<Settings>({
|
||||
defaultValues: () => settingsRepo.findOne({ where: {} }),
|
||||
});
|
||||
const settings = watch();
|
||||
|
||||
const {theme, setTheme, lightColor, setLightColor, darkColor, setDarkColor} =
|
||||
useTheme()
|
||||
const {
|
||||
theme,
|
||||
setTheme,
|
||||
lightColor,
|
||||
setLightColor,
|
||||
darkColor,
|
||||
setDarkColor,
|
||||
} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
NativeModules.SettingsModule.ignoringBattery(setIgnoring)
|
||||
NativeModules.SettingsModule.ignoringBattery(setIgnoring);
|
||||
NativeModules.SettingsModule.is24().then((is24: boolean) => {
|
||||
console.log(`${SettingsPage.name}.focus:`, {is24})
|
||||
if (is24) setFormatOptions(twentyFours)
|
||||
else setFormatOptions(twelveHours)
|
||||
})
|
||||
}, [])
|
||||
console.log(`${SettingsPage.name}.focus:`, { is24 });
|
||||
if (is24) setFormatOptions(twentyFours);
|
||||
else setFormatOptions(twelveHours);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const update = useCallback((key: keyof Settings, value: unknown) => {
|
||||
return settingsRepo
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({ [key]: value })
|
||||
.printSql()
|
||||
.execute();
|
||||
}, []);
|
||||
|
||||
const soundString = useMemo(() => {
|
||||
if (!settings.sound) return null
|
||||
const split = settings.sound.split('/')
|
||||
return split.pop()
|
||||
}, [settings.sound])
|
||||
if (!settings.sound) return null;
|
||||
const split = settings.sound.split("/");
|
||||
return split.pop();
|
||||
}, [settings.sound]);
|
||||
|
||||
const changeSound = useCallback(async () => {
|
||||
const {fileCopyUri} = await DocumentPicker.pickSingle({
|
||||
type: 'audio/*',
|
||||
copyTo: 'documentDirectory',
|
||||
})
|
||||
if (!fileCopyUri) return
|
||||
setValue('sound', fileCopyUri)
|
||||
await settingsRepo.save({...settings, sound: fileCopyUri})
|
||||
toast('Sound will play after rest timers.')
|
||||
}, [settings, setValue])
|
||||
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: "Rest timers", value: settings.alarm, key: "alarm" },
|
||||
{ name: "Vibrate", value: settings.vibrate, key: "vibrate" },
|
||||
{ name: "Disable sound", value: settings.noSound, key: "noSound" },
|
||||
{ name: "Notifications", value: settings.notify, key: "notify" },
|
||||
{ name: "Show images", value: settings.images, key: "images" },
|
||||
{ name: "Show unit", value: settings.showUnit, key: "showUnit" },
|
||||
{ name: "Show steps", value: settings.steps, key: "steps" },
|
||||
{ name: "Show date", value: settings.showDate, key: "showDate" },
|
||||
{ name: "Automatic backup", value: settings.backup, key: "backup" },
|
||||
],
|
||||
[settings],
|
||||
)
|
||||
[settings]
|
||||
);
|
||||
|
||||
const filter = useCallback(
|
||||
({name}) => name.toLowerCase().includes(term.toLowerCase()),
|
||||
[term],
|
||||
)
|
||||
({ name }) => name.toLowerCase().includes(term.toLowerCase()),
|
||||
[term]
|
||||
);
|
||||
|
||||
const changeBoolean = useCallback(
|
||||
async (key: keyof Settings, value: boolean) => {
|
||||
setValue(key, value)
|
||||
await settingsRepo.save({...settings, [key]: value})
|
||||
setValue(key, value);
|
||||
await update(key, value);
|
||||
switch (key) {
|
||||
case 'alarm':
|
||||
if (value) toast('Timers will now run after each set.')
|
||||
else toast('Stopped timers running after each set.')
|
||||
if (value && !ignoring) NativeModules.SettingsModule.ignoreBattery()
|
||||
return
|
||||
case 'vibrate':
|
||||
if (value) toast('Alarms will now vibrate.')
|
||||
else toast('Alarms will no longer vibrate.')
|
||||
return
|
||||
case 'notify':
|
||||
if (value) toast('Show notifications for new records.')
|
||||
else toast('Stopped notifications for new records.')
|
||||
return
|
||||
case 'images':
|
||||
if (value) toast('Show images for sets.')
|
||||
else toast('Hid images for sets.')
|
||||
return
|
||||
case 'showUnit':
|
||||
if (value) toast('Show option to select unit for sets.')
|
||||
else toast('Hid unit option for sets.')
|
||||
return
|
||||
case 'steps':
|
||||
if (value) toast('Show steps for a workout.')
|
||||
else toast('Hid steps for workouts.')
|
||||
return
|
||||
case 'showDate':
|
||||
if (value) toast('Show date for sets.')
|
||||
else toast('Hid date on sets.')
|
||||
return
|
||||
case 'noSound':
|
||||
if (value) toast('Disable sound on rest timer alarms.')
|
||||
else toast('Enabled sound for rest timer alarms.')
|
||||
return
|
||||
case "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;
|
||||
}
|
||||
},
|
||||
[settings, ignoring, setValue],
|
||||
)
|
||||
[ignoring, setValue, update]
|
||||
);
|
||||
|
||||
const renderSwitch = useCallback(
|
||||
(item: Input<boolean>) => (
|
||||
<Switch
|
||||
key={item.name}
|
||||
value={item.value}
|
||||
onChange={value => changeBoolean(item.key, value)}
|
||||
onChange={(value) => changeBoolean(item.key, value)}
|
||||
title={item.name}
|
||||
/>
|
||||
),
|
||||
[changeBoolean],
|
||||
)
|
||||
[changeBoolean]
|
||||
);
|
||||
|
||||
const switchesMarkup = useMemo(
|
||||
() => switches.filter(filter).map(s => renderSwitch(s)),
|
||||
[filter, switches, renderSwitch],
|
||||
)
|
||||
() => switches.filter(filter).map((s) => renderSwitch(s)),
|
||||
[filter, switches, renderSwitch]
|
||||
);
|
||||
|
||||
const changeString = useCallback(
|
||||
async (key: keyof Settings, value: string) => {
|
||||
setValue(key, value)
|
||||
await settingsRepo.save({...settings, [key]: value})
|
||||
setValue(key, value);
|
||||
await update(key, value);
|
||||
switch (key) {
|
||||
case 'date':
|
||||
return toast('Changed date format')
|
||||
case 'darkColor':
|
||||
setDarkColor(value)
|
||||
return toast('Set primary color for dark mode.')
|
||||
case 'lightColor':
|
||||
setLightColor(value)
|
||||
return toast('Set primary color for light mode.')
|
||||
case 'vibrate':
|
||||
return toast('Set primary color for light mode.')
|
||||
case 'sound':
|
||||
return toast('Sound will play after rest timers.')
|
||||
case 'theme':
|
||||
setTheme(value as string)
|
||||
if (value === 'dark') toast('Theme will always be dark.')
|
||||
else if (value === 'light') toast('Theme will always be light.')
|
||||
else if (value === 'system') toast('Theme will follow system.')
|
||||
return
|
||||
case "date":
|
||||
return toast("Changed date format");
|
||||
case "darkColor":
|
||||
setDarkColor(value);
|
||||
return toast("Set primary color for dark mode.");
|
||||
case "lightColor":
|
||||
setLightColor(value);
|
||||
return toast("Set primary color for light mode.");
|
||||
case "vibrate":
|
||||
return toast("Set primary color for light mode.");
|
||||
case "sound":
|
||||
return toast("Sound will play after rest timers.");
|
||||
case "theme":
|
||||
setTheme(value as string);
|
||||
if (value === "dark") toast("Theme will always be dark.");
|
||||
else if (value === "light") toast("Theme will always be light.");
|
||||
else if (value === "system") toast("Theme will follow system.");
|
||||
return;
|
||||
}
|
||||
},
|
||||
[settings, setTheme, setDarkColor, setLightColor, setValue],
|
||||
)
|
||||
[update, setTheme, setDarkColor, setLightColor, setValue]
|
||||
);
|
||||
|
||||
const selects: Input<string>[] = useMemo(() => {
|
||||
const today = new Date()
|
||||
const today = new Date();
|
||||
return [
|
||||
{name: 'Theme', value: theme, items: themeOptions, key: 'theme'},
|
||||
{ name: "Theme", value: theme, items: themeOptions, key: "theme" },
|
||||
{
|
||||
name: 'Dark color',
|
||||
name: "Dark color",
|
||||
value: darkColor,
|
||||
items: lightOptions,
|
||||
key: 'darkColor',
|
||||
key: "darkColor",
|
||||
},
|
||||
{
|
||||
name: 'Light color',
|
||||
name: "Light color",
|
||||
value: lightColor,
|
||||
items: darkOptions,
|
||||
key: 'lightColor',
|
||||
key: "lightColor",
|
||||
},
|
||||
{
|
||||
name: 'Date format',
|
||||
name: "Date format",
|
||||
value: settings.date,
|
||||
items: formatOptions.map(option => ({
|
||||
items: formatOptions.map((option) => ({
|
||||
label: format(today, option),
|
||||
value: option,
|
||||
})),
|
||||
key: 'date',
|
||||
key: "date",
|
||||
},
|
||||
]
|
||||
}, [settings.date, darkColor, formatOptions, theme, lightColor])
|
||||
];
|
||||
}, [settings, darkColor, formatOptions, theme, lightColor]);
|
||||
|
||||
const renderSelect = useCallback(
|
||||
(item: Input<string>) => (
|
||||
(input: Input<string>) => (
|
||||
<Select
|
||||
key={item.name}
|
||||
value={item.value}
|
||||
onChange={value => changeString(item.key, value)}
|
||||
label={item.name}
|
||||
items={item.items}
|
||||
key={input.name}
|
||||
value={input.value}
|
||||
onChange={(value) => changeString(input.key, value)}
|
||||
label={input.name}
|
||||
items={input.items}
|
||||
/>
|
||||
),
|
||||
[changeString],
|
||||
)
|
||||
[changeString]
|
||||
);
|
||||
|
||||
const selectsMarkup = useMemo(
|
||||
() => selects.filter(filter).map(renderSelect),
|
||||
[filter, selects, renderSelect],
|
||||
)
|
||||
[filter, selects, renderSelect]
|
||||
);
|
||||
|
||||
const confirmDelete = useCallback(async () => {
|
||||
setDeleting(false);
|
||||
await AppDataSource.dropDatabase();
|
||||
await AppDataSource.destroy();
|
||||
await AppDataSource.initialize();
|
||||
toast("Database deleted.");
|
||||
}, []);
|
||||
|
||||
const confirmImport = useCallback(async () => {
|
||||
setImporting(false)
|
||||
await AppDataSource.destroy()
|
||||
const result = await DocumentPicker.pickSingle()
|
||||
await FileSystem.cp(result.uri, Dirs.DatabaseDir + '/massive.db')
|
||||
await AppDataSource.initialize()
|
||||
await setRepo.createQueryBuilder().update().set({image: null}).execute()
|
||||
await settingsRepo
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({sound: null})
|
||||
.execute()
|
||||
reset({index: 0, routes: [{name: 'Settings'}]})
|
||||
}, [reset])
|
||||
setImporting(false);
|
||||
await AppDataSource.destroy();
|
||||
const file = await DocumentPicker.pickSingle();
|
||||
await FileSystem.cp(file.uri, Dirs.DatabaseDir + "/massive.db");
|
||||
await AppDataSource.initialize();
|
||||
await setRepo.createQueryBuilder().update().set({ image: null }).execute();
|
||||
await update("sound", null);
|
||||
const { alarm, backup } = await settingsRepo.findOne({ where: {} });
|
||||
console.log({ backup });
|
||||
const directory = await DocumentPicker.pickDirectory();
|
||||
if (backup) NativeModules.BackupModule.start(directory.uri);
|
||||
else NativeModules.BackupModule.stop();
|
||||
NativeModules.SettingsModule.ignoringBattery((isIgnoring: boolean) => {
|
||||
if (alarm && !isIgnoring) NativeModules.SettingsModule.ignoreBattery();
|
||||
reset({ index: 0, routes: [{ name: "Settings" }] });
|
||||
});
|
||||
}, [reset, update]);
|
||||
|
||||
const exportDatabase = useCallback(async () => {
|
||||
const path = Dirs.DatabaseDir + '/massive.db'
|
||||
await FileSystem.cpExternal(path, 'massive.db', 'downloads')
|
||||
toast('Database exported. Check downloads.')
|
||||
}, [])
|
||||
const path = Dirs.DatabaseDir + "/massive.db";
|
||||
await FileSystem.cpExternal(path, "massive.db", "downloads");
|
||||
toast("Database exported. Check downloads.");
|
||||
}, []);
|
||||
|
||||
const buttons = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'Alarm sound',
|
||||
element: (
|
||||
<View
|
||||
key="alarm-sound"
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: ITEM_PADDING,
|
||||
}}>
|
||||
<Subheading style={{width: 100}}>Alarm sound</Subheading>
|
||||
<Button onPress={changeSound}>{soundString || 'Default'}</Button>
|
||||
</View>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Export database',
|
||||
element: (
|
||||
<Button
|
||||
key="export-db"
|
||||
style={{alignSelf: 'flex-start'}}
|
||||
onPress={exportDatabase}>
|
||||
Export database
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Import database',
|
||||
element: (
|
||||
<Button
|
||||
key="import-db"
|
||||
style={{alignSelf: 'flex-start'}}
|
||||
onPress={() => setImporting(true)}>
|
||||
Import database
|
||||
</Button>
|
||||
),
|
||||
name: soundString || "Default",
|
||||
onPress: changeSound,
|
||||
label: "Alarm sound",
|
||||
},
|
||||
{ name: "Export database", onPress: exportDatabase },
|
||||
{ name: "Import database", onPress: () => setImporting(true) },
|
||||
{ name: "Delete database", onPress: () => setDeleting(true) },
|
||||
],
|
||||
[changeSound, exportDatabase, soundString],
|
||||
)
|
||||
[changeSound, exportDatabase, soundString]
|
||||
);
|
||||
|
||||
const buttonsMarkup = useMemo(
|
||||
() => buttons.filter(filter).map(b => b.element),
|
||||
[buttons, filter],
|
||||
)
|
||||
() =>
|
||||
buttons
|
||||
.filter(filter)
|
||||
.map((button) => <SettingButton {...button} key={button.name} />),
|
||||
[buttons, filter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name="Settings" />
|
||||
|
||||
<Page term={term} search={setTerm} style={{flexGrow: 0}}>
|
||||
<View style={{marginTop: MARGIN}}>
|
||||
<Page term={term} search={setTerm} style={{ flexGrow: 1 }}>
|
||||
<ScrollView style={{ marginTop: MARGIN, flex: 1 }}>
|
||||
{switchesMarkup}
|
||||
{selectsMarkup}
|
||||
{buttonsMarkup}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Page>
|
||||
|
||||
<ConfirmDialog
|
||||
title="Are you sure?"
|
||||
onOk={confirmImport}
|
||||
setShow={setImporting}
|
||||
show={importing}>
|
||||
show={importing}
|
||||
>
|
||||
Importing a database overwrites your current data. This action cannot be
|
||||
reversed!
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
title="Are you sure?"
|
||||
onOk={confirmDelete}
|
||||
setShow={setDeleting}
|
||||
show={deleting}
|
||||
>
|
||||
Deleting your database wipes your current data. This action cannot be
|
||||
reversed!
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,36 +1,20 @@
|
|||
import {useNavigation} from '@react-navigation/native'
|
||||
import Share from 'react-native-share'
|
||||
import {FileSystem} from 'react-native-file-access'
|
||||
import {Appbar, IconButton} from 'react-native-paper'
|
||||
import {captureScreen} from 'react-native-view-shot'
|
||||
import useDark from './use-dark'
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { Appbar, IconButton } from "react-native-paper";
|
||||
|
||||
export default function StackHeader({title}: {title: string}) {
|
||||
const navigation = useNavigation()
|
||||
const dark = useDark()
|
||||
export default function StackHeader({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
<IconButton
|
||||
color={dark ? 'white' : 'white'}
|
||||
icon="arrow-back"
|
||||
onPress={navigation.goBack}
|
||||
/>
|
||||
<IconButton icon="arrow-back" onPress={navigation.goBack} />
|
||||
<Appbar.Content title={title} />
|
||||
<IconButton
|
||||
color={dark ? 'white' : 'white'}
|
||||
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"
|
||||
/>
|
||||
{children}
|
||||
</Appbar.Header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
242
StartPlan.tsx
242
StartPlan.tsx
|
@ -1,42 +1,50 @@
|
|||
import {RouteProp, useFocusEffect, useRoute} from '@react-navigation/native'
|
||||
import {useCallback, useMemo, useRef, useState} from 'react'
|
||||
import {FlatList, NativeModules, TextInput, View} from 'react-native'
|
||||
import {Button, ProgressBar} from 'react-native-paper'
|
||||
import {getBestSet} from './best.service'
|
||||
import {PADDING} from './constants'
|
||||
import CountMany from './count-many'
|
||||
import {AppDataSource} from './data-source'
|
||||
import {getNow, setRepo, settingsRepo} from './db'
|
||||
import GymSet from './gym-set'
|
||||
import AppInput from './AppInput'
|
||||
import {PlanPageParams} from './plan-page-params'
|
||||
import Settings from './settings'
|
||||
import StackHeader from './StackHeader'
|
||||
import StartPlanItem from './StartPlanItem'
|
||||
import {toast} from './toast'
|
||||
import {
|
||||
NavigationProp,
|
||||
RouteProp,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { FlatList, NativeModules, TextInput, View } from "react-native";
|
||||
import { Button, IconButton, ProgressBar } from "react-native-paper";
|
||||
import AppInput from "./AppInput";
|
||||
import { getBestSet } from "./best.service";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import CountMany from "./count-many";
|
||||
import { AppDataSource } from "./data-source";
|
||||
import { getNow, setRepo, settingsRepo } from "./db";
|
||||
import { fixNumeric } from "./fix-numeric";
|
||||
import GymSet from "./gym-set";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import Settings from "./settings";
|
||||
import StackHeader from "./StackHeader";
|
||||
import StartPlanItem from "./StartPlanItem";
|
||||
import { toast } from "./toast";
|
||||
|
||||
export default function StartPlan() {
|
||||
const {params} = useRoute<RouteProp<PlanPageParams, 'StartPlan'>>()
|
||||
const [reps, setReps] = useState(params.first?.reps.toString() || '0')
|
||||
const [weight, setWeight] = useState(params.first?.weight.toString() || '0')
|
||||
const [unit, setUnit] = useState<string>(params.first?.unit || 'kg')
|
||||
const [selected, setSelected] = useState(0)
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
const [counts, setCounts] = useState<CountMany[]>()
|
||||
const weightRef = useRef<TextInput>(null)
|
||||
const repsRef = useRef<TextInput>(null)
|
||||
const unitRef = useRef<TextInput>(null)
|
||||
const workouts = useMemo(() => params.plan.workouts.split(','), [params])
|
||||
const { params } = useRoute<RouteProp<PlanPageParams, "StartPlan">>();
|
||||
const [reps, setReps] = useState(params.first?.reps.toString() || "0");
|
||||
const [weight, setWeight] = useState(params.first?.weight.toString() || "0");
|
||||
const [unit, setUnit] = useState<string>(params.first?.unit || "kg");
|
||||
const [selected, setSelected] = useState(0);
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
const [counts, setCounts] = useState<CountMany[]>();
|
||||
const weightRef = useRef<TextInput>(null);
|
||||
const repsRef = useRef<TextInput>(null);
|
||||
const unitRef = useRef<TextInput>(null);
|
||||
const workouts = useMemo(() => params.plan.workouts.split(","), [params]);
|
||||
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
const [selection, setSelection] = useState({
|
||||
start: 0,
|
||||
end: 0,
|
||||
})
|
||||
});
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const questions = workouts
|
||||
.map((workout, index) => `('${workout}',${index})`)
|
||||
.join(',')
|
||||
.join(",");
|
||||
const select = `
|
||||
SELECT workouts.name, COUNT(sets.id) as total, sets.sets
|
||||
FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts
|
||||
|
@ -47,41 +55,45 @@ export default function StartPlan() {
|
|||
ORDER BY workouts.sequence
|
||||
LIMIT -1
|
||||
OFFSET 1
|
||||
`
|
||||
const newCounts = await AppDataSource.manager.query(select)
|
||||
console.log(`${StartPlan.name}.focus:`, {newCounts})
|
||||
setCounts(newCounts)
|
||||
}, [workouts])
|
||||
`;
|
||||
const newCounts = await AppDataSource.manager.query(select);
|
||||
console.log(`${StartPlan.name}.focus:`, { newCounts });
|
||||
setCounts(newCounts);
|
||||
}, [workouts]);
|
||||
|
||||
const select = useCallback(
|
||||
async (index: number, newCounts?: CountMany[]) => {
|
||||
setSelected(index)
|
||||
if (!counts && !newCounts) return
|
||||
const workout = counts ? counts[index] : newCounts[index]
|
||||
console.log(`${StartPlan.name}.next:`, {workout})
|
||||
const newBest = await getBestSet(workout.name)
|
||||
if (!newBest) return
|
||||
delete newBest.id
|
||||
console.log(`${StartPlan.name}.next:`, {newBest})
|
||||
setReps(newBest.reps.toString())
|
||||
setWeight(newBest.weight.toString())
|
||||
setUnit(newBest.unit)
|
||||
setSelected(index);
|
||||
if (!counts && !newCounts) return;
|
||||
const workout = counts ? counts[index] : newCounts[index];
|
||||
console.log(`${StartPlan.name}.next:`, { workout });
|
||||
const last = await setRepo.findOne({
|
||||
where: { name: workout.name },
|
||||
order: { created: "desc" },
|
||||
});
|
||||
console.log({ last });
|
||||
if (!last) return;
|
||||
delete last.id;
|
||||
console.log(`${StartPlan.name}.select:`, { last });
|
||||
setReps(last.reps.toString());
|
||||
setWeight(last.weight.toString());
|
||||
setUnit(last.unit);
|
||||
},
|
||||
[counts],
|
||||
)
|
||||
[counts]
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||
refresh()
|
||||
}, [refresh]),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
refresh();
|
||||
}, [refresh])
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const [{now}] = await getNow()
|
||||
const workout = counts[selected]
|
||||
const best = await getBestSet(workout.name)
|
||||
delete best.id
|
||||
const now = await getNow();
|
||||
const workout = counts[selected];
|
||||
const best = await getBestSet(workout.name);
|
||||
delete best.id;
|
||||
const newSet: GymSet = {
|
||||
...best,
|
||||
weight: +weight,
|
||||
|
@ -89,46 +101,94 @@ export default function StartPlan() {
|
|||
unit,
|
||||
created: now,
|
||||
hidden: false,
|
||||
}
|
||||
await setRepo.save(newSet)
|
||||
await refresh()
|
||||
};
|
||||
await setRepo.save(newSet);
|
||||
await refresh();
|
||||
if (
|
||||
settings.notify &&
|
||||
(+weight > best.weight || (+reps > best.reps && +weight === best.weight))
|
||||
)
|
||||
toast("Great work King! That's a new record.")
|
||||
if (!settings.alarm) return
|
||||
) {
|
||||
toast("Great work King! That's a new record.");
|
||||
}
|
||||
if (!settings.alarm) return;
|
||||
const milliseconds =
|
||||
Number(best.minutes) * 60 * 1000 + Number(best.seconds) * 1000
|
||||
const {vibrate, sound, noSound} = settings
|
||||
const args = [milliseconds, vibrate, sound, noSound]
|
||||
NativeModules.AlarmModule.timer(...args)
|
||||
}
|
||||
Number(best.minutes) * 60 * 1000 + Number(best.seconds) * 1000;
|
||||
NativeModules.AlarmModule.timer(milliseconds);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title={params.plan.days.replace(/,/g, ', ')} />
|
||||
<View style={{padding: PADDING, flex: 1, flexDirection: 'column'}}>
|
||||
<View style={{flex: 1}}>
|
||||
<AppInput
|
||||
label="Reps"
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={setReps}
|
||||
onSubmitEditing={() => weightRef.current?.focus()}
|
||||
selection={selection}
|
||||
onSelectionChange={e => setSelection(e.nativeEvent.selection)}
|
||||
innerRef={repsRef}
|
||||
/>
|
||||
<AppInput
|
||||
label="Weight"
|
||||
keyboardType="numeric"
|
||||
value={weight}
|
||||
onChangeText={setWeight}
|
||||
onSubmitEditing={handleSubmit}
|
||||
innerRef={weightRef}
|
||||
blurOnSubmit
|
||||
/>
|
||||
<StackHeader title={params.plan.days.replace(/,/g, ", ")}>
|
||||
<IconButton
|
||||
onPress={() => navigation.navigate("EditPlan", { plan: params.plan })}
|
||||
icon="edit"
|
||||
/>
|
||||
</StackHeader>
|
||||
<View style={{ padding: PADDING, flex: 1, flexDirection: "column" }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: MARGIN,
|
||||
}}
|
||||
>
|
||||
<AppInput
|
||||
label="Reps"
|
||||
style={{ flex: 1 }}
|
||||
keyboardType="numeric"
|
||||
value={reps}
|
||||
onChangeText={(newReps) => {
|
||||
const fixed = fixNumeric(newReps);
|
||||
setReps(fixed);
|
||||
if (fixed.length !== newReps.length)
|
||||
toast("Reps must be a number");
|
||||
}}
|
||||
onSubmitEditing={() => weightRef.current?.focus()}
|
||||
selection={selection}
|
||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
||||
innerRef={repsRef}
|
||||
/>
|
||||
<IconButton
|
||||
icon="add"
|
||||
onPress={() => setReps((Number(reps) + 1).toString())}
|
||||
/>
|
||||
<IconButton
|
||||
icon="remove"
|
||||
onPress={() => setReps((Number(reps) - 1).toString())}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: MARGIN,
|
||||
}}
|
||||
>
|
||||
<AppInput
|
||||
label="Weight"
|
||||
style={{ flex: 1 }}
|
||||
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
|
||||
/>
|
||||
<IconButton
|
||||
icon="add"
|
||||
onPress={() => setWeight((Number(weight) + 2.5).toString())}
|
||||
/>
|
||||
<IconButton
|
||||
icon="remove"
|
||||
onPress={() => setWeight((Number(weight) - 2.5).toString())}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{settings?.showUnit && (
|
||||
<AppInput
|
||||
autoCapitalize="none"
|
||||
|
@ -141,7 +201,7 @@ export default function StartPlan() {
|
|||
{counts && (
|
||||
<FlatList
|
||||
data={counts}
|
||||
renderItem={props => (
|
||||
renderItem={(props) => (
|
||||
<View>
|
||||
<StartPlanItem
|
||||
{...props}
|
||||
|
@ -157,10 +217,10 @@ export default function StartPlan() {
|
|||
/>
|
||||
)}
|
||||
</View>
|
||||
<Button mode="contained" icon="save" onPress={handleSubmit}>
|
||||
<Button mode="outlined" icon="save" onPress={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,66 +1,101 @@
|
|||
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {GestureResponderEvent, ListRenderItemInfo, View} from 'react-native'
|
||||
import {List, Menu, RadioButton, useTheme} from 'react-native-paper'
|
||||
import {Like} from 'typeorm'
|
||||
import CountMany from './count-many'
|
||||
import {getNow, setRepo} from './db'
|
||||
import {PlanPageParams} from './plan-page-params'
|
||||
import {toast} from './toast'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { GestureResponderEvent, ListRenderItemInfo, View } from "react-native";
|
||||
import { List, Menu, RadioButton, useTheme } from "react-native-paper";
|
||||
import { Like } from "typeorm";
|
||||
import CountMany from "./count-many";
|
||||
import { getNow, setRepo } from "./db";
|
||||
import { PlanPageParams } from "./plan-page-params";
|
||||
import { toast } from "./toast";
|
||||
|
||||
interface Props extends ListRenderItemInfo<CountMany> {
|
||||
onSelect: (index: number) => void
|
||||
selected: number
|
||||
onUndo: () => void
|
||||
onSelect: (index: number) => void;
|
||||
selected: number;
|
||||
onUndo: () => void;
|
||||
}
|
||||
|
||||
export default function StartPlanItem(props: Props) {
|
||||
const {index, item, onSelect, selected, onUndo} = props
|
||||
const {colors} = useTheme()
|
||||
const [anchor, setAnchor] = useState({x: 0, y: 0})
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const {navigate} = useNavigation<NavigationProp<PlanPageParams>>()
|
||||
const { index, item, onSelect, selected, onUndo } = props;
|
||||
const { colors } = useTheme();
|
||||
const [anchor, setAnchor] = useState({ x: 0, y: 0 });
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const { navigate } = useNavigation<NavigationProp<PlanPageParams>>();
|
||||
|
||||
const undo = useCallback(async () => {
|
||||
const [{now}] = await getNow()
|
||||
const created = now.split('T')[0]
|
||||
const now = await getNow();
|
||||
const created = now.split("T")[0];
|
||||
const first = await setRepo.findOne({
|
||||
where: {
|
||||
name: item.name,
|
||||
hidden: 0 as any,
|
||||
created: Like(`${created}%`),
|
||||
},
|
||||
order: {created: 'desc'},
|
||||
})
|
||||
setShowMenu(false)
|
||||
if (!first) return toast('Nothing to undo.')
|
||||
await setRepo.delete(first.id)
|
||||
onUndo()
|
||||
}, [setShowMenu, onUndo, item.name])
|
||||
order: { created: "desc" },
|
||||
});
|
||||
setShowMenu(false);
|
||||
if (!first) return toast("Nothing to undo.");
|
||||
await setRepo.delete(first.id);
|
||||
onUndo();
|
||||
}, [setShowMenu, onUndo, item.name]);
|
||||
|
||||
const longPress = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
|
||||
setShowMenu(true)
|
||||
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY });
|
||||
setShowMenu(true);
|
||||
},
|
||||
[setShowMenu, setAnchor],
|
||||
)
|
||||
[setShowMenu, setAnchor]
|
||||
);
|
||||
|
||||
const edit = async () => {
|
||||
const [{now}] = await getNow()
|
||||
const created = now.split('T')[0]
|
||||
const edit = useCallback(async () => {
|
||||
const now = await getNow();
|
||||
const created = now.split("T")[0];
|
||||
const first = await setRepo.findOne({
|
||||
where: {
|
||||
name: item.name,
|
||||
hidden: 0 as any,
|
||||
created: Like(`${created}%`),
|
||||
},
|
||||
order: {created: 'desc'},
|
||||
})
|
||||
setShowMenu(false)
|
||||
if (!first) return toast('Nothing to edit.')
|
||||
navigate('EditSet', {set: first})
|
||||
}
|
||||
order: { created: "desc" },
|
||||
});
|
||||
setShowMenu(false);
|
||||
if (!first) return toast("Nothing to edit.");
|
||||
navigate("EditSet", { set: first });
|
||||
}, [item.name, navigate]);
|
||||
|
||||
const left = useCallback(
|
||||
() => (
|
||||
<View style={{ alignItems: "center", justifyContent: "center" }}>
|
||||
<RadioButton
|
||||
onPress={() => onSelect(index)}
|
||||
value={index.toString()}
|
||||
status={selected === index ? "checked" : "unchecked"}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
[index, selected, colors.primary, onSelect]
|
||||
);
|
||||
|
||||
const right = useCallback(
|
||||
() => (
|
||||
<View
|
||||
style={{
|
||||
width: "25%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
anchor={anchor}
|
||||
visible={showMenu}
|
||||
onDismiss={() => setShowMenu(false)}
|
||||
>
|
||||
<Menu.Item leadingIcon="edit" onPress={edit} title="Edit" />
|
||||
<Menu.Item leadingIcon="undo" onPress={undo} title="Undo" />
|
||||
</Menu>
|
||||
</View>
|
||||
),
|
||||
[anchor, showMenu, edit, undo]
|
||||
);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
|
@ -70,31 +105,8 @@ export default function StartPlanItem(props: Props) {
|
|||
item.sets ? `${item.total} / ${item.sets}` : item.total.toString()
|
||||
}
|
||||
onPress={() => onSelect(index)}
|
||||
left={() => (
|
||||
<View style={{alignItems: 'center', justifyContent: 'center'}}>
|
||||
<RadioButton
|
||||
onPress={() => onSelect(index)}
|
||||
value={index.toString()}
|
||||
status={selected === index ? 'checked' : 'unchecked'}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
right={() => (
|
||||
<View
|
||||
style={{
|
||||
width: '25%',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<Menu
|
||||
anchor={anchor}
|
||||
visible={showMenu}
|
||||
onDismiss={() => setShowMenu(false)}>
|
||||
<Menu.Item icon="edit" onPress={edit} title="Edit" />
|
||||
<Menu.Item icon="undo" onPress={undo} title="Undo" />
|
||||
</Menu>
|
||||
</View>
|
||||
)}
|
||||
left={left}
|
||||
right={right}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
38
Switch.tsx
38
Switch.tsx
|
@ -1,38 +1,42 @@
|
|||
import React from 'react'
|
||||
import {Platform, Pressable} from 'react-native'
|
||||
import {Switch as PaperSwitch, Text, useTheme} from 'react-native-paper'
|
||||
import {MARGIN} from './constants'
|
||||
import React from "react";
|
||||
import { Platform, Pressable } from "react-native";
|
||||
import { Switch as PaperSwitch, Text, useTheme } from "react-native-paper";
|
||||
import { MARGIN } from "./constants";
|
||||
|
||||
function Switch({
|
||||
value,
|
||||
onChange,
|
||||
title,
|
||||
}: {
|
||||
value?: boolean
|
||||
onChange: (value: boolean) => void
|
||||
title: string
|
||||
value?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
title: string;
|
||||
}) {
|
||||
const {colors} = useTheme()
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => onChange(!value)}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
marginBottom: Platform.OS === 'ios' ? MARGIN : null,
|
||||
}}>
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
marginBottom: Platform.OS === "ios" ? MARGIN : null,
|
||||
}}
|
||||
>
|
||||
<PaperSwitch
|
||||
color={colors.primary}
|
||||
style={{marginRight: MARGIN}}
|
||||
style={{ marginRight: MARGIN }}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
trackColor={{true: colors.primary + '80', false: colors.disabled}}
|
||||
trackColor={{
|
||||
true: colors.primary + "80",
|
||||
false: colors.surfaceDisabled,
|
||||
}}
|
||||
/>
|
||||
<Text>{title}</Text>
|
||||
</Pressable>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Switch)
|
||||
export default React.memo(Switch);
|
||||
|
|
|
@ -1,75 +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'
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Dimensions, NativeModules, View } from "react-native";
|
||||
import { Button, Text, useTheme } from "react-native-paper";
|
||||
import { ProgressCircle } from "react-native-svg-charts";
|
||||
import AppFab from "./AppFab";
|
||||
import { MARGIN, PADDING } from "./constants";
|
||||
import { settingsRepo } from "./db";
|
||||
import DrawerHeader from "./DrawerHeader";
|
||||
import Settings from "./settings";
|
||||
import useTimer from "./use-timer";
|
||||
|
||||
export interface TickEvent {
|
||||
minutes: string
|
||||
seconds: string
|
||||
minutes: string;
|
||||
seconds: string;
|
||||
}
|
||||
|
||||
export default function TimerPage() {
|
||||
const {minutes, seconds} = useTimer()
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
const {colors} = useTheme()
|
||||
const { minutes, seconds } = useTimer();
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
const { colors } = useTheme();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||
}, []),
|
||||
)
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [])
|
||||
);
|
||||
|
||||
const stop = () => {
|
||||
NativeModules.AlarmModule.stop()
|
||||
}
|
||||
NativeModules.AlarmModule.stop();
|
||||
};
|
||||
|
||||
const add = async () => {
|
||||
console.log(`${TimerPage.name}.add:`, settings)
|
||||
const params = [settings.vibrate, settings.sound, settings.noSound]
|
||||
NativeModules.AlarmModule.add(...params)
|
||||
}
|
||||
console.log(`${TimerPage.name}.add:`, settings);
|
||||
NativeModules.AlarmModule.add();
|
||||
};
|
||||
|
||||
const progress = useMemo(() => {
|
||||
return (Number(minutes) * 60 + Number(seconds)) / 210
|
||||
}, [minutes, seconds])
|
||||
return (Number(minutes) * 60 + Number(seconds)) / 210;
|
||||
}, [minutes, seconds]);
|
||||
|
||||
const left = useMemo(() => {
|
||||
return Dimensions.get('screen').width * 0.5 - 60
|
||||
}, [])
|
||||
return Dimensions.get("screen").width * 0.5 - 60;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader name="Timer" />
|
||||
<View style={{flexGrow: 1, padding: PADDING}}>
|
||||
<View style={{ flexGrow: 1, padding: PADDING }}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Text style={{fontSize: 70, position: 'absolute'}}>
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 70, position: "absolute" }}>
|
||||
{minutes}:{seconds}
|
||||
</Text>
|
||||
<ProgressCircle
|
||||
style={{height: 300, width: 300, marginBottom: MARGIN}}
|
||||
style={{ height: 300, width: 300, marginBottom: MARGIN }}
|
||||
progress={progress}
|
||||
strokeWidth={10}
|
||||
progressColor={colors.text}
|
||||
backgroundColor={colors.placeholder}
|
||||
progressColor={colors.primary}
|
||||
backgroundColor={colors.primary + "80"}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Button onPress={add} style={{position: 'absolute', top: '82%', left}}>
|
||||
<Button onPress={add} style={{ position: "absolute", top: "82%", left }}>
|
||||
Add 1 min
|
||||
</Button>
|
||||
<AppFab icon="stop" onPress={stop} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
131
ViewBest.tsx
131
ViewBest.tsx
|
@ -1,131 +0,0 @@
|
|||
import {RouteProp, useRoute} from '@react-navigation/native'
|
||||
import {format} from 'date-fns'
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {List} from 'react-native-paper'
|
||||
import {BestPageParams} from './BestPage'
|
||||
import Chart from './Chart'
|
||||
import {PADDING} from './constants'
|
||||
import {setRepo} from './db'
|
||||
import GymSet from './gym-set'
|
||||
import {Metrics} from './metrics'
|
||||
import {Periods} from './periods'
|
||||
import Select from './Select'
|
||||
import StackHeader from './StackHeader'
|
||||
import Volume from './volume'
|
||||
|
||||
export default function ViewBest() {
|
||||
const {params} = useRoute<RouteProp<BestPageParams, 'ViewBest'>>()
|
||||
const [weights, setWeights] = useState<GymSet[]>([])
|
||||
const [volumes, setVolumes] = useState<Volume[]>([])
|
||||
const [metric, setMetric] = useState(Metrics.Weight)
|
||||
const [period, setPeriod] = useState(Periods.Monthly)
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`${ViewBest.name}.useEffect`, {metric})
|
||||
console.log(`${ViewBest.name}.useEffect`, {period})
|
||||
let difference = '-7 days'
|
||||
if (period === Periods.Monthly) difference = '-1 months'
|
||||
else if (period === Periods.Yearly) difference = '-1 years'
|
||||
let group = '%Y-%m-%d'
|
||||
if (period === Periods.Yearly) group = '%Y-%m'
|
||||
const builder = setRepo
|
||||
.createQueryBuilder()
|
||||
.select("STRFTIME('%Y-%m-%d', created)", 'created')
|
||||
.addSelect('unit')
|
||||
.where('name = :name', {name: params.best.name})
|
||||
.andWhere('NOT hidden')
|
||||
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
||||
difference,
|
||||
})
|
||||
.groupBy('name')
|
||||
.addGroupBy(`STRFTIME('${group}', created)`)
|
||||
switch (metric) {
|
||||
case Metrics.Weight:
|
||||
builder.addSelect('MAX(weight)', 'weight').getRawMany().then(setWeights)
|
||||
break
|
||||
case Metrics.Volume:
|
||||
builder
|
||||
.addSelect('SUM(weight * reps)', 'value')
|
||||
.getRawMany()
|
||||
.then(setVolumes)
|
||||
break
|
||||
default:
|
||||
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
|
||||
builder
|
||||
.addSelect('MAX(weight / (1.0278 - 0.0278 * reps))', 'weight')
|
||||
.getRawMany()
|
||||
.then(newWeights => {
|
||||
console.log({weights: newWeights})
|
||||
setWeights(newWeights)
|
||||
})
|
||||
}
|
||||
}, [params.best.name, metric, period])
|
||||
|
||||
const charts = useMemo(() => {
|
||||
if (
|
||||
(metric === Metrics.Volume && volumes.length === 0) ||
|
||||
(metric === Metrics.Weight && weights.length === 0) ||
|
||||
(metric === Metrics.OneRepMax && weights.length === 0)
|
||||
)
|
||||
return <List.Item title="No data yet." />
|
||||
if (metric === Metrics.Volume)
|
||||
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 (
|
||||
<Chart
|
||||
yData={weights.map(set => set.weight)}
|
||||
yFormat={value => `${value}${weights[0].unit}`}
|
||||
xData={weights}
|
||||
xFormat={(_value, index) =>
|
||||
format(new Date(weights[index].created), 'd/M')
|
||||
}
|
||||
/>
|
||||
)
|
||||
}, [volumes, weights, metric])
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title={params.best.name} />
|
||||
<View style={{padding: PADDING}}>
|
||||
<Select
|
||||
label="Metric"
|
||||
items={[
|
||||
{value: Metrics.Volume, label: Metrics.Volume},
|
||||
{value: Metrics.OneRepMax, label: Metrics.OneRepMax},
|
||||
{
|
||||
label: Metrics.Weight,
|
||||
value: Metrics.Weight,
|
||||
},
|
||||
]}
|
||||
onChange={value => setMetric(value as Metrics)}
|
||||
value={metric}
|
||||
/>
|
||||
<Select
|
||||
label="Period"
|
||||
items={[
|
||||
{value: Periods.Weekly, label: Periods.Weekly},
|
||||
{value: Periods.Monthly, label: Periods.Monthly},
|
||||
{value: Periods.Yearly, label: Periods.Yearly},
|
||||
]}
|
||||
onChange={value => setPeriod(value as Periods)}
|
||||
value={period}
|
||||
/>
|
||||
{charts}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
154
ViewGraph.tsx
Normal file
154
ViewGraph.tsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { FileSystem } from "react-native-file-access";
|
||||
import { IconButton, List } from "react-native-paper";
|
||||
import Share from "react-native-share";
|
||||
import { captureScreen } from "react-native-view-shot";
|
||||
import Chart from "./Chart";
|
||||
import { GraphsPageParams } from "./GraphsPage";
|
||||
import Select from "./Select";
|
||||
import StackHeader from "./StackHeader";
|
||||
import { PADDING } from "./constants";
|
||||
import { setRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
import { Metrics } from "./metrics";
|
||||
import { Periods } from "./periods";
|
||||
import Volume from "./volume";
|
||||
|
||||
export default function ViewGraph() {
|
||||
const { params } = useRoute<RouteProp<GraphsPageParams, "ViewGraph">>();
|
||||
const [weights, setWeights] = useState<GymSet[]>();
|
||||
const [volumes, setVolumes] = useState<Volume[]>();
|
||||
const [metric, setMetric] = useState(Metrics.Weight);
|
||||
const [period, setPeriod] = useState(Periods.Monthly);
|
||||
|
||||
useEffect(() => {
|
||||
let difference = "-7 days";
|
||||
if (period === Periods.Monthly) difference = "-1 months";
|
||||
else if (period === Periods.Yearly) difference = "-1 years";
|
||||
let group = "%Y-%m-%d";
|
||||
if (period === Periods.Yearly) group = "%Y-%m";
|
||||
const builder = setRepo
|
||||
.createQueryBuilder()
|
||||
.select("STRFTIME('%Y-%m-%d', created)", "created")
|
||||
.addSelect("unit")
|
||||
.where("name = :name", { name: params.best.name })
|
||||
.andWhere("NOT hidden")
|
||||
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
||||
difference,
|
||||
})
|
||||
.groupBy("name")
|
||||
.addGroupBy(`STRFTIME('${group}', created)`);
|
||||
switch (metric) {
|
||||
case Metrics.Weight:
|
||||
builder
|
||||
.addSelect("ROUND(MAX(weight), 2)", "weight")
|
||||
.getRawMany()
|
||||
.then(setWeights);
|
||||
break;
|
||||
case Metrics.Volume:
|
||||
builder
|
||||
.addSelect("ROUND(SUM(weight * reps), 2)", "value")
|
||||
.getRawMany()
|
||||
.then(setVolumes);
|
||||
break;
|
||||
default:
|
||||
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
|
||||
builder
|
||||
.addSelect(
|
||||
"ROUND(MAX(weight / (1.0278 - 0.0278 * reps)), 2)",
|
||||
"weight"
|
||||
)
|
||||
.getRawMany()
|
||||
.then((newWeights) => {
|
||||
console.log({ weights: newWeights });
|
||||
setWeights(newWeights);
|
||||
});
|
||||
}
|
||||
}, [params.best.name, metric, period]);
|
||||
|
||||
const charts = useMemo(() => {
|
||||
if (
|
||||
(metric === Metrics.Volume && volumes?.length === 0) ||
|
||||
(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 (
|
||||
<Chart
|
||||
yData={weights?.map((set) => set.weight) || []}
|
||||
yFormat={(value) => `${value}${weights?.[0].unit}`}
|
||||
xData={weights || []}
|
||||
xFormat={(_value, index) =>
|
||||
format(new Date(weights?.[index].created), "d/M")
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [volumes, weights, metric]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackHeader title={params.best.name}>
|
||||
<IconButton
|
||||
onPress={() =>
|
||||
captureScreen().then(async (uri) => {
|
||||
const base64 = await FileSystem.readFile(uri, "base64");
|
||||
const url = `data:image/jpeg;base64,${base64}`;
|
||||
Share.open({
|
||||
type: "image/jpeg",
|
||||
url,
|
||||
});
|
||||
})
|
||||
}
|
||||
icon="share"
|
||||
/>
|
||||
</StackHeader>
|
||||
<View style={{ padding: PADDING }}>
|
||||
<Select
|
||||
label="Metric"
|
||||
items={[
|
||||
{ value: Metrics.Volume, label: Metrics.Volume },
|
||||
{ value: Metrics.OneRepMax, label: Metrics.OneRepMax },
|
||||
{
|
||||
label: Metrics.Weight,
|
||||
value: Metrics.Weight,
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setMetric(value as Metrics)}
|
||||
value={metric}
|
||||
/>
|
||||
<Select
|
||||
label="Period"
|
||||
items={[
|
||||
{ value: Periods.Weekly, label: Periods.Weekly },
|
||||
{ value: Periods.Monthly, label: Periods.Monthly },
|
||||
{ value: Periods.Yearly, label: Periods.Yearly },
|
||||
]}
|
||||
onChange={(value) => setPeriod(value as Periods)}
|
||||
value={period}
|
||||
/>
|
||||
{charts}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
121
WorkoutItem.tsx
121
WorkoutItem.tsx
|
@ -1,87 +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'
|
||||
import { NavigationProp, useNavigation } from "@react-navigation/native";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { GestureResponderEvent, Image } from "react-native";
|
||||
import { List, Menu, Text } from "react-native-paper";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { setRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
import { WorkoutsPageParams } from "./WorkoutsPage";
|
||||
|
||||
export default function WorkoutItem({
|
||||
item,
|
||||
onRemove,
|
||||
images,
|
||||
}: {
|
||||
item: GymSet
|
||||
onRemove: () => void
|
||||
images: boolean
|
||||
item: GymSet;
|
||||
onRemove: () => void;
|
||||
images: boolean;
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const [anchor, setAnchor] = useState({x: 0, y: 0})
|
||||
const [showRemove, setShowRemove] = useState('')
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [anchor, setAnchor] = useState({ x: 0, y: 0 });
|
||||
const [showRemove, setShowRemove] = useState("");
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
await setRepo.delete({name: item.name})
|
||||
setShowMenu(false)
|
||||
onRemove()
|
||||
}, [setShowMenu, onRemove, item.name])
|
||||
await setRepo.delete({ name: item.name });
|
||||
setShowMenu(false);
|
||||
onRemove();
|
||||
}, [setShowMenu, onRemove, item.name]);
|
||||
|
||||
const longPress = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
|
||||
setShowMenu(true)
|
||||
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY });
|
||||
setShowMenu(true);
|
||||
},
|
||||
[setShowMenu, setAnchor],
|
||||
)
|
||||
[setShowMenu, setAnchor]
|
||||
);
|
||||
|
||||
const description = useMemo(() => {
|
||||
const seconds = item.seconds?.toString().padStart(2, '0')
|
||||
return `${item.sets} x ${item.minutes || 0}:${seconds}`
|
||||
}, [item])
|
||||
const seconds = item.seconds?.toString().padStart(2, "0");
|
||||
return `${item.sets} x ${item.minutes || 0}:${seconds}`;
|
||||
}, [item]);
|
||||
|
||||
const left = useCallback(() => {
|
||||
if (!images || !item.image) return null;
|
||||
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})}
|
||||
onPress={() => navigation.navigate("EditWorkout", { value: item })}
|
||||
title={item.name}
|
||||
description={description}
|
||||
onLongPress={longPress}
|
||||
left={() =>
|
||||
images &&
|
||||
item.image && (
|
||||
<Image source={{uri: item.image}} style={{height: 75, width: 75}} />
|
||||
)
|
||||
}
|
||||
right={() => (
|
||||
<Text
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
}}>
|
||||
<Menu
|
||||
anchor={anchor}
|
||||
visible={showMenu}
|
||||
onDismiss={() => setShowMenu(false)}>
|
||||
<Menu.Item
|
||||
icon="delete"
|
||||
onPress={() => {
|
||||
setShowRemove(item.name)
|
||||
setShowMenu(false)
|
||||
}}
|
||||
title="Delete"
|
||||
/>
|
||||
</Menu>
|
||||
</Text>
|
||||
)}
|
||||
left={left}
|
||||
right={right}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title={`Delete ${showRemove}`}
|
||||
show={!!showRemove}
|
||||
setShow={show => (show ? null : setShowRemove(''))}
|
||||
onOk={remove}>
|
||||
setShow={(show) => (show ? null : setShowRemove(""))}
|
||||
onOk={remove}
|
||||
>
|
||||
This irreversibly deletes ALL sets related to this workout. Are you
|
||||
sure?
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
123
WorkoutList.tsx
123
WorkoutList.tsx
|
@ -2,53 +2,52 @@ 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 DrawerHeader from './DrawerHeader'
|
||||
import Page from './Page'
|
||||
import GymSet from './gym-set'
|
||||
import SetList from './SetList'
|
||||
import WorkoutItem from './WorkoutItem'
|
||||
import {WorkoutsPageParams} from './WorkoutsPage'
|
||||
import {setRepo, settingsRepo} from './db'
|
||||
import Settings from './settings'
|
||||
|
||||
const limit = 15
|
||||
} from "@react-navigation/native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import { LIMIT } from "./constants";
|
||||
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";
|
||||
|
||||
export default function WorkoutList() {
|
||||
const [workouts, setWorkouts] = useState<GymSet[]>()
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [term, setTerm] = useState('')
|
||||
const [end, setEnd] = useState(false)
|
||||
const [settings, setSettings] = useState<Settings>()
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
|
||||
const [workouts, setWorkouts] = useState<GymSet[]>();
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [term, setTerm] = useState("");
|
||||
const [end, setEnd] = useState(false);
|
||||
const [settings, setSettings] = useState<Settings>();
|
||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
||||
|
||||
const refresh = useCallback(async (value: string) => {
|
||||
const newWorkouts = await setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.where('name LIKE :name', {name: `%${value}%`})
|
||||
.groupBy('name')
|
||||
.orderBy('name')
|
||||
.limit(limit)
|
||||
.getMany()
|
||||
console.log(`${WorkoutList.name}`, {newWorkout: newWorkouts[0]})
|
||||
setWorkouts(newWorkouts)
|
||||
setOffset(0)
|
||||
setEnd(false)
|
||||
}, [])
|
||||
.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]),
|
||||
)
|
||||
refresh(term);
|
||||
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||
}, [refresh, term])
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: GymSet}) => (
|
||||
({ item }: { item: GymSet }) => (
|
||||
<WorkoutItem
|
||||
images={settings?.images}
|
||||
item={item}
|
||||
|
@ -56,47 +55,47 @@ export default function WorkoutList() {
|
|||
onRemove={() => refresh(term)}
|
||||
/>
|
||||
),
|
||||
[refresh, term, settings?.images],
|
||||
)
|
||||
[refresh, term, settings?.images]
|
||||
);
|
||||
|
||||
const next = useCallback(async () => {
|
||||
if (end) return
|
||||
const newOffset = offset + limit
|
||||
if (end) return;
|
||||
const newOffset = offset + LIMIT;
|
||||
console.log(`${SetList.name}.next:`, {
|
||||
offset,
|
||||
limit,
|
||||
limit: LIMIT,
|
||||
newOffset,
|
||||
term,
|
||||
})
|
||||
});
|
||||
const newWorkouts = await setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.where('name LIKE :name', {name: `%${term}%`})
|
||||
.groupBy('name')
|
||||
.orderBy('name')
|
||||
.limit(limit)
|
||||
.where("name LIKE :name", { name: `%${term.trim()}%` })
|
||||
.groupBy("name")
|
||||
.orderBy("name")
|
||||
.limit(LIMIT)
|
||||
.offset(newOffset)
|
||||
.getMany()
|
||||
if (newWorkouts.length === 0) return setEnd(true)
|
||||
if (!workouts) return
|
||||
setWorkouts([...workouts, ...newWorkouts])
|
||||
if (newWorkouts.length < limit) return setEnd(true)
|
||||
setOffset(newOffset)
|
||||
}, [term, end, offset, workouts])
|
||||
.getMany();
|
||||
if (newWorkouts.length === 0) return setEnd(true);
|
||||
if (!workouts) return;
|
||||
setWorkouts([...workouts, ...newWorkouts]);
|
||||
if (newWorkouts.length < LIMIT) return setEnd(true);
|
||||
setOffset(newOffset);
|
||||
}, [term, end, offset, workouts]);
|
||||
|
||||
const onAdd = useCallback(async () => {
|
||||
navigation.navigate('EditWorkout', {
|
||||
navigation.navigate("EditWorkout", {
|
||||
value: new GymSet(),
|
||||
})
|
||||
}, [navigation])
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const search = useCallback(
|
||||
(value: string) => {
|
||||
setTerm(value)
|
||||
refresh(value)
|
||||
setTerm(value);
|
||||
refresh(value);
|
||||
},
|
||||
[refresh],
|
||||
)
|
||||
[refresh]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -110,13 +109,13 @@ export default function WorkoutList() {
|
|||
) : (
|
||||
<FlatList
|
||||
data={workouts}
|
||||
style={{flex: 1}}
|
||||
style={{ flex: 1 }}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={w => w.name}
|
||||
keyExtractor={(w) => w.name}
|
||||
onEndReached={next}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import {createStackNavigator} from '@react-navigation/stack'
|
||||
import EditWorkout from './EditWorkout'
|
||||
import GymSet from './gym-set'
|
||||
import WorkoutList from './WorkoutList'
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import EditWorkout from "./EditWorkout";
|
||||
import GymSet from "./gym-set";
|
||||
import WorkoutList from "./WorkoutList";
|
||||
|
||||
export type WorkoutsPageParams = {
|
||||
WorkoutList: {}
|
||||
WorkoutList: {};
|
||||
EditWorkout: {
|
||||
value: GymSet
|
||||
}
|
||||
}
|
||||
value: GymSet;
|
||||
};
|
||||
};
|
||||
|
||||
const Stack = createStackNavigator<WorkoutsPageParams>()
|
||||
const Stack = createStackNavigator<WorkoutsPageParams>();
|
||||
|
||||
export default function WorkoutsPage() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{headerShown: false, animationEnabled: false}}>
|
||||
screenOptions={{ headerShown: false, animationEnabled: false }}
|
||||
>
|
||||
<Stack.Screen name="WorkoutList" component={WorkoutList} />
|
||||
<Stack.Screen name="EditWorkout" component={EditWorkout} />
|
||||
</Stack.Navigator>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
CFPropertyList (3.0.6)
|
||||
rexml
|
||||
addressable (2.8.1)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.686.0)
|
||||
aws-sdk-core (3.168.4)
|
||||
aws-partitions (1.780.0)
|
||||
aws-sdk-core (3.175.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.61.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (1.67.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.117.2)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-s3 (1.126.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.2)
|
||||
|
@ -36,8 +36,8 @@ GEM
|
|||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.95.0)
|
||||
faraday (1.10.2)
|
||||
excon (0.100.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
|
@ -65,8 +65,8 @@ GEM
|
|||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.211.0)
|
||||
fastimage (2.2.7)
|
||||
fastlane (2.213.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
|
@ -90,7 +90,7 @@ GEM
|
|||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
|
@ -106,9 +106,9 @@ GEM
|
|||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.32.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-core (0.9.2)
|
||||
google-apis-androidpublisher_v3 (0.43.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
|
@ -117,10 +117,10 @@ GEM
|
|||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.16.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.12.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
|
@ -128,7 +128,7 @@ GEM
|
|||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.0)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
|
@ -137,7 +137,7 @@ GEM
|
|||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.3.0)
|
||||
googleauth (1.5.2)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
|
@ -150,17 +150,17 @@ GEM
|
|||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.6.0)
|
||||
jwt (2.7.1)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
multipart-post (2.3.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
plist (3.7.0)
|
||||
public_suffix (5.0.1)
|
||||
rake (13.0.6)
|
||||
representable (3.2.0)
|
||||
|
@ -178,7 +178,7 @@ GEM
|
|||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
|
@ -194,7 +194,7 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
webrick (1.8.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
|
|
|
@ -1,115 +1,92 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
project.ext.react = [
|
||||
enableHermes: true, // clean and rebuild if changing
|
||||
]
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
|
||||
project.ext.vectoricons = [
|
||||
iconFontNames: ['MaterialIcons.ttf']
|
||||
]
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
def enableSeparateBuildPerCPUArchitecture = true
|
||||
def enableProguardInReleaseBuilds = true
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
def enableHermes = project.ext.react.get("enableHermes", true);
|
||||
|
||||
def reactNativeArchitectures() {
|
||||
def value = project.getProperties().get("reactNativeArchitectures")
|
||||
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
}
|
||||
|
||||
android {
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = true
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
pickFirsts += ['**/armeabi-v7a/libfolly_runtime.so', '**/x86/libfolly_runtime.so', '**/arm64-v8a/libfolly_runtime.so', '**/x86_64/libfolly_runtime.so']
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "com.massive"
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.massive"
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 36133
|
||||
versionName "1.107"
|
||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
arguments "APP_PLATFORM=android-21",
|
||||
"APP_STL=c++_shared",
|
||||
"NDK_TOOLCHAIN_VERSION=clang",
|
||||
"GENERATED_SRC_DIR=$buildDir/generated/source",
|
||||
"PROJECT_BUILD_DIR=$buildDir",
|
||||
"REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
|
||||
"NODE_MODULES_DIR=$rootDir/../node_modules"
|
||||
cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1"
|
||||
cppFlags "-std=c++17"
|
||||
targets "massive_appmodules"
|
||||
}
|
||||
}
|
||||
if (!enableSeparateBuildPerCPUArchitecture) {
|
||||
ndk {
|
||||
abiFilters(*reactNativeArchitectures())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "$projectDir/src/main/jni/Android.mk"
|
||||
}
|
||||
}
|
||||
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
|
||||
def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) {
|
||||
dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck")
|
||||
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
|
||||
into("$buildDir/react-ndk/exported")
|
||||
}
|
||||
def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) {
|
||||
dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck")
|
||||
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
|
||||
into("$buildDir/react-ndk/exported")
|
||||
}
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn(packageReactNdkDebugLibs)
|
||||
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
|
||||
|
||||
configureNdkBuildRelease.dependsOn(preReleaseBuild)
|
||||
configureNdkBuildDebug.dependsOn(preDebugBuild)
|
||||
reactNativeArchitectures().each { architecture ->
|
||||
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
|
||||
dependsOn("preDebugBuild")
|
||||
}
|
||||
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
|
||||
dependsOn("preReleaseBuild")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false
|
||||
include(*reactNativeArchitectures())
|
||||
}
|
||||
versionCode 36174
|
||||
versionName "1.148"
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
|
@ -129,15 +106,14 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
|
@ -145,65 +121,28 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.4.+'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.databinding:databinding-runtime:7.1.2'
|
||||
def work_version = "2.7.1"
|
||||
|
||||
implementation "androidx.work:work-runtime:$work_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation "androidx.work:work-rxjava2:$work_version"
|
||||
androidTestImplementation "androidx.work:work-testing:$work_version"
|
||||
implementation "androidx.work:work-multiprocess:$work_version"
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation "androidx.core:core-ktx:1.8.0"
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation project(':react-native-sqlite-storage')
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group: 'com.facebook.fbjni'
|
||||
}
|
||||
implementation project(':react-native-vector-icons')
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group: 'com.facebook.flipper'
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group: 'com.facebook.flipper'
|
||||
}
|
||||
|
||||
if (enableHermes) {
|
||||
implementation("com.facebook.react:hermes-engine:+") { // From node_modules
|
||||
exclude group: 'com.facebook.fbjni'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
configurations.all {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
substitute(module("com.facebook.react:react-native"))
|
||||
.using(project(":ReactAndroid"))
|
||||
.because("On New Architecture we're building React Native from source")
|
||||
substitute(module("com.facebook.react:hermes-engine"))
|
||||
.using(project(":ReactAndroid:hermes-engine"))
|
||||
.because("On New Architecture we're building Hermes from source")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.implementation
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
def isNewArchitectureEnabled() {
|
||||
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
||||
}
|
||||
project.ext.vectoricons = [
|
||||
iconFontNames: ['MaterialIcons.ttf']
|
||||
]
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
|
|
@ -17,7 +17,6 @@ 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.react.ReactFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
|
@ -25,13 +24,16 @@ 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 ReactFlipperPlugin());
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
|
|
|
@ -1,51 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.massive">
|
||||
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.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.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_NETWORK_STATE"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
||||
tools:node="remove"/>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".TimerDone"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".StopAlarm"
|
||||
android:exported="true"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".AlarmService"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".TimerDone"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".StopAlarm"
|
||||
android:exported="true"
|
||||
android:process=":remote" />
|
||||
|
||||
<service
|
||||
android:name=".AlarmService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -19,7 +19,7 @@ import kotlin.math.floor
|
|||
class AlarmModule constructor(context: ReactApplicationContext?) :
|
||||
ReactContextBaseJavaModule(context) {
|
||||
|
||||
var countdownTimer: CountDownTimer? = null
|
||||
private var countdownTimer: CountDownTimer? = null
|
||||
var currentMs: Long = 0
|
||||
var running = false
|
||||
|
||||
|
@ -38,11 +38,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
private val addReceiver = object : BroadcastReceiver() {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val vibrate = intent?.extras?.getBoolean("vibrate") == true
|
||||
val sound = intent?.extras?.getString("sound")
|
||||
val noSound = intent?.extras?.getBoolean("noSound") == true
|
||||
Log.d("AlarmModule", "vibrate=$vibrate,sound=$sound,noSound=$noSound")
|
||||
add(vibrate, sound, noSound)
|
||||
add()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,15 +55,15 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@ReactMethod
|
||||
fun add(vibrate: Boolean, sound: String?, noSound: Boolean = false) {
|
||||
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, vibrate, sound, noSound)
|
||||
countdownTimer = getTimer(newMs)
|
||||
countdownTimer?.start()
|
||||
running = true
|
||||
val manager = getManager()
|
||||
manager.cancel(NOTIFICATION_ID_DONE)
|
||||
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
|
||||
val intent = Intent(reactApplicationContext, AlarmService::class.java)
|
||||
reactApplicationContext.stopService(intent)
|
||||
}
|
||||
|
@ -81,7 +77,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
val intent = Intent(reactApplicationContext, AlarmService::class.java)
|
||||
reactApplicationContext?.stopService(intent)
|
||||
val manager = getManager()
|
||||
manager.cancel(NOTIFICATION_ID_DONE)
|
||||
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
|
||||
manager.cancel(NOTIFICATION_ID_PENDING)
|
||||
val params = Arguments.createMap().apply {
|
||||
putString("minutes", "00")
|
||||
|
@ -94,14 +90,14 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@ReactMethod
|
||||
fun timer(milliseconds: Int, vibrate: Boolean, sound: String?, noSound: Boolean = false) {
|
||||
fun timer(milliseconds: Int) {
|
||||
Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
|
||||
val manager = getManager()
|
||||
manager.cancel(NOTIFICATION_ID_DONE)
|
||||
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
|
||||
val intent = Intent(reactApplicationContext, AlarmService::class.java)
|
||||
reactApplicationContext.stopService(intent)
|
||||
countdownTimer?.cancel()
|
||||
countdownTimer = getTimer(milliseconds, vibrate, sound, noSound)
|
||||
countdownTimer = getTimer(milliseconds)
|
||||
countdownTimer?.start()
|
||||
running = true
|
||||
}
|
||||
|
@ -109,11 +105,8 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun getTimer(
|
||||
endMs: Int,
|
||||
vibrate: Boolean,
|
||||
sound: String?,
|
||||
noSound: Boolean
|
||||
): CountDownTimer {
|
||||
val builder = getBuilder(vibrate, sound, noSound)
|
||||
val builder = getBuilder()
|
||||
return object : CountDownTimer(endMs.toLong(), 1000) {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onTick(current: Long) {
|
||||
|
@ -140,30 +133,8 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onFinish() {
|
||||
val context = reactApplicationContext
|
||||
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 manager = getManager()
|
||||
manager.notify(NOTIFICATION_ID_DONE, builder.build())
|
||||
manager.cancel(NOTIFICATION_ID_PENDING)
|
||||
Log.d("AlarmModule", "Finished: vibrate=$vibrate,sound=$sound,noSound=$noSound")
|
||||
val alarmIntent = Intent(context, AlarmService::class.java).apply {
|
||||
putExtra("vibrate", vibrate)
|
||||
putExtra("sound", sound)
|
||||
putExtra("noSound", noSound)
|
||||
}
|
||||
context.startService(alarmIntent)
|
||||
reactApplicationContext
|
||||
context.startForegroundService(Intent(context, AlarmService::class.java))
|
||||
context
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
.emit("finish", Arguments.createMap().apply {
|
||||
putString("minutes", "00")
|
||||
|
@ -175,25 +146,18 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun getBuilder(
|
||||
vibrate: Boolean,
|
||||
sound: String?,
|
||||
noSound: Boolean
|
||||
): NotificationCompat.Builder {
|
||||
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(reactApplicationContext.packageName)
|
||||
putExtra("vibrate", vibrate)
|
||||
putExtra("sound", sound)
|
||||
putExtra("noSound", noSound)
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
val pendingAdd =
|
||||
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
|
||||
val stopBroadcast = Intent(STOP_BROADCAST)
|
||||
stopBroadcast.setPackage(reactApplicationContext.packageName)
|
||||
stopBroadcast.setPackage(context.packageName)
|
||||
val pendingStop =
|
||||
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
|
||||
return NotificationCompat.Builder(context, CHANNEL_ID_PENDING)
|
||||
|
@ -206,16 +170,9 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getManager(): NotificationManager {
|
||||
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 notificationManager = reactApplicationContext.getSystemService(
|
||||
NotificationManager::class.java
|
||||
)
|
||||
notificationManager.createNotificationChannel(alarmsChannel)
|
||||
val timersChannel = NotificationChannel(
|
||||
CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
|
@ -229,8 +186,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
|
|||
const val STOP_BROADCAST = "stop-timer-event"
|
||||
const val ADD_BROADCAST = "add-timer-event"
|
||||
const val CHANNEL_ID_PENDING = "Timer"
|
||||
const val CHANNEL_ID_DONE = "Alarm"
|
||||
const val NOTIFICATION_ID_PENDING = 1
|
||||
const val NOTIFICATION_ID_DONE = 2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,65 @@
|
|||
package com.massive
|
||||
|
||||
import android.app.Service
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer.OnPreparedListener
|
||||
import android.media.MediaPlayer
|
||||
import androidx.annotation.RequiresApi
|
||||
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 {
|
||||
var mediaPlayer: MediaPlayer? = null
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var vibrator: Vibrator? = null
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
if (intent.action == "stop") {
|
||||
onDestroy()
|
||||
return START_STICKY
|
||||
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 sound = intent.extras?.getString("sound")
|
||||
val noSound = intent.extras?.getBoolean("noSound") == true
|
||||
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)
|
||||
}
|
||||
|
||||
if (sound == null && !noSound) {
|
||||
|
||||
@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 (sound != null && !noSound) {
|
||||
} else if (settings.sound != null && !settings.noSound) {
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
|
@ -35,13 +67,56 @@ class AlarmService : Service(), OnPreparedListener {
|
|||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setDataSource(applicationContext, Uri.parse(sound))
|
||||
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 =
|
||||
|
@ -55,9 +130,7 @@ class AlarmService : Service(), OnPreparedListener {
|
|||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||
.build()
|
||||
val vibrate = intent.extras!!.getBoolean("vibrate")
|
||||
if (vibrate)
|
||||
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, 1), audioAttributes)
|
||||
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, 1), audioAttributes)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
|
@ -75,4 +148,9 @@ class AlarmService : Service(), OnPreparedListener {
|
|||
mediaPlayer?.release()
|
||||
vibrator?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID_DONE = "Alarm"
|
||||
const val NOTIFICATION_ID_DONE = 2
|
||||
}
|
||||
}
|
84
android/app/src/main/java/com/massive/BackupModule.kt
Normal file
84
android/app/src/main/java/com/massive/BackupModule.kt
Normal file
|
@ -0,0 +1,84 @@
|
|||
package com.massive
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
||||
class BackupModule constructor(context: ReactApplicationContext?) :
|
||||
ReactContextBaseJavaModule(context) {
|
||||
val context: ReactApplicationContext = reactApplicationContext
|
||||
private var targetDir: String? = null
|
||||
|
||||
private val copyReceiver = object : BroadcastReceiver() {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val treeUri: Uri = Uri.parse(targetDir)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@ReactMethod
|
||||
fun start(baseUri: String) {
|
||||
targetDir = baseUri
|
||||
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(COPY_BROADCAST)
|
||||
val pendingIntent =
|
||||
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
pendingIntent.send()
|
||||
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
timeInMillis = System.currentTimeMillis()
|
||||
set(Calendar.HOUR_OF_DAY, 6)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
}
|
||||
|
||||
alarmMgr.setRepeating(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
AlarmManager.INTERVAL_DAY,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@ReactMethod
|
||||
fun stop() {
|
||||
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(COPY_BROADCAST)
|
||||
val pendingIntent =
|
||||
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
alarmMgr.cancel(pendingIntent)
|
||||
}
|
||||
|
||||
init {
|
||||
reactApplicationContext.registerReceiver(copyReceiver, IntentFilter(COPY_BROADCAST))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val COPY_BROADCAST = "copy-event"
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return "BackupModule"
|
||||
}
|
||||
}
|
22
android/app/src/main/java/com/massive/DatabaseHelper.kt
Normal file
22
android/app/src/main/java/com/massive/DatabaseHelper.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
package com.massive
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
|
||||
class DatabaseHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
companion object {
|
||||
private const val DATABASE_NAME = "massive.db"
|
||||
private const val DATABASE_VERSION = 1
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package com.massive
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.NotificationManager.IMPORTANCE_DEFAULT
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import java.io.File
|
||||
|
||||
class DownloadModule internal constructor(context: ReactApplicationContext) :
|
||||
ReactContextBaseJavaModule(context) {
|
||||
override fun getName(): String {
|
||||
return "DownloadModule"
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@ReactMethod
|
||||
fun show(name: String) {
|
||||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, IMPORTANCE_DEFAULT)
|
||||
channel.description = "Notifications for downloaded files."
|
||||
val manager =
|
||||
reactApplicationContext.getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(channel)
|
||||
val intent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(reactApplicationContext, 0, intent, FLAG_IMMUTABLE)
|
||||
val builder = NotificationCompat.Builder(reactApplicationContext, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_baseline_arrow_downward_24)
|
||||
.setContentTitle("Downloaded $name")
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
manager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "MassiveDownloads"
|
||||
private const val NOTIFICATION_ID = 3
|
||||
}
|
||||
}
|
|
@ -2,32 +2,27 @@ package com.massive
|
|||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.ReactRootView
|
||||
import android.os.Bundle;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
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
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return MainActivityDelegate(this, mainComponentName)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
class MainActivityDelegate(activity: ReactActivity?, mainComponentName: String?) :
|
||||
ReactActivityDelegate(activity, mainComponentName) {
|
||||
override fun createRootView(): ReactRootView {
|
||||
val reactRootView = ReactRootView(context)
|
||||
reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED)
|
||||
return reactRootView
|
||||
}
|
||||
|
||||
override fun isConcurrentRootEnabled(): Boolean {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
}
|
||||
return DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName!!, // If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
fabricEnabled
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
package com.massive
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.facebook.react.*
|
||||
import com.facebook.react.config.ReactFeatureFlags
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.massive.newarchitecture.MainApplicationReactNativeHost
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
private val mReactNativeHost: ReactNativeHost = object : ReactNativeHost(this) {
|
||||
private val mReactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) {
|
||||
override fun getUseDeveloperSupport(): Boolean {
|
||||
return BuildConfig.DEBUG
|
||||
}
|
||||
|
@ -23,48 +24,24 @@ class MainApplication : Application(), ReactApplication {
|
|||
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
|
||||
}
|
||||
|
||||
private val mNewArchitectureNativeHost: ReactNativeHost = MainApplicationReactNativeHost(this)
|
||||
override fun getReactNativeHost(): ReactNativeHost {
|
||||
return if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
mNewArchitectureNativeHost
|
||||
} else {
|
||||
mReactNativeHost
|
||||
}
|
||||
return mReactNativeHost
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
SoLoader.init(this, false)
|
||||
initializeFlipper(this, reactNativeHost.reactInstanceManager)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun initializeFlipper(
|
||||
context: Context, reactInstanceManager: ReactInstanceManager
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
val aClass = Class.forName("com.massive.ReactNativeFlipper")
|
||||
aClass
|
||||
.getMethod(
|
||||
"initializeFlipper",
|
||||
Context::class.java,
|
||||
ReactInstanceManager::class.java
|
||||
)
|
||||
.invoke(null, context, reactInstanceManager)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: NoSuchMethodException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IllegalAccessException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: InvocationTargetException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
SoLoader.init(this, /* native exopackage */false)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import com.facebook.react.ReactPackage
|
|||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
import java.util.ArrayList
|
||||
|
||||
class MassivePackage : ReactPackage {
|
||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||
|
@ -16,8 +15,8 @@ class MassivePackage : ReactPackage {
|
|||
): List<NativeModule> {
|
||||
val modules: MutableList<NativeModule> = ArrayList()
|
||||
modules.add(AlarmModule(reactContext))
|
||||
modules.add(DownloadModule(reactContext))
|
||||
modules.add(SettingsModule(reactContext))
|
||||
modules.add(BackupModule(reactContext))
|
||||
return modules
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class TimerDone : AppCompatActivity() {
|
|||
Log.d("TimerDone", "Stopping...")
|
||||
applicationContext.stopService(Intent(applicationContext, AlarmService::class.java))
|
||||
val manager = getManager()
|
||||
manager.cancel(AlarmModule.NOTIFICATION_ID_DONE)
|
||||
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
|
||||
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
@ -33,8 +33,8 @@ class TimerDone : AppCompatActivity() {
|
|||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getManager(): NotificationManager {
|
||||
val alarmsChannel = NotificationChannel(
|
||||
AlarmModule.CHANNEL_ID_DONE,
|
||||
AlarmModule.CHANNEL_ID_DONE,
|
||||
AlarmService.CHANNEL_ID_DONE,
|
||||
AlarmService.CHANNEL_ID_DONE,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "Alarms for rest timers."
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -1,55 +1,22 @@
|
|||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
kotlin_version = '1.6.10'
|
||||
buildToolsVersion = "31.0.0"
|
||||
buildToolsVersion = "33.0.0"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 31
|
||||
targetSdkVersion = 31
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
|
||||
if (System.properties['os.arch'] == "aarch64") {
|
||||
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
||||
ndkVersion = "24.0.8215888"
|
||||
} else {
|
||||
// Otherwise we default to the side-by-side NDK version from AGP.
|
||||
ndkVersion = "21.4.7075529"
|
||||
}
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
ndkVersion = "23.1.7779620"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.android.tools.build:gradle:7.2.1')
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
}
|
||||
maven {
|
||||
// Android JSC is installed from npm
|
||||
url("$rootDir/../node_modules/jsc-android/dist")
|
||||
}
|
||||
mavenCentral {
|
||||
// We don't want to fetch react-native from Maven Central as there are
|
||||
// older versions over there.
|
||||
content {
|
||||
excludeGroup "com.facebook.react"
|
||||
}
|
||||
}
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ android.useAndroidX=true
|
|||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.125.0
|
||||
FLIPPER_VERSION=0.182.0
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
|
@ -38,3 +38,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
|||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
|
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
|
@ -1,5 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
18
android/gradlew
vendored
18
android/gradlew
vendored
|
@ -55,7 +55,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
@ -80,10 +80,10 @@ do
|
|||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && 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"'
|
||||
|
@ -143,12 +143,16 @@ fi
|
|||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
@ -205,6 +209,12 @@ set -- \
|
|||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
|
15
android/gradlew.bat
vendored
15
android/gradlew.bat
vendored
|
@ -14,7 +14,7 @@
|
|||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
|
@ -25,7 +25,8 @@
|
|||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
rootProject.name = 'massive'
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
include ':react-native-sqlite-storage'
|
||||
project(':react-native-sqlite-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sqlite-storage/platforms/android')
|
||||
|
||||
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
|
||||
include(":ReactAndroid")
|
||||
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
|
||||
include(":ReactAndroid:hermes-engine")
|
||||
project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine')
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ module.exports = {
|
|||
'@babel/plugin-transform-flow-strip-types',
|
||||
['@babel/plugin-proposal-decorators', {legacy: true}],
|
||||
['@babel/plugin-proposal-class-properties', {loose: true}],
|
||||
'react-native-reanimated/plugin',
|
||||
'react-native-paper/babel',
|
||||
'react-native-reanimated/plugin',
|
||||
],
|
||||
env: {
|
||||
production: {
|
||||
|
|
|
@ -1,15 +1,42 @@
|
|||
import {setRepo} from './db'
|
||||
import GymSet from './gym-set'
|
||||
import { LIMIT } from "./constants";
|
||||
import { setRepo } from "./db";
|
||||
import GymSet from "./gym-set";
|
||||
|
||||
export const getBestSet = async (name: string): Promise<GymSet> => {
|
||||
return setRepo
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addSelect('MAX(weight)', 'weight')
|
||||
.where('name = :name', {name})
|
||||
.groupBy('name')
|
||||
.addGroupBy('reps')
|
||||
.orderBy('weight', 'DESC')
|
||||
.addOrderBy('reps', 'DESC')
|
||||
.getOne()
|
||||
}
|
||||
.addSelect("MAX(weight)", "weight")
|
||||
.where("name = :name", { name })
|
||||
.groupBy("name")
|
||||
.addGroupBy("reps")
|
||||
.orderBy("weight", "DESC")
|
||||
.addOrderBy("reps", "DESC")
|
||||
.getOne();
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
|
55
colors.ts
55
colors.ts
|
@ -1,40 +1,41 @@
|
|||
import {DarkTheme, DefaultTheme} from 'react-native-paper'
|
||||
import { DefaultTheme, MD3DarkTheme } from "react-native-paper";
|
||||
|
||||
export const lightColors = [
|
||||
{hex: DarkTheme.colors.primary, name: 'Purple'},
|
||||
{hex: '#B3E5FC', name: 'Blue'},
|
||||
{hex: '#FA8072', name: 'Salmon'},
|
||||
{hex: '#FFC0CB', name: 'Pink'},
|
||||
{hex: '#E9DCC9', name: 'Linen'},
|
||||
]
|
||||
{ hex: MD3DarkTheme.colors.primary, name: "Purple" },
|
||||
{ hex: "#B3E5FC", name: "Blue" },
|
||||
{ hex: "#FA8072", name: "Salmon" },
|
||||
{ hex: "#FFC0CB", name: "Pink" },
|
||||
{ hex: "#E9DCC9", name: "Linen" },
|
||||
];
|
||||
|
||||
export const darkColors = [
|
||||
{hex: DefaultTheme.colors.primary, name: 'Purple'},
|
||||
{hex: '#0051a9', name: 'Blue'},
|
||||
{hex: '#000000', name: 'Black'},
|
||||
{hex: '#863c3c', name: 'Red'},
|
||||
{hex: '#1c6000', name: 'Kermit'},
|
||||
]
|
||||
{ hex: DefaultTheme.colors.primary, name: "Purple" },
|
||||
{ hex: "#0051a9", name: "Blue" },
|
||||
{ hex: "#000000", name: "Black" },
|
||||
{ hex: "#863c3c", name: "Red" },
|
||||
{ hex: "#1c6000", name: "Kermit" },
|
||||
];
|
||||
|
||||
export const colorShade = (color: any, amount: number) => {
|
||||
color = color.replace(/^#/, '')
|
||||
if (color.length === 3)
|
||||
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]
|
||||
color = color.replace(/^#/, "");
|
||||
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] = [
|
||||
let [r, g, b] = color.match(/.{2}/g);
|
||||
[r, g, b] = [
|
||||
parseInt(r, 16) + amount,
|
||||
parseInt(g, 16) + amount,
|
||||
parseInt(b, 16) + amount,
|
||||
]
|
||||
];
|
||||
|
||||
r = Math.max(Math.min(255, r), 0).toString(16)
|
||||
g = Math.max(Math.min(255, g), 0).toString(16)
|
||||
b = Math.max(Math.min(255, b), 0).toString(16)
|
||||
r = Math.max(Math.min(255, r), 0).toString(16);
|
||||
g = Math.max(Math.min(255, g), 0).toString(16);
|
||||
b = Math.max(Math.min(255, b), 0).toString(16);
|
||||
|
||||
const rr = (r.length < 2 ? '0' : '') + r
|
||||
const gg = (g.length < 2 ? '0' : '') + g
|
||||
const bb = (b.length < 2 ? '0' : '') + b
|
||||
const rr = (r.length < 2 ? "0" : "") + r;
|
||||
const gg = (g.length < 2 ? "0" : "") + g;
|
||||
const bb = (b.length < 2 ? "0" : "") + b;
|
||||
|
||||
return `#${rr}${gg}${bb}`
|
||||
}
|
||||
return `#${rr}${gg}${bb}`;
|
||||
};
|
||||
|
|
11
constants.ts
11
constants.ts
|
@ -1,5 +1,6 @@
|
|||
export const MARGIN = 10
|
||||
export const PADDING = 10
|
||||
export const ITEM_PADDING = 8
|
||||
export const DARK_RIPPLE = '#444444'
|
||||
export const LIGHT_RIPPLE = '#c2c2c2'
|
||||
export const MARGIN = 10;
|
||||
export const PADDING = 10;
|
||||
export const ITEM_PADDING = 8;
|
||||
export const DARK_RIPPLE = "#444444";
|
||||
export const LIGHT_RIPPLE = "#c2c2c2";
|
||||
export const LIMIT = 15;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export default interface CountMany {
|
||||
name: string
|
||||
total: number
|
||||
sets?: number
|
||||
name: string;
|
||||
total: number;
|
||||
sets?: number;
|
||||
}
|
||||
|
|
|
@ -1,39 +1,40 @@
|
|||
import {DataSource} from 'typeorm'
|
||||
import GymSet from './gym-set'
|
||||
import {Sets1667185586014 as sets1667185586014} from './migrations/1667185586014-sets'
|
||||
import {plans1667186124792} from './migrations/1667186124792-plans'
|
||||
import {settings1667186130041} from './migrations/1667186130041-settings'
|
||||
import {addSound1667186139844} from './migrations/1667186139844-add-sound'
|
||||
import {addHidden1667186159379} from './migrations/1667186159379-add-hidden'
|
||||
import {addNotify1667186166140} from './migrations/1667186166140-add-notify'
|
||||
import {addImage1667186171548} from './migrations/1667186171548-add-image'
|
||||
import {addImages1667186179488} from './migrations/1667186179488-add-images'
|
||||
import {insertSettings1667186203827} from './migrations/1667186203827-insert-settings'
|
||||
import {addSteps1667186211251} from './migrations/1667186211251-add-steps'
|
||||
import {addSets1667186250618} from './migrations/1667186250618-add-sets'
|
||||
import {addMinutes1667186255650} from './migrations/1667186255650-add-minutes'
|
||||
import {addSeconds1667186259174} from './migrations/1667186259174-add-seconds'
|
||||
import {addShowUnit1667186265588} from './migrations/1667186265588-add-show-unit'
|
||||
import {addColor1667186320954} from './migrations/1667186320954-add-color'
|
||||
import {addSteps1667186348425} from './migrations/1667186348425-add-steps'
|
||||
import {addDate1667186431804} from './migrations/1667186431804-add-date'
|
||||
import {addShowDate1667186435051} from './migrations/1667186435051-add-show-date'
|
||||
import {addTheme1667186439366} from './migrations/1667186439366-add-theme'
|
||||
import {addShowSets1667186443614} from './migrations/1667186443614-add-show-sets'
|
||||
import {addSetsCreated1667186451005} from './migrations/1667186451005-add-sets-created'
|
||||
import {addNoSound1667186456118} from './migrations/1667186456118-add-no-sound'
|
||||
import {dropMigrations1667190214743} from './migrations/1667190214743-drop-migrations'
|
||||
import {splitColor1669420187764} from './migrations/1669420187764-split-color'
|
||||
import {Plan} from './plan'
|
||||
import Settings from './settings'
|
||||
import { DataSource } from "typeorm";
|
||||
import GymSet from "./gym-set";
|
||||
import { Sets1667185586014 as sets1667185586014 } from "./migrations/1667185586014-sets";
|
||||
import { plans1667186124792 } from "./migrations/1667186124792-plans";
|
||||
import { settings1667186130041 } from "./migrations/1667186130041-settings";
|
||||
import { addSound1667186139844 } from "./migrations/1667186139844-add-sound";
|
||||
import { addHidden1667186159379 } from "./migrations/1667186159379-add-hidden";
|
||||
import { addNotify1667186166140 } from "./migrations/1667186166140-add-notify";
|
||||
import { addImage1667186171548 } from "./migrations/1667186171548-add-image";
|
||||
import { addImages1667186179488 } from "./migrations/1667186179488-add-images";
|
||||
import { insertSettings1667186203827 } from "./migrations/1667186203827-insert-settings";
|
||||
import { addSteps1667186211251 } from "./migrations/1667186211251-add-steps";
|
||||
import { addSets1667186250618 } from "./migrations/1667186250618-add-sets";
|
||||
import { addMinutes1667186255650 } from "./migrations/1667186255650-add-minutes";
|
||||
import { addSeconds1667186259174 } from "./migrations/1667186259174-add-seconds";
|
||||
import { addShowUnit1667186265588 } from "./migrations/1667186265588-add-show-unit";
|
||||
import { addColor1667186320954 } from "./migrations/1667186320954-add-color";
|
||||
import { addSteps1667186348425 } from "./migrations/1667186348425-add-steps";
|
||||
import { addDate1667186431804 } from "./migrations/1667186431804-add-date";
|
||||
import { addShowDate1667186435051 } from "./migrations/1667186435051-add-show-date";
|
||||
import { addTheme1667186439366 } from "./migrations/1667186439366-add-theme";
|
||||
import { addShowSets1667186443614 } from "./migrations/1667186443614-add-show-sets";
|
||||
import { addSetsCreated1667186451005 } from "./migrations/1667186451005-add-sets-created";
|
||||
import { addNoSound1667186456118 } from "./migrations/1667186456118-add-no-sound";
|
||||
import { dropMigrations1667190214743 } from "./migrations/1667190214743-drop-migrations";
|
||||
import { splitColor1669420187764 } from "./migrations/1669420187764-split-color";
|
||||
import { addBackup1678334268359 } from "./migrations/1678334268359-add-backup";
|
||||
import { Plan } from "./plan";
|
||||
import Settings from "./settings";
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'react-native',
|
||||
database: 'massive.db',
|
||||
location: 'default',
|
||||
type: "react-native",
|
||||
database: "massive.db",
|
||||
location: "default",
|
||||
entities: [GymSet, Plan, Settings],
|
||||
migrationsRun: true,
|
||||
migrationsTableName: 'typeorm_migrations',
|
||||
migrationsTableName: "typeorm_migrations",
|
||||
migrations: [
|
||||
sets1667185586014,
|
||||
plans1667186124792,
|
||||
|
@ -59,5 +60,6 @@ export const AppDataSource = new DataSource({
|
|||
addNoSound1667186456118,
|
||||
dropMigrations1667190214743,
|
||||
splitColor1669420187764,
|
||||
addBackup1678334268359,
|
||||
],
|
||||
})
|
||||
});
|
||||
|
|
25
db.ts
25
db.ts
|
@ -1,14 +1,15 @@
|
|||
import {AppDataSource} from './data-source'
|
||||
import GymSet from './gym-set'
|
||||
import {Plan} from './plan'
|
||||
import Settings from './settings'
|
||||
import { AppDataSource } from "./data-source";
|
||||
import GymSet from "./gym-set";
|
||||
import { Plan } from "./plan";
|
||||
import Settings from "./settings";
|
||||
|
||||
export const setRepo = AppDataSource.manager.getRepository(GymSet)
|
||||
export const planRepo = AppDataSource.manager.getRepository(Plan)
|
||||
export const settingsRepo = AppDataSource.manager.getRepository(Settings)
|
||||
export const setRepo = AppDataSource.manager.getRepository(GymSet);
|
||||
export const planRepo = AppDataSource.manager.getRepository(Plan);
|
||||
export const settingsRepo = AppDataSource.manager.getRepository(Settings);
|
||||
|
||||
export const getNow = (): Promise<{now: string}[]> => {
|
||||
return AppDataSource.manager.query(
|
||||
"SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now",
|
||||
)
|
||||
}
|
||||
export const getNow = async (): Promise<string> => {
|
||||
const query = await AppDataSource.manager.query(
|
||||
"SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now"
|
||||
);
|
||||
return query[0].now;
|
||||
};
|
||||
|
|
11
deno.json
Normal file
11
deno.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"fmt": {
|
||||
"useTabs": false,
|
||||
"lineWidth": 80,
|
||||
"semiColons": false,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "preserve",
|
||||
"include": ["src/"],
|
||||
"exclude": ["src/testdata/", "data/fixtures/**/*.ts"]
|
||||
}
|
||||
}
|
24
deploy.sh
24
deploy.sh
|
@ -2,11 +2,6 @@
|
|||
|
||||
set -ex
|
||||
|
||||
yarn tsc
|
||||
yarn lint
|
||||
yarn jest
|
||||
git push origin HEAD
|
||||
|
||||
cd android || exit 1
|
||||
|
||||
build=app/build.gradle
|
||||
|
@ -28,16 +23,17 @@ sed -i "s/\(^\s*\)versionCode [0-9]*$/\1versionCode $versionCode/" \
|
|||
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
|
||||
|
||||
[ "$1" != "--nobundle" ] && ./gradlew bundleRelease
|
||||
|
||||
bundle install
|
||||
bundle exec fastlane supply --aab app/build/outputs/bundle/release/app-release.aab
|
||||
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 --no-verify --message "Set versionCode=$versionCode"
|
||||
git commit --amend --message \
|
||||
"$(git log -1 --pretty=%B | sed " 1 s/.*/& - $major.$minor/")"
|
||||
git tag "$versionCode"
|
||||
git push origin HEAD &
|
||||
git push origin HEAD
|
||||
git push --tags
|
||||
|
||||
cd ..
|
||||
./install.sh
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export type DrawerParamList = {
|
||||
Home: {}
|
||||
Settings: {}
|
||||
Best: {}
|
||||
Plans: {}
|
||||
Workouts: {}
|
||||
Timer: {}
|
||||
}
|
||||
Home: {};
|
||||
Settings: {};
|
||||
Graphs: {};
|
||||
Plans: {};
|
||||
Workouts: {};
|
||||
Timer: {};
|
||||
};
|
||||
|
|
11
fix-numeric.ts
Normal file
11
fix-numeric.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export const fixNumeric = (text: string) => {
|
||||
let newText = text.replace(/[^0-9.-]/g, "");
|
||||
let parts = newText.split(".");
|
||||
if (parts.length > 2) {
|
||||
newText = parts[0] + "." + parts.slice(1).join("");
|
||||
}
|
||||
if (newText.startsWith("-")) {
|
||||
newText = "-" + newText.slice(1).replace(/-/g, "");
|
||||
}
|
||||
return newText;
|
||||
};
|
60
gym-set.ts
60
gym-set.ts
|
@ -1,53 +1,53 @@
|
|||
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity('sets')
|
||||
@Entity("sets")
|
||||
export default class GymSet {
|
||||
@PrimaryGeneratedColumn()
|
||||
id?: number
|
||||
id?: number;
|
||||
|
||||
@Column('text')
|
||||
name: string
|
||||
@Column("text")
|
||||
name: string;
|
||||
|
||||
@Column('int')
|
||||
reps: number
|
||||
@Column("int")
|
||||
reps: number;
|
||||
|
||||
@Column('int')
|
||||
weight: number
|
||||
@Column("int")
|
||||
weight: number;
|
||||
|
||||
@Column('int')
|
||||
sets = 3
|
||||
@Column("int")
|
||||
sets = 3;
|
||||
|
||||
@Column('int')
|
||||
minutes = 3
|
||||
@Column("int")
|
||||
minutes = 3;
|
||||
|
||||
@Column('int')
|
||||
seconds = 30
|
||||
@Column("int")
|
||||
seconds = 30;
|
||||
|
||||
@Column('boolean')
|
||||
hidden = false
|
||||
@Column("boolean")
|
||||
hidden = false;
|
||||
|
||||
@Column('text')
|
||||
created: string
|
||||
@Column("text")
|
||||
created: string;
|
||||
|
||||
@Column('text')
|
||||
unit: string
|
||||
@Column("text")
|
||||
unit: string;
|
||||
|
||||
@Column('text')
|
||||
image: string
|
||||
@Column("text")
|
||||
image: string;
|
||||
|
||||
@Column('text')
|
||||
steps?: string
|
||||
@Column("text")
|
||||
steps?: string;
|
||||
}
|
||||
|
||||
export const defaultSet: GymSet = {
|
||||
created: '',
|
||||
name: '',
|
||||
image: '',
|
||||
created: "",
|
||||
name: "",
|
||||
image: "",
|
||||
hidden: false,
|
||||
minutes: 3,
|
||||
seconds: 30,
|
||||
reps: 0,
|
||||
sets: 0,
|
||||
unit: 'kg',
|
||||
unit: "kg",
|
||||
weight: 0,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import GymSet from './gym-set'
|
||||
import GymSet from "./gym-set";
|
||||
|
||||
export type HomePageParams = {
|
||||
Sets: {}
|
||||
Sets: {};
|
||||
EditSet: {
|
||||
set: GymSet
|
||||
}
|
||||
set: GymSet;
|
||||
};
|
||||
EditSets: {
|
||||
ids: number[]
|
||||
}
|
||||
}
|
||||
ids: number[];
|
||||
};
|
||||
};
|
||||
|
|
13
index.js
13
index.js
|
@ -1,6 +1,9 @@
|
|||
import {AppRegistry} from 'react-native'
|
||||
import 'react-native-gesture-handler'
|
||||
import App from './App'
|
||||
import {name as appName} from './app.json'
|
||||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App)
|
||||
import {AppRegistry} from 'react-native';
|
||||
import App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
|
12
input.ts
12
input.ts
|
@ -1,9 +1,9 @@
|
|||
import Settings from './settings'
|
||||
import {Item} from './Select'
|
||||
import { Item } from "./Select";
|
||||
import Settings from "./settings";
|
||||
|
||||
export default interface Input<T> {
|
||||
name: string
|
||||
key: keyof Settings
|
||||
value?: T
|
||||
items?: Item[]
|
||||
name: string;
|
||||
key: keyof Settings;
|
||||
value?: T;
|
||||
items?: Item[];
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
set -ex
|
||||
cd android
|
||||
[ "$1" != "--nobuild" ] && ./gradlew assembleRelease
|
||||
adb -d install app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
[ "$1" != "--nobuild" ] && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a
|
||||
adb -d install app/build/outputs/apk/release/app-release.apk
|
||||
|
|
11
ios/.xcode.env
Normal file
11
ios/.xcode.env
Normal file
|
@ -0,0 +1,11 @@
|
|||
# This `.xcode.env` file is versioned and is used to source the environment
|
||||
# used when running script phases inside Xcode.
|
||||
# To customize your local environment, you can create an `.xcode.env.local`
|
||||
# file that is not versioned.
|
||||
|
||||
# NODE_BINARY variable contains the PATH to the node executable.
|
||||
#
|
||||
# Customize the NODE_BINARY variable here.
|
||||
# For example, to use nvm with brew, add the following line
|
||||
# . "$(brew --prefix nvm)/nvm.sh" --no-use
|
||||
export NODE_BINARY=$(command -v node)
|
49
ios/Podfile
49
ios/Podfile
|
@ -1,8 +1,29 @@
|
|||
require_relative '../node_modules/react-native/scripts/react_native_pods'
|
||||
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||
# Resolve react_native_pods.rb with node to allow for hoisting
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
'require.resolve(
|
||||
"react-native/scripts/react_native_pods.rb",
|
||||
{paths: [process.argv[1]]},
|
||||
)', __dir__]).strip
|
||||
|
||||
platform :ios, '12.4'
|
||||
install! 'cocoapods', :deterministic_uuids => false
|
||||
platform :ios, min_ios_version_supported
|
||||
prepare_react_native_project!
|
||||
|
||||
# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
|
||||
# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded
|
||||
#
|
||||
# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js`
|
||||
# ```js
|
||||
# module.exports = {
|
||||
# dependencies: {
|
||||
# ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),
|
||||
# ```
|
||||
flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled
|
||||
|
||||
linkage = ENV['USE_FRAMEWORKS']
|
||||
if linkage != nil
|
||||
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
|
||||
use_frameworks! :linkage => linkage.to_sym
|
||||
end
|
||||
|
||||
target 'massive' do
|
||||
config = use_native_modules!
|
||||
|
@ -12,9 +33,14 @@ target 'massive' do
|
|||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# to enable hermes on iOS, change `false` to `true` and then install pods
|
||||
# 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.
|
||||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
# you should disable the next line.
|
||||
:flipper_configuration => flipper_config,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
|
@ -24,14 +50,13 @@ target 'massive' do
|
|||
# Pods for testing
|
||||
end
|
||||
|
||||
# Enables Flipper.
|
||||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
# you should disable the next line.
|
||||
use_flipper!()
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(installer)
|
||||
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false
|
||||
)
|
||||
__apply_Xcode_12_5_M1_post_install_workaround(installer)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
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 = massive/LaunchScreen.storyboard; 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>"; };
|
||||
CA043791292233DB00942DF1 /* MaterialIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MaterialIcons.ttf; path = "../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -117,7 +116,6 @@
|
|||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CA043790292233A900942DF1 /* Fonts */,
|
||||
13B07FAE1A68108700A75B9A /* massive */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
00E356EF1AD99517003FC87E /* massiveTests */,
|
||||
|
@ -150,14 +148,6 @@
|
|||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CA043790292233A900942DF1 /* Fonts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CA043791292233DB00942DF1 /* MaterialIcons.ttf */,
|
||||
);
|
||||
path = Fonts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -502,6 +492,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -527,6 +518,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -572,7 +564,7 @@
|
|||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
|
@ -606,7 +598,6 @@
|
|||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
|
@ -644,7 +635,7 @@
|
|||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
@ -670,7 +661,6 @@
|
|||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
#import <React/RCTBridgeDelegate.h>
|
||||
#import <RCTAppDelegate.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIWindow *window;
|
||||
@interface AppDelegate : RCTAppDelegate
|
||||
|
||||
@end
|
||||
|
|
|
@ -1,85 +1,17 @@
|
|||
#import "AppDelegate.h"
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTBundleURLProvider.h>
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
#import <React/RCTAppSetupUtils.h>
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
#import <React/CoreModulesPlugins.h>
|
||||
#import <React/RCTCxxBridgeDelegate.h>
|
||||
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
|
||||
#import <React/RCTSurfacePresenter.h>
|
||||
#import <React/RCTSurfacePresenterBridgeAdapter.h>
|
||||
#import <ReactCommon/RCTTurboModuleManager.h>
|
||||
|
||||
#import <react/config/ReactNativeConfig.h>
|
||||
|
||||
static NSString *const kRNConcurrentRoot = @"concurrentRoot";
|
||||
|
||||
@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
|
||||
RCTTurboModuleManager *_turboModuleManager;
|
||||
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
|
||||
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
|
||||
facebook::react::ContextContainer::Shared _contextContainer;
|
||||
}
|
||||
@end
|
||||
#endif
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
RCTAppSetupPrepareApp(application);
|
||||
self.moduleName = @"massive";
|
||||
// You can add your custom initial props in the dictionary below.
|
||||
// They will be passed down to the ViewController used by React Native.
|
||||
self.initialProps = @{};
|
||||
|
||||
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
|
||||
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
|
||||
_contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
|
||||
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
|
||||
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
|
||||
#endif
|
||||
|
||||
NSDictionary *initProps = [self prepareInitialProps];
|
||||
UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"massive", initProps);
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
rootView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
} else {
|
||||
rootView.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
|
||||
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
UIViewController *rootViewController = [UIViewController new];
|
||||
rootViewController.view = rootView;
|
||||
self.window.rootViewController = rootViewController;
|
||||
[self.window makeKeyAndVisible];
|
||||
return YES;
|
||||
}
|
||||
|
||||
/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
|
||||
///
|
||||
/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
|
||||
/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
|
||||
/// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`.
|
||||
- (BOOL)concurrentRootEnabled
|
||||
{
|
||||
// Switch this bool to turn on and off the concurrent root
|
||||
return true;
|
||||
}
|
||||
|
||||
- (NSDictionary *)prepareInitialProps
|
||||
{
|
||||
NSMutableDictionary *initProps = [NSMutableDictionary new];
|
||||
|
||||
#ifdef RCT_NEW_ARCH_ENABLED
|
||||
initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]);
|
||||
#endif
|
||||
|
||||
return initProps;
|
||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
}
|
||||
|
||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
||||
|
@ -91,43 +23,4 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
|
|||
#endif
|
||||
}
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
|
||||
#pragma mark - RCTCxxBridgeDelegate
|
||||
|
||||
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
|
||||
delegate:self
|
||||
jsInvoker:bridge.jsCallInvoker];
|
||||
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
|
||||
}
|
||||
|
||||
#pragma mark RCTTurboModuleManagerDelegate
|
||||
|
||||
- (Class)getModuleClassFromName:(const char *)name
|
||||
{
|
||||
return RCTCoreModulesClassProvider(name);
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
|
||||
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
|
||||
initParams:
|
||||
(const facebook::react::ObjCTurboModule::InitParams &)params
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
|
||||
{
|
||||
return RCTAppSetupDefaultModuleFromClass(moduleClass);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "20x20"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "20x20"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "29x29"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "29x29"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "40x40"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "40x40"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "60x60"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "60x60"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom": "ios-marketing",
|
||||
"scale": "1x",
|
||||
"size": "1024x1024"
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>MaterialIcons.ttf</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
|
@ -21,11 +17,11 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
@ -47,10 +43,6 @@
|
|||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
|
26
jestSetup.ts
26
jestSetup.ts
|
@ -1,21 +1,21 @@
|
|||
import 'react-native-gesture-handler/jestSetup'
|
||||
import {NativeModules} from 'react-native'
|
||||
import { NativeModules } from "react-native";
|
||||
import "react-native-gesture-handler/jestSetup";
|
||||
|
||||
NativeModules.RNViewShot = NativeModules.RNViewShot || {
|
||||
captureScreen: jest.fn(),
|
||||
}
|
||||
};
|
||||
NativeModules.SettingsModule = NativeModules.SettingsModule || {
|
||||
ignoringBattery: jest.fn(),
|
||||
is24: jest.fn(() => Promise.resolve(true)),
|
||||
}
|
||||
};
|
||||
|
||||
jest.mock('react-native-file-access', () => jest.fn())
|
||||
jest.mock('react-native-share', () => jest.fn())
|
||||
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
|
||||
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')
|
||||
jest.mock("react-native-file-access", () => jest.fn());
|
||||
jest.mock("react-native-share", () => jest.fn());
|
||||
jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
|
||||
jest.mock("react-native/Libraries/EventEmitter/NativeEventEmitter");
|
||||
|
||||
//jest.mock('react-native-reanimated', () => {
|
||||
// const Reanimated = require('react-native-reanimated/mock')
|
||||
// Reanimated.default.call = () => {}
|
||||
// return Reanimated
|
||||
//})
|
||||
jest.mock("react-native-reanimated", () => {
|
||||
const Reanimated = require("react-native-reanimated/mock");
|
||||
Reanimated.default.call = () => {};
|
||||
return Reanimated;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum Metrics {
|
||||
Weight = 'Best weight',
|
||||
Volume = 'Volume',
|
||||
OneRepMax = 'One rep max',
|
||||
Weight = "Best weight",
|
||||
Volume = "Volume",
|
||||
OneRepMax = "One rep max",
|
||||
}
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
/**
|
||||
* Metro configuration for React Native
|
||||
* https://github.com/facebook/react-native
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
|
||||
|
||||
module.exports = {
|
||||
transformer: {
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
/**
|
||||
* Metro configuration
|
||||
* https://facebook.github.io/metro/docs/configuration
|
||||
*
|
||||
* @type {import('metro-config').MetroConfig}
|
||||
*/
|
||||
const config = {};
|
||||
|
||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
||||
|
|
|
@ -5,10 +5,11 @@ export class plans1667186124792 implements MigrationInterface {
|
|||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS plans (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
days TEXT NOT NULL,
|
||||
workouts TEXT NOT NULL
|
||||
)
|
||||
`)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
|
|
13
migrations/1678334268359-add-backup.ts
Normal file
13
migrations/1678334268359-add-backup.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||
|
||||
export class addBackup1678334268359 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner
|
||||
.query('ALTER TABLE settings ADD COLUMN backup BOOLEAN DEFAULT false')
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropColumn('settings', 'backup')
|
||||
}
|
||||
}
|
|
@ -1,29 +1,30 @@
|
|||
import {NavigationContainer} from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import React from "react";
|
||||
import {
|
||||
DarkTheme,
|
||||
DefaultTheme,
|
||||
MD3DarkTheme,
|
||||
Provider as PaperProvider,
|
||||
} from 'react-native-paper'
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
|
||||
import {ThemeContext} from './use-theme'
|
||||
} from "react-native-paper";
|
||||
import MaterialIcon from "react-native-vector-icons/MaterialIcons";
|
||||
import { ThemeContext } from "./use-theme";
|
||||
|
||||
export const MockProviders = ({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element | JSX.Element[]
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}) => (
|
||||
<PaperProvider settings={{icon: props => <MaterialIcon {...props} />}}>
|
||||
<PaperProvider settings={{ icon: (props) => <MaterialIcon {...props} /> }}>
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
theme: 'system',
|
||||
theme: "system",
|
||||
setTheme: jest.fn(),
|
||||
lightColor: DefaultTheme.colors.primary,
|
||||
darkColor: DarkTheme.colors.primary,
|
||||
darkColor: MD3DarkTheme.colors.primary,
|
||||
setLightColor: jest.fn(),
|
||||
setDarkColor: jest.fn(),
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<NavigationContainer>{children}</NavigationContainer>
|
||||
</ThemeContext.Provider>
|
||||
</PaperProvider>
|
||||
)
|
||||
);
|
||||
|
|
18
options.ts
18
options.ts
|
@ -1,19 +1,19 @@
|
|||
import {darkColors, lightColors} from './colors'
|
||||
import { darkColors, lightColors } from "./colors";
|
||||
|
||||
export const themeOptions = [
|
||||
{label: 'Follow system theme', value: 'system'},
|
||||
{label: 'Dark theme', value: 'dark'},
|
||||
{label: 'Light theme', value: 'light'},
|
||||
]
|
||||
{ label: "System", value: "system" },
|
||||
{ label: "Dark", value: "dark" },
|
||||
{ label: "Light", value: "light" },
|
||||
];
|
||||
|
||||
export const lightOptions = lightColors.map(color => ({
|
||||
export const lightOptions = lightColors.map((color) => ({
|
||||
label: color.name,
|
||||
value: color.hex,
|
||||
color: color.hex,
|
||||
}))
|
||||
}));
|
||||
|
||||
export const darkOptions = darkColors.map(color => ({
|
||||
export const darkOptions = darkColors.map((color) => ({
|
||||
label: color.name,
|
||||
value: color.hex,
|
||||
color: color.hex,
|
||||
}))
|
||||
}));
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
organize-imports-cli *.ts* && prettier --write *.ts*
|
||||
organize-imports-cli *.ts* tests/*.ts* && deno fmt *.ts* tests/*.ts*
|
||||
|
|
111
package.json
111
package.json
|
@ -1,78 +1,73 @@
|
|||
{
|
||||
"name": "massive",
|
||||
"version": "1.107",
|
||||
"version": "1.148",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
"release": "react-native run-android --variant=release",
|
||||
"android": "react-native run-android --active-arch-only",
|
||||
"release": "react-native run-android --mode release --active-arch-only",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.19.0",
|
||||
"@babel/preset-env": "^7.19.1",
|
||||
"@react-native-masked-view/masked-view": "^0.2.7",
|
||||
"@react-navigation/drawer": "^6.5.0",
|
||||
"@react-navigation/native": "^6.0.13",
|
||||
"@react-navigation/stack": "^6.3.0",
|
||||
"@testing-library/jest-native": "^5.1.2",
|
||||
"@testing-library/react-native": "^11.3.0",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/jest": "^29.2.0",
|
||||
"@types/react-native-sqlite-storage": "^5.0.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.22.7",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.22.5",
|
||||
"@react-native-community/datetimepicker": "^7.4.0",
|
||||
"@react-native-masked-view/masked-view": "^0.2.9",
|
||||
"@react-navigation/drawer": "^6.6.3",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
"@react-navigation/stack": "^6.3.17",
|
||||
"@testing-library/jest-native": "^5.4.2",
|
||||
"@testing-library/react-native": "^12.1.2",
|
||||
"@types/d3-shape": "^3.1.1",
|
||||
"@types/react-native-sqlite-storage": "^6.0.0",
|
||||
"@types/react-native-svg-charts": "^5.0.12",
|
||||
"@types/react-native-vector-icons": "^6.4.12",
|
||||
"babel-jest": "^29.2.2",
|
||||
"@types/react-native-vector-icons": "^6.4.13",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"date-fns": "^2.29.3",
|
||||
"babel-preset-react-native": "^4.0.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"jest": "^29.2.2",
|
||||
"react": "^18.2.0",
|
||||
"react-hook-form": "^7.41.2",
|
||||
"react-native": "^0.70.5",
|
||||
"react-native-document-picker": "^8.1.2",
|
||||
"react-native-file-access": "^2.5.0",
|
||||
"react-native-gesture-handler": "^2.8.0",
|
||||
"react-native-linear-gradient": "^2.6.2",
|
||||
"react-native-pager-view": "^6.0.1",
|
||||
"react-native-paper": "^4.12.5",
|
||||
"react-native-reanimated": "^2.12.0",
|
||||
"react-native-safe-area-context": "^4.4.1",
|
||||
"react-native-screens": "^3.18.2",
|
||||
"react-native-share": "^7.9.1",
|
||||
"react-hook-form": "^7.45.1",
|
||||
"react-native": "^0.72.3",
|
||||
"react-native-document-picker": "^9.0.1",
|
||||
"react-native-file-access": "^3.0.4",
|
||||
"react-native-gesture-handler": "^2.12.0",
|
||||
"react-native-linear-gradient": "^2.7.3",
|
||||
"react-native-pager-view": "^6.2.0",
|
||||
"react-native-paper": "^5.9.1",
|
||||
"react-native-reanimated": "^3.3.0",
|
||||
"react-native-safe-area-context": "^4.7.1",
|
||||
"react-native-screens": "^3.22.1",
|
||||
"react-native-share": "^9.2.3",
|
||||
"react-native-sqlite-storage": "^6.0.1",
|
||||
"react-native-svg": "^13.4.0",
|
||||
"react-native-svg": "^13.10.0",
|
||||
"react-native-svg-charts": "^5.4.0",
|
||||
"react-native-vector-icons": "^9.2.0",
|
||||
"react-native-view-shot": "^3.4.0",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"typeorm": "^0.3.10"
|
||||
"react-native-view-shot": "^3.7.0",
|
||||
"typeorm": "^0.3.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.0",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@react-native-community/eslint-config": "^2.0.0",
|
||||
"@types/node": "^18.11.7",
|
||||
"@types/react-native": "^0.69.0",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@babel/runtime": "^7.22.6",
|
||||
"@react-native/eslint-config": "^0.72.2",
|
||||
"@react-native/metro-config": "^0.72.9",
|
||||
"@tsconfig/react-native": "^3.0.2",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"eslint": "^8.26.0",
|
||||
"metro-react-native-babel-preset": "^0.73.3",
|
||||
"react-native-testing-library": "^6.0.0",
|
||||
"typescript": "^4.8.4"
|
||||
"babel-jest": "^29.6.1",
|
||||
"eslint": "^8.45.0",
|
||||
"jest": "^29.6.1",
|
||||
"metro-react-native-babel-preset": "^0.77.0",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app",
|
||||
"rules": {
|
||||
"curly": "off"
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18"
|
||||
},
|
||||
"packageManager": "yarn@3.2.1"
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum Periods {
|
||||
Weekly = 'This week',
|
||||
Monthly = 'This month',
|
||||
Yearly = 'This year',
|
||||
Weekly = "This week",
|
||||
Monthly = "This month",
|
||||
Yearly = "This year",
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import GymSet from './gym-set'
|
||||
import {Plan} from './plan'
|
||||
import GymSet from "./gym-set";
|
||||
import { Plan } from "./plan";
|
||||
|
||||
export type PlanPageParams = {
|
||||
PlanList: {}
|
||||
PlanList: {};
|
||||
EditPlan: {
|
||||
plan: Plan
|
||||
}
|
||||
plan: Plan;
|
||||
};
|
||||
StartPlan: {
|
||||
plan: Plan
|
||||
first?: GymSet
|
||||
}
|
||||
plan: Plan;
|
||||
first?: GymSet;
|
||||
};
|
||||
EditSet: {
|
||||
set: GymSet
|
||||
}
|
||||
}
|
||||
set: GymSet;
|
||||
};
|
||||
};
|
||||
|
|
17
plan.ts
17
plan.ts
|
@ -1,13 +1,16 @@
|
|||
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity('plans')
|
||||
@Entity("plans")
|
||||
export class Plan {
|
||||
@PrimaryGeneratedColumn()
|
||||
id?: number
|
||||
id?: number;
|
||||
|
||||
@Column('text')
|
||||
days: string
|
||||
@Column("text")
|
||||
title?: string;
|
||||
|
||||
@Column('text')
|
||||
workouts: string
|
||||
@Column("text")
|
||||
days: string;
|
||||
|
||||
@Column("text")
|
||||
workouts: string;
|
||||
}
|
||||
|
|
8
route.ts
8
route.ts
|
@ -1,7 +1,7 @@
|
|||
import {DrawerParamList} from './drawer-param-list'
|
||||
import { DrawerParamList } from "./drawer-param-list";
|
||||
|
||||
export default interface Route {
|
||||
name: keyof DrawerParamList
|
||||
component: React.ComponentType<any>
|
||||
icon: string
|
||||
name: keyof DrawerParamList;
|
||||
component: React.ComponentType<any>;
|
||||
icon: string;
|
||||
}
|
||||
|
|
61
settings.ts
61
settings.ts
|
@ -1,46 +1,49 @@
|
|||
import {Column, Entity, PrimaryColumn} from 'typeorm'
|
||||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export default class Settings {
|
||||
@PrimaryColumn('boolean')
|
||||
alarm: boolean
|
||||
@PrimaryColumn("boolean")
|
||||
alarm: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
vibrate: boolean
|
||||
@Column("boolean")
|
||||
vibrate: boolean;
|
||||
|
||||
@Column('text')
|
||||
sound: string
|
||||
@Column("text")
|
||||
sound: string;
|
||||
|
||||
@Column('boolean')
|
||||
notify: boolean
|
||||
@Column("boolean")
|
||||
notify: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
images: boolean
|
||||
@Column("boolean")
|
||||
images: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
showUnit: boolean
|
||||
@Column("boolean")
|
||||
showUnit: boolean;
|
||||
|
||||
@Column('text')
|
||||
lightColor?: string
|
||||
@Column("text")
|
||||
lightColor?: string;
|
||||
|
||||
@Column('text')
|
||||
darkColor?: string
|
||||
@Column("text")
|
||||
darkColor?: string;
|
||||
|
||||
@Column('boolean')
|
||||
steps: boolean
|
||||
@Column("boolean")
|
||||
steps: boolean;
|
||||
|
||||
@Column('text')
|
||||
date: string
|
||||
@Column("text")
|
||||
date: string;
|
||||
|
||||
@Column('boolean')
|
||||
showDate: boolean
|
||||
@Column("boolean")
|
||||
showDate: boolean;
|
||||
|
||||
@Column('text')
|
||||
theme: string
|
||||
@Column("text")
|
||||
theme: string;
|
||||
|
||||
@Column('boolean')
|
||||
showSets: boolean
|
||||
@Column("boolean")
|
||||
showSets: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
noSound: boolean
|
||||
@Column("boolean")
|
||||
noSound: boolean;
|
||||
|
||||
@Column("boolean")
|
||||
backup: boolean;
|
||||
}
|
||||
|
|
24
tests/App.test.tsx
Normal file
24
tests/App.test.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { render, waitFor } from '@testing-library/react-native'
|
||||
import React from 'react'
|
||||
import 'react-native'
|
||||
import App from '../App'
|
||||
import Settings from '../settings'
|
||||
|
||||
jest.mock('../db.ts', () => ({
|
||||
settingsRepo: {
|
||||
findOne: () => Promise.resolve({} as Settings),
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../data-source.ts', () => ({
|
||||
AppDataSource: {
|
||||
isInitialized: false,
|
||||
initialize: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
test('renders correctly', async () => {
|
||||
const { getAllByText } = render(<App />)
|
||||
const title = await waitFor(() => getAllByText('Home'))
|
||||
expect(title.length).toBeGreaterThan(0)
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user