big refactor #1

This commit is contained in:
Joshua Burman
2024-12-27 15:59:09 -05:00
parent cd8da31f4b
commit 722a152130
27 changed files with 474 additions and 292 deletions

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'activity_type_filter.dart';
class ActivitiesHeader extends StatefulWidget {
const ActivitiesHeader({super.key});
@override
State<ActivitiesHeader> createState() => ActivitiesHeaderState();
}
class ActivitiesHeaderState extends State<ActivitiesHeader> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(bottom: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Text(
style:
TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
'Filters:')),
Padding(
padding: EdgeInsets.only(right: 5),
child: ActivityTypeFilter()),
]));
}
}

View File

@ -0,0 +1,110 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
class ActivityActionView extends StatefulWidget {
const ActivityActionView({super.key, required this.actions});
final List actions;
@override
State<ActivityActionView> createState() => ActivityActionViewState();
}
class ActivityActionViewState extends State<ActivityActionView> {
final ItemScrollController itemScrollController = ItemScrollController();
final ScrollOffsetController scrollOffsetController =
ScrollOffsetController();
final ItemPositionsListener itemPositionsListener =
ItemPositionsListener.create();
final ScrollOffsetListener scrollOffsetListener =
ScrollOffsetListener.create();
@override
Widget build(BuildContext context) {
ActivityTimerModel atm =
Provider.of<ActivityTimerModel>(context, listen: true);
List sets = json.decode(widget.actions[0].set);
// we need to set the scroll controller
// so we can update the selected item position
atm.setScrollController(itemScrollController);
return Expanded(
child: ScrollablePositionedList.builder(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 20),
itemCount: sets.length,
itemScrollController: itemScrollController,
scrollOffsetController: scrollOffsetController,
itemPositionsListener: itemPositionsListener,
scrollOffsetListener: scrollOffsetListener,
itemBuilder: (BuildContext context, int setNum) {
List<GestureDetector> content = [];
List set = sets[setNum];
for (int actionNum = 0; actionNum < set.length; actionNum++) {
Map<String, dynamic> setItem = set[actionNum];
content.add(GestureDetector(
onTap: () {
atm.setAction(setNum, actionNum, 'manual');
atm.setActionCount();
itemScrollController.scrollTo(
index: setNum,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOutCubic);
},
child: Row(children: [
Ink(
width: 70,
padding: const EdgeInsets.all(15),
color: atm.isCurrentItem(setNum, actionNum)
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.onPrimary,
child: Text(
textAlign: TextAlign.center,
'${setNum + 1}.${actionNum + 1} ')),
Expanded(
child: Ink(
padding: const EdgeInsets.all(15),
color: atm.isCurrentItem(setNum, actionNum)
? Theme.of(context).colorScheme.surfaceBright
: Theme.of(context).colorScheme.surfaceContainerLow,
child: Text(
textAlign: TextAlign.center,
'${setItem['name']}: ${setItem['amount']} ${setItem['type']}'.toTitleCase())))
])));
}
if (setNum == 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));
}
// return Column(children: contents);
},
));
}
}

View File

@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/activities/activity_view.dart';
class ActivityCard extends StatefulWidget {
final Activity activity;
const ActivityCard({super.key, required this.activity});
@override
State<ActivityCard> createState() => ActivityCardState();
}
class ActivityCardState extends State<ActivityCard> {
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";
}
@override
Widget build(BuildContext context) {
final ActivityTimerModel atm =
Provider.of<ActivityTimerModel>(context, listen: false);
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(
color: atm.activity?.id == widget.activity.id
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
Animation<Offset> custom = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0))
.animate(animation);
return SlideTransition(
position: custom,
child: Dialog.fullscreen(
child:
ActivityView(activity: widget.activity)));
},
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
}),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
// visualDensity: VisualDensity(horizontal: VisualDensity.maximumDensity),
leading: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
child: Container(
// padding: EdgeInsets.only(top: 5, bottom: 5),
width: 60,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image:
findMediaByType(mediaItems, 'image')),
// color: Colors.blue,
borderRadius: const BorderRadius.all(
Radius.elliptical(8, 8)),
),
)),
title: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
if (atm.activity?.id == widget.activity.id) {
return Text(
maxLines: 1,
"${widget.activity.title.toTitleCase()} (${formattedTime(atm.totalTime)})");
} else {
return Text(
maxLines: 1,
widget.activity.title.toTitleCase());
}
},
),
subtitle:
Text(maxLines: 2, 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: () {
showAdaptiveDialog(
context: context,
builder: (BuildContext context) =>
AlertDialog(
title: const Text('Activity Removal'),
content: const Text(
'Would you like to permanently remove this activity from the current session?'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(
context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => {
ActivitiesDao(
Provider.of<AppDatabase>(
context,
listen: false))
.remove(widget.activity)
.then((result) {
setState(() {});
}),
Navigator.pop(context, 'OK')
},
child: const Text('OK'),
),
],
),
);
},
)
])
// trailing: FittedBox(
// alignment: Alignment.topCenter,
// fit: BoxFit.fitHeight,
// child: IconButton(
// padding: EdgeInsets.all(0),
// alignment: Alignment.topCenter,
// // visualDensity: VisualDensity.compact,
// icon: Icon(Icons.close_rounded),
// onPressed: () {
// showAdaptiveDialog(
// context: context,
// builder: (BuildContext context) =>
// AlertDialog(
// title: const Text('Activity Removal'),
// content: const Text(
// 'Would you like to permanently remove this activity from the current session?'),
// actions: <Widget>[
// TextButton(
// onPressed: () =>
// Navigator.pop(context, 'Cancel'),
// child: const Text('Cancel'),
// ),
// TextButton(
// onPressed: () =>
// Navigator.pop(context, 'OK'),
// child: const Text('OK'),
// ),
// ],
// ),
// );
// },
// ))
),
],
)),
);
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
});
}
ImageProvider findMediaByType(List<MediaItem> media, String type) {
Iterable<MediaItem>? found = media.where((m) => m.type == MediaType.image);
if (found.isNotEmpty) {
return NetworkImage(found.first.reference);
} else {
// Element is not found
return const AssetImage('assets/images/placeholder.jpg');
}
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
class ActivityTypeFilter extends StatefulWidget {
const ActivityTypeFilter({super.key});
@override
State<StatefulWidget> createState() => _ActivityTypeFilterState();
}
class _ActivityTypeFilterState extends State<ActivityTypeFilter> {
@override
Widget build(BuildContext context) {
return const DropdownMenu(
hintText: 'Filter by type of activity',
initialSelection: "fundamentals",
// controller: colorController,
label: Text('Type'),
dropdownMenuEntries: [
DropdownMenuEntry(value: 'fundamentals', label: 'Fundamentals'),
DropdownMenuEntry(value: 'conditioning', label: 'Conditioning'),
DropdownMenuEntry(value: 'advanced', label: 'Advanced'),
DropdownMenuEntry(value: 'custom', label: 'Custom'),
DropdownMenuEntry(value: 'pro', label: 'Pro')
],
// onSelected: (ColorLabel? color) {
// setState(() {
// selectedColor = color;
// });
// },
);
}
}

View File

@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/activities/activity_action_view.dart';
import 'package:sendtrain/widgets/activities/activity_view_categories.dart';
import 'package:sendtrain/widgets/activities/activity_view_media.dart';
import 'package:sendtrain/widgets/activities/activity_view_types.dart';
class ActivityView extends StatefulWidget {
const ActivityView(
{super.key, required this.activity});
final Activity activity;
@override
State<ActivityView> createState() => _ActivityViewState();
}
class _ActivityViewState extends State<ActivityView> {
@override
Widget build(BuildContext context) {
final Activity activity = widget.activity;
ActivityTimerModel atm =
Provider.of<ActivityTimerModel>(context, listen: false);
return FutureBuilder<List>(
future: ActionsDao(Provider.of<AppDatabase>(context))
.fromActivity(activity),
builder: (context, snapshot) {
if (snapshot.hasData) {
List actions = snapshot.data!;
atm.setup(activity, actions);
return Scaffold(
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
distance: 70,
type: ExpandableFabType.up,
overlayStyle: ExpandableFabOverlayStyle(
color: Colors.black.withOpacity(0.5),
blur: 10,
),
children: [
FloatingActionButton.extended(
icon: const Icon(Icons.upload_outlined),
label: Text('Upload Media'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.note_add_outlined),
label: Text('Add Note'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.history_outlined),
label: Text('Restart'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.done_all_outlined),
label: Text('Done'),
onPressed: () {},
),
]),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppBar(
titleSpacing: 0,
centerTitle: true,
title: const Text('Activity',
style: TextStyle(fontSize: 15)),
),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 20, top: 15, bottom: 10),
child: Text(
maxLines: 1,
style: const TextStyle(
fontSize: 25, fontWeight: FontWeight.bold),
activity.title.toTitleCase())),
Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 0, 10),
child: Flex(direction: Axis.horizontal, children: [
ActivityViewCategories(
categories: [activity.category]),
ActivityViewTypes(types: [activity.type])
])),
Padding(
padding: const EdgeInsets.only(
top: 0, bottom: 10, left: 15, right: 15),
child: Text(
textAlign: TextAlign.left,
style: const TextStyle(fontSize: 15),
activity.description)),
const Padding(
padding: EdgeInsets.fromLTRB(15, 20, 0, 10),
child: Text(
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
'Media:')),
ActivityViewMedia(activity: activity),
const Padding(
padding: EdgeInsets.fromLTRB(15, 30, 0, 10),
child: Text(
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
'Actions')),
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<ActivityTimerModel>(
builder: (context, atm, child) {
return IconButton(
alignment:
AlignmentDirectional.center,
icon: atm.isActive
? const Icon(
Icons.pause_rounded)
: const Icon(
Icons.play_arrow_rounded),
onPressed: () => {
atm.isActive
? atm.pause()
: atm.start()
});
},
)),
Expanded(
flex: 1,
child: Stack(
alignment: Alignment.center,
children: [
Container(
alignment: Alignment.center,
child: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
return Text(
style: const TextStyle(
fontSize: 20),
textAlign: TextAlign.center,
'${atm.actionCount} ${atm.currentAction['type']}'.toTitleCase());
},
),
),
Container(
alignment: Alignment.centerRight,
padding:
EdgeInsets.only(right: 15),
child:
Consumer<ActivityTimerModel>(
builder: (context, atm,
child) {
return Text(
style: const TextStyle(
fontSize: 12),
textAlign: TextAlign.right,
'${atm.currentAction['actionID'] + 1} of ${atm.totalActions()}');
})),
])),
]))),
Padding(
padding: EdgeInsets.only(left: 14, right: 14),
child: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
return LinearProgressIndicator(
value: atm.progress,
semanticsLabel: 'Activity Progress',
);
})),
ActivityActionView(actions: actions),
]));
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
});
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
class ActivityViewCategories extends StatelessWidget {
const ActivityViewCategories({super.key, required this.categories});
final List<ActivityCategories> categories;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(right: 10),
itemCount: categories.length,
itemBuilder: (BuildContext context, int index) {
return ActionChip(
visualDensity: VisualDensity.compact,
avatar: const Icon(Icons.category_rounded),
label: Text(maxLines: 1, categories[index].name.toTitleCase()),
tooltip: "Activity Category",
onPressed: () {},
);
},
));
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/media_card.dart';
class ActivityViewMedia extends StatelessWidget {
const ActivityViewMedia({super.key, required this.activity});
final Activity activity;
@override
Widget build(BuildContext context) {
return FutureBuilder<List<MediaItem>>(
future: MediaItemsDao(Provider.of<AppDatabase>(context)).fromActivity(activity),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<MediaItem> mediaItems = snapshot.data!;
List<MediaCard> mediaCards = [];
for (int i = 0; i < mediaItems.length; i++) {
mediaCards.add(MediaCard(media: mediaItems[i]));
}
return Column(
children: [
SizedBox(
width: double.infinity,
height: 100,
child: GridView.count(
padding: const EdgeInsets.fromLTRB(15, 0, 0, 0),
scrollDirection: Axis.horizontal,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
crossAxisCount: 1,
children: mediaCards))
],
);
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
}
);
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
class ActivityViewTypes extends StatelessWidget {
const ActivityViewTypes({super.key, required this.types});
final List<ActivityType> types;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(right: 10),
itemCount: types.length,
itemBuilder: (BuildContext context, int index) {
return ActionChip(
visualDensity: VisualDensity.compact,
avatar: const Icon(Icons.fitness_center_rounded),
label: Text(maxLines: 1, types[index].name.toTitleCase()),
tooltip: "Activity Type",
onPressed: () {},
);
},
));
}
}