diff --git a/lib/constants/snackbars.dart b/lib/constants/snackbars.dart index d09ccb6..d3da9ca 100644 --- a/lib/constants/snackbars.dart +++ b/lib/constants/snackbars.dart @@ -1,106 +1,12 @@ import 'package:flutter/material.dart'; -const invalidJsonSnackBar = SnackBar( - content: Text('File contains invalid JSON'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, +const errorDeletingWorkoutSnackBar = SnackBar( + content: Text('Error deleting workout'), ); -const invalidConfigSnackBar = SnackBar( - content: Text('File contains invalid workout configuration'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const invalidJsonMultipleSnackBar = SnackBar( - content: Text('Not all files imported, found invalid JSON'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const invalidConfigMultipleSnackBar = SnackBar( - content: Text('Not all files imported, found invalid workout configuration'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const successfulImportSnackBar = SnackBar( - content: Text('Import successful!'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const successfulShareSnackBar = SnackBar( - content: Text('Share successful!'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const errorShareSnackBar = SnackBar( - backgroundColor: Color.fromARGB(255, 132, 19, 11), - content: Text('Share not completed', style: TextStyle(color: Colors.white)), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const successfulShareMultipleSnackBar = SnackBar( - content: Text('Files shared successfully!'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const successfulSaveMultipleToDeviceSnackBar = SnackBar( - content: Text('Files successfully saved to device!'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const successfulSaveToDeviceSnackBar = SnackBar( - content: Text('Saved file to device!'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const errorTimerExists = SnackBar( - content: Text('Could not import, timer with same ID exists'), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const errorMultipleTimerExists = SnackBar( - content: Text( - 'Not all files imported, timer with same ID exists', - style: TextStyle(color: Colors.white), - ), - backgroundColor: Color.fromARGB(255, 132, 19, 11), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const errorShareMultipleSnackBar = SnackBar( - content: Text('Share not completed', style: TextStyle(color: Colors.white)), - backgroundColor: Color.fromARGB(255, 132, 19, 11), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); - -const errorSaveMultipleSnackBar = SnackBar( - content: Text('Save not completed', style: TextStyle(color: Colors.white)), - backgroundColor: Color.fromARGB(255, 132, 19, 11), - behavior: SnackBarBehavior.fixed, - duration: Duration(seconds: 4), - showCloseIcon: true, -); +SnackBar createErrorSnackbar({required String errorMessage}) { + return SnackBar( + content: Text(errorMessage), + backgroundColor: Colors.red[900], + ); +} diff --git a/lib/database/database_manager.dart b/lib/database/database_manager.dart index ca24ee6..bc30054 100644 --- a/lib/database/database_manager.dart +++ b/lib/database/database_manager.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:path/path.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import '../workout_data_type/workout_type.dart'; +import '../log/log.dart'; +import '../old/workout_data_type/workout_type.dart'; class DatabaseManager { /// The name of the database. @@ -18,83 +18,43 @@ class DatabaseManager { /// static const String _workoutTableName = "WorkoutTable"; + /// Initialize the database. + /// Future initDB() async { - debugPrint("initDB executed"); + logger.d("Initializing database"); + + /// Get a path to the database. + /// + String path = join(await getDatabasesPath(), _databaseName); + + /// The version of the database. + /// + int databaseVersion = 6; + + /// The version to start any migrations from. + /// + int versionToStartMigration = 2; + /// If the platform is Windows or Linux, use the FFI database factory. + /// Full app functionaly for Windows / Linux is not yet available. + /// if (Platform.isWindows || Platform.isLinux) { + logger.d("Platform is Windows or Linu, using FFI database factory"); + // Initialize FFI sqfliteFfiInit(); + // Change the default factory databaseFactory = databaseFactoryFfiNoIsolate; - } - //Directory documentsDirectory = await getApplicationDocumentsDirectory(); - String path = join(await getDatabasesPath(), _databaseName); - // Clear database for testing - // await deleteDatabase(path); - if (Platform.isWindows || Platform.isLinux) { - return await openDatabase( - inMemoryDatabasePath, - version: 5, - onCreate: (db, version) async { - await db.execute(''' - CREATE TABLE IF NOT EXISTS WorkoutTable(id TEXT PRIMARY KEY, - title TEXT, - numExercises INTEGER, - exercises TEXT, - getReadyTime INTEGER, - exerciseTime INTEGER, - restTime INTEGER, - halfTime INTEGER, - breakTime INTEGER, - warmupTime INTEGER, - cooldownTime INTEGER, - iterations INTEGER, - halfwayMark INTEGER, - workSound TEXT, - restSound TEXT, - halfwaySound TEXT, - completeSound TEXT, - countdownSound TEXT, - colorInt INTEGER, - workoutIndex INTEGER, - showMinutes INTEGER - ) - '''); - }, - onUpgrade: (db, oldVersion, newVersion) async { - if (oldVersion == 1) { - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN colorInt INTEGER;"); - } - if (oldVersion == 2) { - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN workoutIndex INTEGER;"); - } - if (oldVersion == 3) { - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN showMinutes INTEGER;"); - } - if (oldVersion < newVersion) { - print("Add columns"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN getReadyTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN breakTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN warmupTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN cooldownTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN iterations INTEGER;"); - } - }, - ); + // Set the path to an in-memory database + path = inMemoryDatabasePath; } + return await openDatabase( path, - version: 6, - onCreate: (Database db, int version) async { + version: databaseVersion, + onCreate: (db, version) async { await db.execute(''' CREATE TABLE IF NOT EXISTS WorkoutTable(id TEXT PRIMARY KEY, title TEXT, @@ -121,38 +81,49 @@ class DatabaseManager { '''); }, onUpgrade: (db, oldVersion, newVersion) async { - if (oldVersion == 2) { - await db - .execute("ALTER TABLE WorkoutTable ADD COLUMN colorInt INTEGER;"); - } - if (oldVersion == 3) { - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN workoutIndex INTEGER;"); - } - if (oldVersion == 4) { - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN showMinutes INTEGER;"); - } - if (oldVersion < newVersion) { - print("Add columns"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN getReadyTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN breakTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN warmupTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN cooldownTime INTEGER;"); - await db.execute( - "ALTER TABLE WorkoutTable ADD COLUMN iterations INTEGER;"); - } + migrateDatabase(oldVersion, newVersion, db, versionToStartMigration); }, ); } + /// Migrate the database from one version to another. + /// - [oldVersion] The current version of the database. + /// - [newVersion] The new version of the database. + /// - [db] The database to migrate. + /// - [versionToStartMigration] The version to start the migration from. + /// + Future migrateDatabase(int oldVersion, int newVersion, Database db, + int versionToStartMigration) async { + if (oldVersion == versionToStartMigration) { + await db.execute("ALTER TABLE WorkoutTable ADD COLUMN colorInt INTEGER;"); + } + if (oldVersion == versionToStartMigration + 1) { + await db + .execute("ALTER TABLE WorkoutTable ADD COLUMN workoutIndex INTEGER;"); + } + if (oldVersion == versionToStartMigration + 2) { + await db + .execute("ALTER TABLE WorkoutTable ADD COLUMN showMinutes INTEGER;"); + } + if (oldVersion < newVersion) { + await db + .execute("ALTER TABLE WorkoutTable ADD COLUMN getReadyTime INTEGER;"); + await db + .execute("ALTER TABLE WorkoutTable ADD COLUMN breakTime INTEGER;"); + await db + .execute("ALTER TABLE WorkoutTable ADD COLUMN warmupTime INTEGER;"); + await db + .execute("ALTER TABLE WorkoutTable ADD COLUMN cooldownTime INTEGER;"); + await db + .execute("ALTER TABLE WorkoutTable ADD COLUMN iterations INTEGER;"); + } + } + /// Inserts the given list into the given database. + /// - [workout] The workout to insert. + /// - [database] The database to insert the workout into. /// - Future insertList(Workout workout, Database database) async { + Future insertWorkout(Workout workout, Database database) async { /// Get a reference to the database. /// final db = database; @@ -169,8 +140,10 @@ class DatabaseManager { } /// Update the given list in the given database. + /// - [workout] The workout to update. + /// - [database] The database to update the workout in. /// - Future updateList(Workout workout, Database database) async { + Future updateWorkout(Workout workout, Database database) async { /// Get a reference to the database. /// final db = database; @@ -185,12 +158,8 @@ class DatabaseManager { ); } - Future deleteList(String id, Future database) async { - /// Get a reference to the database. - /// - final db = await database; - - await db.delete( + Future deleteWorkout(String id, Database database) async { + await database.delete( _workoutTableName, where: 'id = ?', // Use a `where` clause to delete a specific list. whereArgs: [ @@ -199,14 +168,40 @@ class DatabaseManager { ); } - Future> lists(Future database) async { - /// Get a reference to the database. - /// - final db = await database; + /// Asynchronously deletes a workout list from the database and updates the + /// workout indices of remaining lists accordingly. + /// + /// Parameters: + /// - [workoutArgument]: The 'Workout' object representing the list to be deleted. + /// + /// Returns: + /// - A Future representing the completion of the delete operation. + Future deleteWorkoutAndReorder( + Workout workout, Database database) async { + // Delete the specified workout list from the database. + await DatabaseManager() + .deleteWorkout(workout.id, database) + .then((value) async { + // Retrieve the updated list of workouts from the database. + List workouts = await DatabaseManager().workouts(database); + + // Sort the workouts based on their workout indices. + workouts.sort((a, b) => a.workoutIndex.compareTo(b.workoutIndex)); + + // Update the workout indices of remaining lists after the deleted list. + for (int i = workout.workoutIndex; i < workouts.length; i++) { + workouts[i].workoutIndex = i; + await DatabaseManager() + .updateWorkout(workouts[i], await DatabaseManager().initDB()); + } + }); + } + Future> workouts(Database database) async { /// Query the table for all the TodoLists. /// - final List> maps = await db.query(_workoutTableName); + final List> maps = + await database.query(_workoutTableName); /// Convert the List into a List. /// diff --git a/lib/main.dart b/lib/main.dart index 806f6fd..8b2694f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,62 +1,29 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:logger/logger.dart'; -import 'package:openhiit/helper_widgets/fab_column.dart'; -import 'package:openhiit/import_export/local_file_util.dart'; -import 'package:openhiit/utils/functions.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:openhiit/database/database_manager.dart'; +import 'package:openhiit/log/log.dart'; +import 'package:openhiit/old/constants/snackbars.dart'; +import 'package:openhiit/old/create_workout/select_timer.dart'; +import 'package:openhiit/old/helper_widgets/export_bottom_sheet.dart'; +import 'package:openhiit/old/helper_widgets/fab_column.dart'; +import 'package:openhiit/pages/view_workout/view_workout.dart'; +import 'package:openhiit/pages/home/widgets/reorderable_workout_grid_view.dart'; +import 'package:openhiit/utils/local_file_util.dart'; +import 'package:openhiit/old/workout_data_type/workout_type.dart'; +import 'package:openhiit/pages/home/widgets/workout_card.dart'; import 'package:share_plus/share_plus.dart'; import 'package:sqflite/sqflite.dart'; -import 'constants/snackbars.dart'; -import 'create_workout/select_timer.dart'; -import 'helper_widgets/export_bottom_sheet.dart'; -import 'helper_widgets/loader.dart'; -import 'workout_data_type/workout_type.dart'; -import 'database/database_manager.dart'; -import 'start_workout/view_workout.dart'; -import 'helper_widgets/timer_list_tile.dart'; - -// Global logger instance for logging messages -var logger = Logger( - printer: PrettyPrinter(methodCount: 0), -); // Global flag to indicate if exporting is in progress bool exporting = false; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - if (Platform.isAndroid) { - await Permission.scheduleExactAlarm.isDenied.then((value) { - if (value) { - Permission.scheduleExactAlarm.request(); - } - }); - } - - GoogleFonts.config.allowRuntimeFetching = false; - - /// Monospaced font licensing. - /// - LicenseRegistry.addLicense(() async* { - final license = await rootBundle.loadString('google_fonts/OFL.txt'); - yield LicenseEntryWithLineBreaks(['google_fonts'], license); - }); - - runApp(const WorkoutTimer()); +void main() { + runApp(const MyApp()); } -class WorkoutTimer extends StatelessWidget { - const WorkoutTimer({super.key}); +class MyApp extends StatelessWidget { + const MyApp({super.key}); - /// Application root. + // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( @@ -64,7 +31,7 @@ class WorkoutTimer extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData(), darkTheme: ThemeData.dark(), // standard dark theme - themeMode: ThemeMode.system, + themeMode: ThemeMode.system, // use system theme home: const MyHomePage(), ); } @@ -79,7 +46,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { /// List of workouts for reordering. The newly reordered - /// workout indeices with be saved to the DB. + /// workout indices will be saved to the database. /// List reorderableWorkoutList = []; @@ -88,187 +55,21 @@ class _MyHomePageState extends State { /// late Future> workouts; - /// Initialize... - @override - void initState() { - super.initState(); - workouts = DatabaseManager().lists(DatabaseManager().initDB()); - } - // --- - - /// Callback function for handling the reordering of items in the list. + /// The database instance. /// - /// Parameters: - /// - [oldIndex]: The index of the item before reordering. - /// - [newIndex]: The index where the item is moved to after reordering. - /// - void _onReorder(int oldIndex, int newIndex) async { - // Ensure newIndex does not exceed the length of the list. - if (newIndex > reorderableWorkoutList.length) { - newIndex = reorderableWorkoutList.length; - } - - // Adjust newIndex if oldIndex is less than newIndex. - if (oldIndex < newIndex) newIndex -= 1; - - // Extract the Workout item being reordered. - final Workout item = reorderableWorkoutList[oldIndex]; - // Remove the item from its old position. - reorderableWorkoutList.removeAt(oldIndex); - - // Update the workoutIndex of the item to the new position. - item.workoutIndex = newIndex; - // Insert the item at the new position. - reorderableWorkoutList.insert(newIndex, item); + late Database database; - // Update the workoutIndex for all items in the list. - setState(() { - for (var i = 0; i < reorderableWorkoutList.length; i++) { - reorderableWorkoutList[i].workoutIndex = i; - } - }); - - // Initialize the database and update the workout order in the database. - Database database = await DatabaseManager().initDB(); - - for (var i = 0; i < reorderableWorkoutList.length; i++) { - // Update the workout order in the database. - await DatabaseManager().updateList(reorderableWorkoutList[i], database); - } - } - // --- - - /// Method called when a workout is tapped. Opens up the view workout page - /// for that workout. + /// Load the workouts from the database. /// - void onWorkoutTap(Workout tappedWorkout) { - /// Push the ViewWorkout page. - /// - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ViewWorkout(), - - /// Pass the [tappedWorkout] as an argument to - /// the ViewWorkout page. - settings: RouteSettings( - arguments: tappedWorkout, - ), - ), - ).then((value) { - /// When we come back to the hompage, refresh the - /// list of workouts by reloading from the DB. - /// - setState(() { - workouts = DatabaseManager().lists(DatabaseManager().initDB()); - }); - }); - } - // --- - - /// 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. - children: [ - /// For each workout in the returned DB data snapshot. - /// - for (final workout in snapshot.data) - TimerListTile( - key: Key( - '${workout.workoutIndex}'), // Unique key for each list item. - workout: workout, - onTap: () { - onWorkoutTap(workout); - }, - index: workout.workoutIndex, - ), - ], - ); - } - // --- - - /// 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}'), - ), - ]; - 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('Awaiting result...'), - ), - ]; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: children, - ), - ); + Future> loadWorkouts() async { + try { + database = await DatabaseManager().initDB(); + return DatabaseManager().workouts(database); + } catch (e) { + logger.e("Failed to load workouts: $e"); + rethrow; + } } - // --- /// Load the page for the user to select whether they'd like /// to create a new interval timer or workout. @@ -282,7 +83,7 @@ class _MyHomePageState extends State { /// list of workouts by reloading from the DB. /// setState(() { - workouts = DatabaseManager().lists(DatabaseManager().initDB()); + workouts = DatabaseManager().workouts(database); }); }); } @@ -352,7 +153,12 @@ class _MyHomePageState extends State { LocalFileUtil fileUtil = LocalFileUtil(); - await fileUtil.writeFile(loadedWorkouts); + try { + await fileUtil.writeFile(loadedWorkouts); + } catch (e) { + logger.e("Failed to write file: $e"); + rethrow; + } if (buildContext.mounted) { ShareResult? result = @@ -407,79 +213,667 @@ 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. + /// Method called when a workout is tapped. Opens up the view workout page + /// for that workout. + /// - [tappedWorkout]: The workout that was tapped. /// - 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); - }, - child: child, + void onWorkoutTap(Workout tappedWorkout) { + /// Push the ViewWorkout page. + /// + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ViewWorkout(), + + /// Pass the [tappedWorkout] as an argument to + /// the ViewWorkout page. + settings: RouteSettings( + arguments: tappedWorkout, + ), + ), + ).then((value) { + /// When we come back to the hompage, refresh the + /// list of workouts by reloading from the DB. + /// + setState(() { + workouts = DatabaseManager().workouts(database); + }); + }); + } + // --- + + /// Generates the loading circle, displayed 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('Awaiting result...'), + ), + ]; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: children, + ), + ); + } + // --- + + /// 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, + ), ); } // --- - /// Build the home screen UI. + /// Initialize... + /// - Load workouts from the database. /// @override - Widget build(BuildContext context) { - setStatusBarBrightness(context); - - return Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: SafeArea( - child: Scaffold( - - /// Pushes to [SelectTimer()] - floatingActionButton: Visibility( - visible: !exporting, - child: FABColumn(bulk: bulkExport, create: pushSelectTimerPage), - ), - body: Stack(children: [ - Container( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - child: FutureBuilder( - future: workouts, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - /// When [workouts] has successfully loaded. - if (snapshot.hasData) { - if (snapshot.data!.isEmpty) { - return workoutEmpty(); - } else { - reorderableWorkoutList = snapshot.data; - reorderableWorkoutList.sort((a, b) => - a.workoutIndex.compareTo(b.workoutIndex)); - return workoutListView(snapshot); - } - } - - /// When there was an error loading [workouts]. - else if (snapshot.hasError) { - return workoutFetchError(snapshot); - } - - /// While still waiting to load [workouts]. - else { - return workoutLoading(); - } - }))), - LoaderTransparent( - loadingMessage: "Exporting file(s)", - visibile: exporting, - ) - ])), - )); + void initState() { + super.initState(); + workouts = loadWorkouts(); } // --- + + @override + Widget build(BuildContext context) { + /// Build the item for the ReorderableGridView. + /// - [text] The text to display in the item. + /// - Returns a Card widget with the given text. + /// + Widget buildItem(Workout workout) { + return WorkoutCard( + key: ValueKey(workout.id), + workout: workout, + onWorkoutTap: onWorkoutTap, + ); + } + + return Scaffold( + body: SafeArea( + child: Center( + child: FutureBuilder>( + future: workouts, + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + /// Data has loaded successfully. + if (snapshot.hasData) { + /// Encountered an error loading the data. + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + if (snapshot.data!.isEmpty) { + return workoutEmpty(); + } + + reorderableWorkoutList = snapshot.data!; + + reorderableWorkoutList + .sort((a, b) => a.workoutIndex.compareTo(b.workoutIndex)); + + return ReorderableWorkoutGridView( + onReorder: (oldIndex, newIndex) async { + logger.d( + "Old Index: $oldIndex, New Index: $newIndex, Length: ${reorderableWorkoutList.length}"); + + // Ensure newIndex does not exceed the length of the list. + if (newIndex > reorderableWorkoutList.length) { + newIndex = reorderableWorkoutList.length; + } + + final Workout item = reorderableWorkoutList[oldIndex]; + + reorderableWorkoutList.removeAt(oldIndex); + + item.workoutIndex = newIndex; + + reorderableWorkoutList.insert(newIndex, item); + + // Update the workoutIndex for all items in the list. + setState(() { + for (var i = 0; i < reorderableWorkoutList.length; i++) { + reorderableWorkoutList[i].workoutIndex = i; + } + }); + + // Initialize the database and update the workout order in the database. + for (var i = 0; i < reorderableWorkoutList.length; i++) { + await DatabaseManager() + .updateWorkout(reorderableWorkoutList[i], database); + } + }, + workouts: reorderableWorkoutList + .map((Workout e) => buildItem(e)) + .toList(), + ); + } + } else { + /// While still waiting to load [workouts]. + return workoutLoading(); + } + }, + )), + ), + floatingActionButton: Visibility( + visible: !exporting, + child: FABColumn(bulk: bulkExport, create: pushSelectTimerPage), + ), + ); + } } + +// import 'dart:async'; +// import 'dart:io'; +// import 'dart:ui'; + +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:google_fonts/google_fonts.dart'; +// import 'package:logger/logger.dart'; +// import 'package:openhiit/old/helper_widgets/fab_column.dart'; +// import 'package:openhiit/old/import_export/local_file_util.dart'; +// import 'package:openhiit/old/utils/functions.dart'; +// import 'package:permission_handler/permission_handler.dart'; +// import 'package:share_plus/share_plus.dart'; +// import 'package:sqflite/sqflite.dart'; +// import 'old/constants/snackbars.dart'; +// import 'old/create_workout/select_timer.dart'; +// import 'old/helper_widgets/export_bottom_sheet.dart'; +// import 'old/helper_widgets/loader.dart'; +// import 'old/workout_data_type/workout_type.dart'; +// import 'database/database_manager.dart'; +// import 'old/start_workout/view_workout.dart'; +// import 'old/helper_widgets/timer_list_tile.dart'; + +// // Global logger instance for logging messages +// var logger = Logger( +// printer: PrettyPrinter(methodCount: 0), +// ); + +// // Global flag to indicate if exporting is in progress +// bool exporting = false; + +// void main() async { +// WidgetsFlutterBinding.ensureInitialized(); + +// if (Platform.isAndroid) { +// await Permission.scheduleExactAlarm.isDenied.then((value) { +// if (value) { +// Permission.scheduleExactAlarm.request(); +// } +// }); +// } + +// GoogleFonts.config.allowRuntimeFetching = false; + +// /// Monospaced font licensing. +// /// +// LicenseRegistry.addLicense(() async* { +// final license = await rootBundle.loadString('google_fonts/OFL.txt'); +// yield LicenseEntryWithLineBreaks(['google_fonts'], license); +// }); + +// runApp(const WorkoutTimer()); +// } + +// class WorkoutTimer extends StatelessWidget { +// const WorkoutTimer({super.key}); + +// /// Application root. +// @override +// Widget build(BuildContext context) { +// return MaterialApp( +// title: 'OpenHIIT', +// debugShowCheckedModeBanner: false, +// theme: ThemeData(), +// darkTheme: ThemeData.dark(), // standard dark theme +// themeMode: ThemeMode.system, +// home: const MyHomePage(), +// ); +// } +// } + +// class MyHomePage extends StatefulWidget { +// const MyHomePage({super.key}); + +// @override +// State createState() => _MyHomePageState(); +// } + +// class _MyHomePageState extends State { +// /// List of workouts for reordering. The newly reordered +// /// workout indeices with be saved to the DB. +// /// +// List reorderableWorkoutList = []; + +// /// The initial list of workouts to be loaded fresh +// /// from the DB. +// /// +// late Future> workouts; + +// /// Initialize... +// @override +// void initState() { +// super.initState(); +// workouts = DatabaseManager().workouts(DatabaseManager().initDB()); +// } +// // --- + +// /// Callback function for handling the reordering of items in the list. +// /// +// /// Parameters: +// /// - [oldIndex]: The index of the item before reordering. +// /// - [newIndex]: The index where the item is moved to after reordering. +// /// +// void _onReorder(int oldIndex, int newIndex) async { +// // Ensure newIndex does not exceed the length of the list. +// if (newIndex > reorderableWorkoutList.length) { +// newIndex = reorderableWorkoutList.length; +// } + +// // Adjust newIndex if oldIndex is less than newIndex. +// if (oldIndex < newIndex) newIndex -= 1; + +// // Extract the Workout item being reordered. +// final Workout item = reorderableWorkoutList[oldIndex]; +// // Remove the item from its old position. +// reorderableWorkoutList.removeAt(oldIndex); + +// // Update the workoutIndex of the item to the new position. +// item.workoutIndex = newIndex; +// // Insert the item at the new position. +// reorderableWorkoutList.insert(newIndex, item); + +// // Update the workoutIndex for all items in the list. +// setState(() { +// for (var i = 0; i < reorderableWorkoutList.length; i++) { +// reorderableWorkoutList[i].workoutIndex = i; +// } +// }); + +// // Initialize the database and update the workout order in the database. +// Database database = await DatabaseManager().initDB(); + +// for (var i = 0; i < reorderableWorkoutList.length; i++) { +// // Update the workout order in the database. +// await DatabaseManager() +// .updateWorkout(reorderableWorkoutList[i], database); +// } +// } +// // --- + +// /// Method called when a workout is tapped. Opens up the view workout page +// /// for that workout. +// /// +// void onWorkoutTap(Workout tappedWorkout) { +// /// Push the ViewWorkout page. +// /// +// Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => const ViewWorkout(), + +// /// Pass the [tappedWorkout] as an argument to +// /// the ViewWorkout page. +// settings: RouteSettings( +// arguments: tappedWorkout, +// ), +// ), +// ).then((value) { +// /// When we come back to the hompage, refresh the +// /// list of workouts by reloading from the DB. +// /// +// setState(() { +// workouts = DatabaseManager().workouts(DatabaseManager().initDB()); +// }); +// }); +// } +// // --- + +// /// 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. +// children: [ +// /// For each workout in the returned DB data snapshot. +// /// +// for (final workout in snapshot.data) +// TimerListTile( +// key: Key( +// '${workout.workoutIndex}'), // Unique key for each list item. +// workout: workout, +// onTap: () { +// onWorkoutTap(workout); +// }, +// index: workout.workoutIndex, +// ), +// ], +// ); +// } +// // --- + +// /// 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}'), +// ), +// ]; +// 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('Awaiting result...'), +// ), +// ]; +// return Center( +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// crossAxisAlignment: CrossAxisAlignment.center, +// children: children, +// ), +// ); +// } +// // --- + +// /// 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()), +// ).then((value) { +// /// When we come back to the hompage, refresh the +// /// list of workouts by reloading from the DB. +// /// +// setState(() { +// workouts = DatabaseManager().workouts(DatabaseManager().initDB()); +// }); +// }); +// } +// // --- + +// /// 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; +// }); + +// List loadedWorkouts = await workouts; + +// LocalFileUtil fileUtil = LocalFileUtil(); + +// bool result = await fileUtil.saveFileToDevice(loadedWorkouts); + +// if (result) { +// setState(() { +// logger.i("Exporting complete."); +// exporting = false; +// }); + +// if (mounted) { +// Navigator.pop(context); +// ScaffoldMessenger.of(context) +// .showSnackBar(successfulSaveMultipleToDeviceSnackBar); +// } +// } else { +// setState(() { +// logger.e("Export not completed."); +// exporting = false; +// }); + +// if (mounted) { +// Navigator.pop(context); +// ScaffoldMessenger.of(context).showSnackBar(errorSaveMultipleSnackBar); +// } +// } +// } + +// /// 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 = await workouts; + +// LocalFileUtil fileUtil = LocalFileUtil(); + +// await fileUtil.writeFile(loadedWorkouts); + +// if (buildContext.mounted) { +// ShareResult? result = +// await fileUtil.shareMultipleFiles(loadedWorkouts, 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(errorShareMultipleSnackBar); +// } +// } else { +// setState(() { +// logger.i("Export and share complete."); +// exporting = false; +// }); + +// if (mounted) { +// Navigator.pop(context); +// ScaffoldMessenger.of(context) +// .showSnackBar(successfulShareMultipleSnackBar); +// } +// } +// } +// } +// } + +// /// 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, +// builder: (BuildContext context) { +// return ExportBottomSheet( +// workout: null, +// save: saveWorkouts, +// share: () => shareWorkouts(context), +// ); +// }, +// ); +// } +// // --- + +// /// 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); +// }, +// child: child, +// ); +// } +// // --- + +// /// Build the home screen UI. +// /// +// @override +// Widget build(BuildContext context) { +// setStatusBarBrightness(context); + +// return Container( +// color: Theme.of(context).scaffoldBackgroundColor, +// child: SafeArea( +// child: Scaffold( + +// /// Pushes to [SelectTimer()] +// floatingActionButton: Visibility( +// visible: !exporting, +// child: FABColumn(bulk: bulkExport, create: pushSelectTimerPage), +// ), +// body: Stack(children: [ +// Container( +// padding: const EdgeInsets.all(8.0), +// child: SizedBox( +// child: FutureBuilder( +// future: workouts, +// builder: +// (BuildContext context, AsyncSnapshot snapshot) { +// /// When [workouts] has successfully loaded. +// if (snapshot.hasData) { +// if (snapshot.data!.isEmpty) { +// return workoutEmpty(); +// } else { +// reorderableWorkoutList = snapshot.data; +// reorderableWorkoutList.sort((a, b) => +// a.workoutIndex.compareTo(b.workoutIndex)); +// return workoutListView(snapshot); +// } +// } + +// /// When there was an error loading [workouts]. +// else if (snapshot.hasError) { +// return workoutFetchError(snapshot); +// } + +// /// While still waiting to load [workouts]. +// else { +// return workoutLoading(); +// } +// }))), +// LoaderTransparent( +// loadingMessage: "Exporting file(s)", +// visibile: exporting, +// ) +// ])), +// )); +// } +// // --- +// } diff --git a/lib/card_widgets/card_item_animated.dart b/lib/old/card_widgets/card_item_animated.dart similarity index 100% rename from lib/card_widgets/card_item_animated.dart rename to lib/old/card_widgets/card_item_animated.dart diff --git a/lib/old/constants/snackbars.dart b/lib/old/constants/snackbars.dart new file mode 100644 index 0000000..d09ccb6 --- /dev/null +++ b/lib/old/constants/snackbars.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +const invalidJsonSnackBar = SnackBar( + content: Text('File contains invalid JSON'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const invalidConfigSnackBar = SnackBar( + content: Text('File contains invalid workout configuration'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const invalidJsonMultipleSnackBar = SnackBar( + content: Text('Not all files imported, found invalid JSON'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const invalidConfigMultipleSnackBar = SnackBar( + content: Text('Not all files imported, found invalid workout configuration'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const successfulImportSnackBar = SnackBar( + content: Text('Import successful!'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const successfulShareSnackBar = SnackBar( + content: Text('Share successful!'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const errorShareSnackBar = SnackBar( + backgroundColor: Color.fromARGB(255, 132, 19, 11), + content: Text('Share not completed', style: TextStyle(color: Colors.white)), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const successfulShareMultipleSnackBar = SnackBar( + content: Text('Files shared successfully!'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const successfulSaveMultipleToDeviceSnackBar = SnackBar( + content: Text('Files successfully saved to device!'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const successfulSaveToDeviceSnackBar = SnackBar( + content: Text('Saved file to device!'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const errorTimerExists = SnackBar( + content: Text('Could not import, timer with same ID exists'), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const errorMultipleTimerExists = SnackBar( + content: Text( + 'Not all files imported, timer with same ID exists', + style: TextStyle(color: Colors.white), + ), + backgroundColor: Color.fromARGB(255, 132, 19, 11), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const errorShareMultipleSnackBar = SnackBar( + content: Text('Share not completed', style: TextStyle(color: Colors.white)), + backgroundColor: Color.fromARGB(255, 132, 19, 11), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); + +const errorSaveMultipleSnackBar = SnackBar( + content: Text('Save not completed', style: TextStyle(color: Colors.white)), + backgroundColor: Color.fromARGB(255, 132, 19, 11), + behavior: SnackBarBehavior.fixed, + duration: Duration(seconds: 4), + showCloseIcon: true, +); diff --git a/lib/create_workout/constants/set_timings_constants.dart b/lib/old/create_workout/constants/set_timings_constants.dart similarity index 100% rename from lib/create_workout/constants/set_timings_constants.dart rename to lib/old/create_workout/constants/set_timings_constants.dart diff --git a/lib/create_workout/constants/sound_name_map.dart b/lib/old/create_workout/constants/sound_name_map.dart similarity index 100% rename from lib/create_workout/constants/sound_name_map.dart rename to lib/old/create_workout/constants/sound_name_map.dart diff --git a/lib/create_workout/constants/sounds.dart b/lib/old/create_workout/constants/sounds.dart similarity index 100% rename from lib/create_workout/constants/sounds.dart rename to lib/old/create_workout/constants/sounds.dart diff --git a/lib/create_workout/create_timer.dart b/lib/old/create_workout/create_timer.dart similarity index 94% rename from lib/create_workout/create_timer.dart rename to lib/old/create_workout/create_timer.dart index 0f755ac..07e68ba 100644 --- a/lib/create_workout/create_timer.dart +++ b/lib/old/create_workout/create_timer.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:openhiit/create_workout/main_widgets/create_form.dart'; +import 'package:openhiit/old/create_workout/main_widgets/create_form.dart'; import '../workout_data_type/workout_type.dart'; -import './set_timings.dart'; +import 'set_timings.dart'; import 'main_widgets/submit_button.dart'; class CreateTimer extends StatefulWidget { diff --git a/lib/create_workout/create_workout.dart b/lib/old/create_workout/create_workout.dart similarity index 91% rename from lib/create_workout/create_workout.dart rename to lib/old/create_workout/create_workout.dart index 6d8c3b0..3aec007 100644 --- a/lib/create_workout/create_workout.dart +++ b/lib/old/create_workout/create_workout.dart @@ -1,14 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:logger/logger.dart'; -import 'package:openhiit/create_workout/main_widgets/create_form.dart'; +import 'package:openhiit/old/create_workout/main_widgets/create_form.dart'; import '../workout_data_type/workout_type.dart'; import 'main_widgets/submit_button.dart'; import 'set_exercises.dart'; -var logger = Logger( - printer: PrettyPrinter(methodCount: 0), -); - class CreateWorkout extends StatefulWidget { const CreateWorkout({super.key}); diff --git a/lib/create_workout/form_picker_widgets/clock_picker.dart b/lib/old/create_workout/form_picker_widgets/clock_picker.dart similarity index 100% rename from lib/create_workout/form_picker_widgets/clock_picker.dart rename to lib/old/create_workout/form_picker_widgets/clock_picker.dart diff --git a/lib/create_workout/form_picker_widgets/color_picker.dart b/lib/old/create_workout/form_picker_widgets/color_picker.dart similarity index 100% rename from lib/create_workout/form_picker_widgets/color_picker.dart rename to lib/old/create_workout/form_picker_widgets/color_picker.dart diff --git a/lib/create_workout/form_picker_widgets/expansion_additional_config_tile.dart b/lib/old/create_workout/form_picker_widgets/expansion_additional_config_tile.dart similarity index 100% rename from lib/create_workout/form_picker_widgets/expansion_additional_config_tile.dart rename to lib/old/create_workout/form_picker_widgets/expansion_additional_config_tile.dart diff --git a/lib/create_workout/form_picker_widgets/number_input.dart b/lib/old/create_workout/form_picker_widgets/number_input.dart similarity index 100% rename from lib/create_workout/form_picker_widgets/number_input.dart rename to lib/old/create_workout/form_picker_widgets/number_input.dart diff --git a/lib/create_workout/form_picker_widgets/numerical_input_formatter.dart b/lib/old/create_workout/form_picker_widgets/numerical_input_formatter.dart similarity index 100% rename from lib/create_workout/form_picker_widgets/numerical_input_formatter.dart rename to lib/old/create_workout/form_picker_widgets/numerical_input_formatter.dart diff --git a/lib/create_workout/form_picker_widgets/sound_dropdown.dart b/lib/old/create_workout/form_picker_widgets/sound_dropdown.dart similarity index 100% rename from lib/create_workout/form_picker_widgets/sound_dropdown.dart rename to lib/old/create_workout/form_picker_widgets/sound_dropdown.dart diff --git a/lib/create_workout/form_picker_widgets/time_input_trailing.dart b/lib/old/create_workout/form_picker_widgets/time_input_trailing.dart similarity index 97% rename from lib/create_workout/form_picker_widgets/time_input_trailing.dart rename to lib/old/create_workout/form_picker_widgets/time_input_trailing.dart index 0163522..6d30291 100644 --- a/lib/create_workout/form_picker_widgets/time_input_trailing.dart +++ b/lib/old/create_workout/form_picker_widgets/time_input_trailing.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:openhiit/create_workout/set_timings_utils/set_timings_utils.dart'; +import 'package:openhiit/old/create_workout/set_timings_utils/set_timings_utils.dart'; import 'number_input.dart'; diff --git a/lib/create_workout/form_picker_widgets/time_list_item.dart b/lib/old/create_workout/form_picker_widgets/time_list_item.dart similarity index 100% rename from lib/create_workout/form_picker_widgets/time_list_item.dart rename to lib/old/create_workout/form_picker_widgets/time_list_item.dart diff --git a/lib/create_workout/import_workout.dart b/lib/old/create_workout/import_workout.dart similarity index 97% rename from lib/create_workout/import_workout.dart rename to lib/old/create_workout/import_workout.dart index 509d8ab..7cc1d85 100644 --- a/lib/create_workout/import_workout.dart +++ b/lib/old/create_workout/import_workout.dart @@ -4,12 +4,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'package:openhiit/database/database_manager.dart'; -import 'package:openhiit/helper_widgets/file_error.dart'; -import 'package:openhiit/helper_widgets/loader.dart'; +import 'package:openhiit/old/helper_widgets/file_error.dart'; +import 'package:openhiit/old/helper_widgets/loader.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:uuid/uuid.dart'; import '../helper_widgets/copy_or_skip.dart'; -import '../main.dart'; +import '../../main.dart'; import '../workout_data_type/workout_type.dart'; import 'package:file_picker/file_picker.dart'; @@ -48,7 +48,7 @@ class ImportWorkoutState extends State { "Adding imported workout to database: ${workoutArgument.toString()}"); List workouts = - await DatabaseManager().lists(DatabaseManager().initDB()); + await DatabaseManager().workouts(await DatabaseManager().initDB()); logger.i("Grabbed existing workouts: ${workouts.length}"); @@ -59,10 +59,10 @@ class ImportWorkoutState extends State { for (var i = 0; i < workouts.length; i++) { if (i == 0) { workouts[i].workoutIndex = 0; - await DatabaseManager().insertList(workouts[i], database); + await DatabaseManager().insertWorkout(workouts[i], database); } else { workouts[i].workoutIndex = workouts[i].workoutIndex + 1; - await DatabaseManager().updateList(workouts[i], database); + await DatabaseManager().updateWorkout(workouts[i], database); } } diff --git a/lib/create_workout/main_widgets/create_form.dart b/lib/old/create_workout/main_widgets/create_form.dart similarity index 100% rename from lib/create_workout/main_widgets/create_form.dart rename to lib/old/create_workout/main_widgets/create_form.dart diff --git a/lib/create_workout/main_widgets/submit_button.dart b/lib/old/create_workout/main_widgets/submit_button.dart similarity index 100% rename from lib/create_workout/main_widgets/submit_button.dart rename to lib/old/create_workout/main_widgets/submit_button.dart diff --git a/lib/create_workout/main_widgets/timer_option_card.dart b/lib/old/create_workout/main_widgets/timer_option_card.dart similarity index 100% rename from lib/create_workout/main_widgets/timer_option_card.dart rename to lib/old/create_workout/main_widgets/timer_option_card.dart diff --git a/lib/create_workout/select_timer.dart b/lib/old/create_workout/select_timer.dart similarity index 100% rename from lib/create_workout/select_timer.dart rename to lib/old/create_workout/select_timer.dart diff --git a/lib/create_workout/set_exercises.dart b/lib/old/create_workout/set_exercises.dart similarity index 99% rename from lib/create_workout/set_exercises.dart rename to lib/old/create_workout/set_exercises.dart index e5c53ec..7afddfe 100644 --- a/lib/create_workout/set_exercises.dart +++ b/lib/old/create_workout/set_exercises.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'dart:convert'; import '../workout_data_type/workout_type.dart'; -import './set_timings.dart'; +import 'set_timings.dart'; import 'main_widgets/submit_button.dart'; var logger = Logger( diff --git a/lib/create_workout/set_sounds.dart b/lib/old/create_workout/set_sounds.dart similarity index 95% rename from lib/create_workout/set_sounds.dart rename to lib/old/create_workout/set_sounds.dart index c7ad908..c7cf4a4 100644 --- a/lib/create_workout/set_sounds.dart +++ b/lib/old/create_workout/set_sounds.dart @@ -3,9 +3,9 @@ import 'package:flutter/services.dart'; import 'package:soundpool/soundpool.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; -import '../main.dart'; +import '../../main.dart'; import '../workout_data_type/workout_type.dart'; -import '../database/database_manager.dart'; +import '../../database/database_manager.dart'; import 'form_picker_widgets/sound_dropdown.dart'; import 'main_widgets/submit_button.dart'; import 'constants/sounds.dart'; @@ -45,7 +45,7 @@ class _SetSoundsState extends State { /// if (workoutArgument.id == "") { List workouts = - await DatabaseManager().lists(DatabaseManager().initDB()); + await DatabaseManager().workouts(await DatabaseManager().initDB()); // Give the new workout an ID workoutArgument.id = const Uuid().v1(); @@ -56,10 +56,10 @@ class _SetSoundsState extends State { // Increase the index of all old workouts by 1. for (var i = 0; i < workouts.length; i++) { if (i == 0) { - await DatabaseManager().insertList(workouts[i], database); + await DatabaseManager().insertWorkout(workouts[i], database); } else { workouts[i].workoutIndex = workouts[i].workoutIndex + 1; - await DatabaseManager().updateList(workouts[i], database); + await DatabaseManager().updateWorkout(workouts[i], database); } } } @@ -68,7 +68,7 @@ class _SetSoundsState extends State { /// workout that was edited. Simply update the workout in the DB. /// else { - await DatabaseManager().updateList(workoutArgument, database); + await DatabaseManager().updateWorkout(workoutArgument, database); } } diff --git a/lib/create_workout/set_timings.dart b/lib/old/create_workout/set_timings.dart similarity index 98% rename from lib/create_workout/set_timings.dart rename to lib/old/create_workout/set_timings.dart index edc4ffb..932a4e8 100644 --- a/lib/create_workout/set_timings.dart +++ b/lib/old/create_workout/set_timings.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; -import 'package:openhiit/create_workout/constants/set_timings_constants.dart'; -import './form_picker_widgets/time_input_trailing.dart'; +import 'package:openhiit/old/create_workout/constants/set_timings_constants.dart'; +import 'form_picker_widgets/time_input_trailing.dart'; import '../workout_data_type/workout_type.dart'; import 'main_widgets/submit_button.dart'; -import './form_picker_widgets/time_list_item.dart'; +import 'form_picker_widgets/time_list_item.dart'; import 'set_sounds.dart'; var logger = Logger( diff --git a/lib/create_workout/set_timings_utils/set_timings_utils.dart b/lib/old/create_workout/set_timings_utils/set_timings_utils.dart similarity index 100% rename from lib/create_workout/set_timings_utils/set_timings_utils.dart rename to lib/old/create_workout/set_timings_utils/set_timings_utils.dart diff --git a/lib/helper_widgets/copy_or_skip.dart b/lib/old/helper_widgets/copy_or_skip.dart similarity index 100% rename from lib/helper_widgets/copy_or_skip.dart rename to lib/old/helper_widgets/copy_or_skip.dart diff --git a/lib/helper_widgets/export_bottom_sheet.dart b/lib/old/helper_widgets/export_bottom_sheet.dart similarity index 100% rename from lib/helper_widgets/export_bottom_sheet.dart rename to lib/old/helper_widgets/export_bottom_sheet.dart diff --git a/lib/helper_widgets/fab_column.dart b/lib/old/helper_widgets/fab_column.dart similarity index 100% rename from lib/helper_widgets/fab_column.dart rename to lib/old/helper_widgets/fab_column.dart diff --git a/lib/helper_widgets/file_error.dart b/lib/old/helper_widgets/file_error.dart similarity index 100% rename from lib/helper_widgets/file_error.dart rename to lib/old/helper_widgets/file_error.dart diff --git a/lib/helper_widgets/loader.dart b/lib/old/helper_widgets/loader.dart similarity index 100% rename from lib/helper_widgets/loader.dart rename to lib/old/helper_widgets/loader.dart diff --git a/lib/helper_widgets/start_button.dart b/lib/old/helper_widgets/start_button.dart similarity index 100% rename from lib/helper_widgets/start_button.dart rename to lib/old/helper_widgets/start_button.dart diff --git a/lib/helper_widgets/timer_list_tile.dart b/lib/old/helper_widgets/timer_list_tile.dart similarity index 100% rename from lib/helper_widgets/timer_list_tile.dart rename to lib/old/helper_widgets/timer_list_tile.dart diff --git a/lib/helper_widgets/view_workout_appbar.dart b/lib/old/helper_widgets/view_workout_appbar.dart similarity index 98% rename from lib/helper_widgets/view_workout_appbar.dart rename to lib/old/helper_widgets/view_workout_appbar.dart index 65f3b1a..1497521 100644 --- a/lib/helper_widgets/view_workout_appbar.dart +++ b/lib/old/helper_widgets/view_workout_appbar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:openhiit/helper_widgets/export_bottom_sheet.dart'; +import 'package:openhiit/old/helper_widgets/export_bottom_sheet.dart'; import '../workout_data_type/workout_type.dart'; diff --git a/lib/import_export/local_file_util.dart b/lib/old/import_export/local_file_util.dart similarity index 98% rename from lib/import_export/local_file_util.dart rename to lib/old/import_export/local_file_util.dart index 7d9ae92..0b5cf2f 100644 --- a/lib/import_export/local_file_util.dart +++ b/lib/old/import_export/local_file_util.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; -import 'package:openhiit/workout_data_type/workout_type.dart'; +import 'package:openhiit/old/workout_data_type/workout_type.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; diff --git a/lib/models/list_model.dart b/lib/old/models/list_model.dart similarity index 100% rename from lib/models/list_model.dart rename to lib/old/models/list_model.dart diff --git a/lib/models/list_model_animated.dart b/lib/old/models/list_model_animated.dart similarity index 100% rename from lib/models/list_model_animated.dart rename to lib/old/models/list_model_animated.dart diff --git a/lib/models/list_tile_model.dart b/lib/old/models/list_tile_model.dart similarity index 100% rename from lib/models/list_tile_model.dart rename to lib/old/models/list_tile_model.dart diff --git a/lib/start_workout/workout.dart b/lib/old/start_workout/workout.dart similarity index 100% rename from lib/start_workout/workout.dart rename to lib/old/start_workout/workout.dart diff --git a/lib/utils/functions.dart b/lib/old/utils/functions.dart similarity index 99% rename from lib/utils/functions.dart rename to lib/old/utils/functions.dart index 4dc6577..b1c207a 100644 --- a/lib/utils/functions.dart +++ b/lib/old/utils/functions.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:openhiit/create_workout/import_workout.dart'; +import 'package:openhiit/old/create_workout/import_workout.dart'; import '../create_workout/create_timer.dart'; import '../create_workout/create_workout.dart'; diff --git a/lib/workout_data_type/workout_type.dart b/lib/old/workout_data_type/workout_type.dart similarity index 100% rename from lib/workout_data_type/workout_type.dart rename to lib/old/workout_data_type/workout_type.dart diff --git a/lib/pages/home/widgets/custom_list_tile.dart b/lib/pages/home/widgets/custom_list_tile.dart new file mode 100644 index 0000000..01ef495 --- /dev/null +++ b/lib/pages/home/widgets/custom_list_tile.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +class CustomListTile extends StatelessWidget { + /// Trailing widget for the list tile. + /// + final Widget? trailing; + + /// Cross axis alignment for the row. + /// + final CrossAxisAlignment crossAxisAlignment; + + /// Main axis alignment for the row. + /// + final MainAxisAlignment mainAxisAlignment; + + /// Title style for the list tile. + /// + final TextStyle titleStyle; + + /// Subtitle style for the list tile. + /// + final TextStyle subtitleStyle; + + /// Title for the list tile. + /// + final String title; + + /// Subtitle for the list tile. + /// + final String? subtitle; + + /// Leading widget for the list tile. + /// + final Widget? leading; + + const CustomListTile( + {super.key, + this.trailing, + this.crossAxisAlignment = CrossAxisAlignment.start, + this.mainAxisAlignment = MainAxisAlignment.start, + this.titleStyle = const TextStyle(), + this.subtitleStyle = const TextStyle(), + required this.title, + this.subtitle, + this.leading}); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: crossAxisAlignment, + mainAxisAlignment: mainAxisAlignment, + children: [ + leading ?? Container(), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Column( + crossAxisAlignment: crossAxisAlignment, + mainAxisAlignment: mainAxisAlignment, + children: [ + Text( + title, + style: titleStyle, + ), + Text(subtitle ?? "", style: subtitleStyle), + ], + )), + ), + trailing ?? Container() + ], + ); + } +} diff --git a/lib/pages/home/widgets/reorderable_workout_grid_view.dart b/lib/pages/home/widgets/reorderable_workout_grid_view.dart new file mode 100644 index 0000000..9476c01 --- /dev/null +++ b/lib/pages/home/widgets/reorderable_workout_grid_view.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:reorderable_grid_view/reorderable_grid_view.dart'; + +class ReorderableWorkoutGridView extends StatefulWidget { + const ReorderableWorkoutGridView( + {super.key, required this.onReorder, this.workouts}); + + /// Callback function for when a workout is reordered. + /// + final Function onReorder; + + /// List of workouts to display in the grid view. + /// + final List? workouts; + + @override + State createState() => + _ReorderableWorkoutGridViewState(); +} + +class _ReorderableWorkoutGridViewState + extends State { + @override + Widget build(BuildContext context) { + return ReorderableGridView.count( + dragWidgetBuilderV2: DragWidgetBuilderV2( + builder: (int index, Widget child, ImageProvider? screenshot) { + return child; + }), + childAspectRatio: MediaQuery.of(context).size.width < 550 ? 3 : 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + crossAxisCount: MediaQuery.of(context).size.width < 550 ? 1 : 2, + onReorder: (oldIndex, newIndex) async { + widget.onReorder(oldIndex, newIndex); + }, + children: widget.workouts ?? [], + ); + } +} diff --git a/lib/pages/home/widgets/workout_card.dart b/lib/pages/home/widgets/workout_card.dart new file mode 100644 index 0000000..67ede00 --- /dev/null +++ b/lib/pages/home/widgets/workout_card.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:openhiit/old/workout_data_type/workout_type.dart'; +import 'package:openhiit/pages/home/widgets/custom_list_tile.dart'; + +class WorkoutCard extends StatefulWidget { + const WorkoutCard({ + super.key, + required this.workout, + required this.onWorkoutTap, + }); + + /// The workout to display in the card. + /// + final Workout workout; + + /// Callback function for when the workout is tapped. + /// + final Function(Workout) onWorkoutTap; + + @override + State createState() => _WorkoutCardState(); +} + +class _WorkoutCardState extends State { + static const double titleSmallTextSize = 18; + static const double titleLargeTextSize = 22; + static const double subtitleSmallTextSize = 12; + static const double subtitleLargeTextSize = 15; + + /// Calculate the total time of a workout. + /// - [workout]: The workout to calculate the time for. + /// - Returns: The total time of the workout in minutes. + /// + int calculateWorkoutTime(Workout workout) { + if (workout.iterations > 0) { + return (((workout.workTime * + workout.numExercises * + (workout.iterations + 1)) + + (workout.restTime * + (workout.numExercises - 1) * + workout.iterations) + + (workout.halfTime * workout.numExercises) + + (workout.breakTime * (workout.iterations + 1)) + + workout.warmupTime + + workout.cooldownTime) / + 60) + .ceil(); + } else { + return (((workout.workTime * workout.numExercises) + + (workout.restTime * (workout.numExercises - 1)) + + (workout.halfTime * workout.numExercises) + + workout.warmupTime + + workout.cooldownTime) / + 60) + .ceil(); + } + } + + /// Create a string with information about the workout. + /// - [workout]: The workout to create the information string for. + /// - Returns: A string with information about the workout. + /// + String infoString(Workout workout) { + String info = ""; + + info += 'Intervals - ${workout.numExercises}\n'; + + if (workout.workTime > 0) { + info += 'Work Time - ${workout.workTime} sec\n'; + } + + if (workout.restTime > 0) { + info += 'Rest Time - ${workout.restTime} sec\n'; + } + + info += 'Total Time - ${calculateWorkoutTime(workout)} min'; + + return info; + } + + @override + Widget build(BuildContext context) { + return Card( + color: Color(widget.workout.colorInt), + child: InkWell( + splashColor: Color(widget.workout.colorInt).withAlpha(80), + onTap: () => widget.onWorkoutTap(widget.workout), + child: CustomListTile( + leading: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.workout.exercises.isNotEmpty + ? const Icon( + Icons.fitness_center, + color: Colors.white, + ) + : const Icon( + Icons.timer, + color: Colors.white, + ), + ])), + title: widget.workout.title, + titleStyle: TextStyle( + fontSize: MediaQuery.of(context).size.shortestSide < 550 + ? titleSmallTextSize + : titleLargeTextSize, + color: Colors.white, + ), + subtitle: infoString(widget.workout), + subtitleStyle: TextStyle( + fontSize: MediaQuery.of(context).size.shortestSide < 550 + ? subtitleSmallTextSize + : subtitleLargeTextSize, + color: Colors.white), + trailing: const Padding( + padding: EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.drag_handle, + color: Colors.white, + ), + ])), + ), + ), + ); + } +} diff --git a/lib/pages/view_workout/view_workout.dart b/lib/pages/view_workout/view_workout.dart new file mode 100644 index 0000000..3b6e8bd --- /dev/null +++ b/lib/pages/view_workout/view_workout.dart @@ -0,0 +1,506 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:openhiit/constants/snackbars.dart'; +import 'package:openhiit/database/database_manager.dart'; +import 'package:openhiit/log/log.dart'; +import 'package:openhiit/old/create_workout/create_timer.dart'; +import 'package:openhiit/old/create_workout/create_workout.dart'; +import 'package:openhiit/old/helper_widgets/view_workout_appbar.dart'; +import 'package:openhiit/old/workout_data_type/workout_type.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:uuid/uuid.dart'; + +class ViewWorkout extends StatefulWidget { + const ViewWorkout({super.key}); + + @override + State createState() => _ViewWorkoutState(); +} + +class _ViewWorkoutState extends State { + /// Deletes the workout from the database and reorders the remaining workouts. + /// Displays a snackbar if an error occurs. + /// - [context]: The BuildContext of the current screen. + /// - [workout]: The workout to be deleted. + /// + void delete(Workout workout) async { + Database database = await DatabaseManager().initDB(); + try { + await DatabaseManager().deleteWorkoutAndReorder(workout, database); + } catch (e) { + logger.e('Error deleting workout: $e'); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(errorDeletingWorkoutSnackBar); + } + } + if (mounted) { + Navigator.of(context) + ..pop() + ..pop(); + } + } + + /// Navigates to the specified page. + /// - [page]: The page to navigate to. + /// - [workoutCopy]: The workout object to pass to the page. + /// + void navigateToPage(Widget page, Workout workoutCopy) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => page, + settings: RouteSettings( + arguments: workoutCopy, + ), + ), + ); + } + + /// Copies the current workout and updates the list and the database accordingly. + /// - [workout]: The workout to be copied. + /// + void copy(Workout workout) async { + try { + // Initialize the database. + final databaseManager = DatabaseManager(); + final database = await databaseManager.initDB(); + + // Fetch the list of workouts from the database. + final List workouts = await databaseManager.workouts(database); + + // Increment the workoutIndex of each workout and update it in the database. + for (Workout workout in workouts) { + workout.workoutIndex++; + await databaseManager.updateWorkout(workout, database); + } + + // Create a duplicate of the first workout with a new unique ID and a workoutIndex of 0. + final duplicateWorkout = workouts.first.copy(); + duplicateWorkout.id = const Uuid().v1(); + duplicateWorkout.workoutIndex = 0; + + // Insert the duplicate workout into the database and the beginning of the list. + await databaseManager.insertWorkout(duplicateWorkout, database); + workouts.insert(0, duplicateWorkout); + + // Navigate back to the main screen to show that the workout has been copied. + if (mounted) Navigator.of(context).pop(); + } catch (e) { + logger.e('Error copying workout: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + createErrorSnackbar(errorMessage: 'Error copying workout')); + } + } + } + + @override + Widget build(BuildContext context) { + /// Extracting the Workout object from the route arguments. + /// + Workout workout = ModalRoute.of(context)!.settings.arguments as Workout; + + return Scaffold( + appBar: ViewWorkoutAppBar( + onDelete: () async { + delete(workout); + }, + onEdit: () { + if (jsonDecode(workout.exercises).isEmpty) { + navigateToPage(const CreateTimer(), workout.copy()); + } else { + navigateToPage(const CreateWorkout(), workout.copy()); + } + }, + onCopy: () async { + copy(workout); + }, + workout: workout, + height: 40), + body: Container( + color: Color(workout.colorInt), + ), + ); + } +} + +// class ViewWorkout extends StatelessWidget { +// const ViewWorkout({ +// super.key, +// required this.color, +// }); + +// /// The color of the workout. +// /// +// final Color color; + +// /// Deletes the workout from the database and reorders the remaining workouts. +// /// Displays a snackbar if an error occurs. +// /// - [context]: The BuildContext of the current screen. +// /// - [workout]: The workout to be deleted. +// /// +// void delete(BuildContext context, Workout workout) async { +// Database database = await DatabaseManager().initDB(); +// try { +// await DatabaseManager().deleteWorkoutAndReorder(workout, database); +// } catch (e) { +// logger.e('Error deleting workout: $e'); +// if (context.mounted) { +// ScaffoldMessenger.of(context) +// .showSnackBar(errorDeletingWorkoutSnackBar); +// } +// } +// if (context.mounted) { +// Navigator.of(context).pop(); +// } +// } + +// @override +// Widget build(BuildContext context) { +// /// Extracting the Workout object from the route arguments. +// /// +// Workout workout = ModalRoute.of(context)!.settings.arguments as Workout; + +// return Scaffold( +// appBar: ViewWorkoutAppBar( +// onDelete: () async { +// delete(context, workout); +// }, +// onEdit: () { +// Workout workoutCopy = workout.copy(); + +// if (jsonDecode(workout.exercises).isEmpty) { + +// Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => const CreateTimer(), +// settings: RouteSettings( +// arguments: workout, +// ), +// ), +// ).then((value) { +// if (context.mounted) { +// setState(() { +// workout = ModalRoute.of(context)!.settings.arguments as Workout; +// }); +// } +// }); + +// pushCreateTimer(workoutCopy, context, false, (value) { +// /// When we come back, reload the workout arg. +// /// +// setState(() { +// workout = +// ModalRoute.of(context)!.settings.arguments as Workout; +// }); +// }); +// } else { +// pushCreateWorkout(workoutCopy, context, false, (value) { +// /// When we come back, reload the workout arg. +// /// +// setState(() { +// workout = +// ModalRoute.of(context)!.settings.arguments as Workout; +// }); +// }); +// } +// }, +// onCopy: () {}, +// workout: workout, +// height: 20), +// body: Container( +// color: Color(workout.colorInt), +// ), +// ); +// } +// } + +// import 'dart:convert'; +// import 'dart:io'; +// import 'package:permission_handler/permission_handler.dart'; +// import 'package:uuid/uuid.dart'; +// import 'package:flutter/material.dart'; +// import '../../old/utils/functions.dart'; +// import '../../old/helper_widgets/start_button.dart'; +// import 'package:sqflite/sqflite.dart'; +// import '../../old/card_widgets/card_item_animated.dart'; +// import '../../database/database_manager.dart'; +// import '../../old/helper_widgets/view_workout_appbar.dart'; +// import '../../old/models/list_model.dart'; +// import '../../old/workout_data_type/workout_type.dart'; +// import '../../old/models/list_tile_model.dart'; +// import '../../old/start_workout/workout.dart'; + +// class ViewWorkout extends StatefulWidget { +// const ViewWorkout({super.key}); +// @override +// ViewWorkoutState createState() => ViewWorkoutState(); +// } + +// class ViewWorkoutState extends State { +// /// GlobalKey for the AnimatedList. +// /// +// GlobalKey listKey = GlobalKey(); + +// /// List of objects including all relevant info for each interval. +// /// Example: The String exercise for that interval, such as "Work" +// /// or an entered exercise such as "Bicep Curls". +// /// +// late ListModel intervalInfo; + +// /// Asynchronously deletes a workout list from the database and updates the +// /// workout indices of remaining lists accordingly. +// /// +// /// Parameters: +// /// - [workoutArgument]: The 'Workout' object representing the list to be deleted. +// /// +// /// Returns: +// /// - A Future representing the completion of the delete operation. +// Future deleteList(workoutArgument) async { +// // Initialize the database. +// Future database = DatabaseManager().initDB(); + +// // Delete the specified workout list from the database. +// await DatabaseManager() +// .deleteWorkout(workoutArgument.id, database) +// .then((value) async { +// // Retrieve the updated list of workouts from the database. +// List workouts = +// await DatabaseManager().workouts(await DatabaseManager().initDB()); + +// // Sort the workouts based on their workout indices. +// workouts.sort((a, b) => a.workoutIndex.compareTo(b.workoutIndex)); + +// // Update the workout indices of remaining lists after the deleted list. +// for (int i = workoutArgument.workoutIndex; i < workouts.length; i++) { +// workouts[i].workoutIndex = i; +// await DatabaseManager() +// .updateWorkout(workouts[i], await DatabaseManager().initDB()); +// } +// }); +// } + +// @override +// Widget build(BuildContext context) { +// /// Extracting the Workout object from the route arguments. +// /// +// Workout workout = ModalRoute.of(context)!.settings.arguments as Workout; + +// /// Parsing the exercises data from the Workout object. +// /// +// List exercises = +// workout.exercises != "" ? jsonDecode(workout.exercises) : []; + +// /// Creating a ListModel to manage the list of ListTileModel items. +// /// +// intervalInfo = ListModel( +// listKey: listKey, // Providing a key for the list. +// initialItems: +// listItems(exercises, workout), // Initializing the list with items. +// ); + +// /// Getting the height of the current screen using MediaQuery. +// /// +// double sizeHeight = MediaQuery.of(context).size.height; + +// return Scaffold( +// bottomNavigationBar: Container( +// color: Color(workout.colorInt), +// width: MediaQuery.of(context).size.width, +// height: MediaQuery.of(context).orientation == Orientation.portrait +// ? MediaQuery.of(context).size.height / 8 +// : MediaQuery.of(context).size.height / 5, +// child: StartButton( +// onTap: () async { +// if (Platform.isAndroid) { +// await Permission.scheduleExactAlarm.isDenied.then((value) { +// if (value) { +// Permission.scheduleExactAlarm.request(); +// } +// }); + +// if (await Permission.scheduleExactAlarm.isDenied) { +// return; +// } +// } + +// if (context.mounted) { +// Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => const CountDownTimer(), +// settings: RouteSettings( +// arguments: workout, +// ), +// ), +// ).then((value) { +// if (context.mounted) setStatusBarBrightness(context); +// }); +// } +// }, +// )), +// appBar: ViewWorkoutAppBar( +// workout: workout, +// height: MediaQuery.of(context).orientation == Orientation.portrait +// ? 40 +// : 80, +// onDelete: () { +// deleteList(workout).then((value) => Navigator.pop(context)); +// Navigator.of(context).pop(); +// }, +// onEdit: () { +// Workout workoutCopy = workout.copy(); + +// if (exercises.isEmpty) { +// pushCreateTimer(workoutCopy, context, false, (value) { +// /// When we come back, reload the workout arg. +// /// +// setState(() { +// workout = ModalRoute.of(context)!.settings.arguments as Workout; +// }); +// }); +// } else { +// pushCreateWorkout(workoutCopy, context, false, (value) { +// /// When we come back, reload the workout arg. +// /// +// setState(() { +// workout = ModalRoute.of(context)!.settings.arguments as Workout; +// }); +// }); +// } +// }, +// onCopy: () async { +// /// This function is triggered when the "Copy" button is clicked. +// /// It duplicates the current workout and updates the list and the database accordingly. + +// /// Fetch the list of workouts from the database. +// List workouts = await DatabaseManager() +// .workouts(await DatabaseManager().initDB()); + +// /// Increment the workoutIndex of each workout in the list. +// for (Workout workout in workouts) { +// workout.workoutIndex++; +// } + +// /// Create a duplicate of the current workout with a new unique ID and a workoutIndex of 0. +// Workout duplicateWorkout = Workout( +// const Uuid().v1(), +// workout.title, +// workout.numExercises, +// workout.exercises, +// workout.getReadyTime, +// workout.workTime, +// workout.restTime, +// workout.halfTime, +// workout.breakTime, +// workout.warmupTime, +// workout.cooldownTime, +// workout.iterations, +// workout.halfwayMark, +// workout.workSound, +// workout.restSound, +// workout.halfwaySound, +// workout.completeSound, +// workout.countdownSound, +// workout.colorInt, +// 0, +// workout.showMinutes, +// ); + +// /// Insert the duplicate workout at the beginning of the list. +// workouts.insert(0, duplicateWorkout); + +// /// Initialize the database. +// Database database = await DatabaseManager().initDB(); + +// /// Insert the duplicate workout into the database. +// await DatabaseManager().insertWorkout(duplicateWorkout, database); + +// /// Update the workoutIndex of each workout in the database. +// for (Workout workout in workouts) { +// await DatabaseManager().updateWorkout(workout, database); +// } + +// /// Navigate back to the main screen to show that the workout has been copied. +// /// The check for context.mounted ensures that the Navigator.pop() method is only called if the widget is still in the widget tree. +// if (context.mounted) Navigator.of(context).pop(); +// }, +// ), +// body: Container( +// color: Color(workout.colorInt), +// child: +// Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ +// Visibility( +// visible: +// MediaQuery.of(context).orientation == Orientation.portrait +// ? true +// : false, +// child: Expanded( +// flex: 4, +// child: Row( +// mainAxisAlignment: MainAxisAlignment.center, +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// const Spacer(), +// Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Icon( +// Icons.timer, +// color: Colors.white, +// size: sizeHeight * .07, +// ), +// Text( +// "${calculateWorkoutTime(workout)} minutes", +// style: TextStyle( +// fontWeight: FontWeight.bold, +// color: Colors.white, +// fontSize: sizeHeight * .03), +// ) +// ], +// ), +// const Spacer(), +// Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Icon( +// Icons.view_timeline, +// color: Colors.white, +// size: sizeHeight * .07, +// ), +// Text( +// "${workout.numExercises} intervals", +// style: TextStyle( +// fontWeight: FontWeight.bold, +// color: Colors.white, +// fontSize: sizeHeight * .03), +// ) +// ], +// ), +// const Spacer(), +// ], +// ))), +// Expanded( +// flex: 10, +// child: AnimatedList( +// key: listKey, +// initialItemCount: intervalInfo.length, +// itemBuilder: (context, index, animation) { +// return CardItemAnimated( +// animation: animation, +// item: intervalInfo[index], +// fontColor: Colors.white, +// fontWeight: +// index == 0 ? FontWeight.bold : FontWeight.normal, +// backgroundColor: Color(workout.colorInt), +// sizeMultiplier: 1, +// ); +// }, +// )) +// ])), +// ); +// } +// } diff --git a/lib/start_workout/view_workout.dart b/lib/start_workout/view_workout.dart deleted file mode 100644 index 5f32b74..0000000 --- a/lib/start_workout/view_workout.dart +++ /dev/null @@ -1,286 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:uuid/uuid.dart'; -import 'package:flutter/material.dart'; -import '../utils/functions.dart'; -import '../helper_widgets/start_button.dart'; -import 'package:sqflite/sqflite.dart'; -import '../card_widgets/card_item_animated.dart'; -import '../database/database_manager.dart'; -import '../helper_widgets/view_workout_appbar.dart'; -import '../models/list_model.dart'; -import '../workout_data_type/workout_type.dart'; -import '../models/list_tile_model.dart'; -import 'workout.dart'; - -class ViewWorkout extends StatefulWidget { - const ViewWorkout({super.key}); - @override - ViewWorkoutState createState() => ViewWorkoutState(); -} - -class ViewWorkoutState extends State { - /// GlobalKey for the AnimatedList. - /// - GlobalKey listKey = GlobalKey(); - - /// List of objects including all relevant info for each interval. - /// Example: The String exercise for that interval, such as "Work" - /// or an entered exercise such as "Bicep Curls". - /// - late ListModel intervalInfo; - - /// Asynchronously deletes a workout list from the database and updates the - /// workout indices of remaining lists accordingly. - /// - /// Parameters: - /// - [workoutArgument]: The 'Workout' object representing the list to be deleted. - /// - /// Returns: - /// - A Future representing the completion of the delete operation. - Future deleteList(workoutArgument) async { - // Initialize the database. - Future database = DatabaseManager().initDB(); - - // Delete the specified workout list from the database. - await DatabaseManager() - .deleteList(workoutArgument.id, database) - .then((value) async { - // Retrieve the updated list of workouts from the database. - List workouts = - await DatabaseManager().lists(DatabaseManager().initDB()); - - // Sort the workouts based on their workout indices. - workouts.sort((a, b) => a.workoutIndex.compareTo(b.workoutIndex)); - - // Update the workout indices of remaining lists after the deleted list. - for (int i = workoutArgument.workoutIndex; i < workouts.length; i++) { - workouts[i].workoutIndex = i; - await DatabaseManager() - .updateList(workouts[i], await DatabaseManager().initDB()); - } - }); - } - - @override - Widget build(BuildContext context) { - /// Extracting the Workout object from the route arguments. - /// - Workout workout = ModalRoute.of(context)!.settings.arguments as Workout; - - /// Parsing the exercises data from the Workout object. - /// - List exercises = - workout.exercises != "" ? jsonDecode(workout.exercises) : []; - - /// Creating a ListModel to manage the list of ListTileModel items. - /// - intervalInfo = ListModel( - listKey: listKey, // Providing a key for the list. - initialItems: - listItems(exercises, workout), // Initializing the list with items. - ); - - /// Getting the height of the current screen using MediaQuery. - /// - double sizeHeight = MediaQuery.of(context).size.height; - - return Scaffold( - bottomNavigationBar: Container( - color: Color(workout.colorInt), - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).orientation == Orientation.portrait - ? MediaQuery.of(context).size.height / 8 - : MediaQuery.of(context).size.height / 5, - child: StartButton( - onTap: () async { - if (Platform.isAndroid) { - await Permission.scheduleExactAlarm.isDenied.then((value) { - if (value) { - Permission.scheduleExactAlarm.request(); - } - }); - - if (await Permission.scheduleExactAlarm.isDenied) { - return; - } - } - - if (context.mounted) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CountDownTimer(), - settings: RouteSettings( - arguments: workout, - ), - ), - ).then((value) { - setStatusBarBrightness(context); - }); - } - }, - )), - appBar: ViewWorkoutAppBar( - workout: workout, - height: MediaQuery.of(context).orientation == Orientation.portrait - ? 40 - : 80, - onDelete: () { - deleteList(workout).then((value) => Navigator.pop(context)); - Navigator.of(context).pop(); - }, - onEdit: () { - Workout workoutCopy = workout.copy(); - - if (exercises.isEmpty) { - pushCreateTimer(workoutCopy, context, false, (value) { - /// When we come back, reload the workout arg. - /// - setState(() { - workout = ModalRoute.of(context)!.settings.arguments as Workout; - }); - }); - } else { - pushCreateWorkout(workoutCopy, context, false, (value) { - /// When we come back, reload the workout arg. - /// - setState(() { - workout = ModalRoute.of(context)!.settings.arguments as Workout; - }); - }); - } - }, - onCopy: () async { - /// This function is triggered when the "Copy" button is clicked. - /// It duplicates the current workout and updates the list and the database accordingly. - - /// Fetch the list of workouts from the database. - List workouts = - await DatabaseManager().lists(DatabaseManager().initDB()); - - /// Increment the workoutIndex of each workout in the list. - for (Workout workout in workouts) { - workout.workoutIndex++; - } - - /// Create a duplicate of the current workout with a new unique ID and a workoutIndex of 0. - Workout duplicateWorkout = Workout( - const Uuid().v1(), - workout.title, - workout.numExercises, - workout.exercises, - workout.getReadyTime, - workout.workTime, - workout.restTime, - workout.halfTime, - workout.breakTime, - workout.warmupTime, - workout.cooldownTime, - workout.iterations, - workout.halfwayMark, - workout.workSound, - workout.restSound, - workout.halfwaySound, - workout.completeSound, - workout.countdownSound, - workout.colorInt, - 0, - workout.showMinutes, - ); - - /// Insert the duplicate workout at the beginning of the list. - workouts.insert(0, duplicateWorkout); - - /// Initialize the database. - Database database = await DatabaseManager().initDB(); - - /// Insert the duplicate workout into the database. - await DatabaseManager().insertList(duplicateWorkout, database); - - /// Update the workoutIndex of each workout in the database. - for (Workout workout in workouts) { - await DatabaseManager().updateList(workout, database); - } - - /// Navigate back to the main screen to show that the workout has been copied. - /// The check for context.mounted ensures that the Navigator.pop() method is only called if the widget is still in the widget tree. - if (context.mounted) Navigator.of(context).pop(); - }, - ), - body: Container( - color: Color(workout.colorInt), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Visibility( - visible: - MediaQuery.of(context).orientation == Orientation.portrait - ? true - : false, - child: Expanded( - flex: 4, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.timer, - color: Colors.white, - size: sizeHeight * .07, - ), - Text( - "${calculateWorkoutTime(workout)} minutes", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - fontSize: sizeHeight * .03), - ) - ], - ), - const Spacer(), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.view_timeline, - color: Colors.white, - size: sizeHeight * .07, - ), - Text( - "${workout.numExercises} intervals", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - fontSize: sizeHeight * .03), - ) - ], - ), - const Spacer(), - ], - ))), - Expanded( - flex: 10, - child: AnimatedList( - key: listKey, - initialItemCount: intervalInfo.length, - itemBuilder: (context, index, animation) { - return CardItemAnimated( - animation: animation, - item: intervalInfo[index], - fontColor: Colors.white, - fontWeight: - index == 0 ? FontWeight.bold : FontWeight.normal, - backgroundColor: Color(workout.colorInt), - sizeMultiplier: 1, - ); - }, - )) - ])), - ); - } -} diff --git a/lib/utils/local_file_util.dart b/lib/utils/local_file_util.dart new file mode 100644 index 0000000..fd6a75e --- /dev/null +++ b/lib/utils/local_file_util.dart @@ -0,0 +1,130 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:openhiit/log/log.dart'; +import 'package:openhiit/old/workout_data_type/workout_type.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +class LocalFileUtil { + Future get _localPath async { + final directory = await getApplicationDocumentsDirectory(); + + return directory.path; + } + + /// Returns the local file path for the given list of [workouts]. + /// - [workouts]: The list of workouts to export. + /// + Future localFilePath(List workouts) async { + String fileTitle = ""; + if (workouts.length == 1) { + fileTitle = "exported_openhiit_timer_${workouts[0].title}.json"; + } else { + fileTitle = "exported_openhiit_timers.json"; + } + + final path = await _localPath; + return File('$path/$fileTitle'); + } + + Future writeFile(List workouts) async { + final file = await localFilePath(workouts); + + // Write the file + return file.writeAsString(jsonEncode(workouts)); + } + + /// Shares the local file of the given [workout] using the Share plugin. + /// + /// The [workout] parameter represents the workout to be shared. + /// + /// Returns an integer value indicating the success of the file sharing operation. + /// If the file sharing is successful, it returns 1. If an error occurs, it returns 0. + /// + Future shareFile( + List workouts, BuildContext context) async { + try { + final file = await localFilePath(workouts); + + if (context.mounted) { + final box = context.findRenderObject() as RenderBox?; + + ShareResult result = await Share.shareXFiles([XFile(file.path)], + text: 'OpenHIIT Export', + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); + + return result; + } + + return null; + } catch (e) { + // If encountering an error, return null + return null; + } + } + + /// Shares the local file of the given [workout] using the Share plugin. + /// This method is used to share multiple files. + /// - [workout] Represents the workout to be shared. + /// Returns an integer value indicating the success of the file sharing operation. + /// + Future shareMultipleFiles( + List workouts, BuildContext context) async { + try { + List files = []; + + files.add(XFile((await localFilePath(workouts)).path)); + + if (context.mounted) { + final box = context.findRenderObject() as RenderBox?; + + ShareResult result = await Share.shareXFiles(files, + text: 'Export', + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); + return result; + } + + return null; + } catch (e) { + // If encountering an error, return 0 + return null; + } + } + + /// Saves the file to the device. + /// - [workoutsToExport] The list of workouts to export. + /// + Future saveFileToDevice(List workoutsToExport) async { + String fileTitle = ""; + if (workoutsToExport.length == 1) { + fileTitle = "exported_openhiit_timer_${workoutsToExport[0].title}.json"; + } else { + fileTitle = "exported_openhiit_timers.json"; + } + + try { + String? outputFile = await FilePicker.platform.saveFile( + fileName: fileTitle, + allowedExtensions: ["json", "txt"], + type: FileType.custom, + bytes: utf8.encode(jsonEncode(workoutsToExport)), + ); + + if (outputFile == null) { + return false; + } + + if (Platform.isIOS) { + File(outputFile) + .writeAsBytes(utf8.encode(jsonEncode(workoutsToExport))); + } + + return true; + } on Exception catch (e) { + logger.e("Error saving file to device: $e"); + return false; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 772b75b..d521806 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -712,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + reorderable_grid_view: + dependency: "direct main" + description: + name: reorderable_grid_view + sha256: e36c6229a97105a10c79e15ab4b9b14ee9f6c488574ff2be9e858c82af47cda6 + url: "https://pub.dev" + source: hosted + version: "2.2.8" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 719cbf4..41d7c15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: file_saver: ^0.2.13 permission_handler: ^11.3.1 shared_preferences: ^2.2.3 + reorderable_grid_view: ^2.2.8 flutter_launcher_icons: android: "launcher_icon"