diff --git a/lib/constants/strings.dart b/lib/constants/strings.dart new file mode 100644 index 00000000..1410e9e8 --- /dev/null +++ b/lib/constants/strings.dart @@ -0,0 +1,17 @@ +const String aboutText = "OpenHIIT is a free and open-source interval timer."; + +const String intervalTimerTitle = "Interval Timer"; +const String intervalTimerDescription = + "Create an interval timer that alternates between work and rest periods. Includes options for warm-up, cool-down, and more."; +const String workoutTitle = "Workout"; +const String workoutDescription = + "Create a workout that lets you define exercises with an interval timer."; +const String importTitle = "Import"; +const String importDescription = + "Import a workout or timer from a file on your device."; + +const String noSavedTimers = "No saved timers"; +const String noSavedTimersDescription = + "Hit the + at the bottom to get started!"; +const String fetchingTimers = "Fetching timers..."; +const String exportingFiles = "Exporting file(s)"; diff --git a/lib/pages/create_timer/create_timer.dart b/lib/pages/create_timer/create_timer.dart index 7bf99494..a0e53d52 100644 --- a/lib/pages/create_timer/create_timer.dart +++ b/lib/pages/create_timer/create_timer.dart @@ -20,11 +20,7 @@ class CreateTimerState extends State { Widget build(BuildContext context) { final formKey = GlobalKey(); - /// Submit and form, save the workout values, and move - /// to the next view. - /// void submitForm(TimerType timer) { - // Validate returns true if the form is valid, or false otherwise. final form = formKey.currentState!; if (form.validate()) { form.save(); @@ -38,7 +34,6 @@ class CreateTimerState extends State { ); } } - // --- return Scaffold( appBar: AppBar( diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 9b0dc241..942d93ff 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:openhiit/constants/snackbars.dart'; +import 'package:openhiit/constants/strings.dart'; import 'package:openhiit/data/timer_type.dart'; import 'package:openhiit/pages/select_timer/select_timer.dart'; import 'package:openhiit/pages/view_timer/view_timer.dart'; @@ -17,7 +18,6 @@ import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; -// Global flag to indicate if exporting is in progress bool exporting = false; class MyHomePage extends StatefulWidget { @@ -28,7 +28,7 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List reorderableWorkoutList = []; + List reorderableTimerList = []; late WorkoutProvider workoutProvider; @override @@ -38,55 +38,38 @@ class _MyHomePageState extends State { } void _onReorder(int oldIndex, int newIndex) async { - // Ensure newIndex does not exceed the length of the list. - if (newIndex > reorderableWorkoutList.length) { - newIndex = reorderableWorkoutList.length; + if (newIndex > reorderableTimerList.length) { + newIndex = reorderableTimerList.length; } - - // Adjust newIndex if oldIndex is less than newIndex. if (oldIndex < newIndex) newIndex -= 1; - // Extract the Workout item being reordered. - final TimerType item = reorderableWorkoutList[oldIndex]; - // Remove the item from its old position. - reorderableWorkoutList.removeAt(oldIndex); - - // Update the workoutIndex of the item to the new position. + final TimerType item = reorderableTimerList.removeAt(oldIndex); item.timerIndex = newIndex; - // Insert the item at the new position. - reorderableWorkoutList.insert(newIndex, item); + reorderableTimerList.insert(newIndex, item); - // Update the workoutIndex for all items in the list. setState(() { - for (var i = 0; i < reorderableWorkoutList.length; i++) { - reorderableWorkoutList[i].timerIndex = i; + for (var i = 0; i < reorderableTimerList.length; i++) { + reorderableTimerList[i].timerIndex = i; } }); - DatabaseManager().updateTimers(reorderableWorkoutList); + DatabaseManager().updateTimers(reorderableTimerList); } - // --- - /// Widget for displaying a ReorderableListView of workout items. - /// - /// Parameters: - /// - [snapshot]: The data snapshot from the database containing workout information. Widget workoutListView(snapshot) { return ReorderableListView( - onReorder: _onReorder, // Callback for handling item reordering. - proxyDecorator: proxyDecorator, // Decorator for the dragged item. + onReorder: _onReorder, + proxyDecorator: proxyDecorator, children: [ for (final timer in snapshot.data) TimerListTile( - key: Key('${timer.timerIndex}'), // Unique key for each list item. + key: Key('${timer.timerIndex}'), timer: timer, onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => ViewTimer( - timer: timer, - ), + builder: (context) => ViewTimer(timer: timer), ), ).then((value) { if (mounted) { @@ -102,198 +85,74 @@ class _MyHomePageState extends State { ); } - /// Generates the empty message for no [workouts] in DB. - /// - Widget workoutEmpty() { - List children; - children = [ - const Text( - 'No saved timers', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 5), - const Text( - 'Hit the + at the bottom to get started!', - ), - ]; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: children, - ), - ); - } - // --- - - /// Generates the error message for an issue loading [workouts]. - /// - Widget workoutFetchError(snapshot) { - List children; - children = [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 60, - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text('Error: ${snapshot.error}'), - ), - ]; + Widget buildMessageWidget({required String summary, String? description}) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, - children: children, - ), - ); - } - // --- - - /// Generates the loading circle, display as workouts - /// are being loaded from the DB. - /// - Widget workoutLoading() { - List children; - children = const [ - SizedBox( - width: 60, - height: 60, - child: CircularProgressIndicator(), - ), - Padding( - padding: EdgeInsets.only(top: 16), - child: Text('Fetching timers...'), - ), - ]; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: children, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text(summary, style: const TextStyle(fontSize: 20)), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text(description ?? ""), + ), + ], ), ); } - // --- - /// Load the page for the user to select whether they'd like - /// to create a new interval timer or workout. - /// void pushSelectTimerPage() async { Navigator.push( context, MaterialPageRoute(builder: (context) => const SelectTimer()), ); } - // --- - /// Saves the workouts to the device. - /// - /// This function exports the workouts to the device by saving them to a file. - /// It sets the [exporting] flag to true to indicate that the export is in progress. - /// It then retrieves the loaded workouts using the [workouts] variable. - /// The workouts are saved to the device using the [LocalFileUtil] class. - /// After the export is complete, the [exporting] flag is set to false. - /// If the context is still mounted, a snackbar is shown to indicate that the workouts have been exported. - /// Finally, the function logs the completion of the export. void saveWorkouts() async { - // Export workouts to device logger.i("Exporting workouts to device..."); - - setState(() { - exporting = true; - }); + setState(() => exporting = true); LocalFileUtil fileUtil = LocalFileUtil(); bool result = await fileUtil.saveFileToDevice(workoutProvider.timers); - if (result) { - setState(() { - logger.i("Exporting complete."); - exporting = false; - }); - - if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context) - .showSnackBar(createSuccessSnackBar("Saved to device!")); - } - } else { - setState(() { - logger.e("Export not completed."); - exporting = false; - }); - - if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context) - .showSnackBar(createErrorSnackBar("Save not completed")); - } + setState(() => exporting = false); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + result + ? createSuccessSnackBar("Saved to device!") + : createErrorSnackBar("Save not completed"), + ); } } - /// Exports and shares the workouts. - /// - /// This function exports the workouts and shares them with other applications. - /// It sets the [exporting] flag to true to indicate that the export process is in progress. - /// It uses the [LocalFileUtil] class to write each workout to a file. - /// After exporting and sharing the workouts, it sets the [exporting] flag to false. - /// It also shows a success message using a snackbar. void shareWorkouts(BuildContext buildContext) async { - // Export and share workouts logger.i("Exporting and sharing workouts..."); - setState(() { - exporting = true; - }); - // List loadedWorkouts = workoutProvider.workouts; + setState(() => exporting = true); LocalFileUtil fileUtil = LocalFileUtil(); - await fileUtil.writeFile(workoutProvider.timers); if (buildContext.mounted) { ShareResult? result = await fileUtil.shareMultipleFiles( workoutProvider.timers, buildContext); - if (result != null) { - if (result.status == ShareResultStatus.dismissed || - result.status == ShareResultStatus.unavailable) { - setState(() { - logger.e("Share not completed."); - exporting = false; - }); - - if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context) - .showSnackBar(createErrorSnackBar("Share not completed")); - } - } else { - setState(() { - logger.i("Export and share complete."); - exporting = false; - }); - - if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context) - .showSnackBar(createSuccessSnackBar("Shared successfully!")); - } - } + setState(() => exporting = false); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + result != null && result.status == ShareResultStatus.success + ? createSuccessSnackBar("Shared successfully!") + : createErrorSnackBar("Share not completed"), + ); } } } - /// Function to handle bulk export of workouts. - /// This function displays a modal bottom sheet and provides options to save or share workouts. - /// When the save option is selected, the function exports the workouts to the device. - /// When the share option is selected, the function exports the workouts to the device and then shares them. void bulkExport() async { - // Display modal bottom sheet showModalBottomSheet( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), context: context, @@ -306,28 +165,18 @@ class _MyHomePageState extends State { }, ); } - // --- - /// The widget to return for a workout tile as it's being dragged. - /// This AnimatedBuilder will slightly increase the elevation of the dragged - /// workout without changing other UI elements. - /// Widget proxyDecorator(Widget child, int index, Animation animation) { return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { final double animValue = Curves.easeInOut.transform(animation.value); final double scale = lerpDouble(1, 1.02, animValue)!; - return Transform.scale( - scale: scale, - // Create a Card based on the color and the content of the dragged one - // and set its elevation to the animated value. - child: child); + return Transform.scale(scale: scale, child: child); }, child: child, ); } - // --- @override Widget build(BuildContext context) { @@ -336,81 +185,83 @@ class _MyHomePageState extends State { )); return Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: SafeArea( - child: Scaffold( - appBar: AppBar( - toolbarHeight: 30, - actions: [ - IconButton( - icon: const Icon(Icons.info_outline), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text("About OpenHIIT"), - content: const Text( - "OpenHIIT is a free and open-source interval timer."), - actions: [ - TextButton( - onPressed: () async { - final Uri url = Uri.parse( - 'https://a-mabe.github.io/OpenHIIT/'); - if (!await launchUrl(url)) { - throw Exception('Could not launch $url'); - } - }, - child: const Text("View privacy policy"), - ), - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text("Close"), - ), - ], - ); - }); + color: Theme.of(context).scaffoldBackgroundColor, + child: SafeArea( + child: Scaffold( + appBar: AppBar( + toolbarHeight: 30, + actions: [ + IconButton( + icon: const Icon(Icons.info_outline), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text("About OpenHIIT"), + content: const Text(aboutText), + actions: [ + TextButton( + onPressed: () async { + final Uri url = Uri.parse( + 'https://a-mabe.github.io/OpenHIIT/'); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + }, + child: const Text("View privacy policy"), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Close"), + ), + ], + ); }, - ), - ], + ); + }, ), - - /// Pushes to [SelectTimer()] - floatingActionButton: Visibility( - visible: !exporting, - child: FABColumn(bulk: bulkExport, create: pushSelectTimerPage), + ], + ), + floatingActionButton: Visibility( + visible: !exporting, + child: FABColumn(bulk: bulkExport, create: pushSelectTimerPage), + ), + body: Stack( + children: [ + Container( + padding: const EdgeInsets.all(8.0), + child: FutureBuilder( + future: workoutProvider.loadWorkoutData(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + if (snapshot.data!.isEmpty) { + return buildMessageWidget( + summary: noSavedTimers, + description: noSavedTimersDescription); + } else { + reorderableTimerList = snapshot.data; + reorderableTimerList.sort( + (a, b) => a.timerIndex.compareTo(b.timerIndex)); + return workoutListView(snapshot); + } + } else if (snapshot.hasError) { + return buildMessageWidget( + summary: 'Error: ${snapshot.error}'); + } else { + return buildMessageWidget(summary: fetchingTimers); + } + }, + ), ), - body: Stack(children: [ - Container( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - child: FutureBuilder( - future: workoutProvider.loadWorkoutData(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - if (snapshot.data!.isEmpty) { - return workoutEmpty(); - } else { - reorderableWorkoutList = snapshot.data; - reorderableWorkoutList.sort((a, b) => - a.timerIndex.compareTo(b.timerIndex)); - return workoutListView(snapshot); - } - } else if (snapshot.hasError) { - return workoutFetchError(snapshot); - } else { - return workoutLoading(); - } - }))), - LoaderTransparent( - loadingMessage: "Exporting file(s)", - visible: exporting, - ) - ])), - )); + LoaderTransparent( + loadingMessage: exportingFiles, + visible: exporting, + ), + ], + ), + ), + ), + ); } - // --- } diff --git a/lib/pages/home/widgets/fab_column.dart b/lib/pages/home/widgets/fab_column.dart index 29ba3b4a..bd1970d6 100644 --- a/lib/pages/home/widgets/fab_column.dart +++ b/lib/pages/home/widgets/fab_column.dart @@ -1,13 +1,7 @@ import 'package:flutter/material.dart'; class FABColumn extends StatelessWidget { - /// Funtion to execute when the bulk FAB is pressed. - /// final void Function() bulk; - - /// Function to execute when the create timer FAB - /// is pressed. - /// final void Function() create; const FABColumn({super.key, required this.bulk, required this.create}); diff --git a/lib/pages/select_timer/select_timer.dart b/lib/pages/select_timer/select_timer.dart index d2e071b3..9f666984 100644 --- a/lib/pages/select_timer/select_timer.dart +++ b/lib/pages/select_timer/select_timer.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:openhiit/constants/strings.dart'; import 'package:openhiit/data/timer_type.dart'; import 'package:openhiit/pages/create_timer/create_timer.dart'; import 'package:openhiit/pages/import_workout/import_workout.dart'; -import '../../data/workout_type.dart'; import 'widgets/timer_option_card.dart'; class SelectTimer extends StatefulWidget { @@ -13,9 +13,6 @@ class SelectTimer extends StatefulWidget { } class SelectTimerState extends State { - final workout = Workout.empty(); - final timer = TimerType.empty(); - @override Widget build(BuildContext context) { return Scaffold( @@ -30,32 +27,30 @@ class SelectTimerState extends State { context, MaterialPageRoute( builder: (context) => CreateTimer( - timer: timer, + timer: TimerType.empty(), workout: false, ), ), ); }, optionIcon: Icons.timer, - optionTitle: "Interval Timer", - optionDescription: - "An interval timer is a tool that helps you track the time spent working and resting during a workout."), + optionTitle: intervalTimerTitle, + optionDescription: intervalTimerDescription), TimerOptionCard( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => CreateTimer( - timer: timer, + timer: TimerType.empty(), workout: true, ), ), ); }, optionIcon: Icons.fitness_center, - optionTitle: "Workout", - optionDescription: - "A workout is a planned set of exercises combined with an interval timer.", + optionTitle: workoutTitle, + optionDescription: workoutDescription, ), TimerOptionCard( onTap: () { @@ -63,15 +58,12 @@ class SelectTimerState extends State { context, MaterialPageRoute( builder: (context) => const ImportWorkout(), - settings: RouteSettings( - arguments: workout, - ), ), ); }, optionIcon: Icons.upload_file, - optionTitle: "Import", - optionDescription: "Import a workout or timer from file.", + optionTitle: importTitle, + optionDescription: importDescription, ), ], ))); diff --git a/lib/pages/select_timer/widgets/timer_option_card.dart b/lib/pages/select_timer/widgets/timer_option_card.dart index 9fe57ebe..f33bd8ab 100644 --- a/lib/pages/select_timer/widgets/timer_option_card.dart +++ b/lib/pages/select_timer/widgets/timer_option_card.dart @@ -1,22 +1,9 @@ import 'package:flutter/material.dart'; class TimerOptionCard extends StatefulWidget { - /// Vars - - /// Function to run when the InkWell is tapped. - /// final Function? onTap; - - /// Icon to display on the card. - /// final IconData optionIcon; - - /// Title text to display on the card. - /// final String optionTitle; - - /// Description text to display on the card. - /// final String optionDescription; const TimerOptionCard( diff --git a/lib/pages/set_exercises/set_exercises.dart b/lib/pages/set_exercises/set_exercises.dart index ce82a552..6a6cfc91 100644 --- a/lib/pages/set_exercises/set_exercises.dart +++ b/lib/pages/set_exercises/set_exercises.dart @@ -12,26 +12,10 @@ class SetExercises extends StatefulWidget { State createState() => _SetExercisesState(); } -// Define a corresponding State class. -// This class holds the data related to the Form. class _SetExercisesState extends State { - /// The list of validators to be used in the form. Each - /// validator will correspond to one TextFormField. - /// List validators = []; - - /// The list of validators to be used in the form. Each - /// validator will correspond to one TextFormField. - /// List controllers = []; - - /// The list of exercises the user had filled out in the form. Each - /// validator will correspond to one TextFormField. - /// List exercises = []; - - /// The global key for the form. - /// final formKey = GlobalKey(); void generateTextControllers(TimerType timer) { @@ -42,18 +26,14 @@ class _SetExercisesState extends State { for (var i = 0; i < timer.activeIntervals; i++) { validators.add(false); if (i < currentNumWorkoutExercises) { - // If there might be a previously set exercise, use it! controllers .add(TextEditingController(text: currentWorkoutExercises[i])); } else { - // Otherwise, blank text controller. controllers.add(TextEditingController()); } } } - /// Generate the list of TextFormFields based off of the number of exercises. - /// List generateTextFormFields(TimerType timer) { return List.generate(timer.activeIntervals, (int index) { return Padding( @@ -62,8 +42,6 @@ class _SetExercisesState extends State { key: Key('exercise-$index'), textCapitalization: TextCapitalization.sentences, maxLength: 40, - - /// Validate that the field is filled out. validator: (value) { if (value == null || value.isEmpty) { return 'Please enter some text'; @@ -75,15 +53,12 @@ class _SetExercisesState extends State { labelText: 'Exercise #${index + 1}', errorText: validators[index] ? 'Value Can\'t Be Empty' : null, ), - // onSaved push the value into the list of exercises. onSaved: (val) => setState(() => exercises.add(val!)), ), ); }); } - /// Submit the form and call [pushTimings]. - /// void submitExercises( GlobalKey formKey, TimerType timer, List exercises) { final form = formKey.currentState!; diff --git a/lib/pages/set_sounds/widgets/sound_dropdown.dart b/lib/pages/set_sounds/widgets/sound_dropdown.dart index 410d2189..3d4a70a9 100644 --- a/lib/pages/set_sounds/widgets/sound_dropdown.dart +++ b/lib/pages/set_sounds/widgets/sound_dropdown.dart @@ -2,28 +2,14 @@ import 'package:flutter/material.dart'; import 'package:soundpool/soundpool.dart'; import '../constants/sounds.dart'; -/// Possible interval states -// enum IntervalStates { start, work, rest, complete } - -/// -/// Background service countdown interval timer. -/// class SoundDropdown extends StatefulWidget { final String title; - final String initialSelection; - final Soundpool pool; - final Key dropdownKey; - final List soundsList; - final Function? onFinished; - /// - /// Simple countdown timer - /// const SoundDropdown({ super.key, required this.title, @@ -38,9 +24,6 @@ class SoundDropdown extends StatefulWidget { SoundDropdownState createState() => SoundDropdownState(); } -/// -/// State of timer -/// class SoundDropdownState extends State with WidgetsBindingObserver { @override @@ -70,7 +53,6 @@ class SoundDropdownState extends State child: Text( widget.title, style: const TextStyle( - // color: Colors.grey[700], fontWeight: FontWeight.bold, ), ), @@ -80,7 +62,6 @@ class SoundDropdownState extends State width: 240, initialSelection: initialSelection, onSelected: (String? value) { - // This is called when the user selects an item. widget.onFinished!(value); }, dropdownMenuEntries: widget.soundsList