Compare commits
81 Commits
88021cdade
...
main
Author | SHA1 | Date | |
---|---|---|---|
bdc4fee8c2 | |||
8dcffcca11 | |||
2eda797375 | |||
9f5fb0d1ad | |||
23663f484b | |||
60bc571987 | |||
0cf62ec4b4 | |||
2288cba78e | |||
1027439848 | |||
6012a1541e | |||
d9a5599a4b | |||
0b0ca884bc | |||
8ec531c0ea | |||
6917754933 | |||
ebca90e69a | |||
7ead6ba631 | |||
a034c16160 | |||
acab37eb60 | |||
fec4eaaf92 | |||
ecc9aa3abc | |||
95701c73a6 | |||
2eb5d8f171 | |||
6e0b1263ba | |||
9fc5fb5d22 | |||
32826abcea | |||
2206720810 | |||
48f716cdb0 | |||
e78788d67a | |||
e36d2a837a | |||
10332ec8be | |||
5f628d6b48 | |||
afe633e697 | |||
8e0ec614a0 | |||
fa374a5bc2 | |||
26d9386812 | |||
722a152130 | |||
cd8da31f4b | |||
029f037f90 | |||
3c2f2e9bae | |||
604b099010 | |||
c6030f8ac5 | |||
ffd696053a | |||
fb0b73ecaf | |||
1234a300e1 | |||
3153bf13f9 | |||
68443b3427 | |||
5d27744ead | |||
67d7a374d4 | |||
54d47245ae | |||
7f2cf0b49f | |||
8fe60e7ae3 | |||
b2e2eb67b0 | |||
0dc7c3ced0 | |||
13fe7e2ef4 | |||
d6e62024d7 | |||
4094f7edba | |||
586d2355c9 | |||
4e5eeec937 | |||
0c0f596fbb | |||
f781001d3b | |||
19f835d8f2 | |||
56b25a6963 | |||
5bae1aa416 | |||
29479e8aba | |||
9ffa0d178c | |||
932e9cd6a4 | |||
d42696df61 | |||
1564d6cd83 | |||
4fd36246ae | |||
e8e1737875 | |||
baa0f603cf | |||
d7bd755c57 | |||
8890346b59 | |||
a2812b40a0 | |||
1c8f03c97b | |||
b273979ac0 | |||
780e270c15 | |||
e3a09458d8 | |||
6b319a8d96 | |||
0fe3d65bb8 | |||
ec6381f04d |
145
.gitignore
vendored
145
.gitignore
vendored
@ -9,6 +9,7 @@
|
|||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
|
bkp
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
@ -31,6 +32,8 @@ migrate_working_dir/
|
|||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
pubspec.lock
|
||||||
|
devtools_options.yaml
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
@ -42,3 +45,145 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
|
||||||
|
# Do not remove or rename entries in this file, only add new ones
|
||||||
|
# See https://github.com/flutter/flutter/issues/128635 for more context.
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.lock
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Visual Studio Code related
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
.vscode/*
|
||||||
|
|
||||||
|
# Flutter repo-specific
|
||||||
|
/bin/cache/
|
||||||
|
/bin/internal/bootstrap.bat
|
||||||
|
/bin/internal/bootstrap.sh
|
||||||
|
/bin/mingit/
|
||||||
|
/dev/benchmarks/mega_gallery/
|
||||||
|
/dev/bots/.recipe_deps
|
||||||
|
/dev/bots/android_tools/
|
||||||
|
/dev/devicelab/ABresults*.json
|
||||||
|
/dev/docs/doc/
|
||||||
|
/dev/docs/api_docs.zip
|
||||||
|
/dev/docs/flutter.docs.zip
|
||||||
|
/dev/docs/lib/
|
||||||
|
/dev/docs/pubspec.yaml
|
||||||
|
/dev/integration_tests/**/xcuserdata
|
||||||
|
/dev/integration_tests/**/Pods
|
||||||
|
/packages/flutter/coverage/
|
||||||
|
version
|
||||||
|
analysis_benchmark.json
|
||||||
|
|
||||||
|
# packages file containing multi-root paths
|
||||||
|
.packages.generated
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
**/generated_plugin_registrant.dart
|
||||||
|
.packages
|
||||||
|
.pub-preload-cache/
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
flutter_*.png
|
||||||
|
linked_*.ds
|
||||||
|
unlinked.ds
|
||||||
|
unlinked_spec.ds
|
||||||
|
|
||||||
|
# Android related
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
.gradle/
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
**/android/key.properties
|
||||||
|
*.jks
|
||||||
|
|
||||||
|
# iOS/XCode related
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Flutter.podspec
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/ephemeral
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
**/Flutter/ephemeral/
|
||||||
|
**/Pods/
|
||||||
|
**/macos/Flutter/GeneratedPluginRegistrant.swift
|
||||||
|
**/macos/Flutter/ephemeral
|
||||||
|
**/xcuserdata/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
**/windows/flutter/ephemeral/
|
||||||
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
**/linux/flutter/ephemeral/
|
||||||
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
**/linux/flutter/generated_plugin_registrant.h
|
||||||
|
**/linux/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Symbols
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||||
|
!/dev/ci/**/Gemfile.lock
|
||||||
|
!.vscode/settings.json
|
34
.metadata
34
.metadata
@ -1,11 +1,11 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
|
||||||
channel: stable
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
@ -13,26 +13,26 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
- platform: android
|
- platform: android
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
- platform: ios
|
- platform: ios
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
- platform: linux
|
- platform: linux
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
- platform: macos
|
- platform: macos
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
- platform: web
|
- platform: web
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
- platform: windows
|
- platform: windows
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
74
.vscode/dart.code-snippets
vendored
74
.vscode/dart.code-snippets
vendored
@ -1,74 +0,0 @@
|
|||||||
{
|
|
||||||
// Place your snippets for dart here. Each snippet is defined under a snippet name and has a prefix, body and
|
|
||||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
|
||||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
|
|
||||||
// same ids are connected.
|
|
||||||
// Example:
|
|
||||||
// "Print to console": {
|
|
||||||
// "prefix": "log",
|
|
||||||
// "body": [
|
|
||||||
// "console.log('$1');",
|
|
||||||
// "$2"
|
|
||||||
// ],
|
|
||||||
// "description": "Log output to console"
|
|
||||||
// }
|
|
||||||
"Stateless Widget": {
|
|
||||||
"prefix": "stateless",
|
|
||||||
"body": [
|
|
||||||
"import 'package:flutter/material.dart';",
|
|
||||||
"\n",
|
|
||||||
"class ${1:MyClass} extends StatelessWidget {",
|
|
||||||
"@override",
|
|
||||||
"Widget build(BuildContext context) {",
|
|
||||||
"return Container($0);}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Stateful Widget": {
|
|
||||||
"prefix": "stateful",
|
|
||||||
"body": [
|
|
||||||
"import 'package:flutter/material.dart';",
|
|
||||||
"\n",
|
|
||||||
"class ${1:MyClass} extends StatefulWidget {",
|
|
||||||
"const ${1:MyClass}({Key key, $2}) : super(key: key);",
|
|
||||||
"\n",
|
|
||||||
"@override",
|
|
||||||
"_${1:MyClass}State createState() => _${1:MyClass}State();",
|
|
||||||
"}",
|
|
||||||
"\n",
|
|
||||||
"class _${1:MyClass}State extends State<${1:MyClass}> {",
|
|
||||||
"\n",
|
|
||||||
"@override",
|
|
||||||
"void initState() {",
|
|
||||||
"super.initState();",
|
|
||||||
"}",
|
|
||||||
"\n",
|
|
||||||
"@override",
|
|
||||||
"void dispose() {",
|
|
||||||
"super.dispose();",
|
|
||||||
"}",
|
|
||||||
"\n",
|
|
||||||
"@override",
|
|
||||||
"Widget build(BuildContext context) {",
|
|
||||||
"return Container($0);}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Widget Test": {
|
|
||||||
"prefix": "widgettest",
|
|
||||||
"body": [
|
|
||||||
"import 'package:flutter_test/flutter_test.dart';",
|
|
||||||
"import 'package:flutter/material.dart';",
|
|
||||||
"//import 'package:piota/${1:widgetfile}.dart';",
|
|
||||||
"\n",
|
|
||||||
"import 'ui_test_util.dart';",
|
|
||||||
"\n",
|
|
||||||
"void main() {",
|
|
||||||
"group('${2:groupname}', () {",
|
|
||||||
"final testableWidget = testWidget(${3:Container()},Size(375, 667));",
|
|
||||||
"testWidgets('${3:Container()} test', (WidgetTester tester) async {",
|
|
||||||
"final finder = find.byKey(Key('${5:keyname}'));",
|
|
||||||
"await tester.pumpWidget(testableWidget);",
|
|
||||||
"expect(finder, findsOneWidget);",
|
|
||||||
"});});}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
# SendTrain
|
# SendTrain v0.2.2
|
||||||
|
|
||||||
Mobile app for community driven climbing training and support.
|
Mobile app for community driven climbing training and support.
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@
|
|||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- "**/*.g.dart"
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
2
android/.gitignore
vendored
2
android/.gitignore
vendored
@ -7,7 +7,7 @@ gradle-wrapper.jar
|
|||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
# Remember to never publicly share your keystore.
|
# Remember to never publicly share your keystore.
|
||||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
# See https://flutter.dev/to/reference-keystore
|
||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
@ -1,71 +1,44 @@
|
|||||||
def localProperties = new Properties()
|
plugins {
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
id "com.android.application"
|
||||||
if (localPropertiesFile.exists()) {
|
id "kotlin-android"
|
||||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
localProperties.load(reader)
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
|
||||||
if (flutterRoot == null) {
|
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
|
||||||
if (flutterVersionCode == null) {
|
|
||||||
flutterVersionCode = '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
|
||||||
if (flutterVersionName == null) {
|
|
||||||
flutterVersionName = '1.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
namespace = "com.example.sendtrain"
|
||||||
ndkVersion flutter.ndkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.sendtrain.sendtrain"
|
applicationId = "com.example.sendtrain"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdkVersion flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode = flutter.versionCode
|
||||||
versionName flutterVersionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
signingConfig signingConfigs.debug
|
signingConfig = signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source '../..'
|
source = "../.."
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
}
|
}
|
||||||
|
1
android/app/proguard-rules.pro
vendored
Normal file
1
android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
-keep class androidx.lifecycle.DefaultLifecycleObserver
|
@ -1,5 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.sendtrain.sendtrain">
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.sendtrain.sendtrain">
|
<uses-feature android:name="android.hardware.location.network" android:required="false" />
|
||||||
<application
|
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||||
android:label="SendTrain"
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<application
|
||||||
|
android:label="sendtrain"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
@ -17,12 +20,12 @@
|
|||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
@ -31,4 +34,18 @@
|
|||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
<meta-data android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE" />
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.example.sendtrain
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity()
|
@ -1,6 +0,0 @@
|
|||||||
package com.sendtrain.sendtrain
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.sendtrain.sendtrain">
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
@ -1,16 +1,3 @@
|
|||||||
buildscript {
|
|
||||||
ext.kotlin_version = '1.7.10'
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@ -18,12 +5,12 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = "../build"
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("clean", Delete) {
|
tasks.register("clean", Delete) {
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
include ':app'
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}()
|
||||||
|
|
||||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
repositories {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
plugins {
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
id "com.android.application" version "8.1.0" apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
||||||
|
BIN
assets/audio/count_finish.mp3
Normal file
BIN
assets/audio/count_finish.mp3
Normal file
Binary file not shown.
BIN
assets/audio/count_tone.mp3
Normal file
BIN
assets/audio/count_tone.mp3
Normal file
Binary file not shown.
22617
assets/exercises.json
Normal file
22617
assets/exercises.json
Normal file
File diff suppressed because it is too large
Load Diff
9
build.yaml
Normal file
9
build.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
targets:
|
||||||
|
$default:
|
||||||
|
builders:
|
||||||
|
drift_dev:
|
||||||
|
options:
|
||||||
|
schema_dir: lib/database/drift_schemas/
|
||||||
|
databases:
|
||||||
|
# Required: A name for the database and it's path
|
||||||
|
sendtrain: lib/database/database.dart
|
@ -1,33 +0,0 @@
|
|||||||
PODS:
|
|
||||||
- Flutter (1.0.0)
|
|
||||||
- flutter_inappwebview (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_inappwebview/Core (= 0.0.1)
|
|
||||||
- OrderedSet (~> 5.0)
|
|
||||||
- flutter_inappwebview/Core (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- OrderedSet (~> 5.0)
|
|
||||||
- OrderedSet (5.0.0)
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
|
||||||
- Flutter (from `Flutter`)
|
|
||||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
|
||||||
trunk:
|
|
||||||
- OrderedSet
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
|
||||||
Flutter:
|
|
||||||
:path: Flutter
|
|
||||||
flutter_inappwebview:
|
|
||||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
|
||||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
|
||||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
|
||||||
|
|
||||||
COCOAPODS: 1.12.0
|
|
@ -1,5 +1,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
import GoogleMaps
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
@ -7,7 +8,8 @@ import Flutter
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
GMSServices.provideAPIKey("AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE")
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,13 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>The Photo library is used when selecting a photo to upload as media for a Session</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
12
ios/RunnerTests/RunnerTests.swift
Normal file
12
ios/RunnerTests/RunnerTests.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,15 +1,89 @@
|
|||||||
|
import 'package:sendtrain/classes/media.dart';
|
||||||
|
|
||||||
class ActivityAction {
|
class ActivityAction {
|
||||||
int id;
|
int id;
|
||||||
String title;
|
String title;
|
||||||
String description;
|
String description;
|
||||||
Set activityActionSet;
|
Set activityActionSet;
|
||||||
|
List<Media>? media;
|
||||||
|
|
||||||
ActivityAction({
|
ActivityAction({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.activityActionSet,
|
required this.activityActionSet,
|
||||||
|
this.media,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
List<List<Map<String, dynamic>>> items() {
|
||||||
|
List<List<Map<String, dynamic>>> sets = [];
|
||||||
|
Reps reps = activityActionSet.reps;
|
||||||
|
int totalActions = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < activityActionSet.total; i++) {
|
||||||
|
List<Map<String, dynamic>> actions = [];
|
||||||
|
int? weight = _setWeight(i);
|
||||||
|
|
||||||
|
actions.add({
|
||||||
|
'actionID': totalActions++,
|
||||||
|
'name': title,
|
||||||
|
'type': reps.type,
|
||||||
|
'amount': reps.amounts[i],
|
||||||
|
'weight': weight,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activityActionSet.type == 'alternating') {
|
||||||
|
if (reps.rest != null) {
|
||||||
|
actions.add({
|
||||||
|
'actionID': totalActions++,
|
||||||
|
'name': 'Rest',
|
||||||
|
'type': 'seconds',
|
||||||
|
'amount': reps.rest! ~/ 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.add({
|
||||||
|
'actionID': totalActions++,
|
||||||
|
'name': title,
|
||||||
|
'type': reps.type,
|
||||||
|
'amount': reps.amounts[i],
|
||||||
|
'weights': weight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.add({
|
||||||
|
'actionID': totalActions++,
|
||||||
|
'name': 'Rest',
|
||||||
|
'type': 'seconds',
|
||||||
|
'amount': activityActionSet.rest ~/ 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
sets.add(actions);
|
||||||
|
|
||||||
|
// sets.add([{
|
||||||
|
// 'actionID': totalActions++,
|
||||||
|
// 'name': 'Rest',
|
||||||
|
// 'type': 'seconds',
|
||||||
|
// 'amount': activityActionSet.rest ~/ 1000,
|
||||||
|
// }]);
|
||||||
|
|
||||||
|
// for (int j = 0; i < activityActionSet.reps.amounts; j++) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sets;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _setWeight(setNum) {
|
||||||
|
Reps reps = activityActionSet.reps;
|
||||||
|
|
||||||
|
if (reps.weights.length == activityActionSet.total) {
|
||||||
|
return reps.weights[setNum];
|
||||||
|
} else if (reps.weights.length == 1) {
|
||||||
|
return reps.weights[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Set {
|
class Set {
|
||||||
@ -30,14 +104,14 @@ class Reps {
|
|||||||
String type;
|
String type;
|
||||||
List<int> tempo;
|
List<int> tempo;
|
||||||
List<int> amounts;
|
List<int> amounts;
|
||||||
List<int> weights;
|
List<int> weights = [];
|
||||||
int rest;
|
int? rest;
|
||||||
|
|
||||||
Reps({
|
Reps({
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.tempo,
|
required this.tempo,
|
||||||
required this.amounts,
|
required this.amounts,
|
||||||
required this.weights,
|
required this.weights,
|
||||||
required this.rest,
|
this.rest,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
64
lib/daos/actions_dao.dart
Normal file
64
lib/daos/actions_dao.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
part 'actions_dao.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [Actions])
|
||||||
|
class ActionsDao extends DatabaseAccessor<AppDatabase> with _$ActionsDaoMixin {
|
||||||
|
ActionsDao(super.db);
|
||||||
|
|
||||||
|
Future<List<Action>> all() async {
|
||||||
|
return await select(actions).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Action> find(int id) async {
|
||||||
|
return await (select(actions)..where((action) => action.id.equals(id) )).getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Action>> fromActivity(Activity activity, Session session) async {
|
||||||
|
final result = select(db.activityActions).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
db.actions,
|
||||||
|
db.actions.id.equalsExp(db.activityActions.actionId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(db.activityActions.activityId.equals(activity.id))
|
||||||
|
..where(db.activityActions.sessionId.equals(session.id));
|
||||||
|
|
||||||
|
final actions = (await result.get())
|
||||||
|
.map((e) => e.readTable(db.actions))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Action>> watchActivityActions(Activity activity, Session session) {
|
||||||
|
final result = select(db.activityActions).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
db.actions,
|
||||||
|
db.actions.id.equalsExp(db.activityActions.actionId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(db.activityActions.activityId.equals(activity.id))
|
||||||
|
..where(db.activityActions.sessionId.equals(session.id));
|
||||||
|
|
||||||
|
// final actions = result.watch().map((rows) {
|
||||||
|
// return rows.map((row) {
|
||||||
|
// row.readTable(db.actions);
|
||||||
|
// }).toList();
|
||||||
|
// });
|
||||||
|
|
||||||
|
final actions = (result.watch()).map((rows) {
|
||||||
|
return rows.map((row) => row.readTable(db.actions)).toList();
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future createOrUpdate(ActionsCompanion action) => into(actions).insertOnConflictUpdate(action);
|
||||||
|
Future replace(Action action) => update(actions).replace(action);
|
||||||
|
}
|
8
lib/daos/actions_dao.g.dart
Normal file
8
lib/daos/actions_dao.g.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'actions_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$ActionsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$ActionsTable get actions => attachedDatabase.actions;
|
||||||
|
}
|
89
lib/daos/activities_dao.dart
Normal file
89
lib/daos/activities_dao.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
part 'activities_dao.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [Activities])
|
||||||
|
class ActivitiesDao extends DatabaseAccessor<AppDatabase>
|
||||||
|
with _$ActivitiesDaoMixin {
|
||||||
|
ActivitiesDao(super.db);
|
||||||
|
|
||||||
|
Future<List<Activity>> all() async {
|
||||||
|
return await select(activities).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Activity> find(int id) async {
|
||||||
|
return await (select(activities)
|
||||||
|
..where((activity) => activity.id.equals(id)))
|
||||||
|
.getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future remove(Activity activity) => delete(activities).delete(activity);
|
||||||
|
|
||||||
|
Future<List<Activity>> contains(value) async {
|
||||||
|
return (select(activities)
|
||||||
|
..where((t) =>
|
||||||
|
t.title.contains(value) | t.description.contains(value) | t.category.contains(value)))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Activity>> activitiesFromSession(int id) async {
|
||||||
|
final result = select(db.sessionActivities).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
db.activities,
|
||||||
|
db.activities.id.equalsExp(db.sessionActivities.activityId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)..where(db.sessionActivities.sessionId.equals(id));
|
||||||
|
|
||||||
|
final activities =
|
||||||
|
(await result.get()).map((e) => e.readTable(db.activities)).toList();
|
||||||
|
|
||||||
|
return activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Activity>> watchSessionActivities(int id) {
|
||||||
|
final query = select(db.sessionActivities).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
db.activities,
|
||||||
|
db.activities.id.equalsExp(db.sessionActivities.activityId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)..where(db.sessionActivities.sessionId.equals(id));
|
||||||
|
|
||||||
|
return query.watch().map((rows) {
|
||||||
|
final activities = (rows).map((e) => e.readTable(db.activities)).toList();
|
||||||
|
|
||||||
|
return activities;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiSelectable<SessionActivity> _selectableSessionActivities(int id) {
|
||||||
|
// // return select(db.sessionActivities)..limit(1, offset: 1);
|
||||||
|
// // final query = select(db.sessionActivities).join(
|
||||||
|
// // [
|
||||||
|
// // innerJoin(
|
||||||
|
// // db.activities,
|
||||||
|
// // db.activities.id.equalsExp(db.sessionActivities.activityId),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // )..where(db.sessionActivities.sessionId.equals(id));
|
||||||
|
|
||||||
|
// // return query;
|
||||||
|
|
||||||
|
// final query = select(db.sessionActivities)..where((row) => row.sessionId.equals(id));
|
||||||
|
|
||||||
|
// query.join(
|
||||||
|
// [
|
||||||
|
// innerJoin(
|
||||||
|
// db.activities,
|
||||||
|
// db.activities.id.equalsExp(db.sessionActivities.activityId),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return query;
|
||||||
|
// }
|
||||||
|
}
|
8
lib/daos/activities_dao.g.dart
Normal file
8
lib/daos/activities_dao.g.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'activities_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$ActivitiesDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$ActivitiesTable get activities => attachedDatabase.activities;
|
||||||
|
}
|
31
lib/daos/activity_actions_dao.dart
Normal file
31
lib/daos/activity_actions_dao.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
part 'activity_actions_dao.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [ActivityActions])
|
||||||
|
class ActivityActionsDao extends DatabaseAccessor<AppDatabase> with _$ActivityActionsDaoMixin {
|
||||||
|
ActivityActionsDao(super.db);
|
||||||
|
|
||||||
|
Future<List<ActivityAction>> all() => select(activityActions).get();
|
||||||
|
Stream<List<ActivityAction>> watch() => select(activityActions).watch();
|
||||||
|
Future insert(ActivityAction activityAction) => into(activityActions).insert(activityAction);
|
||||||
|
Future replace(ActivityAction activityAction) => update(activityActions).replace(activityAction);
|
||||||
|
Future remove(ActivityAction activityAction) => delete(activityActions).delete(activityAction);
|
||||||
|
Future createOrUpdate(ActivityActionsCompanion activityAction) => into(activityActions).insertOnConflictUpdate(activityAction);
|
||||||
|
|
||||||
|
// Future<List<ActivityAction>> all() async {
|
||||||
|
// return await select(activityActions).get();
|
||||||
|
// }
|
||||||
|
|
||||||
|
Future<ActivityAction> find(int id) async {
|
||||||
|
return await (select(activityActions)..where((activityAction) => activityAction.id.equals(id) )).getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ActivityAction>> fromActivityId(int id) async {
|
||||||
|
final result = db.managers.activityActions
|
||||||
|
.filter((activityAction) => activityAction.activityId.id(id));
|
||||||
|
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
}
|
11
lib/daos/activity_actions_dao.g.dart
Normal file
11
lib/daos/activity_actions_dao.g.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'activity_actions_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$ActivityActionsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$ActivitiesTable get activities => attachedDatabase.activities;
|
||||||
|
$ActionsTable get actions => attachedDatabase.actions;
|
||||||
|
$SessionsTable get sessions => attachedDatabase.sessions;
|
||||||
|
$ActivityActionsTable get activityActions => attachedDatabase.activityActions;
|
||||||
|
}
|
84
lib/daos/media_items_dao.dart
Normal file
84
lib/daos/media_items_dao.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
part 'media_items_dao.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [MediaItems])
|
||||||
|
class MediaItemsDao extends DatabaseAccessor<AppDatabase>
|
||||||
|
with _$MediaItemsDaoMixin {
|
||||||
|
MediaItemsDao(super.db);
|
||||||
|
|
||||||
|
Future createOrUpdate(MediaItemsCompanion mediaItem) =>
|
||||||
|
into(mediaItems).insertOnConflictUpdate(mediaItem);
|
||||||
|
|
||||||
|
Future<List<MediaItem>> all() async {
|
||||||
|
return await select(mediaItems).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MediaItem> find(int id) async {
|
||||||
|
return await (select(mediaItems)
|
||||||
|
..where((mediaItem) => mediaItem.id.equals(id)))
|
||||||
|
.getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<MediaItem>> fromActivity(Activity activity) async {
|
||||||
|
final result = select(db.objectMediaItems).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
db.mediaItems,
|
||||||
|
db.mediaItems.id.equalsExp(db.objectMediaItems.mediaId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(db.objectMediaItems.objectType.equals(ObjectType.activities.name))
|
||||||
|
..where(db.objectMediaItems.objectId.equals(activity.id));
|
||||||
|
|
||||||
|
final mediaItems =
|
||||||
|
(await result.get()).map((e) => e.readTable(db.mediaItems)).toList();
|
||||||
|
|
||||||
|
return mediaItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<MediaItem>> fromSession(int sessionId) async {
|
||||||
|
final result = select(db.objectMediaItems).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
db.mediaItems,
|
||||||
|
db.mediaItems.id.equalsExp(db.objectMediaItems.mediaId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(db.objectMediaItems.objectType.equals(ObjectType.sessions.name))
|
||||||
|
..where(db.objectMediaItems.objectId.equals(sessionId));
|
||||||
|
|
||||||
|
final mediaItems =
|
||||||
|
(await result.get()).map((e) => e.readTable(db.mediaItems)).toList();
|
||||||
|
|
||||||
|
return mediaItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<MediaItem>> watchSessionMediaItems(int id) {
|
||||||
|
final query = select(db.objectMediaItems).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
db.mediaItems,
|
||||||
|
db.mediaItems.id.equalsExp(db.objectMediaItems.mediaId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(db.objectMediaItems.objectType.equals(ObjectType.sessions.name))
|
||||||
|
..where(db.objectMediaItems.objectId.equals(id));
|
||||||
|
|
||||||
|
return query.watch().map((rows) {
|
||||||
|
final mediaItems = (rows).map((e) => e.readTable(db.mediaItems)).toList();
|
||||||
|
|
||||||
|
return mediaItems;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future remove(MediaItem mediaItem) => delete(mediaItems).delete(mediaItem);
|
||||||
|
Future removeAll(Iterable<int> mediaItemIds) {
|
||||||
|
return (delete(mediaItems)..where((table) => table.id.isIn(mediaItemIds)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
}
|
8
lib/daos/media_items_dao.g.dart
Normal file
8
lib/daos/media_items_dao.g.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'media_items_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$MediaItemsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$MediaItemsTable get mediaItems => attachedDatabase.mediaItems;
|
||||||
|
}
|
19
lib/daos/object_media_items_dao.dart
Normal file
19
lib/daos/object_media_items_dao.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
part 'object_media_items_dao.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [ObjectMediaItems])
|
||||||
|
class ObjectMediaItemsDao extends DatabaseAccessor<AppDatabase> with _$ObjectMediaItemsDaoMixin {
|
||||||
|
ObjectMediaItemsDao(super.db);
|
||||||
|
|
||||||
|
Future createOrUpdate(ObjectMediaItemsCompanion objectMediaItem) => into(objectMediaItems).insertOnConflictUpdate(objectMediaItem);
|
||||||
|
|
||||||
|
Future<List<ObjectMediaItem>> all() async {
|
||||||
|
return await select(objectMediaItems).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ObjectMediaItem> find(int id) async {
|
||||||
|
return await (select(objectMediaItems)..where((objectMediaItem) => objectMediaItem.id.equals(id) )).getSingle();
|
||||||
|
}
|
||||||
|
}
|
10
lib/daos/object_media_items_dao.g.dart
Normal file
10
lib/daos/object_media_items_dao.g.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'object_media_items_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$ObjectMediaItemsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$MediaItemsTable get mediaItems => attachedDatabase.mediaItems;
|
||||||
|
$ObjectMediaItemsTable get objectMediaItems =>
|
||||||
|
attachedDatabase.objectMediaItems;
|
||||||
|
}
|
42
lib/daos/session_activities_dao.dart
Normal file
42
lib/daos/session_activities_dao.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
part 'session_activities_dao.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [SessionActivities])
|
||||||
|
class SessionActivitiesDao extends DatabaseAccessor<AppDatabase>
|
||||||
|
with _$SessionActivitiesDaoMixin {
|
||||||
|
SessionActivitiesDao(super.db);
|
||||||
|
|
||||||
|
Future createOrUpdate(SessionActivitiesCompanion sessionActivity) =>
|
||||||
|
into(sessionActivities).insertOnConflictUpdate(sessionActivity);
|
||||||
|
|
||||||
|
Future<List<SessionActivity>> all() async {
|
||||||
|
return await select(sessionActivities).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SessionActivity> find(int id) async {
|
||||||
|
return await (select(sessionActivities)
|
||||||
|
..where((sessionActivity) => sessionActivity.id.equals(id)))
|
||||||
|
.getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SessionActivity>> fromSessionId(int id) async {
|
||||||
|
final result = db.managers.sessionActivities
|
||||||
|
.filter((sessionActivity) => sessionActivity.sessionId.id(id));
|
||||||
|
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future remove(SessionActivity sessionActivity) =>
|
||||||
|
delete(sessionActivities).delete(sessionActivity);
|
||||||
|
|
||||||
|
Future removeAssociation(int activityId, int sessionId) {
|
||||||
|
return (delete(sessionActivities)
|
||||||
|
..where((t) =>
|
||||||
|
t.sessionId.equals(sessionId) & t.activityId.equals(activityId)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go();
|
11
lib/daos/session_activities_dao.g.dart
Normal file
11
lib/daos/session_activities_dao.g.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'session_activities_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$SessionActivitiesDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$SessionsTable get sessions => attachedDatabase.sessions;
|
||||||
|
$ActivitiesTable get activities => attachedDatabase.activities;
|
||||||
|
$SessionActivitiesTable get sessionActivities =>
|
||||||
|
attachedDatabase.sessionActivities;
|
||||||
|
}
|
17
lib/daos/sessions_dao.dart
Normal file
17
lib/daos/sessions_dao.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
part 'sessions_dao.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [Sessions])
|
||||||
|
class SessionsDao extends DatabaseAccessor<AppDatabase> with _$SessionsDaoMixin {
|
||||||
|
SessionsDao(super.db);
|
||||||
|
|
||||||
|
Future<Session> find(int id) => (select(sessions)..where((session) => session.id.equals(id) )).getSingle();
|
||||||
|
Stream<Session> watchSession(int id) => (select(sessions)..where((session) => session.id.equals(id) )).watchSingle();
|
||||||
|
Future<List<Session>> all() => select(sessions).get();
|
||||||
|
Stream<List<Session>> watch() => select(sessions).watch();
|
||||||
|
Future createOrUpdate(SessionsCompanion session) => into(sessions).insertOnConflictUpdate(session);
|
||||||
|
Future replace(Session session) => update(sessions).replace(session);
|
||||||
|
Future remove(Session session) => delete(sessions).delete(session);
|
||||||
|
}
|
8
lib/daos/sessions_dao.g.dart
Normal file
8
lib/daos/sessions_dao.g.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'sessions_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$SessionsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$SessionsTable get sessions => attachedDatabase.sessions;
|
||||||
|
}
|
222
lib/database/database.dart
Normal file
222
lib/database/database.dart
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
|
import 'package:sendtrain/daos/actions_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/activities_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/activity_actions_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/media_items_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/object_media_items_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/session_activities_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/sessions_dao.dart';
|
||||||
|
import 'package:sendtrain/database/seed.dart';
|
||||||
|
|
||||||
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(tables: [
|
||||||
|
Sessions,
|
||||||
|
SessionActivities,
|
||||||
|
Activities,
|
||||||
|
ActivityActions,
|
||||||
|
Actions,
|
||||||
|
ObjectMediaItems,
|
||||||
|
MediaItems
|
||||||
|
], daos: [
|
||||||
|
SessionsDao,
|
||||||
|
ActivitiesDao,
|
||||||
|
MediaItemsDao,
|
||||||
|
ObjectMediaItemsDao,
|
||||||
|
SessionActivitiesDao,
|
||||||
|
ActivityActionsDao,
|
||||||
|
ActionsDao
|
||||||
|
])
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
// After generating code, this class needs to define a `schemaVersion` getter
|
||||||
|
// and a constructor telling drift where the database should be stored.
|
||||||
|
// These are described in the getting started guide: https://drift.simonbinder.eu/setup/
|
||||||
|
AppDatabase() : super(_openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 35;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration {
|
||||||
|
return MigrationStrategy(
|
||||||
|
onCreate: (m) async {
|
||||||
|
await m.createAll().then((r) async {
|
||||||
|
await seedDb(this);
|
||||||
|
}); // create all tables
|
||||||
|
},
|
||||||
|
beforeOpen: (details) async {
|
||||||
|
/// Enable foreign_keys
|
||||||
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QueryExecutor _openConnection() {
|
||||||
|
// `driftDatabase` from `package:drift_flutter` stores the database in
|
||||||
|
// `getApplicationDocumentsDirectory()`.
|
||||||
|
return driftDatabase(name: 'sendtrain');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SessionStatus { pending, started, completed, missed }
|
||||||
|
|
||||||
|
class Sessions extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get title => text().withLength(min: 3, max: 32)();
|
||||||
|
TextColumn get content => text().named('body')();
|
||||||
|
TextColumn get status => textEnum<SessionStatus>()();
|
||||||
|
TextColumn get achievements => text().nullable()();
|
||||||
|
TextColumn get address => text().withLength(min: 3, max: 256).nullable()();
|
||||||
|
DateTimeColumn get date => dateTime().nullable()();
|
||||||
|
DateTimeColumn get createdAt =>
|
||||||
|
dateTime().withDefault(Variable(DateTime.now()))();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionActivities extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get sessionId =>
|
||||||
|
integer().references(Sessions, #id, onDelete: KeyAction.cascade)();
|
||||||
|
IntColumn get activityId =>
|
||||||
|
integer().references(Activities, #id, onDelete: KeyAction.cascade)();
|
||||||
|
IntColumn get position => integer()();
|
||||||
|
TextColumn get results => text().nullable()();
|
||||||
|
DateTimeColumn get createdAt =>
|
||||||
|
dateTime().withDefault(Variable(DateTime.now()))();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityCategories { fundamentals, conditioning, advanced, custom, pro }
|
||||||
|
|
||||||
|
enum ActivityType {
|
||||||
|
strength,
|
||||||
|
stretching,
|
||||||
|
plyometrics,
|
||||||
|
strongman,
|
||||||
|
powerlifting,
|
||||||
|
cardio,
|
||||||
|
olympicWeightlifting
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityLevel { beginner, intermediate, expert }
|
||||||
|
|
||||||
|
enum ActivityMechanic { compound, isolation }
|
||||||
|
|
||||||
|
enum ActivityEquipment {
|
||||||
|
bodyOnly,
|
||||||
|
machine,
|
||||||
|
other,
|
||||||
|
foamRoll,
|
||||||
|
kettlebells,
|
||||||
|
dumbbell,
|
||||||
|
cable,
|
||||||
|
barbell,
|
||||||
|
bands,
|
||||||
|
medicineBall,
|
||||||
|
exerciseBall,
|
||||||
|
eZCurlBar
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityMuscle {
|
||||||
|
abdominals,
|
||||||
|
hamstrings,
|
||||||
|
calves,
|
||||||
|
shoulders,
|
||||||
|
adductors,
|
||||||
|
glutes,
|
||||||
|
quadriceps,
|
||||||
|
biceps,
|
||||||
|
forearms,
|
||||||
|
abductors,
|
||||||
|
triceps,
|
||||||
|
chest,
|
||||||
|
lowerBack,
|
||||||
|
traps,
|
||||||
|
middleBack,
|
||||||
|
lats,
|
||||||
|
neck
|
||||||
|
}
|
||||||
|
|
||||||
|
class Activities extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get title => text().withLength(min: 3, max: 100)();
|
||||||
|
TextColumn get type => textEnum<ActivityType>().nullable()();
|
||||||
|
TextColumn get description => text().named('body').nullable()();
|
||||||
|
TextColumn get category => textEnum<ActivityCategories>().nullable()();
|
||||||
|
// from exercises.json
|
||||||
|
TextColumn get force => text().nullable()();
|
||||||
|
TextColumn get level => textEnum<ActivityLevel>().nullable()();
|
||||||
|
TextColumn get mechanic => textEnum<ActivityMechanic>().nullable()();
|
||||||
|
TextColumn get equipment => textEnum<ActivityEquipment>().nullable()();
|
||||||
|
TextColumn get primaryMuscles => textEnum<ActivityMuscle>().nullable()();
|
||||||
|
TextColumn get secondaryMuscles => textEnum<ActivityMuscle>().nullable()();
|
||||||
|
DateTimeColumn get createdAt =>
|
||||||
|
dateTime().withDefault(Variable(DateTime.now()))();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityActions extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get activityId => integer().references(Activities, #id, onDelete: KeyAction.cascade)();
|
||||||
|
IntColumn get actionId =>
|
||||||
|
integer().references(Actions, #id, onDelete: KeyAction.cascade)();
|
||||||
|
IntColumn get sessionId => integer().references(Sessions, #id, onDelete: KeyAction.cascade)();
|
||||||
|
IntColumn get position => integer()();
|
||||||
|
DateTimeColumn get createdAt =>
|
||||||
|
dateTime().withDefault(Variable(DateTime.now()))();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepType { time, count }
|
||||||
|
|
||||||
|
enum ActionStatus { pending, started, paused, complete }
|
||||||
|
|
||||||
|
class Actions extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get title => text().withLength(min: 3, max: 64)();
|
||||||
|
TextColumn get description => text().named('body')();
|
||||||
|
IntColumn get totalSets => integer()();
|
||||||
|
TextColumn get totalReps => text().withLength(min: 1, max: 32)();
|
||||||
|
IntColumn get restBeforeSets => integer().nullable()();
|
||||||
|
IntColumn get restBetweenSets => integer().nullable()();
|
||||||
|
IntColumn get restBetweenReps => integer().nullable()();
|
||||||
|
IntColumn get restAfterSets => integer().nullable()();
|
||||||
|
TextColumn get repType => textEnum<RepType>()();
|
||||||
|
IntColumn get repLength => integer().nullable()();
|
||||||
|
TextColumn get repWeights => text().nullable()();
|
||||||
|
TextColumn get setWeights => text().nullable()();
|
||||||
|
BoolColumn get isAlternating => boolean().withDefault(Variable(false))();
|
||||||
|
TextColumn get tempo => text().withLength(min: 6, max: 36).nullable()();
|
||||||
|
TextColumn get status =>
|
||||||
|
textEnum<ActionStatus>().withDefault(Variable('pending'))();
|
||||||
|
TextColumn get state => text().withDefault(Variable(
|
||||||
|
"{\"currentSet\": 0, \"currentRep\": 0, \"currentActionType\": 0, \"currentTime\": 0, \"currentAction\": 0}"))();
|
||||||
|
TextColumn get set => text()();
|
||||||
|
DateTimeColumn get createdAt =>
|
||||||
|
dateTime().withDefault(Variable(DateTime.now()))();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ObjectType {
|
||||||
|
actions,
|
||||||
|
activities,
|
||||||
|
sessions,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectMediaItems extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get objectId => integer()();
|
||||||
|
TextColumn get objectType => textEnum<ObjectType>()();
|
||||||
|
IntColumn get mediaId =>
|
||||||
|
integer().references(MediaItems, #id, onDelete: KeyAction.cascade)();
|
||||||
|
DateTimeColumn get createdAt =>
|
||||||
|
dateTime().withDefault(Variable(DateTime.now()))();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MediaType { youtube, image, location, localImage, localVideo }
|
||||||
|
|
||||||
|
class MediaItems extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get title => text().withLength(min: 3, max: 64)();
|
||||||
|
TextColumn get description => text().named('body')();
|
||||||
|
TextColumn get reference => text()();
|
||||||
|
TextColumn get type => textEnum<MediaType>()();
|
||||||
|
DateTimeColumn get createdAt =>
|
||||||
|
dateTime().withDefault(Variable(DateTime.now()))();
|
||||||
|
}
|
6064
lib/database/database.g.dart
Normal file
6064
lib/database/database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
5899
lib/database/database.steps.dart
Normal file
5899
lib/database/database.steps.dart
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
270
lib/database/seed.dart
Normal file
270
lib/database/seed.dart
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:dart_casing/dart_casing.dart';
|
||||||
|
import 'package:flutter/services.dart' as root_bundle;
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
Future<void> seedDb(AppDatabase database) async {
|
||||||
|
// seed data setup
|
||||||
|
final List<List> sessionValues = [
|
||||||
|
[
|
||||||
|
'Projecting',
|
||||||
|
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
'Climbers Rock Inc.'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Moonboard',
|
||||||
|
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
'Beta Bloc'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Off-Wall Training',
|
||||||
|
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
'Climbers Rock Inc.'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Climbing Outdoors',
|
||||||
|
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
'Gravity Hamilton'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Volume Session',
|
||||||
|
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
'Up the Bloc'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<List> mediaItems = [
|
||||||
|
[
|
||||||
|
'https://www.climbing.com/wp-content/uploads/2022/06/campus-board-e1655470701154.jpeg',
|
||||||
|
MediaType.image
|
||||||
|
],
|
||||||
|
['BgheYcxhrsw', MediaType.youtube]
|
||||||
|
];
|
||||||
|
|
||||||
|
// final List<String> actionTypes = [
|
||||||
|
// "[[{\"actionID\": 0, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 1, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 2, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 3, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 4, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 5, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}]]",
|
||||||
|
// "[[{\"actionID\": 0, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 1, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 2, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 3, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 4, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 5, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 6, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 7, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 8, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 9, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 10, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 11, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 12, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 13, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 14, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 15, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 16, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 17, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 18, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 19, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}]]"
|
||||||
|
// ];
|
||||||
|
|
||||||
|
final int totalSessions = 15;
|
||||||
|
final int totalActivities = 6;
|
||||||
|
// final int totalActions = 5;
|
||||||
|
final int totalMedia = 5;
|
||||||
|
final random = Random();
|
||||||
|
final whitespaceRE = RegExp(r"(?! )\s+| \s+");
|
||||||
|
// we gotta build all the activities!
|
||||||
|
final jsondata =
|
||||||
|
await root_bundle.rootBundle.loadString('assets/exercises.json');
|
||||||
|
final exercises = json.decode(jsondata);
|
||||||
|
List<int> activityIds = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < exercises.length; i++) {
|
||||||
|
var exercise = exercises[i];
|
||||||
|
var images = [];
|
||||||
|
if (exercise['images'] != null) {
|
||||||
|
for (int j = 0; j < exercise['images'].length; j++) {
|
||||||
|
var image = exercise['images'][j];
|
||||||
|
images.add(
|
||||||
|
"https://raw.githubusercontent.com/yuhonas/free-exercise-db/main/exercises/$image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Symbol, Value> payload = {
|
||||||
|
Symbol('title'): Value<String>(
|
||||||
|
exercise['name'].toString().trim().replaceAll(whitespaceRE, " ")),
|
||||||
|
Symbol('description'): Value<String>(json.encode(exercise['instructions']
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.replaceAll(whitespaceRE, " "))),
|
||||||
|
Symbol('force'): Value<String>(exercise['force'] ?? "")
|
||||||
|
};
|
||||||
|
|
||||||
|
// well this fucking sucks
|
||||||
|
if (exercise['category'] != null) {
|
||||||
|
payload[Symbol('type')] = Value<ActivityType>(ActivityType.values
|
||||||
|
.firstWhere((e) =>
|
||||||
|
e.toString() ==
|
||||||
|
"ActivityType.${Casing.camelCase(exercise['category'])}"));
|
||||||
|
}
|
||||||
|
if (exercise['level'] != null) {
|
||||||
|
payload[Symbol('level')] = Value<ActivityLevel>(ActivityLevel.values
|
||||||
|
.firstWhere((e) =>
|
||||||
|
e.toString() ==
|
||||||
|
"ActivityLevel.${Casing.camelCase(exercise['level'])}"));
|
||||||
|
}
|
||||||
|
if (exercise['mechanic'] != null) {
|
||||||
|
payload[Symbol('mechanic')] = Value<ActivityMechanic>(
|
||||||
|
ActivityMechanic.values.firstWhere((e) =>
|
||||||
|
e.toString() ==
|
||||||
|
"ActivityMechanic.${Casing.camelCase(exercise['mechanic'])}"));
|
||||||
|
}
|
||||||
|
if (exercise['equipment'] != null) {
|
||||||
|
payload[Symbol('equipment')] = Value<ActivityEquipment>(
|
||||||
|
ActivityEquipment.values.firstWhere((e) =>
|
||||||
|
e.toString() ==
|
||||||
|
"ActivityEquipment.${Casing.camelCase(exercise['equipment'])}"));
|
||||||
|
}
|
||||||
|
if (exercise['primaryMuscles'].isNotEmpty) {
|
||||||
|
payload[Symbol('primaryMuscles')] = Value<ActivityMuscle>(
|
||||||
|
ActivityMuscle.values.firstWhere((e) =>
|
||||||
|
e.toString() ==
|
||||||
|
"ActivityMuscle.${Casing.camelCase(exercise['primaryMuscles'].first)}"));
|
||||||
|
}
|
||||||
|
if (exercise['secondaryMuscles'].isNotEmpty) {
|
||||||
|
payload[Symbol('secondaryMuscles')] = Value<ActivityMuscle>(
|
||||||
|
ActivityMuscle.values.firstWhere((e) =>
|
||||||
|
e.toString() ==
|
||||||
|
"ActivityMuscle.${Casing.camelCase(exercise['secondaryMuscles'].first)}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
activityIds.add(await database
|
||||||
|
.into(database.activities)
|
||||||
|
.insert(Function.apply(ActivitiesCompanion.new, [], payload))
|
||||||
|
.then((activityId) async {
|
||||||
|
for (int m = 0; m < images.length; m++) {
|
||||||
|
final mediaItem = images[m];
|
||||||
|
await database
|
||||||
|
.into(database.mediaItems)
|
||||||
|
.insert(MediaItemsCompanion.insert(
|
||||||
|
title: exercise['name'],
|
||||||
|
description: exercise['name'],
|
||||||
|
reference: mediaItem,
|
||||||
|
type: MediaType.image))
|
||||||
|
.then((mediaId) async {
|
||||||
|
await database.into(database.objectMediaItems).insert(
|
||||||
|
ObjectMediaItemsCompanion.insert(
|
||||||
|
objectId: activityId,
|
||||||
|
mediaId: mediaId,
|
||||||
|
objectType: ObjectType.activities));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return activityId;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// seed loop
|
||||||
|
for (int i = 0; i < totalSessions; i++) {
|
||||||
|
// session things
|
||||||
|
var status = SessionStatus.completed;
|
||||||
|
if (i == 0) status = SessionStatus.started;
|
||||||
|
if (i == 1) status = SessionStatus.pending;
|
||||||
|
|
||||||
|
final sessionValue = sessionValues[random.nextInt(sessionValues.length)];
|
||||||
|
await database
|
||||||
|
.into(database.sessions)
|
||||||
|
.insert(SessionsCompanion.insert(
|
||||||
|
title: sessionValue[0],
|
||||||
|
content: sessionValue[1],
|
||||||
|
status: status,
|
||||||
|
address: Value(sessionValue[2]),
|
||||||
|
achievements: Value(
|
||||||
|
"[\"achievement 1\", \"achievement 2\", \"achievement 3\"]"),
|
||||||
|
date: Value(DateTime.now())))
|
||||||
|
.then((sessionId) async {
|
||||||
|
// activities things
|
||||||
|
for (int j = 0; j <= random.nextInt(totalActivities); j++) {
|
||||||
|
int activityId = random.nextInt(activityIds.length);
|
||||||
|
activityIds.removeAt(activityId);
|
||||||
|
|
||||||
|
await database
|
||||||
|
.into(database.sessionActivities)
|
||||||
|
.insert(SessionActivitiesCompanion.insert(
|
||||||
|
sessionId: sessionId,
|
||||||
|
activityId: activityId,
|
||||||
|
position: j,
|
||||||
|
results: Value("results json, will need to test"),
|
||||||
|
));
|
||||||
|
|
||||||
|
// actions
|
||||||
|
// await database
|
||||||
|
// .into(database.actions)
|
||||||
|
// .insert(ActionsCompanion.insert(
|
||||||
|
// title: 'Test action',
|
||||||
|
// description:
|
||||||
|
// 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
// totalSets: 5,
|
||||||
|
// totalReps: "[1]",
|
||||||
|
// restBeforeSets: Value(30000),
|
||||||
|
// restBetweenSets: Value(300000),
|
||||||
|
// restBetweenReps: Value(15000),
|
||||||
|
// restAfterSets: Value(300000),
|
||||||
|
// repType: RepType.time,
|
||||||
|
// repLength: Value(10000),
|
||||||
|
// repWeights: Value("[110]"),
|
||||||
|
// setWeights: Value("[1]"),
|
||||||
|
// isAlternating: Value(true),
|
||||||
|
// set: actionTypes[random.nextInt(actionTypes.length)]))
|
||||||
|
// .then((actionId) async {
|
||||||
|
// // add activity action association
|
||||||
|
// await database.into(database.activityActions).insert(
|
||||||
|
// ActivityActionsCompanion.insert(
|
||||||
|
// activityId: activityId, actionId: actionId, sessionId: sessionId, position: 0));
|
||||||
|
// });
|
||||||
|
// for (int k = 0; k <= random.nextInt(totalActions); k++) {
|
||||||
|
// await database
|
||||||
|
// .into(database.actions)
|
||||||
|
// .insert(ActionsCompanion.insert(
|
||||||
|
// title: 'Test action $k',
|
||||||
|
// description:
|
||||||
|
// '$k Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
// totalSets: 5,
|
||||||
|
// totalReps: "[1]",
|
||||||
|
// restBeforeSets: Value(30000),
|
||||||
|
// restBetweenSets: Value(300000),
|
||||||
|
// restBetweenReps: Value(15000),
|
||||||
|
// restAfterSets: Value(300000),
|
||||||
|
// repType: RepType.time,
|
||||||
|
// repLength: Value(10000),
|
||||||
|
// repWeights: Value("[110]"),
|
||||||
|
// setWeights: Value("[1]"),
|
||||||
|
// isAlternating: Value(true),
|
||||||
|
// set: actionTypes[random.nextInt(actionTypes.length)]))
|
||||||
|
// .then((actionId) async {
|
||||||
|
// // add activity action association
|
||||||
|
// await database.into(database.activityActions).insert(
|
||||||
|
// ActivityActionsCompanion.insert(
|
||||||
|
// activityId: activityId, actionId: actionId, position: k));
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int n = 0; n <= random.nextInt(totalMedia); n++) {
|
||||||
|
final mediaItem = mediaItems[random.nextInt(mediaItems.length)];
|
||||||
|
await database
|
||||||
|
.into(database.mediaItems)
|
||||||
|
.insert(MediaItemsCompanion.insert(
|
||||||
|
title: 'Media title $n',
|
||||||
|
description:
|
||||||
|
'Media description $n Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||||
|
reference: mediaItem[0],
|
||||||
|
type: mediaItem[1]))
|
||||||
|
.then((mediaId) async {
|
||||||
|
await database.into(database.objectMediaItems).insert(
|
||||||
|
ObjectMediaItemsCompanion.insert(
|
||||||
|
objectId: sessionId,
|
||||||
|
mediaId: mediaId,
|
||||||
|
objectType: ObjectType.sessions));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await database
|
||||||
|
.into(database.mediaItems)
|
||||||
|
.insert(MediaItemsCompanion.insert(
|
||||||
|
title: 'Locations details',
|
||||||
|
description:
|
||||||
|
'5155 Harvester Rd #1, Burlington, ON L7L 6V2, Canada',
|
||||||
|
reference:
|
||||||
|
'https://lh3.googleusercontent.com/places/ANXAkqHwtb5oRMGG3haJkaHeTxdTI1lQ17RgvkCXwzA1dGV53BXPbHrdXIs1mLC_-4exyRW8dbYhMOeiOCHJqGeVBx-dNtABZAl9tQA=s4800-w800',
|
||||||
|
type: MediaType.location))
|
||||||
|
.then((mediaId) async {
|
||||||
|
await database.into(database.objectMediaItems).insert(
|
||||||
|
ObjectMediaItemsCompanion.insert(
|
||||||
|
objectId: sessionId,
|
||||||
|
mediaId: mediaId,
|
||||||
|
objectType: ObjectType.sessions));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
1
lib/database/sendtrain/drift_schema_v1.json
Normal file
1
lib/database/sendtrain/drift_schema_v1.json
Normal file
File diff suppressed because one or more lines are too long
84
lib/extensions/string_extensions.dart
Normal file
84
lib/extensions/string_extensions.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
List<String> exceptions = [
|
||||||
|
'a',
|
||||||
|
'abaft',
|
||||||
|
'about',
|
||||||
|
'above',
|
||||||
|
'afore',
|
||||||
|
'after',
|
||||||
|
'along',
|
||||||
|
'amid',
|
||||||
|
'among',
|
||||||
|
'an',
|
||||||
|
'apud',
|
||||||
|
'as',
|
||||||
|
'aside',
|
||||||
|
'at',
|
||||||
|
'atop',
|
||||||
|
'below',
|
||||||
|
'but',
|
||||||
|
'by',
|
||||||
|
'circa',
|
||||||
|
'down',
|
||||||
|
'for',
|
||||||
|
'from',
|
||||||
|
'given',
|
||||||
|
'in',
|
||||||
|
'into',
|
||||||
|
'lest',
|
||||||
|
'like',
|
||||||
|
'mid',
|
||||||
|
'midst',
|
||||||
|
'minus',
|
||||||
|
'near',
|
||||||
|
'next',
|
||||||
|
'of',
|
||||||
|
'off',
|
||||||
|
'on',
|
||||||
|
'onto',
|
||||||
|
'out',
|
||||||
|
'over',
|
||||||
|
'pace',
|
||||||
|
'past',
|
||||||
|
'per',
|
||||||
|
'plus',
|
||||||
|
'pro',
|
||||||
|
'qua',
|
||||||
|
'round',
|
||||||
|
'sans',
|
||||||
|
'save',
|
||||||
|
'since',
|
||||||
|
'than',
|
||||||
|
'thru',
|
||||||
|
'till',
|
||||||
|
'times',
|
||||||
|
'to',
|
||||||
|
'under',
|
||||||
|
'until',
|
||||||
|
'unto',
|
||||||
|
'up',
|
||||||
|
'upon',
|
||||||
|
'via',
|
||||||
|
'vice',
|
||||||
|
'with',
|
||||||
|
'worth',
|
||||||
|
'the","and',
|
||||||
|
'nor',
|
||||||
|
'or',
|
||||||
|
'yet',
|
||||||
|
'so'
|
||||||
|
];
|
||||||
|
|
||||||
|
extension TitleCase on String {
|
||||||
|
String toTitleCase() {
|
||||||
|
return toLowerCase().replaceAllMapped(
|
||||||
|
RegExp(
|
||||||
|
r'[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+'),
|
||||||
|
(Match match) {
|
||||||
|
|
||||||
|
// if (exceptions.contains(match[0])) {
|
||||||
|
// return match[0]!;
|
||||||
|
// }
|
||||||
|
return "${match[0]![0].toUpperCase()}${match[0]!.substring(1).toLowerCase()}";
|
||||||
|
}).replaceAll(RegExp(r'(_|-)+'), ' ');
|
||||||
|
}
|
||||||
|
}
|
16
lib/helpers/date_time_helpers.dart
Normal file
16
lib/helpers/date_time_helpers.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
final DateFormat dateFormat = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
|
String formattedTime(int timeInSecond) {
|
||||||
|
int sec = timeInSecond % 60;
|
||||||
|
int min = (timeInSecond / 60).floor();
|
||||||
|
String minute = min.toString().length <= 1 ? "0$min" : "$min";
|
||||||
|
String second = sec.toString().length <= 1 ? "0$sec" : "$sec";
|
||||||
|
return "$minute:$second";
|
||||||
|
}
|
||||||
|
|
||||||
|
int toSeconds(int milliseconds) {
|
||||||
|
int sec = (milliseconds / 1000).floor();
|
||||||
|
return sec;
|
||||||
|
}
|
16
lib/helpers/media_helpers.dart
Normal file
16
lib/helpers/media_helpers.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
ImageProvider findMediaByType(List<MediaItem> media, MediaType type) {
|
||||||
|
Iterable<MediaItem>? found = media.where((m) => m.type == type);
|
||||||
|
Image image;
|
||||||
|
|
||||||
|
if (found.isNotEmpty) {
|
||||||
|
image = Image.network(found.first.reference);
|
||||||
|
} else {
|
||||||
|
// Element is not found
|
||||||
|
image = Image.asset('assets/images/placeholder.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
return image.image;
|
||||||
|
}
|
62
lib/helpers/widget_helpers.dart
Normal file
62
lib/helpers/widget_helpers.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/widgets/media/media_details.dart';
|
||||||
|
|
||||||
|
showMediaDetailWidget(BuildContext context, MediaItem media) {
|
||||||
|
showEditorSheet(context, MediaDetails(media: media));
|
||||||
|
}
|
||||||
|
|
||||||
|
showGenericSheet(BuildContext context, Widget widget,
|
||||||
|
[Color? backgroundColor]) {
|
||||||
|
backgroundColor ??= Theme.of(context).colorScheme.surfaceBright;
|
||||||
|
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)),
|
||||||
|
),
|
||||||
|
context: context,
|
||||||
|
showDragHandle: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return widget;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showEditorSheet(BuildContext context, Widget widget) {
|
||||||
|
showGenericSheet(context, widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonToDescription(List text) {
|
||||||
|
String content = '';
|
||||||
|
|
||||||
|
for (int i = 0; i < text.length; i++) {
|
||||||
|
if (content.isEmpty) {
|
||||||
|
content = text[i];
|
||||||
|
} else {
|
||||||
|
content = "$content\n\n${text[i]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget formItemWrapper(Widget content,
|
||||||
|
[EdgeInsets padding = const EdgeInsets.fromLTRB(0, 0, 0, 0)]) {
|
||||||
|
return Expanded(child: Padding(padding: padding, child: content));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DropdownMenuEntry> numericDropDownItems(String type, int itemLimit) {
|
||||||
|
final List<DropdownMenuEntry> items = [];
|
||||||
|
|
||||||
|
// String entryName = type;
|
||||||
|
|
||||||
|
for (int i = 0; i < itemLimit; i++) {
|
||||||
|
// if (i != 0) entryName = "${type}s";
|
||||||
|
items.add(DropdownMenuEntry(value: i + 1, label: "${i + 1}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
@ -1,15 +1,38 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sendtrain/screens/activities_screen.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:sendtrain/screens/sessions_screen.dart';
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/helpers/widget_helpers.dart';
|
||||||
|
import 'package:sendtrain/models/activity_timer_model.dart';
|
||||||
|
import 'package:sendtrain/providers/action_timer.dart';
|
||||||
|
import 'package:sendtrain/widgets/screens/activities_screen.dart';
|
||||||
|
import 'package:sendtrain/widgets/screens/sessions_screen.dart';
|
||||||
|
// ignore: unused_import
|
||||||
|
import 'package:sendtrain/database/seed.dart';
|
||||||
|
import 'package:sendtrain/widgets/sessions/session_editor.dart';
|
||||||
|
|
||||||
class SendTrain extends StatelessWidget {
|
class SendTrain extends StatelessWidget {
|
||||||
const SendTrain({super.key});
|
const SendTrain({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData themeData = ThemeData.dark(useMaterial3: true);
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: "Sendtrain",
|
title: "Sendtrain",
|
||||||
theme: ThemeData.dark(useMaterial3: true),
|
theme: themeData.copyWith(
|
||||||
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12))),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
fillColor: themeData.colorScheme.surface,
|
||||||
|
),
|
||||||
|
),
|
||||||
home: const App());
|
home: const App());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +65,7 @@ class _AppState extends State<App> {
|
|||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 50, 0, 0),
|
padding: const EdgeInsets.fromLTRB(0, 50, 0, 0),
|
||||||
child: <Widget>[
|
child: <Widget>[
|
||||||
const SessionsScreen(),
|
SessionsScreen(),
|
||||||
const ActivitiesScreen(),
|
const ActivitiesScreen(),
|
||||||
Container(
|
Container(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@ -56,6 +79,10 @@ class _AppState extends State<App> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text('In Progress...'),
|
child: const Text('In Progress...'),
|
||||||
),
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Text('Profile in Progress...'),
|
||||||
|
),
|
||||||
][currentPageIndex]),
|
][currentPageIndex]),
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
onDestinationSelected: (int index) {
|
onDestinationSelected: (int index) {
|
||||||
@ -68,25 +95,37 @@ class _AppState extends State<App> {
|
|||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.sports), label: "Sessions"),
|
icon: Icon(Icons.sports), label: "Sessions"),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.landscape), label: "Activities"),
|
icon: Icon(Icons.sports_gymnastics_rounded),
|
||||||
|
label: "Activities"),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.calendar_month_rounded), label: "Plan"),
|
icon: Icon(Icons.calendar_month_rounded), label: "Plan"),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.group), label: "Team Send"),
|
icon: Icon(Icons.group), label: "Team Send"),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.analytics), label: "Progress")
|
icon: Icon(Icons.analytics), label: "Progress"),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.account_circle_rounded), label: "Profile"),
|
||||||
]),
|
]),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Add your onPressed code here!
|
showEditorSheet(context, SessionEditor());
|
||||||
},
|
},
|
||||||
label: const Text('New Session'),
|
label: const Text('New Session'),
|
||||||
icon: const Icon(Icons.add_chart),
|
icon: const Icon(Icons.add_chart),
|
||||||
backgroundColor: Colors.deepPurple,
|
// backgroundColor: Colors.deepPurple,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const SendTrain());
|
var db = AppDatabase();
|
||||||
|
runApp(MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider<AppDatabase>(
|
||||||
|
create: (context) => db, dispose: (context, db) => db.close()),
|
||||||
|
ChangeNotifierProvider(create: (context) => ActivityTimerModel()),
|
||||||
|
ChangeNotifierProvider(create: (context) => ActionTimer()),
|
||||||
|
],
|
||||||
|
child: const SendTrain(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
260
lib/models/action_model.dart
Normal file
260
lib/models/action_model.dart
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:sendtrain/daos/actions_dao.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/helpers/date_time_helpers.dart';
|
||||||
|
|
||||||
|
class ActionModel {
|
||||||
|
final ActionsDao dao;
|
||||||
|
List<Item> items;
|
||||||
|
Action action;
|
||||||
|
|
||||||
|
ActionModel({required this.action, required AppDatabase db})
|
||||||
|
: dao = ActionsDao(db),
|
||||||
|
items = _generateItems(action);
|
||||||
|
|
||||||
|
int get id => action.id;
|
||||||
|
ActionStatus get status => action.status;
|
||||||
|
Map get state => json.decode(action.state);
|
||||||
|
List<Set> get sets => items.whereType<Set>().toList();
|
||||||
|
List<Item> get allItems => _flattenedItems();
|
||||||
|
int get totalTime {
|
||||||
|
int time = 0;
|
||||||
|
for (int i = 0; i < allItems.length; i++) {
|
||||||
|
Item item = allItems[i];
|
||||||
|
time += item.time ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toSeconds(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Item> _flattenedItems() {
|
||||||
|
List<Item> items = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < this.items.length; i++) {
|
||||||
|
Item item = this.items[i];
|
||||||
|
if (item.runtimeType == Set) {
|
||||||
|
Set setItem = item as Set;
|
||||||
|
for (int j = 0; j < setItem.items.length; j++) {
|
||||||
|
items.add(setItem.items[j]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Item> _generateItems(Action action) {
|
||||||
|
int totalItems = 0;
|
||||||
|
int setItems = 0;
|
||||||
|
List<Item> items = [];
|
||||||
|
final List setReps = json.decode(action.totalReps);
|
||||||
|
|
||||||
|
if (action.restBeforeSets != null) {
|
||||||
|
items.add(Rest(
|
||||||
|
id: totalItems,
|
||||||
|
position: totalItems,
|
||||||
|
action: action,
|
||||||
|
time: action.restBeforeSets!,
|
||||||
|
name: 'prepare'));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < action.totalSets; i++) {
|
||||||
|
final int totalReps;
|
||||||
|
|
||||||
|
if (setReps.length == 1) {
|
||||||
|
totalReps = setReps.first;
|
||||||
|
} else {
|
||||||
|
totalReps = setReps[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
totalItems += 1;
|
||||||
|
items.add(Set(
|
||||||
|
id: totalItems,
|
||||||
|
setOrder: setItems++,
|
||||||
|
position: totalItems,
|
||||||
|
action: action,
|
||||||
|
totalReps: totalReps));
|
||||||
|
|
||||||
|
if (action.restBetweenSets != null && i < action.totalSets - 1) {
|
||||||
|
totalItems += 1;
|
||||||
|
items.add(Rest(
|
||||||
|
id: totalItems,
|
||||||
|
position: totalItems,
|
||||||
|
action: action,
|
||||||
|
time: action.restBetweenSets!,
|
||||||
|
name: 'rest'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.restAfterSets != null && totalItems != items.length) {
|
||||||
|
totalItems += 1;
|
||||||
|
items.add(Rest(
|
||||||
|
id: totalItems,
|
||||||
|
position: totalItems,
|
||||||
|
action: action,
|
||||||
|
time: action.restAfterSets!,
|
||||||
|
name: 'cooldown'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Action> updateStatus(ActionStatus status) async {
|
||||||
|
Action newAction = action.copyWith(id: action.id, status: status);
|
||||||
|
await dao.createOrUpdate(newAction.toCompanion(true));
|
||||||
|
action = newAction;
|
||||||
|
return newAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Action> updateState(String state) async {
|
||||||
|
Action newAction = action.copyWith(id: action.id, state: state);
|
||||||
|
await dao.createOrUpdate(newAction.toCompanion(true));
|
||||||
|
action = newAction;
|
||||||
|
return newAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Item {
|
||||||
|
final int id;
|
||||||
|
final Action action;
|
||||||
|
int position;
|
||||||
|
List<Item> items = [];
|
||||||
|
dynamic value;
|
||||||
|
final String name;
|
||||||
|
int? parentId;
|
||||||
|
int? time;
|
||||||
|
|
||||||
|
Item(
|
||||||
|
{required this.id,
|
||||||
|
required this.position,
|
||||||
|
required this.action,
|
||||||
|
this.parentId,
|
||||||
|
this.time})
|
||||||
|
: name = action.title;
|
||||||
|
|
||||||
|
RepType get valueType => action.repType;
|
||||||
|
String get humanValueType => valueType == RepType.time ? 'seconds' : 'reps';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Set extends Item {
|
||||||
|
final int totalReps;
|
||||||
|
int? setOrder;
|
||||||
|
|
||||||
|
Set(
|
||||||
|
{required super.id,
|
||||||
|
required super.action,
|
||||||
|
required super.position,
|
||||||
|
required this.totalReps,
|
||||||
|
this.setOrder}) {
|
||||||
|
items = _generateItems(action, id, totalReps);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? get weightMultiplyer =>
|
||||||
|
action.setWeights != null ? json.decode(action.setWeights!)[id] : null;
|
||||||
|
List<Reps> get reps => items.whereType<Reps>().toList();
|
||||||
|
|
||||||
|
static List<Item> _generateItems(action, id, totalReps) {
|
||||||
|
List<Item> items = [];
|
||||||
|
// add item for exercise
|
||||||
|
int position = 0;
|
||||||
|
|
||||||
|
if (action.repType == RepType.time) {
|
||||||
|
for (int i = 0; i < totalReps; i++) {
|
||||||
|
position = position > 0 ? position + 1 : position;
|
||||||
|
|
||||||
|
// don't show a rest before first rep
|
||||||
|
if (i > 0) {
|
||||||
|
items.add(Rest(
|
||||||
|
id: position,
|
||||||
|
position: position,
|
||||||
|
parentId: id,
|
||||||
|
action: action,
|
||||||
|
time: action.restBetweenReps,
|
||||||
|
name: 'rest'));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(Reps(
|
||||||
|
id: ++position, position: position, parentId: id, action: action));
|
||||||
|
|
||||||
|
if (action.isAlternating) {
|
||||||
|
items.add(Rest(
|
||||||
|
id: ++position,
|
||||||
|
position: position,
|
||||||
|
parentId: id,
|
||||||
|
action: action,
|
||||||
|
time: action.restBetweenReps,
|
||||||
|
name: 'alternate'));
|
||||||
|
items.add(Reps(
|
||||||
|
id: ++position,
|
||||||
|
position: position,
|
||||||
|
parentId: id,
|
||||||
|
action: action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.add(Reps(id: id, position: position, action: action));
|
||||||
|
|
||||||
|
if (action.isAlternating) {
|
||||||
|
items.add(Rest(
|
||||||
|
id: ++position,
|
||||||
|
position: position,
|
||||||
|
parentId: id,
|
||||||
|
action: action,
|
||||||
|
time: action.restBetweenReps,
|
||||||
|
name: 'alternate'));
|
||||||
|
items.add(Reps(id: id, position: ++position, action: action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reps extends Item {
|
||||||
|
Reps(
|
||||||
|
{required super.id,
|
||||||
|
required super.position,
|
||||||
|
required super.action,
|
||||||
|
super.parentId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic get value => type == RepType.time ? time : count;
|
||||||
|
|
||||||
|
RepType get type => action.repType;
|
||||||
|
@override
|
||||||
|
int? get time => toSeconds(action.repLength!);
|
||||||
|
int? get count => getReps(id, json.decode(action.totalReps));
|
||||||
|
int? get weight =>
|
||||||
|
action.repWeights != null ? json.decode(action.repWeights!)[id] : null;
|
||||||
|
|
||||||
|
static int getReps(setId, reps) {
|
||||||
|
if (reps.length > 1) {
|
||||||
|
return reps[setId];
|
||||||
|
} else {
|
||||||
|
return reps.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rest extends Item {
|
||||||
|
@override
|
||||||
|
String name;
|
||||||
|
|
||||||
|
Rest(
|
||||||
|
{required super.id,
|
||||||
|
required super.position,
|
||||||
|
required super.action,
|
||||||
|
super.parentId,
|
||||||
|
required super.time,
|
||||||
|
required this.name});
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// String get name => 'Rest';
|
||||||
|
@override
|
||||||
|
int get value => toSeconds(time ?? 0);
|
||||||
|
@override
|
||||||
|
RepType get valueType => RepType.time;
|
||||||
|
}
|
165
lib/models/activity_timer_model.dart
Normal file
165
lib/models/activity_timer_model.dart
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
|
||||||
|
class ActivityTimerModel with ChangeNotifier {
|
||||||
|
int _actionCounter = 0;
|
||||||
|
Activity? _activity;
|
||||||
|
List _sets = [];
|
||||||
|
// List _actions = [];
|
||||||
|
int _currentActionNum = 0;
|
||||||
|
int _currentSetNum = 0;
|
||||||
|
Timer? _periodicTimer;
|
||||||
|
double _progress = 0;
|
||||||
|
ItemScrollController? _isc;
|
||||||
|
int _totalTime = 0;
|
||||||
|
|
||||||
|
int get actionCount => _actionCounter;
|
||||||
|
int get currentActionNum => _currentActionNum;
|
||||||
|
dynamic get currentAction => currentSet.isNotEmpty ? currentSet[_currentActionNum] : {};
|
||||||
|
int get currentSetNum => _currentSetNum;
|
||||||
|
dynamic get currentSet => _sets.isNotEmpty ? _sets[_currentSetNum] : {};
|
||||||
|
Activity? get activity => _activity;
|
||||||
|
List get sets => _sets;
|
||||||
|
Timer? get periodicTimer => _periodicTimer;
|
||||||
|
bool get isActive => _isActive();
|
||||||
|
double get progress => _progress;
|
||||||
|
int get totalTime => _totalTime;
|
||||||
|
|
||||||
|
void setup(Activity activity, List actions) {
|
||||||
|
if (_activity == null || activity.id != _activity?.id) {
|
||||||
|
_periodicTimer?.cancel();
|
||||||
|
_progress = 0;
|
||||||
|
_isc = null;
|
||||||
|
_activity = activity;
|
||||||
|
// only one action for now
|
||||||
|
_sets = actions.isNotEmpty ? json.decode(actions[0].set) : [];
|
||||||
|
// _actions = actions;
|
||||||
|
_currentActionNum = 0;
|
||||||
|
_currentSetNum = 0;
|
||||||
|
setActionCount();
|
||||||
|
getTotalTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToIndex(_currentSetNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void getTotalTime() {
|
||||||
|
int time = 0;
|
||||||
|
for(int setIndex = 0; _sets.length > setIndex; setIndex++) {
|
||||||
|
for (int actionIndex = 0; _sets[setIndex].length > actionIndex; actionIndex++) {
|
||||||
|
var action = _sets[setIndex][actionIndex];
|
||||||
|
if (action['type'] == 'seconds') {
|
||||||
|
time = time + action['amount'] as int;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_totalTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_progress = 0;
|
||||||
|
_currentActionNum = 0;
|
||||||
|
_currentSetNum = 0;
|
||||||
|
_periodicTimer!.cancel();
|
||||||
|
setActionCount();
|
||||||
|
moveToIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setScrollController(ItemScrollController isc) {
|
||||||
|
_isc = isc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCurrentItem(int setNum, int actionNum) {
|
||||||
|
if (setNum == _currentSetNum && actionNum == _currentActionNum) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalActions() {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < _sets.length; i++) {
|
||||||
|
count = count + _sets[i].length as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setActionCount() {
|
||||||
|
_actionCounter = currentAction.isNotEmpty ? currentAction['amount'] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
_periodicTimer!.cancel();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
_periodicTimer = Timer.periodic(const Duration(seconds: 1), (Timer timer) {
|
||||||
|
switch (currentAction['type']) {
|
||||||
|
// we don't want to count down
|
||||||
|
// if its repititions
|
||||||
|
case 'repititions':
|
||||||
|
break;
|
||||||
|
case 'seconds':
|
||||||
|
if (_actionCounter > 0) {
|
||||||
|
_actionCounter--;
|
||||||
|
_totalTime--;
|
||||||
|
} else {
|
||||||
|
nextAction(_currentActionNum + 1);
|
||||||
|
setActionCount();
|
||||||
|
}
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProgress() {
|
||||||
|
_progress = (currentAction['actionID'] +
|
||||||
|
(1.0 - _actionCounter / currentAction['amount'])) /
|
||||||
|
totalActions();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAction(int setNum, int actionNum, String type) {
|
||||||
|
_currentActionNum = actionNum;
|
||||||
|
_currentSetNum = setNum;
|
||||||
|
notifyListeners();
|
||||||
|
moveToIndex(_currentSetNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nextAction(int nextActionIndex) {
|
||||||
|
if (currentSet.length > nextActionIndex) {
|
||||||
|
setAction(_currentSetNum, nextActionIndex, 'automatic');
|
||||||
|
} else if (_sets.length > _currentSetNum + 1) {
|
||||||
|
// if the item isn't in the set
|
||||||
|
// increment the set and reset action index
|
||||||
|
setAction(_currentSetNum + 1, 0, 'automatic');
|
||||||
|
} else {
|
||||||
|
// if we're done all the sets
|
||||||
|
// cancel timer and reset activity
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveToIndex(int index) {
|
||||||
|
if (_isc != null && _isc!.isAttached) {
|
||||||
|
_isc?.scrollTo(
|
||||||
|
index: index,
|
||||||
|
duration: Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOutCubic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isActive() {
|
||||||
|
return (_periodicTimer != null && _periodicTimer!.isActive) ? true : false;
|
||||||
|
}
|
||||||
|
}
|
12
lib/models/google_place_model.dart
Normal file
12
lib/models/google_place_model.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class GooglePlaceModel {
|
||||||
|
final String placeId;
|
||||||
|
final String description;
|
||||||
|
final String address;
|
||||||
|
final List<dynamic>? imageReferences;
|
||||||
|
|
||||||
|
GooglePlaceModel(
|
||||||
|
{required this.placeId,
|
||||||
|
required this.description,
|
||||||
|
required this.address,
|
||||||
|
this.imageReferences});
|
||||||
|
}
|
223
lib/providers/action_timer.dart
Normal file
223
lib/providers/action_timer.dart
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_sound/flutter_sound.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/models/action_model.dart';
|
||||||
|
import 'package:vibration/vibration.dart';
|
||||||
|
|
||||||
|
class ActionTimer with ChangeNotifier {
|
||||||
|
ActionModel? actionModel;
|
||||||
|
double _progress = 0;
|
||||||
|
int _currentTime = 0;
|
||||||
|
final List<ItemScrollController> _scrollControllers = [];
|
||||||
|
final FlutterSoundPlayer _mPlayer = FlutterSoundPlayer();
|
||||||
|
|
||||||
|
ActionTimer();
|
||||||
|
|
||||||
|
Map get state => actionModel?.state ?? _stateConstructor();
|
||||||
|
ActionStatus get status => actionModel?.status ?? ActionStatus.pending;
|
||||||
|
bool get started => status == ActionStatus.started;
|
||||||
|
bool get paused => status == ActionStatus.paused;
|
||||||
|
bool get pending => status == ActionStatus.pending;
|
||||||
|
bool get complete => status == ActionStatus.complete;
|
||||||
|
bool get available => paused | pending;
|
||||||
|
List<Set> get sets => actionModel!.sets;
|
||||||
|
List<Item> get items => actionModel!.items;
|
||||||
|
Set get currentSet => sets[state['currentSet']];
|
||||||
|
Reps get currentRep => currentSet.reps[state['currentRep']];
|
||||||
|
Item get currentAction => allActions[state['currentAction']];
|
||||||
|
int get currentTime => _currentTime;
|
||||||
|
dynamic get currentValue => currentAction.valueType == RepType.time
|
||||||
|
? currentTime
|
||||||
|
: currentAction.value;
|
||||||
|
List<Item> get allActions => actionModel?.allItems ?? [];
|
||||||
|
String get repType =>
|
||||||
|
actionModel!.action.repType == RepType.time ? 'Seconds' : 'Reps';
|
||||||
|
int? get repLength => currentRep.value;
|
||||||
|
int? get repCount => currentRep.count;
|
||||||
|
dynamic get repValue =>
|
||||||
|
actionModel!.action.repType == RepType.time ? repLength : repCount;
|
||||||
|
double get progress => _progress;
|
||||||
|
int get totalTime => actionModel!.totalTime;
|
||||||
|
Timer? _periodicTimer;
|
||||||
|
|
||||||
|
Map _stateConstructor() {
|
||||||
|
return {
|
||||||
|
'currentSet': 0,
|
||||||
|
'currentRep': 0,
|
||||||
|
'currentTime': 0,
|
||||||
|
'currentAction': 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup(ActionModel actionModel, ItemScrollController scrollController,
|
||||||
|
[bool resetOnLoad = true]) async {
|
||||||
|
_scrollControllers.clear();
|
||||||
|
_scrollControllers.add(scrollController);
|
||||||
|
|
||||||
|
if (resetOnLoad) {
|
||||||
|
if (this.actionModel == actionModel) {
|
||||||
|
reset();
|
||||||
|
_scrollControllers.add(scrollController);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionModel = actionModel;
|
||||||
|
setAction(currentAction.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future pause() async =>
|
||||||
|
await actionModel?.updateStatus(ActionStatus.paused).whenComplete(() {
|
||||||
|
_periodicTimer?.cancel();
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// _mPlayer.stopPlayer();
|
||||||
|
// Be careful : you must `close` the audio session when you have finished with it.
|
||||||
|
});
|
||||||
|
|
||||||
|
Future start() async {
|
||||||
|
await actionModel!.updateStatus(ActionStatus.started);
|
||||||
|
await _mPlayer.openPlayer();
|
||||||
|
|
||||||
|
Uint8List? countTone;
|
||||||
|
Uint8List? finishTone;
|
||||||
|
await rootBundle
|
||||||
|
.load('assets/audio/count_tone.mp3')
|
||||||
|
.then((data) => countTone = data.buffer.asUint8List());
|
||||||
|
await rootBundle
|
||||||
|
.load('assets/audio/count_finish.mp3')
|
||||||
|
.then((data) => finishTone = data.buffer.asUint8List());
|
||||||
|
|
||||||
|
// start timer
|
||||||
|
if (_periodicTimer == null || _periodicTimer!.isActive == false) {
|
||||||
|
_periodicTimer =
|
||||||
|
Timer.periodic(const Duration(seconds: 1), (Timer timer) async {
|
||||||
|
switch (currentAction.valueType) {
|
||||||
|
case RepType.count:
|
||||||
|
break;
|
||||||
|
case RepType.time:
|
||||||
|
_currentTime--;
|
||||||
|
|
||||||
|
if (_currentTime <= 3 && _currentTime != 0) {
|
||||||
|
await _mPlayer
|
||||||
|
.startPlayer(fromDataBuffer: countTone, codec: Codec.mp3)
|
||||||
|
.then((duration) async {
|
||||||
|
if (await Vibration.hasVibrator()) {
|
||||||
|
Vibration.vibrate(duration: 250);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentTime == 0) {
|
||||||
|
// move to next action
|
||||||
|
await _mPlayer
|
||||||
|
.startPlayer(fromDataBuffer: finishTone, codec: Codec.mp3)
|
||||||
|
.then((duration) async {
|
||||||
|
if (await Vibration.hasVibrator()) {
|
||||||
|
Vibration.vibrate(duration: 250);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await setAction(state['currentAction'] + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProgress();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future close() async => await actionModel!
|
||||||
|
.updateStatus(ActionStatus.complete)
|
||||||
|
.whenComplete(() async {
|
||||||
|
_periodicTimer!.cancel();
|
||||||
|
_mPlayer.closePlayer();
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
Future reset() async {
|
||||||
|
await actionModel?.updateStatus(ActionStatus.pending);
|
||||||
|
await actionModel?.updateState(json.encode(_stateConstructor()));
|
||||||
|
_periodicTimer?.cancel();
|
||||||
|
_progress = 0;
|
||||||
|
_scrollControllers.clear();
|
||||||
|
_mPlayer.closePlayer();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future clear() async {
|
||||||
|
await reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
double timeUsed() {
|
||||||
|
Iterable<Item> usedItems = allActions.getRange(0, state['currentAction']);
|
||||||
|
return usedItems.fold(0.0, (p, c) => p + c.value!);
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalComplete() {
|
||||||
|
Iterable<Item> usedItems = allActions.getRange(0, state['currentAction']);
|
||||||
|
return usedItems.length / allActions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress() {
|
||||||
|
double repUsed = (currentAction.value - currentTime) / currentAction.value;
|
||||||
|
_progress =
|
||||||
|
totalComplete() + ((repUsed < 0 ? 0 : repUsed) / allActions.length);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAction(int actionNum, [bool isManual = false]) async {
|
||||||
|
if (actionNum < allActions.length) {
|
||||||
|
Item item = allActions[actionNum];
|
||||||
|
Map newState = state;
|
||||||
|
|
||||||
|
newState['currentAction'] = actionNum;
|
||||||
|
newState['currentSet'] = item.parentId;
|
||||||
|
newState['currentRep'] = item.id;
|
||||||
|
newState['currentTime'] = _currentTime = item.value!;
|
||||||
|
|
||||||
|
await actionModel!
|
||||||
|
.updateState(json.encode(newState))
|
||||||
|
.whenComplete(() async {
|
||||||
|
// if manual select, pause next action
|
||||||
|
if (isManual) {
|
||||||
|
await pause();
|
||||||
|
await updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = currentAction.parentId != null
|
||||||
|
? currentAction.parentId!
|
||||||
|
: currentAction.id;
|
||||||
|
|
||||||
|
if (_scrollControllers.isNotEmpty) {
|
||||||
|
for (int i = 0; i < _scrollControllers.length; i++) {
|
||||||
|
ItemScrollController sc = _scrollControllers[i];
|
||||||
|
|
||||||
|
sc.scrollTo(
|
||||||
|
index: index,
|
||||||
|
duration: Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOutCubic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// _scrollController?.scrollTo(
|
||||||
|
// index: index,
|
||||||
|
// duration: Duration(milliseconds: 500),
|
||||||
|
// curve: Curves.easeInOutCubic);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await actionModel?.updateStatus(ActionStatus.complete).whenComplete(() {
|
||||||
|
_periodicTimer?.cancel();
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../widgets/activities_header.dart';
|
|
||||||
import '../widgets/activity_card.dart';
|
|
||||||
|
|
||||||
class ActivitiesScreen extends StatefulWidget {
|
|
||||||
const ActivitiesScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _ActivitiesScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActivitiesScreenState extends State<ActivitiesScreen> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<Widget> activities = List.generate(10, (i) => ActivityCard());
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
const ActivitiesHeader(),
|
|
||||||
Expanded(
|
|
||||||
child: GridView.count(
|
|
||||||
primary: false,
|
|
||||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
|
||||||
crossAxisSpacing: 10,
|
|
||||||
mainAxisSpacing: 10,
|
|
||||||
crossAxisCount: 2,
|
|
||||||
children: activities,
|
|
||||||
))
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../widgets/session_card.dart';
|
|
||||||
|
|
||||||
class SessionsScreen extends StatelessWidget {
|
|
||||||
const SessionsScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<Widget> previousSessions =
|
|
||||||
List.generate(10, (i) => const SessionCard(state: 1, type: 1));
|
|
||||||
Widget upcomingSession = const SessionCard(state: 2);
|
|
||||||
Widget currentSession = const SessionCard();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(15, 5, 0, 0),
|
|
||||||
child: Text(
|
|
||||||
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
|
||||||
'Current:')),
|
|
||||||
currentSession,
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
|
||||||
child: Text(
|
|
||||||
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
|
||||||
'Upcoming:')),
|
|
||||||
upcomingSession,
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
|
||||||
child: Text(
|
|
||||||
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
|
||||||
'Previous:')),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 160,
|
|
||||||
child: GridView.count(
|
|
||||||
padding: const EdgeInsets.fromLTRB(15, 10, 0, 0),
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
crossAxisSpacing: 5,
|
|
||||||
mainAxisSpacing: 5,
|
|
||||||
crossAxisCount: 1,
|
|
||||||
children: previousSessions))
|
|
||||||
// Flexible(
|
|
||||||
// child: ListView(
|
|
||||||
// scrollDirection: Axis.vertical,
|
|
||||||
// children: previousSessions,
|
|
||||||
// )),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
68
lib/services/functional/debouncer.dart
Normal file
68
lib/services/functional/debouncer.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
|
||||||
|
|
||||||
|
class Debouncer {
|
||||||
|
Debouncer(this._duration, this._callback);
|
||||||
|
|
||||||
|
final Duration _duration;
|
||||||
|
final dynamic _callback;
|
||||||
|
late final _Debounceable<dynamic, String> _debouncedSearch = _debounce<dynamic, String>(_callback);
|
||||||
|
|
||||||
|
/// Returns a new function that is a debounced version of the given function.
|
||||||
|
///
|
||||||
|
/// This means that the original function will be called only after no calls
|
||||||
|
/// have been made for the given Duration.
|
||||||
|
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
|
||||||
|
_DebounceTimer? debounceTimer;
|
||||||
|
|
||||||
|
return (T parameter) async {
|
||||||
|
if (debounceTimer != null && !debounceTimer!.isCompleted) {
|
||||||
|
debounceTimer!.cancel();
|
||||||
|
}
|
||||||
|
debounceTimer = _DebounceTimer(_duration);
|
||||||
|
try {
|
||||||
|
await debounceTimer!.future;
|
||||||
|
} on _CancelException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return function(parameter);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
process(data) {
|
||||||
|
return _debouncedSearch(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wrapper around Timer used for debouncing.
|
||||||
|
class _DebounceTimer {
|
||||||
|
final Duration debounceDuration;
|
||||||
|
|
||||||
|
_DebounceTimer(
|
||||||
|
this.debounceDuration
|
||||||
|
) {
|
||||||
|
_timer = Timer(debounceDuration, _onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final Timer _timer;
|
||||||
|
final Completer<void> _completer = Completer<void>();
|
||||||
|
|
||||||
|
void _onComplete() {
|
||||||
|
_completer.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> get future => _completer.future;
|
||||||
|
|
||||||
|
bool get isCompleted => _completer.isCompleted;
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
_timer.cancel();
|
||||||
|
_completer.completeError(const _CancelException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An exception indicating that the timer was canceled.
|
||||||
|
class _CancelException implements Exception {
|
||||||
|
const _CancelException();
|
||||||
|
}
|
43
lib/services/search/activity_finder_service.dart
Normal file
43
lib/services/search/activity_finder_service.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:sendtrain/daos/activities_dao.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/helpers/widget_helpers.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/form_search_input.dart';
|
||||||
|
|
||||||
|
class ActivityFinderService {
|
||||||
|
final BuildContext context;
|
||||||
|
final ActivitiesDao dao;
|
||||||
|
|
||||||
|
ActivityFinderService(this.context)
|
||||||
|
: dao = ActivitiesDao(Provider.of<AppDatabase>(context, listen: false));
|
||||||
|
|
||||||
|
void finish() {}
|
||||||
|
|
||||||
|
Future<List<Suggestion>?> fetchSuggestions(String input) async {
|
||||||
|
List<Activity> activities = await dao.contains(input);
|
||||||
|
|
||||||
|
if (activities.isNotEmpty) {
|
||||||
|
return activities
|
||||||
|
.map<Suggestion>((activity) => Suggestion<Activity>(activity))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget resultWidget(Activity activity, Function? callback) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(activity.title),
|
||||||
|
subtitle: Text(jsonToDescription(json.decode(activity.description ?? "")),
|
||||||
|
maxLines: 2, softWrap: true, overflow: TextOverflow.ellipsis),
|
||||||
|
onTap: () {
|
||||||
|
if (callback != null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
90
lib/services/search/google_places_service.dart
Normal file
90
lib/services/search/google_places_service.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:sendtrain/models/google_place_model.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/form_search_input.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class GooglePlacesService {
|
||||||
|
final sessionToken = Uuid().v4();
|
||||||
|
final apiKey = "AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE";
|
||||||
|
final client = Client();
|
||||||
|
|
||||||
|
void finish() {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Suggestion>?> fetchSuggestions(String input) async {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Goog-Api-Key': apiKey,
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
'X-Goog-FieldMask':
|
||||||
|
'places.displayName,places.id,places.formattedAddress,places.photos'
|
||||||
|
};
|
||||||
|
var request = Request('POST',
|
||||||
|
Uri.parse('https://places.googleapis.com/v1/places:searchText'));
|
||||||
|
request.body = json.encode({"textQuery": input});
|
||||||
|
request.headers.addAll(headers);
|
||||||
|
|
||||||
|
StreamedResponse response = await request.send();
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final result = json.decode(await response.stream.bytesToString());
|
||||||
|
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
return result['places']
|
||||||
|
.map<Suggestion>((p) => Suggestion<GooglePlaceModel>(
|
||||||
|
GooglePlaceModel(
|
||||||
|
placeId: p['id'],
|
||||||
|
description: p['displayName']['text'],
|
||||||
|
address: p['formattedAddress'],
|
||||||
|
imageReferences: p['photos'])))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception(response.reasonPhrase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future fetchPhoto(String name) async {
|
||||||
|
var headers = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
};
|
||||||
|
|
||||||
|
var request = Request(
|
||||||
|
'GET',
|
||||||
|
Uri.parse(
|
||||||
|
'https://places.googleapis.com/v1/$name/media?key=$apiKey&maxWidthPx=800&skipHttpRedirect=true'));
|
||||||
|
request.headers.addAll(headers);
|
||||||
|
|
||||||
|
StreamedResponse response = await request.send();
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final result = json.decode(await response.stream.bytesToString());
|
||||||
|
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception(response.reasonPhrase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget resultWidget(GooglePlaceModel place, Function? callback) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(place.description),
|
||||||
|
onTap: () async {
|
||||||
|
if (callback != null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
lib/widgets/achievements/achievement_editor.dart
Normal file
63
lib/widgets/achievements/achievement_editor.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:sendtrain/daos/sessions_dao.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/form_text_input.dart';
|
||||||
|
|
||||||
|
class AchievementEditor extends StatelessWidget {
|
||||||
|
AchievementEditor({super.key, required this.session, this.callback});
|
||||||
|
|
||||||
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||||
|
final TextEditingController tec = TextEditingController();
|
||||||
|
final Session session;
|
||||||
|
final Function? callback;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(15, 0, 15, 15),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 10, bottom: 10),
|
||||||
|
child: Text('Create Achievement',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge)),
|
||||||
|
FormTextInput(controller: tec, title: 'Achievement', icon: Icon(Icons.military_tech_rounded)),
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 10),
|
||||||
|
child: FilledButton(
|
||||||
|
child: Text('Submit'),
|
||||||
|
onPressed: () async {
|
||||||
|
session.achievements;
|
||||||
|
List achievements =
|
||||||
|
json.decode(session.achievements ?? "[]");
|
||||||
|
achievements.add(tec.text);
|
||||||
|
Session updatedSession = session.copyWith(
|
||||||
|
achievements:
|
||||||
|
Value<String>(json.encode(achievements)));
|
||||||
|
|
||||||
|
SessionsDao(Provider.of<AppDatabase>(context,
|
||||||
|
listen: false))
|
||||||
|
.replace(updatedSession);
|
||||||
|
|
||||||
|
Navigator.pop(_formKey.currentContext!, 'Submit');
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
await callback!();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
])
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../widgets/activity_type_filter.dart';
|
import 'activity_type_filter.dart';
|
||||||
|
|
||||||
class ActivitiesHeader extends StatefulWidget {
|
class ActivitiesHeader extends StatefulWidget {
|
||||||
const ActivitiesHeader({super.key});
|
const ActivitiesHeader({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ActivitiesHeaderState createState() => _ActivitiesHeaderState();
|
State<ActivitiesHeader> createState() => ActivitiesHeaderState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActivitiesHeaderState extends State<ActivitiesHeader> {
|
class ActivitiesHeaderState extends State<ActivitiesHeader> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
305
lib/widgets/activities/activity_action_editor.dart
Normal file
305
lib/widgets/activities/activity_action_editor.dart
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
|
import 'package:flutter/material.dart' hide Action;
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:sendtrain/daos/actions_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/activity_actions_dao.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/helpers/widget_helpers.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/form_drop_down.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/form_text_input.dart';
|
||||||
|
|
||||||
|
class ActivityActionEditor extends StatefulWidget {
|
||||||
|
const ActivityActionEditor(
|
||||||
|
{super.key,
|
||||||
|
required this.session,
|
||||||
|
required this.activity,
|
||||||
|
this.action,
|
||||||
|
this.callback});
|
||||||
|
|
||||||
|
final Session session;
|
||||||
|
final Activity activity;
|
||||||
|
final Action? action;
|
||||||
|
final Function? callback;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ActivityActionEditor> createState() => _ActivityActionEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActivityActionEditorState extends State<ActivityActionEditor> {
|
||||||
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
final Map<String, TextEditingController> actionEditController = {
|
||||||
|
'sets': TextEditingController(),
|
||||||
|
'reps': TextEditingController(),
|
||||||
|
'weight': TextEditingController(),
|
||||||
|
'repLength': TextEditingController(),
|
||||||
|
'preparation': TextEditingController(),
|
||||||
|
'setRest': TextEditingController(),
|
||||||
|
'repRest': TextEditingController(),
|
||||||
|
'cooldown': TextEditingController(),
|
||||||
|
'type': TextEditingController(),
|
||||||
|
'alternating': TextEditingController(),
|
||||||
|
};
|
||||||
|
|
||||||
|
late final AppDatabase db;
|
||||||
|
|
||||||
|
bool isAlternating = false;
|
||||||
|
bool isTimed = false;
|
||||||
|
String editorType = 'Create';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
|
||||||
|
// if we're editing a session, we'll want to populate it with the appropriate values
|
||||||
|
if (widget.action != null) {
|
||||||
|
final Action action = widget.action!;
|
||||||
|
editorType = 'Edit';
|
||||||
|
isAlternating = action.isAlternating;
|
||||||
|
isTimed = action.repType == RepType.time ? true : false;
|
||||||
|
|
||||||
|
actionEditController['sets']?.text = action.totalSets.toString();
|
||||||
|
actionEditController['reps']?.text =
|
||||||
|
json.decode(action.totalReps)[0].toString();
|
||||||
|
actionEditController['weight']?.text =
|
||||||
|
json.decode(action.repWeights ?? "")[0].toString();
|
||||||
|
actionEditController['repLength']?.text =
|
||||||
|
((action.repLength ?? 0) ~/ 1000).toString();
|
||||||
|
actionEditController['preparation']?.text =
|
||||||
|
((action.restBeforeSets ?? 0) ~/ 1000).toString();
|
||||||
|
actionEditController['setRest']?.text =
|
||||||
|
((action.restBetweenSets ?? 0) ~/ 1000).toString();
|
||||||
|
actionEditController['repRest']?.text =
|
||||||
|
((action.restBetweenReps ?? 0) ~/ 1000).toString();
|
||||||
|
actionEditController['cooldown']?.text =
|
||||||
|
((action.restAfterSets ?? 0) ~/ 1000).toString();
|
||||||
|
actionEditController['isTimed']?.text = isTimed.toString();
|
||||||
|
actionEditController['alternating']?.text = isAlternating.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.action != null) {
|
||||||
|
editorType = 'Edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(15, 0, 15, 15),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 10, bottom: 10),
|
||||||
|
child: Text('$editorType Action',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge)),
|
||||||
|
Row(children: [
|
||||||
|
formItemWrapper(
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text("Reps alternate? (eg. Left/Right Hand)"),
|
||||||
|
value: isAlternating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(10.0)),
|
||||||
|
),
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
setState(() {
|
||||||
|
isAlternating = value!;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
EdgeInsets.fromLTRB(10, 10, 10, 10)),
|
||||||
|
]),
|
||||||
|
Row(children: [
|
||||||
|
formItemWrapper(
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text("Are reps timed?"),
|
||||||
|
value: isTimed,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(10.0)),
|
||||||
|
),
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
setState(() {
|
||||||
|
isTimed = value!;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
EdgeInsets.fromLTRB(10, 10, 10, 15))
|
||||||
|
]),
|
||||||
|
Row(children: [
|
||||||
|
FormDropDown(
|
||||||
|
title: 'Sets',
|
||||||
|
entries: numericDropDownItems('Set', 50),
|
||||||
|
controller: actionEditController['sets']!),
|
||||||
|
FormDropDown(
|
||||||
|
title: 'Reps',
|
||||||
|
entries: numericDropDownItems('Rep', 100),
|
||||||
|
controller: actionEditController['reps']!,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
Row(children: [
|
||||||
|
formItemWrapper(
|
||||||
|
FormTextInput(
|
||||||
|
type: InputTypes.number,
|
||||||
|
controller: actionEditController['preparation']!,
|
||||||
|
title: 'Preparation (sec)',
|
||||||
|
hint: 'time before start',
|
||||||
|
requiresValidation: false),
|
||||||
|
EdgeInsets.fromLTRB(10, 5, 10, 0)),
|
||||||
|
formItemWrapper(
|
||||||
|
FormTextInput(
|
||||||
|
type: InputTypes.number,
|
||||||
|
controller: actionEditController['cooldown']!,
|
||||||
|
title: 'Cooldown (sec)',
|
||||||
|
hint: 'rest after completion',
|
||||||
|
requiresValidation: false),
|
||||||
|
EdgeInsets.fromLTRB(10, 5, 10, 0)),
|
||||||
|
]),
|
||||||
|
Row(children: [
|
||||||
|
formItemWrapper(
|
||||||
|
FormTextInput(
|
||||||
|
type: InputTypes.number,
|
||||||
|
controller: actionEditController['setRest']!,
|
||||||
|
title: 'Set Rest (sec)',
|
||||||
|
hint: 'Rest between sets',
|
||||||
|
requiresValidation: false),
|
||||||
|
EdgeInsets.only(left: 10, right: 10)),
|
||||||
|
formItemWrapper(
|
||||||
|
FormTextInput(
|
||||||
|
type: InputTypes.number,
|
||||||
|
controller: actionEditController['repRest']!,
|
||||||
|
title: 'Rep Rest (sec)',
|
||||||
|
hint: 'Rest between reps',
|
||||||
|
requiresValidation: false),
|
||||||
|
EdgeInsets.only(left: 10, right: 10)),
|
||||||
|
]),
|
||||||
|
Row(children: [
|
||||||
|
formItemWrapper(
|
||||||
|
FormTextInput(
|
||||||
|
type: InputTypes.number,
|
||||||
|
controller: actionEditController['repLength']!,
|
||||||
|
title: 'Rep Length (sec)',
|
||||||
|
hint: 'Total rep time (not required)',
|
||||||
|
requiresValidation: false),
|
||||||
|
EdgeInsets.only(left: 10, right: 10)),
|
||||||
|
formItemWrapper(
|
||||||
|
FormTextInput(
|
||||||
|
type: InputTypes.number,
|
||||||
|
controller: actionEditController['weight']!,
|
||||||
|
title: 'Weight',
|
||||||
|
hint: 'Weight for reps',
|
||||||
|
requiresValidation: false),
|
||||||
|
EdgeInsets.only(left: 10, right: 10)),
|
||||||
|
]),
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 10, right: 10),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
if (widget.action != null) {
|
||||||
|
Action newAction = widget.action!.copyWith(
|
||||||
|
totalSets: int.parse(
|
||||||
|
actionEditController['sets']!.text),
|
||||||
|
totalReps: json.encode([
|
||||||
|
int.parse(
|
||||||
|
actionEditController['reps']!.text)
|
||||||
|
]),
|
||||||
|
repLength: Value<int>(int.parse(
|
||||||
|
actionEditController['repLength']!
|
||||||
|
.text) *
|
||||||
|
1000),
|
||||||
|
restBeforeSets: Value<int>(int.parse(
|
||||||
|
actionEditController['preparation']!
|
||||||
|
.text) *
|
||||||
|
1000),
|
||||||
|
restBetweenSets: Value<int>(int.parse(
|
||||||
|
actionEditController['setRest']!
|
||||||
|
.text) *
|
||||||
|
1000),
|
||||||
|
restBetweenReps: Value<int>(int.parse(
|
||||||
|
actionEditController['repRest']!
|
||||||
|
.text) *
|
||||||
|
1000),
|
||||||
|
restAfterSets: Value<int>(int.parse(
|
||||||
|
actionEditController['cooldown']!
|
||||||
|
.text) *
|
||||||
|
1000),
|
||||||
|
repType: int.parse(actionEditController[
|
||||||
|
'repLength']!
|
||||||
|
.text) >
|
||||||
|
0
|
||||||
|
? RepType.time
|
||||||
|
: RepType.count,
|
||||||
|
repWeights: Value<String>(json.encode([
|
||||||
|
int.parse(
|
||||||
|
actionEditController['weight']!.text)
|
||||||
|
])),
|
||||||
|
// setWeights: Value<String>(json.encode([actionEditController['setWeights']!.text])),
|
||||||
|
isAlternating: isAlternating,
|
||||||
|
);
|
||||||
|
|
||||||
|
// var result = await ActionsDao(db).createOrUpdate(
|
||||||
|
// newAction.toCompanion(true));
|
||||||
|
await ActionsDao(db).replace(newAction);
|
||||||
|
} else {
|
||||||
|
// create action
|
||||||
|
await ActionsDao(db)
|
||||||
|
.createOrUpdate(ActionsCompanion(
|
||||||
|
title: Value('rep'),
|
||||||
|
description: Value('exercise action'),
|
||||||
|
totalSets: Value(int.parse(
|
||||||
|
actionEditController['sets']!
|
||||||
|
.text)),
|
||||||
|
totalReps: Value(json.encode(
|
||||||
|
[int.parse(actionEditController['reps']!.text)])),
|
||||||
|
repLength: Value<int>(
|
||||||
|
int.parse(actionEditController['repLength']!.text) *
|
||||||
|
1000),
|
||||||
|
restBeforeSets: Value<int>(
|
||||||
|
int.parse(actionEditController['preparation']!.text) *
|
||||||
|
1000),
|
||||||
|
restBetweenSets: Value<int>(
|
||||||
|
int.parse(actionEditController['setRest']!.text) *
|
||||||
|
1000),
|
||||||
|
restBetweenReps:
|
||||||
|
Value<int>(int.parse(actionEditController['repRest']!.text) * 1000),
|
||||||
|
restAfterSets: Value<int>(int.parse(actionEditController['cooldown']!.text) * 1000),
|
||||||
|
repType: Value(int.parse(actionEditController['repLength']!.text) > 0 ? RepType.time : RepType.count),
|
||||||
|
repWeights: Value<String>(json.encode([int.parse(actionEditController['weight']!.text)])),
|
||||||
|
// setWeights: Value<String>(json.encode([actionEditController['setWeights']!.text])),
|
||||||
|
isAlternating: Value<bool>(isAlternating),
|
||||||
|
// repType: RepType.values.firstWhere((e) => e.toString() == "RepType.${actionEditController['repType']!.text}"),
|
||||||
|
set: Value("")))
|
||||||
|
.then((actionId) {
|
||||||
|
ActivityActionsDao(db).createOrUpdate(
|
||||||
|
ActivityActionsCompanion(
|
||||||
|
activityId:
|
||||||
|
Value(widget.activity.id),
|
||||||
|
sessionId: Value(widget.session.id),
|
||||||
|
actionId: Value(actionId),
|
||||||
|
position: Value(0)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.pop(
|
||||||
|
_formKey.currentContext!, 'Submit');
|
||||||
|
|
||||||
|
if (widget.callback != null) {
|
||||||
|
await widget.callback!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('Submit')))
|
||||||
|
])
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
250
lib/widgets/activities/activity_action_view.dart
Normal file
250
lib/widgets/activities/activity_action_view.dart
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/extensions/string_extensions.dart';
|
||||||
|
import 'package:sendtrain/helpers/widget_helpers.dart';
|
||||||
|
import 'package:sendtrain/models/action_model.dart';
|
||||||
|
import 'package:sendtrain/providers/action_timer.dart';
|
||||||
|
import 'package:sendtrain/widgets/activities/activity_action_editor.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/add_card_generic.dart';
|
||||||
|
|
||||||
|
// class ActivityActionView extends StatefulWidget {
|
||||||
|
class ActivityActionView extends StatelessWidget {
|
||||||
|
ActivityActionView(
|
||||||
|
{super.key,
|
||||||
|
required this.session,
|
||||||
|
required this.activity,
|
||||||
|
required this.actions,
|
||||||
|
this.callback,
|
||||||
|
this.resetOnLoad = true});
|
||||||
|
final Session session;
|
||||||
|
final Activity activity;
|
||||||
|
final List actions;
|
||||||
|
final Function? callback;
|
||||||
|
final bool resetOnLoad;
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<ActivityActionView> createState() => ActivityActionViewState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class ActivityActionViewState extends State<ActivityActionView> {
|
||||||
|
// class ActivityActionView extends StatelessWidget {
|
||||||
|
// ActivityActionView({super.key, required this.actions});
|
||||||
|
|
||||||
|
// final List actions;
|
||||||
|
final ItemScrollController itemScrollController = ItemScrollController();
|
||||||
|
final ScrollOffsetController scrollOffsetController =
|
||||||
|
ScrollOffsetController();
|
||||||
|
final ItemPositionsListener itemPositionsListener =
|
||||||
|
ItemPositionsListener.create();
|
||||||
|
final ScrollOffsetListener scrollOffsetListener =
|
||||||
|
ScrollOffsetListener.create();
|
||||||
|
|
||||||
|
late final ActionTimer at;
|
||||||
|
// int actionCount = 0;
|
||||||
|
|
||||||
|
GestureDetector gtBuild(
|
||||||
|
ActionTimer at, Item item, int actionNum, int selectedIndex,
|
||||||
|
{int? order}) {
|
||||||
|
// default, for rests
|
||||||
|
String setItemRef = '-';
|
||||||
|
|
||||||
|
// non rests decimal reference to item
|
||||||
|
if (order != null) {
|
||||||
|
setItemRef = '${order + 1}.${item.position + 1}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(onTap: () {
|
||||||
|
at.setAction(actionNum, true);
|
||||||
|
}, child: Consumer<ActionTimer>(builder: (context, at, child) {
|
||||||
|
return Row(children: [
|
||||||
|
Ink(
|
||||||
|
width: 70,
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
color: item == at.currentAction
|
||||||
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
|
: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
child: Text(textAlign: TextAlign.center, setItemRef)),
|
||||||
|
Expanded(
|
||||||
|
child: Ink(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
color: item == at.currentAction
|
||||||
|
? Theme.of(context).colorScheme.surfaceBright
|
||||||
|
: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
child: Text(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
'${item.name}: ${item.value} ${item.humanValueType}'
|
||||||
|
.toTitleCase())))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// void initState() {
|
||||||
|
// super.initState();
|
||||||
|
// at = Provider.of<ActionTimer>(context, listen: false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
at = Provider.of<ActionTimer>(context, listen: false);
|
||||||
|
int actionCount = 0;
|
||||||
|
if (actions.isNotEmpty) {
|
||||||
|
at.setup(
|
||||||
|
ActionModel(
|
||||||
|
action: actions.first,
|
||||||
|
db: Provider.of<AppDatabase>(context)),
|
||||||
|
itemScrollController,
|
||||||
|
resetOnLoad);
|
||||||
|
|
||||||
|
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// if (itemScrollController.isAttached) {
|
||||||
|
// itemScrollController.scrollTo(
|
||||||
|
// index: at.currentAction.parentId != null
|
||||||
|
// ? at.currentAction.parentId!
|
||||||
|
// : at.currentAction.id,
|
||||||
|
// duration: Duration(milliseconds: 500),
|
||||||
|
// curve: Curves.easeInOutCubic);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: Column(children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||||
|
child: Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10),
|
||||||
|
topRight: Radius.circular(10)),
|
||||||
|
),
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
child: Row(children: [
|
||||||
|
Ink(
|
||||||
|
width: 70,
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Consumer<ActionTimer>(
|
||||||
|
builder: (context, at, child) {
|
||||||
|
return IconButton(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
icon: at.available
|
||||||
|
? const Icon(Icons.play_arrow_rounded)
|
||||||
|
: const Icon(Icons.pause_rounded),
|
||||||
|
onPressed: () => {
|
||||||
|
if (at.started)
|
||||||
|
{at.pause()}
|
||||||
|
else if (at.available || at.complete)
|
||||||
|
{at.start()}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Stack(alignment: Alignment.center, children: [
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Consumer<ActionTimer>(
|
||||||
|
builder: (context, at, child) {
|
||||||
|
return Text(
|
||||||
|
style: const TextStyle(fontSize: 20),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
'${at.currentValue} ${at.currentAction.humanValueType}'
|
||||||
|
.toTitleCase());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
padding: EdgeInsets.only(right: 15),
|
||||||
|
child: Consumer<ActionTimer>(
|
||||||
|
builder: (context, at, child) {
|
||||||
|
return Text(
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
'${at.state['currentAction'] + 1} of ${at.allActions.length}');
|
||||||
|
})),
|
||||||
|
])),
|
||||||
|
]))),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 14, right: 14),
|
||||||
|
child: Consumer<ActionTimer>(builder: (context, at, child) {
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: at.progress,
|
||||||
|
semanticsLabel: 'Activity Progress',
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
Expanded(
|
||||||
|
child: ScrollablePositionedList.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 0, 10, 20),
|
||||||
|
itemCount: at.items.length,
|
||||||
|
// initialScrollIndex: at.currentAction.parentId != null
|
||||||
|
// ? at.currentAction.parentId!
|
||||||
|
// : at.currentAction.id,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
scrollOffsetController: scrollOffsetController,
|
||||||
|
itemPositionsListener: itemPositionsListener,
|
||||||
|
scrollOffsetListener: scrollOffsetListener,
|
||||||
|
itemBuilder: (BuildContext context, int itemNum) {
|
||||||
|
if (itemNum == 0) {
|
||||||
|
actionCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GestureDetector> content = [];
|
||||||
|
Item item = at.items[itemNum];
|
||||||
|
if (item.runtimeType == Rest) {
|
||||||
|
content.add(gtBuild(at, item, actionCount++, itemNum));
|
||||||
|
} else if (item.runtimeType == Set) {
|
||||||
|
List<Item> setItems = item.items;
|
||||||
|
|
||||||
|
for (int setItemNum = 0;
|
||||||
|
setItemNum < setItems.length;
|
||||||
|
setItemNum++) {
|
||||||
|
Item setItem = setItems[setItemNum];
|
||||||
|
content.add(gtBuild(at, setItem, actionCount++, itemNum,
|
||||||
|
order: (item as Set).setOrder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemNum == 0) {
|
||||||
|
return Card(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(0),
|
||||||
|
topRight: Radius.circular(0),
|
||||||
|
bottomLeft: Radius.circular(10),
|
||||||
|
bottomRight: Radius.circular(10)),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(children: content));
|
||||||
|
} else {
|
||||||
|
return Card(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10),
|
||||||
|
topRight: Radius.circular(10),
|
||||||
|
bottomLeft: Radius.circular(10),
|
||||||
|
bottomRight: Radius.circular(10)),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(children: content));
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
return AddCardGeneric(
|
||||||
|
title: 'Add an Action!',
|
||||||
|
description:
|
||||||
|
'Click here to create an exercise template (sets and reps, etc) for your activity!',
|
||||||
|
action: () {
|
||||||
|
showEditorSheet(
|
||||||
|
context,
|
||||||
|
ActivityActionEditor(
|
||||||
|
session: session,
|
||||||
|
activity: activity,
|
||||||
|
callback: callback));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
115
lib/widgets/activities/activity_card.dart
Normal file
115
lib/widgets/activities/activity_card.dart
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:sendtrain/daos/media_items_dao.dart';
|
||||||
|
import 'package:sendtrain/daos/session_activities_dao.dart';
|
||||||
|
import 'package:sendtrain/database/database.dart';
|
||||||
|
import 'package:sendtrain/extensions/string_extensions.dart';
|
||||||
|
import 'package:sendtrain/helpers/date_time_helpers.dart';
|
||||||
|
import 'package:sendtrain/helpers/media_helpers.dart';
|
||||||
|
import 'package:sendtrain/helpers/widget_helpers.dart';
|
||||||
|
import 'package:sendtrain/models/activity_timer_model.dart';
|
||||||
|
import 'package:sendtrain/widgets/activities/activity_view.dart';
|
||||||
|
import 'package:sendtrain/widgets/builders/dialogs.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/card_image.dart';
|
||||||
|
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart';
|
||||||
|
|
||||||
|
class ActivityCard extends StatefulWidget {
|
||||||
|
final Activity activity;
|
||||||
|
final Session session;
|
||||||
|
final Function? callback;
|
||||||
|
|
||||||
|
const ActivityCard(
|
||||||
|
{super.key,
|
||||||
|
required this.activity,
|
||||||
|
required this.session,
|
||||||
|
this.callback});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ActivityCard> createState() => ActivityCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityCardState extends State<ActivityCard> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ActivityTimerModel atm = Provider.of<ActivityTimerModel>(context);
|
||||||
|
|
||||||
|
return FutureBuilder<List<MediaItem>>(
|
||||||
|
future: MediaItemsDao(Provider.of<AppDatabase>(context))
|
||||||
|
.fromActivity(widget.activity),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
List<MediaItem> mediaItems = snapshot.data!;
|
||||||
|
|
||||||
|
return Card.outlined(
|
||||||
|
color: atm.activity?.id == widget.activity.id
|
||||||
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
|
: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => showGenericDialog(
|
||||||
|
ActivityView(session: widget.session, activity: widget.activity), context),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
leading: CardImage(
|
||||||
|
image:
|
||||||
|
findMediaByType(mediaItems, MediaType.image)),
|
||||||
|
title: Consumer<ActivityTimerModel>(
|
||||||
|
builder: (context, atm, child) {
|
||||||
|
if (atm.activity?.id == widget.activity.id) {
|
||||||
|
return Text(
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
"${widget.activity.title.toTitleCase()} (${formattedTime(atm.totalTime)})");
|
||||||
|
} else {
|
||||||
|
return Text(
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
widget.activity.title.toTitleCase());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 2,
|
||||||
|
softWrap: true,
|
||||||
|
jsonToDescription(json
|
||||||
|
.decode(widget.activity.description ?? ""))),
|
||||||
|
contentPadding: EdgeInsets.only(left: 13),
|
||||||
|
trailing: Flex(
|
||||||
|
direction: Axis.vertical,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: Icon(Icons.close_rounded),
|
||||||
|
onPressed: () {
|
||||||
|
showRemovalDialog(
|
||||||
|
'Activity Removal',
|
||||||
|
'Would you like to permanently remove this activity from the current session?',
|
||||||
|
context, () {
|
||||||
|
SessionActivitiesDao(
|
||||||
|
Provider.of<AppDatabase>(context,
|
||||||
|
listen: false))
|
||||||
|
.removeAssociation(widget.activity.id,
|
||||||
|
widget.session.id);
|
||||||
|
}).then((result) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
])),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return GenericProgressIndicator();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user