diff --git a/lib/database.dart b/lib/database.dart index b0b0456..acc1271 100644 --- a/lib/database.dart +++ b/lib/database.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:fmassive/gym_set.dart'; import 'package:fmassive/main.dart'; +import 'package:fmassive/plans.dart'; import 'package:moor/ffi.dart'; import 'package:moor/moor.dart'; import 'package:path/path.dart'; @@ -11,7 +12,7 @@ import 'settings.dart'; part 'database.g.dart'; -@UseMoor(tables: [Settings, GymSets]) +@UseMoor(tables: [Settings, GymSets, Plans]) class MyDatabase extends _$MyDatabase { MyDatabase() : super(_openConnection()); diff --git a/lib/database.g.dart b/lib/database.g.dart index 2f9739c..da3949c 100644 --- a/lib/database.g.dart +++ b/lib/database.g.dart @@ -1239,12 +1239,220 @@ class $GymSetsTable extends GymSets with TableInfo<$GymSetsTable, GymSet> { } } +class Plan extends DataClass implements Insertable { + final int id; + final String days; + final String workouts; + Plan({required this.id, required this.days, required this.workouts}); + factory Plan.fromData(Map data, GeneratedDatabase db, + {String? prefix}) { + final effectivePrefix = prefix ?? ''; + return Plan( + id: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + days: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}days'])!, + workouts: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}workouts'])!, + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['days'] = Variable(days); + map['workouts'] = Variable(workouts); + return map; + } + + PlansCompanion toCompanion(bool nullToAbsent) { + return PlansCompanion( + id: Value(id), + days: Value(days), + workouts: Value(workouts), + ); + } + + factory Plan.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return Plan( + id: serializer.fromJson(json['id']), + days: serializer.fromJson(json['days']), + workouts: serializer.fromJson(json['workouts']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'days': serializer.toJson(days), + 'workouts': serializer.toJson(workouts), + }; + } + + Plan copyWith({int? id, String? days, String? workouts}) => Plan( + id: id ?? this.id, + days: days ?? this.days, + workouts: workouts ?? this.workouts, + ); + @override + String toString() { + return (StringBuffer('Plan(') + ..write('id: $id, ') + ..write('days: $days, ') + ..write('workouts: $workouts') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, days, workouts); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Plan && + other.id == this.id && + other.days == this.days && + other.workouts == this.workouts); +} + +class PlansCompanion extends UpdateCompanion { + final Value id; + final Value days; + final Value workouts; + const PlansCompanion({ + this.id = const Value.absent(), + this.days = const Value.absent(), + this.workouts = const Value.absent(), + }); + PlansCompanion.insert({ + this.id = const Value.absent(), + required String days, + required String workouts, + }) : days = Value(days), + workouts = Value(workouts); + static Insertable custom({ + Expression? id, + Expression? days, + Expression? workouts, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (days != null) 'days': days, + if (workouts != null) 'workouts': workouts, + }); + } + + PlansCompanion copyWith( + {Value? id, Value? days, Value? workouts}) { + return PlansCompanion( + id: id ?? this.id, + days: days ?? this.days, + workouts: workouts ?? this.workouts, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (days.present) { + map['days'] = Variable(days.value); + } + if (workouts.present) { + map['workouts'] = Variable(workouts.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlansCompanion(') + ..write('id: $id, ') + ..write('days: $days, ') + ..write('workouts: $workouts') + ..write(')')) + .toString(); + } +} + +class $PlansTable extends Plans with TableInfo<$PlansTable, Plan> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlansTable(this.attachedDatabase, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: const IntType(), + requiredDuringInsert: false, + defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); + final VerificationMeta _daysMeta = const VerificationMeta('days'); + @override + late final GeneratedColumn days = GeneratedColumn( + 'days', aliasedName, false, + type: const StringType(), requiredDuringInsert: true); + final VerificationMeta _workoutsMeta = const VerificationMeta('workouts'); + @override + late final GeneratedColumn workouts = GeneratedColumn( + 'workouts', aliasedName, false, + type: const StringType(), requiredDuringInsert: true); + @override + List get $columns => [id, days, workouts]; + @override + String get aliasedName => _alias ?? 'plans'; + @override + String get actualTableName => 'plans'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('days')) { + context.handle( + _daysMeta, days.isAcceptableOrUnknown(data['days']!, _daysMeta)); + } else if (isInserting) { + context.missing(_daysMeta); + } + if (data.containsKey('workouts')) { + context.handle(_workoutsMeta, + workouts.isAcceptableOrUnknown(data['workouts']!, _workoutsMeta)); + } else if (isInserting) { + context.missing(_workoutsMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + Plan map(Map data, {String? tablePrefix}) { + return Plan.fromData(data, attachedDatabase, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + @override + $PlansTable createAlias(String alias) { + return $PlansTable(attachedDatabase, alias); + } +} + abstract class _$MyDatabase extends GeneratedDatabase { _$MyDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e); late final $SettingsTable settings = $SettingsTable(this); late final $GymSetsTable gymSets = $GymSetsTable(this); + late final $PlansTable plans = $PlansTable(this); @override Iterable get allTables => allSchemaEntities.whereType(); @override - List get allSchemaEntities => [settings, gymSets]; + List get allSchemaEntities => + [settings, gymSets, plans]; } diff --git a/lib/edit_plan.dart b/lib/edit_plan.dart new file mode 100644 index 0000000..4e72602 --- /dev/null +++ b/lib/edit_plan.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' as material; +import 'package:fmassive/database.dart'; +import 'package:fmassive/main.dart'; +import 'package:moor_flutter/moor_flutter.dart'; + +class EditPlanPage extends StatefulWidget { + final PlansCompanion plan; + + const EditPlanPage({required this.plan, super.key}); + + @override + createState() => _EditPlanPageState(); +} + +class _EditPlanPageState extends State { + final TextEditingController _daysController = TextEditingController(); + final TextEditingController _workoutsController = TextEditingController(); + late PlansCompanion plan; + final daysNode = FocusNode(); + final workoutsNode = FocusNode(); + + @override + void initState() { + super.initState(); + plan = widget.plan; + _daysController.text = plan.days.value; + _workoutsController.text = plan.workouts.value; + if (plan.id.present) + workoutsNode.requestFocus(); + else + daysNode.requestFocus(); + } + + @override + dispose() { + daysNode.dispose(); + workoutsNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + List actions = []; + if (widget.plan.id.present) + actions.add(IconButton( + onPressed: () async { + await db.plans.deleteOne(widget.plan); + if (!mounted) return; + Navigator.pop(context); + }, + icon: const Icon(Icons.delete))); + + return SafeArea( + child: Scaffold( + appBar: AppBar(title: const Text('Edit Plan'), actions: actions), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: material.Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _daysController, + focusNode: daysNode, + decoration: const InputDecoration(labelText: 'Days'), + onTap: () { + _daysController.selection = TextSelection( + baseOffset: 0, extentOffset: _daysController.text.length); + }, + onChanged: (value) { + setState(() { + plan = plan.copyWith(days: Value(value)); + }); + }, + ), + TextFormField( + controller: _workoutsController, + focusNode: workoutsNode, + onTap: () { + _workoutsController.selection = TextSelection( + baseOffset: 0, + extentOffset: _workoutsController.text.length); + }, + decoration: const InputDecoration(labelText: 'Workouts'), + onChanged: (value) { + setState(() { + plan = plan.copyWith(workouts: Value(value)); + }); + }, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + if (_daysController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please enter days'))); + daysNode.requestFocus(); + return; + } + if (plan.id.present) + await db.update(db.plans).write(plan); + else + await db.into(db.plans).insert(plan); + if (!mounted) return; + Navigator.pop(context); + }, + child: const Icon(Icons.check), + ), + )); + } +} diff --git a/lib/edit_set.dart b/lib/edit_set.dart index 012d2a5..6195191 100644 --- a/lib/edit_set.dart +++ b/lib/edit_set.dart @@ -54,7 +54,8 @@ class _EditGymSetPageState extends State { }, icon: const Icon(Icons.delete))); - return Scaffold( + return SafeArea( + child: Scaffold( appBar: AppBar(title: const Text('Edit Gym Set'), actions: actions), body: Padding( padding: const EdgeInsets.all(16.0), @@ -111,6 +112,12 @@ class _EditGymSetPageState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () async { + if (_nameController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please enter a name'))); + nameNode.requestFocus(); + return; + } if (gymSet.id.present) await db.update(db.gymSets).write(gymSet); else { @@ -123,6 +130,6 @@ class _EditGymSetPageState extends State { }, child: const Icon(Icons.check), ), - ); + )); } } diff --git a/lib/home_page.dart b/lib/home_page.dart index dc222b0..aa4e0e4 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -51,7 +51,7 @@ class RootPage extends State { case 'Best': return const BestPage(); case 'Plans': - return const PlansPage(); + return PlansPage(search: search); default: return _HomePageWidget(search: search); } diff --git a/lib/plans.dart b/lib/plans.dart new file mode 100644 index 0000000..cda4dcc --- /dev/null +++ b/lib/plans.dart @@ -0,0 +1,7 @@ +import 'package:moor/moor.dart'; + +class Plans extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get days => text()(); + TextColumn get workouts => text()(); +} diff --git a/lib/plans_page.dart b/lib/plans_page.dart index 74788ac..8f12392 100644 --- a/lib/plans_page.dart +++ b/lib/plans_page.dart @@ -1,163 +1,102 @@ import 'package:flutter/material.dart'; -import 'package:flutter/material.dart' as material; import 'package:fmassive/database.dart'; +import 'package:fmassive/edit_plan.dart'; import 'package:fmassive/main.dart'; -import 'package:fmassive/settings.dart'; import 'package:moor/moor.dart'; class PlansPage extends StatelessWidget { - const PlansPage({super.key}); + const PlansPage({super.key, required this.search}); + + final String search; @override Widget build(BuildContext context) { - return const Scaffold( + return Scaffold( body: Center( - child: _PlansPage(), + child: _PlansPage(search: search), ), ); } } class _PlansPage extends StatefulWidget { - const _PlansPage({Key? key}) : super(key: key); + const _PlansPage({Key? key, required this.search}) : super(key: key); + + final String search; @override createState() => _PlansPageState(); } class _PlansPageState extends State<_PlansPage> { - late Stream stream; + bool showSearch = false; + late Stream> stream; - final TextEditingController searchController = TextEditingController(); + @override + initState() { + super.initState(); + setStream(); + } - void reset() async { - var data = await db.select(db.settings).get(); - if (data.isEmpty) await db.into(db.settings).insert(defaultSettings); - setState(() { - if (data.isEmpty) return; - }); + void setStream() { + stream = (db.select(db.plans) + ..where((gymSet) => gymSet.days.contains(widget.search)) + ..limit(10, offset: 0)) + .watch(); } @override - void initState() { - super.initState(); - stream = db.select(db.settings).watchSingle(); + didUpdateWidget(covariant _PlansPage oldWidget) { + super.didUpdateWidget(oldWidget); + setStream(); + } + + void toggleSearch() { + setState(() { + showSearch = !showSearch; + }); } @override Widget build(BuildContext context) { return Scaffold( - body: StreamBuilder( - stream: stream, - builder: (context, snapshot) { - final settings = snapshot.data; + body: StreamBuilder>( + stream: stream, + builder: (context, snapshot) { + final plans = snapshot.data; - if (settings == null) - return const Center(child: CircularProgressIndicator()); + if (plans == null) + return const Center(child: CircularProgressIndicator()); - return SingleChildScrollView( - padding: const EdgeInsets.all(8.0), - child: material.Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SwitchListTile( - title: const Text('Alarm'), - value: settings.alarm, - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(alarm: Value(value))); - }, - ), - SwitchListTile( - title: const Text('Vibrate'), - value: settings.vibrate, - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(vibrate: Value(value))); - }, - ), - SwitchListTile( - title: const Text('Notify'), - value: settings.notify, - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(notify: Value(value))); - }, - ), - SwitchListTile( - title: const Text('Images'), - value: settings.images, - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(images: Value(value))); - }, - ), - SwitchListTile( - title: const Text('Show Unit'), - value: settings.showUnit, - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(showUnit: Value(value))); - }, - ), - SwitchListTile( - title: const Text('Steps'), - value: settings.steps, - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(steps: Value(value))); - }, - ), - TextField( - decoration: const InputDecoration( - labelText: 'Sound', - ), - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(sound: Value(value))); - }, - ), - TextField( - decoration: const InputDecoration( - labelText: 'Light Color', - ), - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(lightColor: Value(value))); - }, - ), - TextField( - decoration: const InputDecoration( - labelText: 'Dark Color', - ), - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(darkColor: Value(value))); - }, - ), - TextField( - decoration: const InputDecoration( - labelText: 'Date', - ), - onChanged: (value) { - db - .update(db.settings) - .write(SettingsCompanion(date: Value(value))); - }, - ), - ], - ), - ); - }), - ); + return ListView.builder( + itemCount: plans.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(plans[index].days), + subtitle: Text(plans[index].workouts), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditPlanPage( + plan: plans[index].toCompanion(false)), + ), + ); + }); + }, + ); + }), + floatingActionButton: FloatingActionButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const EditPlanPage( + plan: + PlansCompanion(days: Value(''), workouts: Value(''))), + ), + ); + }, + child: const Icon(Icons.add))); } }