added sound during countdown, and upgraded minsdkversion

This commit is contained in:
Joshua Burman
2025-02-10 17:08:13 -05:00
parent 60bc571987
commit 23663f484b
26 changed files with 6661 additions and 145 deletions

View File

@ -1,39 +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 StatelessWidget {
ActivityActionEditor({super.key, required this.action, this.callback});
class ActivityActionEditor extends StatefulWidget {
const ActivityActionEditor(
{super.key,
required this.session,
required this.activity,
this.action,
this.callback});
final Action action;
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) {
String editorType = 'Create';
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)),
FormTextInput(
controller: actionEditController['sets']!,
title: 'Total Sets'),])));
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('title'),
description: Value('description'),
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')))
])
])));
}
}
}

View File

@ -3,20 +3,32 @@ 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 {
const ActivityActionView({super.key, required this.actions, this.resetOnLoad = true});
// 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();
}
// @override
// State<ActivityActionView> createState() => ActivityActionViewState();
// }
class ActivityActionViewState extends State<ActivityActionView> {
// class ActivityActionViewState extends State<ActivityActionView> {
// class ActivityActionView extends StatelessWidget {
// ActivityActionView({super.key, required this.actions});
@ -30,7 +42,7 @@ class ActivityActionViewState extends State<ActivityActionView> {
ScrollOffsetListener.create();
late final ActionTimer at;
int actionCount = 0;
// int actionCount = 0;
GestureDetector gtBuild(
ActionTimer at, Item item, int actionNum, int selectedIndex,
@ -68,17 +80,23 @@ class ActivityActionViewState extends State<ActivityActionView> {
}));
}
@override
void initState() {
super.initState();
at = Provider.of<ActionTimer>(context, listen: false);
}
// @override
// void initState() {
// super.initState();
// at = Provider.of<ActionTimer>(context, listen: false);
// }
@override
Widget build(BuildContext context) {
if (widget.actions.isNotEmpty) {
at.setup(ActionModel(
action: widget.actions.first, db: Provider.of<AppDatabase>(context)), itemScrollController, widget.resetOnLoad);
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) {
@ -117,7 +135,7 @@ class ActivityActionViewState extends State<ActivityActionView> {
onPressed: () => {
if (at.started)
{at.pause()}
else if (at.available)
else if (at.available || at.complete)
{at.start()}
});
},
@ -220,7 +238,12 @@ class ActivityActionViewState extends State<ActivityActionView> {
description:
'Click here to create an exercise template (sets and reps, etc) for your activity!',
action: () {
print('teset');
showEditorSheet(
context,
ActivityActionEditor(
session: session,
activity: activity,
callback: callback));
});
}
}

View File

@ -49,7 +49,7 @@ class ActivityCardState extends State<ActivityCard> {
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => showGenericDialog(
ActivityView(activity: widget.activity), context),
ActivityView(session: widget.session, activity: widget.activity), context),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[

View File

@ -7,6 +7,7 @@ import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/providers/action_timer.dart';
import 'package:sendtrain/widgets/activities/activity_action_editor.dart';
import 'package:sendtrain/widgets/activities/activity_action_view.dart';
import 'package:sendtrain/widgets/activities/activity_view_categories.dart';
@ -14,7 +15,9 @@ import 'package:sendtrain/widgets/activities/activity_view_media.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
class ActivityView extends StatefulWidget {
const ActivityView({super.key, required this.activity});
const ActivityView(
{super.key, required this.session, required this.activity});
final Session session;
final Activity activity;
@override
@ -22,6 +25,17 @@ class ActivityView extends StatefulWidget {
}
class _ActivityViewState extends State<ActivityView> {
final _fabKey = GlobalKey<ExpandableFabState>();
void resetState() async {
final state = _fabKey.currentState;
if (state != null && state.isOpen) {
state.toggle();
}
setState(() {});
}
List<ActivityMuscle> activityMuscle(Activity activity) {
List<ActivityMuscle> muscles = [];
@ -39,10 +53,11 @@ class _ActivityViewState extends State<ActivityView> {
@override
Widget build(BuildContext context) {
final Activity activity = widget.activity;
final Session session = widget.session;
return FutureBuilder<List>(
future: ActionsDao(Provider.of<AppDatabase>(context))
.fromActivity(activity),
.fromActivity(activity, session),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Action> actions = snapshot.data! as List<Action>;
@ -61,12 +76,19 @@ class _ActivityViewState extends State<ActivityView> {
child: Scaffold(
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
key: _fabKey,
distance: 70,
type: ExpandableFabType.up,
overlayStyle: ExpandableFabOverlayStyle(
color: Colors.black.withOpacity(0.5),
blur: 10,
),
onOpen: () {
// pause the activity on open
ActionTimer at =
Provider.of<ActionTimer>(context, listen: false);
if (at.started) at.pause();
},
children: [
// FloatingActionButton.extended(
// icon: const Icon(Icons.upload_outlined),
@ -80,7 +102,10 @@ class _ActivityViewState extends State<ActivityView> {
showEditorSheet(
context,
ActivityActionEditor(
action: actions.first, callback: () {}));
session: session,
activity: activity,
action: actions.first,
callback: resetState));
},
),
FloatingActionButton.extended(
@ -228,7 +253,10 @@ class _ActivityViewState extends State<ActivityView> {
context,
Column(children: [
ActivityActionView(
session: session,
activity: activity,
actions: actions,
callback: resetState,
resetOnLoad: false)
]),
Theme.of(context).colorScheme.surface);
@ -237,7 +265,11 @@ class _ActivityViewState extends State<ActivityView> {
alignment: Alignment.bottomCenter,
)
])),
ActivityActionView(actions: actions)
ActivityActionView(
session: session,
activity: activity,
actions: actions,
callback: resetState)
])));
// ] +
// action(actions, context)));

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
class FormDropDown extends StatelessWidget {
const FormDropDown(
{super.key,
required this.title,
required this.entries,
required this.controller});
final List<DropdownMenuEntry> entries;
final String title;
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return formItemWrapper(
DropdownMenu(
leadingIcon: Icon(Icons.select_all_rounded),
initialSelection: controller.text,
controller: controller,
expandedInsets: EdgeInsets.zero,
inputDecorationTheme: InputDecorationTheme(
filled: true,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12))),
label: Text(title),
dropdownMenuEntries: entries),
EdgeInsets.fromLTRB(10, 5, 10, 5));
}
}

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
enum InputTypes { text, number }
class FormTextInput extends StatelessWidget {
const FormTextInput(
@ -9,7 +12,10 @@ class FormTextInput extends StatelessWidget {
this.maxLines,
this.minLines,
this.onTap,
this.requiresValidation=true});
this.requiresValidation = true,
this.type = InputTypes.text,
this.hint,
this.validations});
final TextEditingController controller;
final String title;
@ -18,12 +24,25 @@ class FormTextInput extends StatelessWidget {
final Icon? icon;
final dynamic onTap;
final bool requiresValidation;
final InputTypes type;
final String? hint;
final Function? validations;
@override
Widget build(BuildContext context) {
final Map params = {};
if (type == InputTypes.number) {
params['keyboardType'] = TextInputType.number;
params['inputFormatters'] = <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
];
}
return Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField(
keyboardType: params['keyboardType'] ?? TextInputType.text,
inputFormatters: params['inputFormatters'] ?? [],
minLines: minLines ?? 1,
maxLines: maxLines ?? 1,
controller: controller,
@ -34,6 +53,7 @@ class FormTextInput extends StatelessWidget {
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12)),
labelText: title,
hintText: hint ?? '',
),
validator: (String? value) {
if (requiresValidation == true) {
@ -41,9 +61,11 @@ class FormTextInput extends StatelessWidget {
return 'Please enter some text';
}
if (value.length < 3) {
return 'Please enter a minimum of 3 characters';
}
if (validations != null) validations!(value);
// if (value.length < 3) {
// return 'Please enter a minimum of 3 characters';
// }
}
return null;
},

View File

@ -1,6 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -8,7 +6,6 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
import 'package:video_player/video_player.dart';
class MediaCard extends StatelessWidget {
const MediaCard(