Compare commits

...

169 Commits

Author SHA1 Message Date
314b09017b
Add title to Plan 2023-08-21 14:25:29 +02:00
331597e3ee Add increment/decrement buttons to reps/weight - 1.148
Closes brandon.presley/Massive#164
2023-08-14 13:32:10 +12:00
79cde3a219 Use accurate theme color for switch text
Only if no custom color is provided
2023-08-14 10:55:24 +12:00
63e1db7349 Rename variable in SettingsPage 2023-08-14 10:50:44 +12:00
da17f8899c Paginate graphs
Also factor out LIMIT constant
2023-08-14 10:42:15 +12:00
8648cf166e Remove prettier from project deps 2023-08-13 20:58:36 +12:00
af96ec8507 Validate numbers in EditWorkout - 1.147 2023-08-12 15:41:32 +12:00
f51284e4ea Validate and fix numbers in StartPlan 2023-08-12 15:30:47 +12:00
f778426aba Run prettier
Something happened with the deno formatter,
I can't remember what! Hahahahahaahahaha
2023-08-12 15:23:02 +12:00
44283fc990 Validate reps+weight on EditSet
Numbers shouldn't contain dashes, spaces or commas.
2023-08-12 15:22:00 +12:00
c97ba1151e Optimize GraphsList
Instead of getting a list of max weights,
then looping in JS to run SQL queries for max
reps, use a subquery.
2023-08-12 15:03:31 +12:00
95681c0b3d Save plan before starting it
Also after saving it makes more sense to just
navigate to PlanList instead of calling
navigation.goBack(). This way we can make sure
we have up-to-date data.

The old way would typically lead to us seeing
stale data. E.g.
1. Tap on a plan
2. Tap on edit
3. Change details of the plan
4. Press save
5. See old plan

Now when we save we instead see the original list of plans.
2023-07-31 15:54:32 +12:00
158dd61668 Prevent sets dissapearing after updating - 1.146
Previously we would always update the created
field by making a new Date object, even if
the user didn't pick a new date. On some
devices this might slightly change the time
of the day, meaning it jumps somewhere else
on the home page.
2023-07-27 13:31:50 +12:00
e628d345ca Reduce escaping of characters 2023-07-23 14:52:22 +12:00
85915b9aa0 Retrieve last set when running a plan - 1.145
Closes brandon.presley/Massive#162

We should just keep it simple and get the most recent set instead of
trying to figure out what the best kind of maximum would be.
2023-07-23 13:59:11 +12:00
9833752bab Deno fmt 2023-07-20 14:55:19 +12:00
556347e632 Include date picker for new sets - 1.144
Not sure why I had this hidden,
probably made sense in the earlier
versions of this app.
2023-07-17 18:52:38 +12:00
9dc188e6ec Remove duplicated permission in AndroidManifest
Also add something to remove the "advertising ID"
or whatever the hell that means. (Thanks google?)
2023-07-17 18:43:15 +12:00
82da62f699 Remove unused variable from ListMenu 2023-07-17 18:39:09 +12:00
36d3de401b Fix a few instances of react/no-unstable-nested-components 2023-07-17 18:38:28 +12:00
040d588b5a Ignore mock-providers.tsx 2023-07-17 16:46:23 +12:00
47d4532169 Fix .eslintrc.js 2023-07-17 16:45:21 +12:00
3e41c3bbd8 Use outlined buttons instead of contained ones
I like them better! Ahahahahahahahahah
Bwahahahahahahahahahahahahahahahahahah
2023-07-17 16:24:09 +12:00
b776d88327 Fix snackbar color for button 2023-07-17 16:21:56 +12:00
adc2d05b2c Add back in missing titleStyle in Select.tsx 2023-07-15 15:02:29 +12:00
c3a3e33e25 Fix install.sh script for new react-native output
Now they just put it in app-release.apk i guess
2023-07-15 15:02:13 +12:00
89606b9d21 Fix type errors related to upgrade 2023-07-15 14:38:46 +12:00
6dabb7049f Upgrade all packages 2023-07-15 14:19:08 +12:00
4b42ab5f21 Upgrade react-native-paper to v5 2023-07-15 13:21:09 +12:00
a7da93583d Upgrade to react-native 0.72.3 2023-07-15 12:16:42 +12:00
1b2cbab370 Simplify SetItem
It had a pointless react fragment
wrapping it's only element.
2023-07-07 13:41:48 +12:00
09354829a8 Update yarn.lock 2023-07-07 13:21:44 +12:00
514efc6467 Upgrade react-native to 0.72.1
This unfortunately has broken the unit tests.
Will have to worry about fixing them later.
Everything seems to be working other
than that.
2023-07-07 13:17:19 +12:00
1603496424 Rework Best -> Graphs
I was considering adding weight tracking,
so then this graph page would include body
weight graphs. Meaning it's not really
recording the "best" of anything.
It currently only shows the best on the list
page anyway.
2023-07-04 11:35:52 +12:00
0beb1397a6 Hide play button from new plans - 1.143
It doesn't make sense to start a plan
that hasn't been created yet.
2023-07-04 11:20:07 +12:00
a5b6673e9a Don't auto-focus weight when editing a set - 1.142
I find myself opening up a set often to
just read the fields rather than changing
anything.
It makes sense to auto-focus the name for
a brand new set, since it's required and
typically what you first fill out.
2023-06-29 16:31:15 +12:00
6a7bd632e5 Add delete database button - 1.141
Semi-related to brandon.presley/Massive#160
If a user manages to import a database that ultimately
breaks the app elsewhere, deleting the database is a nice
tool to try and fix things.
2023-06-29 15:34:14 +12:00
4303fe2cc4 Use deno fmt instead of prettier 2023-06-27 15:16:59 +12:00
23ed95dcdb Reduce debug logging in ViewBest - 1.140 2023-06-24 13:06:35 +12:00
8f1f9f6e7d Ran bundle update 2023-06-22 10:26:01 +12:00
bdd5e23f32 Change date formats to be day/month instead of month/day - 1.139
Also added the ISO one yyyy-mm-dd, time.
Closes #157.
2023-06-18 11:36:35 +12:00
9c9a5fdd63 Trim search queries - 1.138
Closes #156
2023-06-13 14:18:49 +12:00
90db607190 Easily swap between edit/start for plans - 1.137 2023-03-28 12:20:32 +13:00
457134df6b Only show share button on best view 2023-03-28 12:04:54 +13:00
db5cc566ea Remove double permissions request and fix import - 1.136 2023-03-27 14:45:28 +13:00
76e5aeacfd Choose directory when backing up automatically - 1.135
Related to #146.
2023-03-27 14:34:17 +13:00
2fb46e1dcc Fix audio type on document picker 2023-03-27 14:01:37 +13:00
d1342c0efa Update fastlane 2023-03-24 19:32:26 +13:00
288ae1ae0c Disable timers if rest time is set to zero - 1.134 2023-03-24 19:16:35 +13:00
0e7920bde9 Show a toast when EACCESS from backing up - 1.133
I don't like having a bunch of backups saying
massive (x).db. Also this masks the issue of permissions,
which is easily fixed by deleting/moving the old massive.db.
2023-03-24 17:51:16 +13:00
d2a1c432bb Handle EACCESS in BackupModule 2023-03-24 17:43:28 +13:00
5dd569ef72 Upgrade to react-native 0.70.2 - 1.132 2023-03-21 17:09:24 +13:00
dfc4f73ca4 Upgrade react-native to 0.70.6 2023-03-21 16:59:21 +13:00
79a48b1e47 Run automatic backups after database imports - 1.131 2023-03-09 18:48:32 +13:00
13b340f5be Add setting to automatically backup - 1.130
Every day at 6am (also immediately when toggled)
we will copy the massive.db file to the Download
directory.
2023-03-09 17:16:08 +13:00
4db820f10a Remove DownloadModule
This was no longer in use
2023-03-07 18:23:38 +13:00
7b401388b5 Get last now excludes todays sets - 1.129
Otherwise the minute you enter something it
becomes the last set. Much more useful to be
showing yesterdays working set instead.
2023-03-06 18:32:32 +13:00
a1643c349d Revert "Update timer page screenshot"
This reverts commit 640a25a0f4.
2023-03-02 19:15:26 +13:00
640a25a0f4 Update timer page screenshot 2023-03-02 19:14:45 +13:00
c9b1ab1f9d Change default reps/weight for starting a plan - 1.128
Previously we used the globally best reps+weight
set by default. More commonly we build up to our
last best working set. People can still check out
their best sets on the best page.

Closes #153
2023-03-02 19:11:24 +13:00
00d4edcfc3 Request FOREGROUND_SERVICE permission - 1.127
Related to #142
2023-02-24 19:30:48 +13:00
8dd8f786ef Round graphs to 2dp - 1.126
Closes #152
2023-02-22 19:44:23 +13:00
a84cab6bbf Optimize batteries after importing database - 1.125
Closes #151
2023-02-14 16:50:14 +13:00
f4db61aeec Fix unit tests - 1.124 2023-02-14 16:41:30 +13:00
3af3e1faf2 Order plan workouts alphabetically
Closes #150
2023-02-14 16:37:09 +13:00
7bc9c00a63 Merge branch 'master' of gitea.presley.nz:brandon.presley/Massive 2023-02-13 11:05:32 +13:00
a03731c6ff Prevent empty flicker on best view - 1.122 - 1.123
Closes #148
2023-02-13 10:43:03 +13:00
f1e0911488 Prevent empty flicker on best view - 1.122
Closes #148
2023-02-04 15:15:34 -07:00
1a75d8897d Skip deploy checks for -n flag - 1.119 2023-02-04 14:16:31 -07:00
9f7cbba80a Add -n flag to deploy.sh 2023-02-04 14:14:29 -07:00
de2aa67e6e Version 1.118 2023-01-26 20:04:53 -07:00
28ec021258 Fix copying sets - 1.117
Related to #143
2023-01-17 10:22:21 -07:00
04ef72e48b Fix unit tests - 1.116 2023-01-08 18:10:24 +13:00
467df629b0 Change edit headers to add when adding 2023-01-08 18:05:59 +13:00
e7f85a9954 Add date/time picker to EditSet - 1.115 2023-01-08 18:02:17 +13:00
5e6896eaba Ignore coverage directory for linting 2023-01-08 14:01:43 +13:00
6438a9c48a Use the same colors as switch for timer page - 1.114 2023-01-08 14:00:27 +13:00
8e8961419c Add plan list unit tests 2023-01-05 17:15:16 +13:00
b0696d1d58 Add view best unit tests 2023-01-05 17:07:06 +13:00
73d9b1c617 Remove duplicate git push from deploy.sh - 1.113
We already do a git push at the end.
2023-01-05 16:43:56 +13:00
a6130b3a10 Merge branch 'master' of gitea.presley.nz:brandon.presley/Massive 2023-01-05 16:41:47 +13:00
7bee8ae732 Try amending last commit for deployment script - 1.112
By including the version name in the last commit,
if that commit references an issue users now know
what version number to expect the fix in.
2023-01-05 16:40:10 +13:00
6c8731c17a Try amending last commit for deployment script
By including the version name in the last commit,
if that commit references an issue users now know
what version number to expect the fix in.
2023-01-05 16:35:53 +13:00
6fa2bbb506 Add versionName to commit message for deployment - 1.102
Users are seeing the version name the version code so we should
try to use it as much as possible.
2023-01-05 16:33:48 +13:00
069f770c96 Set versionCode=36137 2023-01-04 13:47:22 +13:00
b41c30d886 Replace it with test for jest files
Test explains what it is we are writing, whereas it
doesn't.
2023-01-04 13:44:28 +13:00
495b89fba3 Add unit to save set test 2023-01-04 13:27:17 +13:00
42912040ff Simplify getNow 2023-01-04 13:24:49 +13:00
c7952738b5 Add selected title for plans + sets
Inspired by the stock Files app in Android.
2023-01-03 17:21:51 +13:00
cffc458338 Start alarms as a foreground service
Related to #142.
Can't be sure this fixed anything because I can't replicate the error
on my emulators running android 13. I need to install android 13
on a real device and try replicate + see if this fixes it.
2023-01-03 17:04:51 +13:00
05237fc293 Set versionCode=36136 2023-01-03 15:15:33 +13:00
5fd7e75908 Fix settings persistence issues 2023-01-03 14:59:19 +13:00
705052f1b4 Set versionCode=36135 2023-01-03 11:33:43 +13:00
efc97bdf47 Fix settings page on shorter devices 2023-01-03 11:31:37 +13:00
d0702b7675 Reduce redundancy of labels for theme setting 2023-01-03 11:31:25 +13:00
24e230e8b9 Use a scroll view for settings page
This way shorter screens dont cut off content
2023-01-02 23:29:46 +13:00
67689f4af8 Set versionCode=36134 2023-01-02 18:56:53 +13:00
a2721e9f12 Use sqlite in Android code for alarm settings
Closes #129
2023-01-02 18:54:35 +13:00
bafdecd3e3 Add unit tests for EditSets
Closes #138
2023-01-01 20:47:07 +13:00
e432c1b711 Add app test 2023-01-01 18:33:03 +13:00
08f91bf531 Test saving a plan 2023-01-01 18:13:45 +13:00
80f2dfdff5 Improve performance of Routes.tsx 2023-01-01 18:05:11 +13:00
3c9b93f0bc Test adding a new set 2023-01-01 18:01:46 +13:00
f221ebb8df Test editing a workout 2023-01-01 15:21:56 +13:00
a68d4d6a69 Include tests directory in organize.sh 2023-01-01 15:21:42 +13:00
5d9df37778 Organize imports 2023-01-01 15:20:56 +13:00
8246155c13 Test start plan + edit plan 2023-01-01 15:06:42 +13:00
58f1c905b2 Test edit set component 2023-01-01 14:47:16 +13:00
805b7bdc34 Simplify tests 2023-01-01 14:35:22 +13:00
a772e36160 Add tests to deploy script 2023-01-01 14:32:23 +13:00
ad95438120 Test all pages 2023-01-01 14:32:10 +13:00
3c953530a4 Add working test for homepage 2023-01-01 14:19:32 +13:00
13e1d4cc21 Move the theme object to be inline for mock-providers
This way we get better auto completion
2023-01-01 14:19:15 +13:00
21773e3b4f Remove fake timers from jestSetup
I believe this was from ages ago when I was using `setInterval` to try
and keep state of the timers from JavaScript. Now we are relying on our
timers in Kotlin so we don't need this any more.
2023-01-01 14:18:36 +13:00
27ff4861d9 Reduce logs in SetList 2023-01-01 14:18:27 +13:00
e43188ccdf Merge branch 'master' into unit-tests 2023-01-01 13:57:01 +13:00
9287c31e70 Update fastlane 2023-01-01 13:47:04 +13:00
5612df5d8c Set versionCode=36133 2023-01-01 13:46:26 +13:00
a78e07dac8 Keep list menu open after selecting all
Typically when you select all records it's because you want
to operate on those records immediately. The only case
this wouldn't be true is if you were scrolling, then selecting all
a bunch of times. I think it's more likely someone just wants to
select all, then delete.
2023-01-01 13:43:55 +13:00
86f01eb002 Memoize the most expensive parts of SettingsPage 2023-01-01 13:39:10 +13:00
5335f4afbc Memoize switches in SettingsPage 2023-01-01 13:32:26 +13:00
d71ad8c170 Set versionCode=36132 2023-01-01 13:18:13 +13:00
53799fdcc4 Change mode of text input from outlined to default
Related to #140
2023-01-01 13:16:08 +13:00
651b130caa Remove pdf I accidentally committed of the readme 2023-01-01 13:15:23 +13:00
73c7486eb3 Update plan start image 2022-12-30 20:46:39 +13:00
ea2ff913db Update screenshots 2022-12-30 20:45:12 +13:00
0be8f03133 Set versionCode=36131 2022-12-30 20:40:15 +13:00
7863b9caa0 Improve speed of setting sound string as well 2022-12-30 20:38:34 +13:00
3fdc5900e3 Improve performance of setttings toggles
I was awaiting the result of the sqlite operations
when really I should have just set the state immediately,
and done the operations in the background.
2022-12-30 20:37:08 +13:00
e51aad21f3 Set versionCode=36130 2022-12-30 20:31:34 +13:00
f48124123c Delete unused variable from Routes.tsx 2022-12-30 20:29:47 +13:00
3603c67133 Add filtering back in to SettingsPage
I accidentally removed it and pushed to production...
Woopsie.
2022-12-30 20:29:16 +13:00
a9000898f3 Replace children with title for Switch
Apparently, the children prop makes React.memo
not work any more. I read about it in
https://stackoverflow.com/questions/53074551/when-should-you-not-use-react-memo
2022-12-30 19:49:54 +13:00
dd7cb0406b Use React.memo for Switch 2022-12-30 19:42:30 +13:00
a5ddf5c94d Use React.memo in Select.tsx 2022-12-30 19:39:35 +13:00
9be10610d2 Use React.memo on AppInput 2022-12-30 19:39:20 +13:00
5e37490c2d Add react-hook-form for settings page
This seems to have improved performance.

Related to #135.
2022-12-30 15:47:12 +13:00
7f1513f0a5 Set versionCode=36129 2022-12-30 13:37:42 +13:00
14edb66e28 Add common date formats
Add yyyy-MM-d and yyyy.MM.d formats

Closes #139
2022-12-30 13:35:11 +13:00
3ed2d4f0cd Never pass falsy value to date format
Closes #141

After adding TypeORM I had to remove the strict checking in TypeScript.
This leads to null errors such as this. Kind of annoying, but I think
the large reduction in code from adding TypeORM is worth it. We
shall see...
2022-12-30 13:30:13 +13:00
46dd50adfb Pause adding unit tests 2022-12-30 13:25:47 +13:00
e430873771 Remove commented code from jestSetup 2022-12-30 12:35:19 +13:00
a9266ba77b Set versionCode=36128 2022-12-29 20:11:34 +13:00
051df31925 Fix inactive track color on mismatched themes
Before if your system theme was dark and your app
theme was set to light, the track colors were wrong.
2022-12-29 19:30:26 +13:00
a3138c48b5 Combine several state operations in SettingsPage 2022-12-29 18:20:40 +13:00
2b302bab73 Move theme line of SettingsPage
Because I am schizophrenic.
2022-12-29 17:28:31 +13:00
7bf802ea45 Set versionCode=36127 2022-12-29 16:43:16 +13:00
5115055280 Reword MassiveX as AppX 2022-12-29 13:57:19 +13:00
41ed9464c9 Use single settings object for state
Since all this state was being set at the same time,
on load, it makes more sense to have it as a single state.
Also they are all connected to the same table so it makes more
sense this way.

Also, I removed the use focus effect in favor of just a
use effect which runs on mount. The things being refreshed here
weren't very important to be updated frequently, and focus effect
has a cost on performance.
2022-12-29 13:52:38 +13:00
596b695c5b Remove unlabelled log from StartPlan 2022-12-28 15:00:00 +13:00
c664a9603c Add missing toasts to some settings
- Date format
- Dark color
- Light color
2022-12-28 14:59:39 +13:00
186b7e0fe9 Remove ios check from SettingsPage
These things are pointless unless I
can get the app deployed to the iOS store.
2022-12-28 14:59:19 +13:00
d6e7d6158c Make sure alarms aren't on with app being optimized
If battery optimizations are on for the app,
alarms will have several unpredictable bugs.
For example, sometimes sounds won't play,
sometimes re focuisng the app won't work.
2022-12-28 14:56:29 +13:00
2c6a773548 Change alarm sound toast
Closes #137
2022-12-28 14:15:02 +13:00
b33a829816 Set versionCode=36126 2022-12-27 00:39:22 +13:00
85ea20640d Fix migrations on android 10
Closes #136
2022-12-27 00:29:45 +13:00
Tiffany Barclay
2176acd924 Clean up timer page styling
Closes #110
2022-12-25 01:05:48 +13:00
Tiffany Barclay
7e81424f60 Merge branch 'master' of gitea.presley.nz:brandon.presley/Massive 2022-12-25 00:44:26 +13:00
Tiffany Barclay
736cee5ccd Remove react-native-picker 2022-12-25 00:41:25 +13:00
a9b86fb555 Set versionCode=36125 2022-12-24 20:10:26 +13:00
60cc619e39 Move button filter to memoized call
This reduces re-renders
2022-12-24 19:58:55 +13:00
48432188c3 Simplify Switch.tsx 2022-12-24 19:55:38 +13:00
27b7e91e91 Factor out buttons in SettingsPage 2022-12-24 13:36:11 +13:00
fc6f5e3b53 Add missing key prop to SettingsPage 2022-12-24 13:18:03 +13:00
8625ca2189 Set versionCode=36124 2022-12-24 13:13:55 +13:00
250335800f Move clear button after select all 2022-12-24 13:10:40 +13:00
d89721c718 Set versionCode=36123 2022-12-23 18:35:11 +13:00
930ebdc9ca Move clear after select all 2022-12-23 18:32:46 +13:00
777eddf943 Remove usage of FlatList in Settings page
Doing so looks like it improved the performance
of the switches.

Related to #135.
2022-12-22 19:18:44 +13:00
135 changed files with 8104 additions and 6563 deletions

View File

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

@ -73,3 +73,4 @@ massive-build
!.yarn/releases
!.yarn/sdks
!.yarn/versions
coverage

View File

@ -1,8 +0,0 @@
module.exports = {
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: false,
singleQuote: true,
trailingComma: 'all',
semi: false,
};

127
App.tsx
View File

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

31
AppFab.tsx Normal file
View File

@ -0,0 +1,31 @@
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 fabColor = useMemo(
() =>
lightColors.map((color) => color.hex).includes(colors.primary)
? CombinedDarkTheme.colors.background
: CombinedDefaultTheme.colors.background,
[colors.primary]
);
return (
<FAB
icon="add"
testID="add"
color={fabColor}
style={{
position: "absolute",
right: 20,
bottom: 20,
backgroundColor: colors.primary,
}}
{...props}
/>
);
}

26
AppInput.tsx Normal file
View File

@ -0,0 +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";
function AppInput(
props: Partial<ComponentProps<typeof TextInput>> & {
innerRef?: Ref<any>;
}
) {
const dark = useDark();
return (
<TextInput
selectionColor={dark ? "#2A2A2A" : CombinedDefaultTheme.colors.border}
style={{ marginBottom: MARGIN, minWidth: 100 }}
selectTextOnFocus
ref={props.innerRef}
blurOnSubmit={false}
{...props}
/>
);
}
export default React.memo(AppInput);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,102 +3,133 @@ 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}
onValueChange={value => toggleDay(value, day)}
onPress={() => toggleDay(!days.includes(day), day)}
value={days.includes(day)}>
{day}
</Switch>
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}
onValueChange={value => toggleWorkout(value, name)}
onChange={(value) => toggleWorkout(value, name)}
value={workouts.includes(name)}
onPress={() => toggleWorkout(!workouts.includes(name), name)}>
{name}
</Switch>
title={name}
/>
))
)}
</ScrollView>
@ -106,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({
@ -122,4 +157,4 @@ const styles = StyleSheet.create({
marginBottom: MARGIN,
},
button: {},
})
});

View File

@ -1,124 +1,152 @@
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 MassiveInput from './MassiveInput'
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}}>
<MassiveInput
<View style={{ padding: PADDING, flex: 1 }}>
<AppInput
label="Name"
value={name}
onChangeText={setName}
@ -127,29 +155,68 @@ export default function EditSet() {
onSubmitEditing={() => repsRef.current?.focus()}
/>
<MassiveInput
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>
<MassiveInput
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 && (
<MassiveInput
<AppInput
autoCapitalize="none"
label="Unit"
value={unit}
@ -158,28 +225,30 @@ export default function EditSet() {
/>
)}
{typeof set.id === 'number' && settings.showDate && (
<MassiveInput
{settings.showDate && (
<AppInput
label="Created"
disabled
value={format(new Date(set.created), settings.date)}
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>
</>
)
);
}

View File

@ -3,85 +3,85 @@ 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 MassiveInput from './MassiveInput'
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}}>
<MassiveInput
<View style={{ padding: PADDING, flex: 1 }}>
<AppInput
label={`Names: ${names}`}
value={name}
onChangeText={setName}
@ -89,26 +89,60 @@ export default function EditSets() {
autoFocus={!name}
/>
<MassiveInput
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>
<MassiveInput
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 && (
<MassiveInput
<AppInput
autoCapitalize="none"
label={`Units: ${units}`}
value={unit}
@ -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>
</>
)
);
}

View File

@ -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 MassiveInput from './MassiveInput'
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,40 +79,40 @@ 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}}>
<MassiveInput
<StackHeader title={params.value.name ? "Edit workout" : "Add workout"} />
<View style={{ padding: PADDING, flex: 1 }}>
<ScrollView style={{ flex: 1 }}>
<AppInput
autoFocus
label="Name"
value={name}
@ -118,7 +120,7 @@ export default function EditWorkout() {
onSubmitEditing={submitName}
/>
{settings?.steps && (
<MassiveInput
<AppInput
innerRef={stepsRef}
selectTextOnFocus={false}
value={steps}
@ -128,25 +130,35 @@ export default function EditWorkout() {
onSubmitEditing={() => setsRef.current?.focus()}
/>
)}
<MassiveInput
<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()}
/>
{settings?.alarm && (
<>
<MassiveInput
<AppInput
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"
/>
<MassiveInput
<AppInput
innerRef={secondsRef}
value={seconds}
onChangeText={setSeconds}
@ -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>
</>
)
);
}

View File

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

@ -0,0 +1,23 @@
import { createStackNavigator } from "@react-navigation/stack";
import GraphsList from "./GraphsList";
import GymSet from "./gym-set";
import ViewGraph from "./ViewGraph";
const Stack = createStackNavigator<GraphsPageParams>();
export type GraphsPageParams = {
GraphsList: {};
ViewGraph: {
best: GymSet;
};
};
export default function GraphsPage() {
return (
<Stack.Navigator
screenOptions={{ headerShown: false, animationEnabled: false }}
>
<Stack.Screen name="GraphsList" component={GraphsList} />
<Stack.Screen name="ViewGraph" component={ViewGraph} />
</Stack.Navigator>
);
}

View File

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

View File

@ -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,85 +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 = () => {
setShowMenu(false)
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="edit"
leadingIcon="clear"
title="Clear"
onPress={clear}
disabled={ids?.length === 0}
/>
<Menu.Item
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}
/>
<Menu.Item
icon="clear"
title="Clear"
onPress={clear}
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?</>
) : (
@ -97,5 +90,5 @@ export default function ListMenu({
)}
</ConfirmDialog>
</Menu>
)
);
}

View File

@ -1,30 +0,0 @@
import {ComponentProps, useMemo} from 'react'
import {FAB, useTheme} from 'react-native-paper'
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
import {lightColors} from './colors'
export default function MassiveFab(props: Partial<ComponentProps<typeof FAB>>) {
const {colors} = useTheme()
const fabColor = useMemo(
() =>
lightColors.map(color => color.hex).includes(colors.primary)
? CombinedDarkTheme.colors.background
: CombinedDefaultTheme.colors.background,
[colors.primary],
)
return (
<FAB
icon="add"
color={fabColor}
style={{
position: 'absolute',
right: 20,
bottom: 20,
backgroundColor: colors.primary,
}}
{...props}
/>
)
}

View File

@ -1,25 +0,0 @@
import {ComponentProps, Ref} from 'react'
import {TextInput} from 'react-native-paper'
import {CombinedDefaultTheme} from './App'
import {MARGIN} from './constants'
import useDark from './use-dark'
export default function MassiveInput(
props: Partial<ComponentProps<typeof TextInput>> & {
innerRef?: Ref<any>
},
) {
const dark = useDark()
return (
<TextInput
selectionColor={dark ? '#2A2A2A' : CombinedDefaultTheme.colors.border}
mode="outlined"
style={{marginBottom: MARGIN, minWidth: 100}}
selectTextOnFocus
ref={props.innerRef}
blurOnSubmit={false}
{...props}
/>
)
}

View File

@ -1,7 +1,7 @@
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {Searchbar} from 'react-native-paper'
import {PADDING} from './constants'
import MassiveFab from './MassiveFab'
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]}>
@ -26,9 +26,9 @@ export default function Page({
clearIcon="clear"
/>
{children}
{onAdd && <MassiveFab onPress={onAdd} />}
{onAdd && <AppFab onPress={onAdd} />}
</View>
)
);
}
const styles = StyleSheet.create({
@ -36,4 +36,4 @@ const styles = StyleSheet.create({
padding: PADDING,
flexGrow: 1,
},
})
});

View File

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

View File

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

View File

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

Binary file not shown.

View File

@ -1,57 +1,57 @@
import {createDrawerNavigator} from '@react-navigation/drawer'
import {useMemo} from 'react'
import {Platform} from 'react-native'
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
.filter(route => {
if (Platform.OS === 'ios' && route.name === 'Timer') return false
return true
})
.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>
)
);
}

View File

@ -1,49 +1,50 @@
import {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;
}
export default function Select({
function Select({
value,
onChange,
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,20 +52,24 @@ export default 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);

View File

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

View File

@ -2,57 +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,
length: newSets.length,
})
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}
@ -62,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}
@ -151,7 +146,7 @@ export default function SetList() {
settings && (
<FlatList
data={sets}
style={{flex: 1}}
style={{ flex: 1 }}
renderItem={renderItem}
onEndReached={next}
/>
@ -159,5 +154,5 @@ export default function SetList() {
)}
</Page>
</>
)
);
}

33
SettingButton.tsx Normal file
View File

@ -0,0 +1,33 @@
import { View } from "react-native";
import { Button, Subheading } from "react-native-paper";
import { ITEM_PADDING } from "./constants";
export default function SettingButton({
name: text,
label,
onPress,
}: {
name: string;
label?: string;
onPress: () => void;
}) {
if (label) {
return (
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingLeft: ITEM_PADDING,
}}
>
<Subheading style={{ width: 100 }}>{label}</Subheading>
<Button onPress={onPress}>{text}</Button>
</View>
);
}
return (
<Button style={{ alignSelf: "flex-start" }} onPress={onPress}>
{text}
</Button>
);
}

View File

@ -1,316 +1,352 @@
import {
NavigationProp,
useFocusEffect,
useNavigation,
} from '@react-navigation/native'
import {format} from 'date-fns'
import {useCallback, useMemo, useState} from 'react'
import {
DeviceEventEmitter,
FlatList,
NativeModules,
Platform,
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 Switch from './Switch'
import {toast} from './toast'
import {useTheme} from './use-theme'
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { format } from "date-fns";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { NativeModules, ScrollView } from "react-native";
import DocumentPicker from "react-native-document-picker";
import { Dirs, FileSystem } from "react-native-file-access";
import ConfirmDialog from "./ConfirmDialog";
import { MARGIN } from "./constants";
import { AppDataSource } from "./data-source";
import { setRepo, settingsRepo } from "./db";
import { DrawerParamList } from "./drawer-param-list";
import DrawerHeader from "./DrawerHeader";
import Input from "./input";
import { darkOptions, lightOptions, themeOptions } from "./options";
import Page from "./Page";
import Select from "./Select";
import SettingButton from "./SettingButton";
import Settings from "./settings";
import Switch from "./Switch";
import { toast } from "./toast";
import { useTheme } from "./use-theme";
const defaultFormats = ['P', 'Pp', 'ccc p', 'p']
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 [vibrate, setVibrate] = useState(false)
const [alarm, setAlarm] = useState(false)
const [sound, setSound] = useState('')
const [notify, setNotify] = useState(false)
const [images, setImages] = useState(false)
const [showUnit, setShowUnit] = useState(false)
const [steps, setSteps] = useState(false)
const [date, setDate] = useState('P')
const {theme, setTheme, lightColor, setLightColor, darkColor, setDarkColor} =
useTheme()
const [showDate, setShowDate] = useState(false)
const [noSound, setNoSound] = useState(false)
const [formatOptions, setFormatOptions] = useState<string[]>(defaultFormats)
const [importing, setImporting] = useState(false)
const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
const today = new Date()
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>>();
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({where: {}}).then(settings => {
console.log(`${SettingsPage.name}.focus:`, settings)
setAlarm(settings.alarm)
setVibrate(settings.vibrate)
setSound(settings.sound)
setNotify(settings.notify)
setImages(settings.images)
setShowUnit(settings.showUnit)
setSteps(settings.steps)
setDate(settings.date)
setShowDate(settings.showDate)
setNoSound(settings.noSound)
})
if (Platform.OS !== 'android') return
NativeModules.SettingsModule.ignoringBattery(setIgnoring)
NativeModules.SettingsModule.is24().then((is24: boolean) => {
console.log(`${SettingsPage.name}.focus:`, {is24})
if (is24) setFormatOptions(['P', 'P, k:m', 'ccc k:m', 'k:m'])
else setFormatOptions(defaultFormats)
})
}, []),
)
const { watch, setValue } = useForm<Settings>({
defaultValues: () => settingsRepo.findOne({ where: {} }),
});
const settings = watch();
const changeAlarmEnabled = useCallback(
(enabled: boolean) => {
if (enabled)
DeviceEventEmitter.emit('toast', {
value: 'Timers will now run after each set',
timeout: 4000,
})
else toast('Stopped timers running after each set.')
if (enabled && !ignoring) NativeModules.SettingsModule.ignoreBattery()
setAlarm(enabled)
settingsRepo.update({}, {alarm: enabled})
},
[ignoring],
)
const {
theme,
setTheme,
lightColor,
setLightColor,
darkColor,
setDarkColor,
} = useTheme();
const changeVibrate = useCallback((enabled: boolean) => {
if (enabled) toast('When a timer completes, vibrate your phone.')
else toast('Stop vibrating at the end of timers.')
setVibrate(enabled)
settingsRepo.update({}, {vibrate: enabled})
}, [])
useEffect(() => {
NativeModules.SettingsModule.ignoringBattery(setIgnoring);
NativeModules.SettingsModule.is24().then((is24: boolean) => {
console.log(`${SettingsPage.name}.focus:`, { is24 });
if (is24) setFormatOptions(twentyFours);
else setFormatOptions(twelveHours);
});
}, []);
const changeSound = useCallback(async () => {
const {fileCopyUri} = await DocumentPicker.pickSingle({
type: 'audio/*',
copyTo: 'documentDirectory',
})
if (!fileCopyUri) return
settingsRepo.update({}, {sound: fileCopyUri})
setSound(fileCopyUri)
toast('This song will now play after rest timers complete.')
}, [])
const changeNotify = useCallback((enabled: boolean) => {
setNotify(enabled)
settingsRepo.update({}, {notify: enabled})
if (enabled) toast('Show when a set is a new record.')
else toast('Stopped showing notifications for new records.')
}, [])
const changeImages = useCallback((enabled: boolean) => {
setImages(enabled)
settingsRepo.update({}, {images: enabled})
if (enabled) toast('Show images for sets.')
else toast('Stopped showing images for sets.')
}, [])
const changeUnit = useCallback((enabled: boolean) => {
setShowUnit(enabled)
settingsRepo.update({}, {showUnit: enabled})
if (enabled) toast('Show option to select unit for sets.')
else toast('Hid unit option for sets.')
}, [])
const changeSteps = useCallback((enabled: boolean) => {
setSteps(enabled)
settingsRepo.update({}, {steps: enabled})
if (enabled) toast('Show steps for a workout.')
else toast('Stopped showing steps for workouts.')
}, [])
const changeShowDate = useCallback((enabled: boolean) => {
setShowDate(enabled)
settingsRepo.update({}, {showDate: enabled})
if (enabled) toast('Show date for sets by default.')
else toast('Stopped showing date for sets by default.')
}, [])
const changeNoSound = useCallback((enabled: boolean) => {
setNoSound(enabled)
settingsRepo.update({}, {noSound: enabled})
if (enabled) toast('Disable sound on rest timer alarms.')
else toast('Enabled sound for rest timer alarms.')
}, [])
const switches: Input<boolean>[] = [
{name: 'Rest timers', value: alarm, onChange: changeAlarmEnabled},
{name: 'Vibrate', value: vibrate, onChange: changeVibrate},
{name: 'Disable sound', value: noSound, onChange: changeNoSound},
{name: 'Notifications', value: notify, onChange: changeNotify},
{name: 'Show images', value: images, onChange: changeImages},
{name: 'Show unit', value: showUnit, onChange: changeUnit},
{name: 'Show steps', value: steps, onChange: changeSteps},
{name: 'Show date', value: showDate, onChange: changeShowDate},
].filter(({name}) => name.toLowerCase().includes(term.toLowerCase()))
const changeTheme = useCallback(
(value: string) => {
settingsRepo.update({}, {theme: value})
setTheme(value)
},
[setTheme],
)
const changeDate = useCallback((value: string) => {
settingsRepo.update({}, {date: value})
setDate(value)
}, [])
const soundString = useMemo(() => {
if (!sound) return null
const split = sound.split('/')
return split.pop()
}, [sound])
const changeDarkColor = useCallback(
(value: string) => {
setDarkColor(value)
settingsRepo.update({}, {darkColor: value})
},
[setDarkColor],
)
const changeLightColor = useCallback(
(value: string) => {
setLightColor(value)
settingsRepo.update({}, {lightColor: value})
},
[setLightColor],
)
const renderSwitch = useCallback(
({item}: {item: Input<boolean>}) => (
<Switch
onPress={() => item.onChange(!item.value)}
key={item.name}
value={item.value}
onValueChange={item.onChange}>
{item.name}
</Switch>
),
[],
)
const selects: Input<string>[] = [
{name: 'Theme', value: theme, onChange: changeTheme, items: themeOptions},
{
name: 'Dark color',
value: darkColor,
onChange: changeDarkColor,
items: lightOptions,
},
{
name: 'Light color',
value: lightColor,
onChange: changeLightColor,
items: darkOptions,
},
{
name: 'Date format',
value: date,
onChange: changeDate,
items: formatOptions.map(option => ({
label: format(today, option),
value: option,
})),
},
].filter(({name}) => name.toLowerCase().includes(term.toLowerCase()))
const renderSelect = useCallback(
({item}: {item: Input<string>}) => (
<Select
value={item.value}
onChange={item.onChange}
label={item.name}
items={item.items}
/>
),
[],
)
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
const update = useCallback((key: keyof Settings, value: unknown) => {
return settingsRepo
.createQueryBuilder()
.update()
.set({sound: null})
.execute()
reset({index: 0, routes: [{name: 'Settings'}]})
}, [reset])
.set({ [key]: value })
.printSql()
.execute();
}, []);
const soundString = useMemo(() => {
if (!settings.sound) return null;
const split = settings.sound.split("/");
return split.pop();
}, [settings.sound]);
const changeSound = useCallback(async () => {
const { fileCopyUri } = await DocumentPicker.pickSingle({
type: DocumentPicker.types.audio,
copyTo: "documentDirectory",
});
if (!fileCopyUri) return;
setValue("sound", fileCopyUri);
await update("sound", fileCopyUri);
toast("Sound will play after rest timers.");
}, [setValue, update]);
const switches: Input<boolean>[] = useMemo(
() => [
{ name: "Rest timers", value: settings.alarm, key: "alarm" },
{ name: "Vibrate", value: settings.vibrate, key: "vibrate" },
{ name: "Disable sound", value: settings.noSound, key: "noSound" },
{ name: "Notifications", value: settings.notify, key: "notify" },
{ name: "Show images", value: settings.images, key: "images" },
{ name: "Show unit", value: settings.showUnit, key: "showUnit" },
{ name: "Show steps", value: settings.steps, key: "steps" },
{ name: "Show date", value: settings.showDate, key: "showDate" },
{ name: "Automatic backup", value: settings.backup, key: "backup" },
],
[settings]
);
const filter = useCallback(
({ name }) => name.toLowerCase().includes(term.toLowerCase()),
[term]
);
const changeBoolean = useCallback(
async (key: keyof Settings, value: boolean) => {
setValue(key, value);
await update(key, value);
switch (key) {
case "alarm":
if (value) toast("Timers will now run after each set.");
else toast("Stopped timers running after each set.");
if (value && !ignoring) NativeModules.SettingsModule.ignoreBattery();
return;
case "vibrate":
if (value) toast("Alarms will now vibrate.");
else toast("Alarms will no longer vibrate.");
return;
case "notify":
if (value) toast("Show notifications for new records.");
else toast("Stopped notifications for new records.");
return;
case "images":
if (value) toast("Show images for sets.");
else toast("Hid images for sets.");
return;
case "showUnit":
if (value) toast("Show option to select unit for sets.");
else toast("Hid unit option for sets.");
return;
case "steps":
if (value) toast("Show steps for a workout.");
else toast("Hid steps for workouts.");
return;
case "showDate":
if (value) toast("Show date for sets.");
else toast("Hid date on sets.");
return;
case "noSound":
if (value) toast("Disable sound on rest timer alarms.");
else toast("Enabled sound for rest timer alarms.");
return;
case "backup":
if (value) {
const result = await DocumentPicker.pickDirectory();
toast("Backup database daily.");
NativeModules.BackupModule.start(result.uri);
} else {
toast("Stopped backing up daily");
NativeModules.BackupModule.stop();
}
return;
}
},
[ignoring, setValue, update]
);
const renderSwitch = useCallback(
(item: Input<boolean>) => (
<Switch
key={item.name}
value={item.value}
onChange={(value) => changeBoolean(item.key, value)}
title={item.name}
/>
),
[changeBoolean]
);
const switchesMarkup = useMemo(
() => switches.filter(filter).map((s) => renderSwitch(s)),
[filter, switches, renderSwitch]
);
const changeString = useCallback(
async (key: keyof Settings, value: string) => {
setValue(key, value);
await update(key, value);
switch (key) {
case "date":
return toast("Changed date format");
case "darkColor":
setDarkColor(value);
return toast("Set primary color for dark mode.");
case "lightColor":
setLightColor(value);
return toast("Set primary color for light mode.");
case "vibrate":
return toast("Set primary color for light mode.");
case "sound":
return toast("Sound will play after rest timers.");
case "theme":
setTheme(value as string);
if (value === "dark") toast("Theme will always be dark.");
else if (value === "light") toast("Theme will always be light.");
else if (value === "system") toast("Theme will follow system.");
return;
}
},
[update, setTheme, setDarkColor, setLightColor, setValue]
);
const selects: Input<string>[] = useMemo(() => {
const today = new Date();
return [
{ name: "Theme", value: theme, items: themeOptions, key: "theme" },
{
name: "Dark color",
value: darkColor,
items: lightOptions,
key: "darkColor",
},
{
name: "Light color",
value: lightColor,
items: darkOptions,
key: "lightColor",
},
{
name: "Date format",
value: settings.date,
items: formatOptions.map((option) => ({
label: format(today, option),
value: option,
})),
key: "date",
},
];
}, [settings, darkColor, formatOptions, theme, lightColor]);
const renderSelect = useCallback(
(input: Input<string>) => (
<Select
key={input.name}
value={input.value}
onChange={(value) => changeString(input.key, value)}
label={input.name}
items={input.items}
/>
),
[changeString]
);
const selectsMarkup = useMemo(
() => selects.filter(filter).map(renderSelect),
[filter, selects, renderSelect]
);
const confirmDelete = useCallback(async () => {
setDeleting(false);
await AppDataSource.dropDatabase();
await AppDataSource.destroy();
await AppDataSource.initialize();
toast("Database deleted.");
}, []);
const confirmImport = useCallback(async () => {
setImporting(false);
await AppDataSource.destroy();
const file = await DocumentPicker.pickSingle();
await FileSystem.cp(file.uri, Dirs.DatabaseDir + "/massive.db");
await AppDataSource.initialize();
await setRepo.createQueryBuilder().update().set({ image: null }).execute();
await update("sound", null);
const { alarm, backup } = await settingsRepo.findOne({ where: {} });
console.log({ backup });
const directory = await DocumentPicker.pickDirectory();
if (backup) NativeModules.BackupModule.start(directory.uri);
else NativeModules.BackupModule.stop();
NativeModules.SettingsModule.ignoringBattery((isIgnoring: boolean) => {
if (alarm && !isIgnoring) NativeModules.SettingsModule.ignoreBattery();
reset({ index: 0, routes: [{ name: "Settings" }] });
});
}, [reset, update]);
const exportDatabase = useCallback(async () => {
const path = Dirs.DatabaseDir + '/massive.db'
await FileSystem.cpExternal(path, 'massive.db', 'downloads')
toast('Database exported. Check downloads.')
}, [])
const path = Dirs.DatabaseDir + "/massive.db";
await FileSystem.cpExternal(path, "massive.db", "downloads");
toast("Database exported. Check downloads.");
}, []);
const buttons = useMemo(
() => [
{
name: soundString || "Default",
onPress: changeSound,
label: "Alarm sound",
},
{ name: "Export database", onPress: exportDatabase },
{ name: "Import database", onPress: () => setImporting(true) },
{ name: "Delete database", onPress: () => setDeleting(true) },
],
[changeSound, exportDatabase, soundString]
);
const buttonsMarkup = useMemo(
() =>
buttons
.filter(filter)
.map((button) => <SettingButton {...button} key={button.name} />),
[buttons, filter]
);
return (
<>
<DrawerHeader name="Settings" />
<Page term={term} search={setTerm} style={{flexGrow: 0}}>
<FlatList
style={{marginTop: MARGIN}}
data={switches}
renderItem={renderSwitch}
/>
<FlatList data={selects} renderItem={renderSelect} />
{'alarm sound'.includes(term.toLowerCase()) && (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: ITEM_PADDING,
}}>
<Subheading style={{width: 100}}>Alarm sound</Subheading>
<Button onPress={changeSound}>{soundString || 'Default'}</Button>
</View>
)}
{'export database'.includes(term.toLowerCase()) && (
<Button style={{alignSelf: 'flex-start'}} onPress={exportDatabase}>
Export database
</Button>
)}
{'import database'.includes(term.toLowerCase()) && (
<Button
style={{alignSelf: 'flex-start'}}
onPress={() => setImporting(true)}>
Import database
</Button>
)}
<Page term={term} search={setTerm} style={{ flexGrow: 1 }}>
<ScrollView style={{ marginTop: MARGIN, flex: 1 }}>
{switchesMarkup}
{selectsMarkup}
{buttonsMarkup}
</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>
</>
)
);
}

View File

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

View File

@ -1,44 +1,50 @@
import {RouteProp, useFocusEffect, useRoute} from '@react-navigation/native'
import {useCallback, useMemo, useRef, useState} from 'react'
import {NativeModules, TextInput, View} from 'react-native'
import {FlatList} from 'react-native-gesture-handler'
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 MassiveInput from './MassiveInput'
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(',')
console.log({questions, workouts})
.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
@ -49,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,
@ -91,48 +101,96 @@ 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}}>
<MassiveInput
label="Reps"
keyboardType="numeric"
value={reps}
onChangeText={setReps}
onSubmitEditing={() => weightRef.current?.focus()}
selection={selection}
onSelectionChange={e => setSelection(e.nativeEvent.selection)}
innerRef={repsRef}
/>
<MassiveInput
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 && (
<MassiveInput
<AppInput
autoCapitalize="none"
label="Unit"
value={unit}
@ -143,7 +201,7 @@ export default function StartPlan() {
{counts && (
<FlatList
data={counts}
renderItem={props => (
renderItem={(props) => (
<View>
<StartPlanItem
{...props}
@ -159,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>
</>
)
);
}

View File

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

View File

@ -1,36 +1,42 @@
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";
export default function Switch({
function Switch({
value,
onValueChange,
onPress,
children,
onChange,
title,
}: {
value?: boolean
onValueChange: (value: boolean) => void
onPress: () => void
children: string
value?: boolean;
onChange: (value: boolean) => void;
title: string;
}) {
const {colors} = useTheme()
const { colors } = useTheme();
return (
<Pressable
onPress={onPress}
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={onValueChange}
onValueChange={onChange}
trackColor={{
true: colors.primary + "80",
false: colors.surfaceDisabled,
}}
/>
<Text>{children}</Text>
<Text>{title}</Text>
</Pressable>
)
);
}
export default React.memo(Switch);

View File

@ -1,79 +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 {MARGIN, PADDING} from './constants'
import {settingsRepo} from './db'
import DrawerHeader from './DrawerHeader'
import MassiveFab from './MassiveFab'
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 - 85
}, [])
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',
}}>
<View>
<Text style={{fontSize: 70, top: 150}}>
{minutes}:{seconds}
</Text>
</View>
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 70, position: "absolute" }}>
{minutes}:{seconds}
</Text>
<ProgressCircle
style={{height: 300, width: 500, marginBottom: MARGIN, top: -50}}
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: '85%', left: left + 25}}>
<Button onPress={add} style={{ position: "absolute", top: "82%", left }}>
Add 1 min
</Button>
<MassiveFab icon="stop" onPress={stop} />
<AppFab icon="stop" onPress={stop} />
</>
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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.657.0)
aws-sdk-core (3.166.0)
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.59.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.1)
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.93.1)
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.210.1)
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.31.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-core (0.9.1)
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)
@ -148,20 +148,20 @@ GEM
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
jwt (2.5.0)
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.11.0)
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)
public_suffix (5.0.0)
plist (3.7.0)
public_suffix (5.0.1)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.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)

View File

@ -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 36122
versionName "1.96"
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"

View File

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

View File

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

View File

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

View File

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

View 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"
}
}

View 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) {
}
}

View File

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

View File

@ -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)
return DefaultReactActivityDelegate(
this,
mainComponentName!!, // If you opted-in for the New Architecture, we enable the Fabric Renderer.
fabricEnabled
)
}
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
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
export default interface CountMany {
name: string
total: number
sets?: number
name: string;
total: number;
sets?: number;
}

View File

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

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

@ -0,0 +1,11 @@
{
"fmt": {
"useTabs": false,
"lineWidth": 80,
"semiColons": false,
"singleQuote": true,
"proseWrap": "preserve",
"include": ["src/"],
"exclude": ["src/testdata/", "data/fixtures/**/*.ts"]
}
}

View File

@ -2,10 +2,6 @@
set -ex
yarn tsc
yarn lint
git push origin HEAD
cd android || exit 1
build=app/build.gradle
@ -27,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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import {Item} from './Select'
import { Item } from "./Select";
import Settings from "./settings";
export default interface Input<T> {
name: string
value?: T
onChange: (value: T) => void
items?: Item[]
name: string;
key: keyof Settings;
value?: T;
items?: Item[];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"info": {
"version": 1,
"author": "xcode"
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

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

View File

@ -1,29 +1,21 @@
import 'react-native-gesture-handler/jestSetup'
import {NativeModules as RNNativeModules} from 'react-native'
import { NativeModules } from "react-native";
import "react-native-gesture-handler/jestSetup";
//RNNativeModules.UIManager = RNNativeModules.UIManager || {};
//RNNativeModules.UIManager.RCTView = RNNativeModules.UIManager.RCTView || {};
//RNNativeModules.RNGestureHandlerModule =
// RNNativeModules.RNGestureHandlerModule || {
// State: {BEGAN: 'BEGAN', FAILED: 'FAILED', ACTIVE: 'ACTIVE', END: 'END'},
// attachGestureHandler: jest.fn(),
// createGestureHandler: jest.fn(),
// dropGestureHandler: jest.fn(),
// updateGestureHandler: jest.fn(),
// };
//RNNativeModules.PlatformConstants = RNNativeModules.PlatformConstants || {
// forceTouchAvailable: false,
//};
RNNativeModules.RNViewShot = RNNativeModules.RNViewShot || {
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.useFakeTimers()
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock')
Reanimated.default.call = () => {}
return Reanimated
})
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;
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

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

View File

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

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