Compare commits
6 Commits
use_alarm_
...
master
Author | SHA1 | Date | |
---|---|---|---|
26f1e95db0 | |||
d0e76f574b | |||
52f642c2af | |||
b7b974fb02 | |||
6d22bee440 | |||
745f9fb046 |
|
@ -23,13 +23,14 @@ Massive tracks your reps and sets at the gym. No internet connectivity or high s
|
||||||
|
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/home.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/home.png" width="318"/>
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/edit.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/edit.png" width="318"/>
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/timer.png" width="318"/>
|
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/plans.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/plans.png" width="318"/>
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/plan-edit.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/plan-edit.png" width="318"/>
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/plan-start.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/plan-start.png" width="318"/>
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/best-view.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/best-view.png" width="318"/>
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/settings.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/settings.png" width="318"/>
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/drawer.png" width="318"/>
|
<img src="metadata/en-US/images/phoneScreenshots/drawer.png" width="318"/>
|
||||||
|
<img src="metadata/en-US/images/phoneScreenshots/exercises.png" width="318"/>
|
||||||
|
<img src="metadata/en-US/images/phoneScreenshots/exercise-edit.png" width="318"/>
|
||||||
|
|
||||||
# Building from Source
|
# Building from Source
|
||||||
|
|
||||||
|
|
|
@ -511,7 +511,7 @@ export default function SettingsPage() {
|
||||||
style={{ alignSelf: "flex-start" }}
|
style={{ alignSelf: "flex-start" }}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
const result = await DocumentPicker.pickDirectory();
|
const result = await DocumentPicker.pickDirectory();
|
||||||
await NativeModules.BackupModule.exportToCSV(result.uri);
|
await NativeModules.BackupModule.exportSets(result.uri);
|
||||||
toast("Exported sets as CSV.");
|
toast("Exported sets as CSV.");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -519,6 +519,21 @@ export default function SettingsPage() {
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Export plans as CSV",
|
||||||
|
renderItem: (name: string) => (
|
||||||
|
<Button
|
||||||
|
style={{ alignSelf: "flex-start" }}
|
||||||
|
onPress={async () => {
|
||||||
|
const result = await DocumentPicker.pickDirectory();
|
||||||
|
await NativeModules.BackupModule.exportPlans(result.uri);
|
||||||
|
toast("Exported plans as CSV.");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Import database",
|
name: "Import database",
|
||||||
renderItem: (name: string) => (
|
renderItem: (name: string) => (
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
|
||||||
import { RouteProp, useFocusEffect, useRoute } from "@react-navigation/native";
|
import { RouteProp, useFocusEffect, useRoute } from "@react-navigation/native";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
@ -6,8 +7,9 @@ import { FileSystem } from "react-native-file-access";
|
||||||
import { IconButton, List } from "react-native-paper";
|
import { IconButton, List } from "react-native-paper";
|
||||||
import Share from "react-native-share";
|
import Share from "react-native-share";
|
||||||
import { captureScreen } from "react-native-view-shot";
|
import { captureScreen } from "react-native-view-shot";
|
||||||
import { StackParams } from "./AppStack";
|
import AppInput from "./AppInput";
|
||||||
import AppLineChart from "./AppLineChart";
|
import AppLineChart from "./AppLineChart";
|
||||||
|
import { StackParams } from "./AppStack";
|
||||||
import Select from "./Select";
|
import Select from "./Select";
|
||||||
import StackHeader from "./StackHeader";
|
import StackHeader from "./StackHeader";
|
||||||
import { MARGIN, PADDING } from "./constants";
|
import { MARGIN, PADDING } from "./constants";
|
||||||
|
@ -15,10 +17,9 @@ import { setRepo, settingsRepo } from "./db";
|
||||||
import GymSet from "./gym-set";
|
import GymSet from "./gym-set";
|
||||||
import { Metrics } from "./metrics";
|
import { Metrics } from "./metrics";
|
||||||
import { Periods } from "./periods";
|
import { Periods } from "./periods";
|
||||||
import Volume from "./volume";
|
|
||||||
import AppInput from "./AppInput";
|
|
||||||
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
|
|
||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
|
import Volume from "./volume";
|
||||||
|
import { convert } from "./conversions";
|
||||||
|
|
||||||
export default function ViewGraph() {
|
export default function ViewGraph() {
|
||||||
const { params } = useRoute<RouteProp<StackParams, "ViewGraph">>();
|
const { params } = useRoute<RouteProp<StackParams, "ViewGraph">>();
|
||||||
|
@ -26,20 +27,16 @@ export default function ViewGraph() {
|
||||||
const [volumes, setVolumes] = useState<Volume[]>();
|
const [volumes, setVolumes] = useState<Volume[]>();
|
||||||
const [metric, setMetric] = useState(Metrics.OneRepMax);
|
const [metric, setMetric] = useState(Metrics.OneRepMax);
|
||||||
const [period, setPeriod] = useState(Periods.Monthly);
|
const [period, setPeriod] = useState(Periods.Monthly);
|
||||||
const [unit, setUnit] = useState('kg');
|
const [unit, setUnit] = useState("kg");
|
||||||
const [start, setStart] = useState<Date | null>(null)
|
const [start, setStart] = useState<Date | null>(null);
|
||||||
const [end, setEnd] = useState<Date | null>(null)
|
const [end, setEnd] = useState<Date | null>(null);
|
||||||
const [settings, setSettings] = useState<Settings>({} as Settings);
|
const [settings, setSettings] = useState<Settings>({} as Settings);
|
||||||
|
|
||||||
useFocusEffect(useCallback(() => {
|
useFocusEffect(
|
||||||
settingsRepo.findOne({ where: {} }).then(setSettings)
|
useCallback(() => {
|
||||||
}, []))
|
settingsRepo.findOne({ where: {} }).then(setSettings);
|
||||||
|
}, [])
|
||||||
const convertWeight = (weight: number, unitFrom: string, unitTo: string) => {
|
);
|
||||||
if (unitFrom === unitTo) return weight
|
|
||||||
if (unitFrom === 'lb' && unitTo === 'kg') return weight * 0.453592;
|
|
||||||
if (unitFrom === 'kg' && unitTo === 'lb') return weight * 2.20462;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let difference = "-7 days";
|
let difference = "-7 days";
|
||||||
|
@ -58,34 +55,50 @@ export default function ViewGraph() {
|
||||||
.select("STRFTIME('%Y-%m-%d', created)", "created")
|
.select("STRFTIME('%Y-%m-%d', created)", "created")
|
||||||
.addSelect("unit")
|
.addSelect("unit")
|
||||||
.where("name = :name", { name: params.name })
|
.where("name = :name", { name: params.name })
|
||||||
.andWhere("NOT hidden")
|
.andWhere("NOT hidden");
|
||||||
|
|
||||||
if (start)
|
if (start) builder.andWhere("DATE(created) >= :start", { start });
|
||||||
builder.andWhere("DATE(created) >= :start", { start });
|
if (end) builder.andWhere("DATE(created) <= :end", { end });
|
||||||
if (end)
|
|
||||||
builder.andWhere("DATE(created) <= :end", { end });
|
|
||||||
if (difference)
|
if (difference)
|
||||||
builder.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
builder.andWhere(
|
||||||
|
"DATE(created) >= DATE('now', 'weekday 0', :difference)",
|
||||||
|
{
|
||||||
difference,
|
difference,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.groupBy("name").addGroupBy(`STRFTIME('${group}', created)`);
|
||||||
builder
|
|
||||||
.groupBy("name")
|
|
||||||
.addGroupBy(`STRFTIME('${group}', created)`);
|
|
||||||
switch (metric) {
|
switch (metric) {
|
||||||
case Metrics.Best:
|
case Metrics.Best:
|
||||||
builder
|
builder
|
||||||
.addSelect("ROUND(MAX(weight), 2)", "weight")
|
.addSelect("ROUND(MAX(weight), 2)", "weight")
|
||||||
.getRawMany()
|
.getRawMany()
|
||||||
.then(newWeights => newWeights.map(set => ({ ...set, weight: convertWeight(set.weight, set.unit, unit) })))
|
.then((newWeights) =>
|
||||||
|
newWeights.map((set) => {
|
||||||
|
let weight = convert(set.weight, set.unit, unit);
|
||||||
|
if (isNaN(weight)) weight = 0;
|
||||||
|
return ({
|
||||||
|
...set,
|
||||||
|
weight: weight
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
.then(setWeights);
|
.then(setWeights);
|
||||||
break;
|
break;
|
||||||
case Metrics.Volume:
|
case Metrics.Volume:
|
||||||
builder
|
builder
|
||||||
.addSelect("ROUND(SUM(weight * reps), 2)", "value")
|
.addSelect("ROUND(SUM(weight * reps), 2)", "value")
|
||||||
.getRawMany()
|
.getRawMany()
|
||||||
.then(newWeights => newWeights.map(set => ({ ...set, value: convertWeight(set.value, set.unit, unit) })))
|
.then((newWeights) =>
|
||||||
|
newWeights.map((set) => {
|
||||||
|
let weight = convert(set.value, set.unit, unit);
|
||||||
|
if (isNaN(weight)) weight = 0;
|
||||||
|
return ({
|
||||||
|
...set,
|
||||||
|
value: weight
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
.then(setVolumes);
|
.then(setVolumes);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -96,9 +109,20 @@ export default function ViewGraph() {
|
||||||
"weight"
|
"weight"
|
||||||
)
|
)
|
||||||
.getRawMany()
|
.getRawMany()
|
||||||
.then(newWeights => newWeights.map(set => ({ ...set, weight: convertWeight(set.weight, set.unit, unit) })))
|
.then((newWeights) =>
|
||||||
|
newWeights.map((set) => {
|
||||||
|
let weight = convert(set.weight, set.unit, unit);
|
||||||
|
if (isNaN(weight)) weight = 0;
|
||||||
|
return ({
|
||||||
|
...set,
|
||||||
|
weight: weight,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
.then((newWeights) => {
|
.then((newWeights) => {
|
||||||
console.log(`${ViewGraph.name}.oneRepMax:`, { weights: newWeights });
|
console.log(`${ViewGraph.name}.oneRepMax:`, {
|
||||||
|
weights: newWeights,
|
||||||
|
});
|
||||||
setWeights(newWeights);
|
setWeights(newWeights);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -137,7 +161,7 @@ export default function ViewGraph() {
|
||||||
DateTimePickerAndroid.open({
|
DateTimePickerAndroid.open({
|
||||||
value: start || new Date(),
|
value: start || new Date(),
|
||||||
onChange: (event, date) => {
|
onChange: (event, date) => {
|
||||||
if (event.type === 'dismissed') return;
|
if (event.type === "dismissed") return;
|
||||||
if (date === start) return;
|
if (date === start) return;
|
||||||
setStart(date);
|
setStart(date);
|
||||||
setPeriod(Periods.AllTime);
|
setPeriod(Periods.AllTime);
|
||||||
|
@ -151,7 +175,7 @@ export default function ViewGraph() {
|
||||||
DateTimePickerAndroid.open({
|
DateTimePickerAndroid.open({
|
||||||
value: end || new Date(),
|
value: end || new Date(),
|
||||||
onChange: (event, date) => {
|
onChange: (event, date) => {
|
||||||
if (event.type === 'dismissed') return;
|
if (event.type === "dismissed") return;
|
||||||
if (date === end) return;
|
if (date === end) return;
|
||||||
setEnd(date);
|
setEnd(date);
|
||||||
setPeriod(Periods.AllTime);
|
setPeriod(Periods.AllTime);
|
||||||
|
@ -209,7 +233,7 @@ export default function ViewGraph() {
|
||||||
value={period}
|
value={period}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', marginBottom: MARGIN }}>
|
<View style={{ flexDirection: "row", marginBottom: MARGIN }}>
|
||||||
<AppInput
|
<AppInput
|
||||||
label="Start date"
|
label="Start date"
|
||||||
value={start ? format(start, settings.date || "Pp") : null}
|
value={start ? format(start, settings.date || "Pp") : null}
|
||||||
|
@ -229,9 +253,9 @@ export default function ViewGraph() {
|
||||||
value={unit}
|
value={unit}
|
||||||
onChange={setUnit}
|
onChange={setUnit}
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Pounds (lb)', value: 'lb' },
|
{ label: "Pounds (lb)", value: "lb" },
|
||||||
{ label: 'Kilograms (kg)', value: 'kg' },
|
{ label: "Kilograms (kg)", value: "kg" },
|
||||||
{ label: 'Stone', value: 'stone' },
|
{ label: "Stone", value: "stone" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<View style={{ paddingTop: PADDING }}>
|
<View style={{ paddingTop: PADDING }}>
|
||||||
|
|
|
@ -87,8 +87,8 @@ android {
|
||||||
applicationId "com.massive"
|
applicationId "com.massive"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 36246
|
versionCode 36249
|
||||||
versionName "2.31"
|
versionName "2.34"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {
|
release {
|
||||||
|
|
|
@ -95,10 +95,22 @@ class BackupModule(context: ReactApplicationContext?) :
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun exportToCSV(target: String, promise: Promise) {
|
fun exportPlans(target: String, promise: Promise) {
|
||||||
try {
|
try {
|
||||||
val db = DatabaseHelper(reactApplicationContext)
|
val db = DatabaseHelper(reactApplicationContext)
|
||||||
db.exportToCSV(target, reactApplicationContext)
|
db.exportPlans(target, reactApplicationContext)
|
||||||
|
promise.resolve("Export successful!")
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
promise.reject("ERROR", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun exportSets(target: String, promise: Promise) {
|
||||||
|
try {
|
||||||
|
val db = DatabaseHelper(reactApplicationContext)
|
||||||
|
db.exportSets(target, reactApplicationContext)
|
||||||
promise.resolve("Export successful!")
|
promise.resolve("Export successful!")
|
||||||
}
|
}
|
||||||
catch (e: Exception) {
|
catch (e: Exception) {
|
||||||
|
|
|
@ -18,8 +18,8 @@ class DatabaseHelper(context: Context) :
|
||||||
private const val DATABASE_VERSION = 1
|
private const val DATABASE_VERSION = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportToCSV(target: String, context: Context) {
|
fun exportSets(target: String, context: Context) {
|
||||||
Log.d("DatabaseHelper", "exportToCSV $target")
|
Log.d("DatabaseHelper", "exportSets $target")
|
||||||
val treeUri: Uri = Uri.parse(target)
|
val treeUri: Uri = Uri.parse(target)
|
||||||
val documentFile = context.let { DocumentFile.fromTreeUri(it, treeUri) }
|
val documentFile = context.let { DocumentFile.fromTreeUri(it, treeUri) }
|
||||||
val file = documentFile?.createFile("application/octet-stream", "sets.csv") ?: return
|
val file = documentFile?.createFile("application/octet-stream", "sets.csv") ?: return
|
||||||
|
@ -43,6 +43,31 @@ class DatabaseHelper(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun exportPlans(target: String, context: Context) {
|
||||||
|
Log.d("DatabaseHelper", "exportPlans $target")
|
||||||
|
val treeUri: Uri = Uri.parse(target)
|
||||||
|
val documentFile = context.let { DocumentFile.fromTreeUri(it, treeUri) }
|
||||||
|
val file = documentFile?.createFile("application/octet-stream", "plans.csv") ?: return
|
||||||
|
|
||||||
|
context.contentResolver.openOutputStream(file.uri).use { outputStream ->
|
||||||
|
val csvWrite = CSVWriter(outputStream?.writer())
|
||||||
|
val db = this.readableDatabase
|
||||||
|
val cursor = db.rawQuery("SELECT * FROM plans", null)
|
||||||
|
csvWrite.writeNext(cursor.columnNames)
|
||||||
|
|
||||||
|
while(cursor.moveToNext()) {
|
||||||
|
val arrStr = arrayOfNulls<String>(cursor.columnCount)
|
||||||
|
for(i in 0 until cursor.columnCount) {
|
||||||
|
arrStr[i] = cursor.getString(i)
|
||||||
|
}
|
||||||
|
csvWrite.writeNext(arrStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
csvWrite.close()
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 64 KiB |
BIN
metadata/en-US/images/phoneScreenshots/exercise-edit.png
Normal file
After Width: | Height: | Size: 149 KiB |
BIN
metadata/en-US/images/phoneScreenshots/exercises.png
Normal file
After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 184 KiB |
Before Width: | Height: | Size: 123 KiB |
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "massive",
|
"name": "massive",
|
||||||
"version": "2.28",
|
"version": "2.33",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "massive",
|
"name": "massive",
|
||||||
"version": "2.28",
|
"version": "2.33",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "massive",
|
"name": "massive",
|
||||||
"version": "2.31",
|
"version": "2.34",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
1
privacy-policy.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
No user data is collected.
|