Compare commits

..

5 Commits

10 changed files with 276 additions and 113 deletions

View File

@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:fmassive/gym_set.dart'; import 'package:fmassive/gym_sets.dart';
import 'package:fmassive/main.dart'; import 'package:fmassive/main.dart';
import 'package:fmassive/plans.dart'; import 'package:fmassive/plans.dart';
import 'package:moor/ffi.dart'; import 'package:moor/ffi.dart';
@ -22,12 +22,18 @@ class MyDatabase extends _$MyDatabase {
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async { onCreate: (Migrator m) async {
print('Creating...');
await m.createAll(); await m.createAll();
var data = await (db.select(db.settings)..limit(1)).get(); var data = await (db.select(db.settings)..limit(1)).get();
if (data.isEmpty) await db.into(db.settings).insert(defaultSettings); if (data.isEmpty) await db.into(db.settings).insert(defaultSettings);
}, },
onUpgrade: (Migrator m, int from, int to) async { onUpgrade: (Migrator m, int from, int to) async {
if (from == 1) { if (from == 1) {
await m.create(db.gymSets);
await db.customInsert('''
INSERT INTO gym_sets(id, name, reps, weight, created, unit, hidden, image, sets, minutes, seconds, steps)
SELECT id, name, reps, weight, created, unit, hidden, image, sets, minutes, seconds, steps FROM sets
''');
await m.addColumn(settings, settings.darkColor); await m.addColumn(settings, settings.darkColor);
await db.customStatement(''' await db.customStatement('''
UPDATE settings SET dark_color = darkColor UPDATE settings SET dark_color = darkColor

View File

@ -1248,8 +1248,8 @@ class $GymSetsTable extends GymSets with TableInfo<$GymSetsTable, GymSet> {
class Plan extends DataClass implements Insertable<Plan> { class Plan extends DataClass implements Insertable<Plan> {
final int id; final int id;
final String days; final String days;
final String workouts; final String exercises;
Plan({required this.id, required this.days, required this.workouts}); Plan({required this.id, required this.days, required this.exercises});
factory Plan.fromData(Map<String, dynamic> data, GeneratedDatabase db, factory Plan.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) { {String? prefix}) {
final effectivePrefix = prefix ?? ''; final effectivePrefix = prefix ?? '';
@ -1258,8 +1258,8 @@ class Plan extends DataClass implements Insertable<Plan> {
.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, .mapFromDatabaseResponse(data['${effectivePrefix}id'])!,
days: const StringType() days: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}days'])!, .mapFromDatabaseResponse(data['${effectivePrefix}days'])!,
workouts: const StringType() exercises: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}workouts'])!, .mapFromDatabaseResponse(data['${effectivePrefix}exercises'])!,
); );
} }
@override @override
@ -1267,7 +1267,7 @@ class Plan extends DataClass implements Insertable<Plan> {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['id'] = Variable<int>(id); map['id'] = Variable<int>(id);
map['days'] = Variable<String>(days); map['days'] = Variable<String>(days);
map['workouts'] = Variable<String>(workouts); map['exercises'] = Variable<String>(exercises);
return map; return map;
} }
@ -1275,7 +1275,7 @@ class Plan extends DataClass implements Insertable<Plan> {
return PlansCompanion( return PlansCompanion(
id: Value(id), id: Value(id),
days: Value(days), days: Value(days),
workouts: Value(workouts), exercises: Value(exercises),
); );
} }
@ -1285,7 +1285,7 @@ class Plan extends DataClass implements Insertable<Plan> {
return Plan( return Plan(
id: serializer.fromJson<int>(json['id']), id: serializer.fromJson<int>(json['id']),
days: serializer.fromJson<String>(json['days']), days: serializer.fromJson<String>(json['days']),
workouts: serializer.fromJson<String>(json['workouts']), exercises: serializer.fromJson<String>(json['exercises']),
); );
} }
@override @override
@ -1294,69 +1294,69 @@ class Plan extends DataClass implements Insertable<Plan> {
return <String, dynamic>{ return <String, dynamic>{
'id': serializer.toJson<int>(id), 'id': serializer.toJson<int>(id),
'days': serializer.toJson<String>(days), 'days': serializer.toJson<String>(days),
'workouts': serializer.toJson<String>(workouts), 'exercises': serializer.toJson<String>(exercises),
}; };
} }
Plan copyWith({int? id, String? days, String? workouts}) => Plan( Plan copyWith({int? id, String? days, String? exercises}) => Plan(
id: id ?? this.id, id: id ?? this.id,
days: days ?? this.days, days: days ?? this.days,
workouts: workouts ?? this.workouts, exercises: exercises ?? this.exercises,
); );
@override @override
String toString() { String toString() {
return (StringBuffer('Plan(') return (StringBuffer('Plan(')
..write('id: $id, ') ..write('id: $id, ')
..write('days: $days, ') ..write('days: $days, ')
..write('workouts: $workouts') ..write('exercises: $exercises')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(id, days, workouts); int get hashCode => Object.hash(id, days, exercises);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is Plan && (other is Plan &&
other.id == this.id && other.id == this.id &&
other.days == this.days && other.days == this.days &&
other.workouts == this.workouts); other.exercises == this.exercises);
} }
class PlansCompanion extends UpdateCompanion<Plan> { class PlansCompanion extends UpdateCompanion<Plan> {
final Value<int> id; final Value<int> id;
final Value<String> days; final Value<String> days;
final Value<String> workouts; final Value<String> exercises;
const PlansCompanion({ const PlansCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.days = const Value.absent(), this.days = const Value.absent(),
this.workouts = const Value.absent(), this.exercises = const Value.absent(),
}); });
PlansCompanion.insert({ PlansCompanion.insert({
this.id = const Value.absent(), this.id = const Value.absent(),
required String days, required String days,
required String workouts, required String exercises,
}) : days = Value(days), }) : days = Value(days),
workouts = Value(workouts); exercises = Value(exercises);
static Insertable<Plan> custom({ static Insertable<Plan> custom({
Expression<int>? id, Expression<int>? id,
Expression<String>? days, Expression<String>? days,
Expression<String>? workouts, Expression<String>? exercises,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
if (days != null) 'days': days, if (days != null) 'days': days,
if (workouts != null) 'workouts': workouts, if (exercises != null) 'exercises': exercises,
}); });
} }
PlansCompanion copyWith( PlansCompanion copyWith(
{Value<int>? id, Value<String>? days, Value<String>? workouts}) { {Value<int>? id, Value<String>? days, Value<String>? exercises}) {
return PlansCompanion( return PlansCompanion(
id: id ?? this.id, id: id ?? this.id,
days: days ?? this.days, days: days ?? this.days,
workouts: workouts ?? this.workouts, exercises: exercises ?? this.exercises,
); );
} }
@ -1369,8 +1369,8 @@ class PlansCompanion extends UpdateCompanion<Plan> {
if (days.present) { if (days.present) {
map['days'] = Variable<String>(days.value); map['days'] = Variable<String>(days.value);
} }
if (workouts.present) { if (exercises.present) {
map['workouts'] = Variable<String>(workouts.value); map['exercises'] = Variable<String>(exercises.value);
} }
return map; return map;
} }
@ -1380,7 +1380,7 @@ class PlansCompanion extends UpdateCompanion<Plan> {
return (StringBuffer('PlansCompanion(') return (StringBuffer('PlansCompanion(')
..write('id: $id, ') ..write('id: $id, ')
..write('days: $days, ') ..write('days: $days, ')
..write('workouts: $workouts') ..write('exercises: $exercises')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@ -1403,13 +1403,13 @@ class $PlansTable extends Plans with TableInfo<$PlansTable, Plan> {
late final GeneratedColumn<String?> days = GeneratedColumn<String?>( late final GeneratedColumn<String?> days = GeneratedColumn<String?>(
'days', aliasedName, false, 'days', aliasedName, false,
type: const StringType(), requiredDuringInsert: true); type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _workoutsMeta = const VerificationMeta('workouts'); final VerificationMeta _exercisesMeta = const VerificationMeta('exercises');
@override @override
late final GeneratedColumn<String?> workouts = GeneratedColumn<String?>( late final GeneratedColumn<String?> exercises = GeneratedColumn<String?>(
'workouts', aliasedName, false, 'exercises', aliasedName, false,
type: const StringType(), requiredDuringInsert: true); type: const StringType(), requiredDuringInsert: true);
@override @override
List<GeneratedColumn> get $columns => [id, days, workouts]; List<GeneratedColumn> get $columns => [id, days, exercises];
@override @override
String get aliasedName => _alias ?? 'plans'; String get aliasedName => _alias ?? 'plans';
@override @override
@ -1428,11 +1428,11 @@ class $PlansTable extends Plans with TableInfo<$PlansTable, Plan> {
} else if (isInserting) { } else if (isInserting) {
context.missing(_daysMeta); context.missing(_daysMeta);
} }
if (data.containsKey('workouts')) { if (data.containsKey('exercises')) {
context.handle(_workoutsMeta, context.handle(_exercisesMeta,
workouts.isAcceptableOrUnknown(data['workouts']!, _workoutsMeta)); exercises.isAcceptableOrUnknown(data['exercises']!, _exercisesMeta));
} else if (isInserting) { } else if (isInserting) {
context.missing(_workoutsMeta); context.missing(_exercisesMeta);
} }
return context; return context;
} }

View File

@ -2,15 +2,15 @@ import 'package:flutter/material.dart';
class Days extends StatefulWidget { class Days extends StatefulWidget {
final ValueChanged<String> onChanged; final ValueChanged<String> onChanged;
final String days;
const Days({required this.onChanged, super.key}); const Days({required this.onChanged, required this.days, super.key});
@override @override
createState() => _DaysState(); createState() => _DaysState();
} }
class _DaysState extends State<Days> { class _DaysState extends State<Days> {
final List<bool> _selections = List.generate(7, (_) => false);
final List<String> _days = [ final List<String> _days = [
'Monday', 'Monday',
'Tuesday', 'Tuesday',
@ -20,14 +20,19 @@ class _DaysState extends State<Days> {
'Saturday', 'Saturday',
'Sunday' 'Sunday'
]; ];
late List<bool> _selections;
@override
void initState() {
super.initState();
final dayList = widget.days.split(',');
_selections = _days.map((day) => dayList.contains(day)).toList();
}
String _getSelectedDaysString() { String _getSelectedDaysString() {
List<String> selectedDays = []; List<String> selectedDays = [];
for (int i = 0; i < _selections.length; i++) { for (int i = 0; i < _selections.length; i++)
if (_selections[i]) { if (_selections[i]) selectedDays.add(_days[i]);
selectedDays.add(_days[i]);
}
}
return selectedDays.join(","); return selectedDays.join(",");
} }
@ -40,7 +45,10 @@ class _DaysState extends State<Days> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(children: [
Text('Days', style: Theme.of(context).textTheme.headlineSmall),
Expanded(
child: ListView(
children: List.generate(7, (index) { children: List.generate(7, (index) {
return SwitchListTile( return SwitchListTile(
title: Text(_days[index]), title: Text(_days[index]),
@ -48,6 +56,8 @@ class _DaysState extends State<Days> {
onChanged: (value) => _updateSelections(index), onChanged: (value) => _updateSelections(index),
); );
}), }),
); ),
),
]);
} }
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/material.dart' as material; import 'package:flutter/material.dart' as material;
import 'package:fmassive/database.dart'; import 'package:fmassive/database.dart';
import 'package:fmassive/days.dart'; import 'package:fmassive/days.dart';
import 'package:fmassive/exercises.dart';
import 'package:fmassive/main.dart'; import 'package:fmassive/main.dart';
import 'package:moor_flutter/moor_flutter.dart'; import 'package:moor_flutter/moor_flutter.dart';
@ -15,37 +16,69 @@ class EditPlanPage extends StatefulWidget {
} }
class _EditPlanPageState extends State<EditPlanPage> { class _EditPlanPageState extends State<EditPlanPage> {
final TextEditingController _daysController = TextEditingController();
final TextEditingController _workoutsController = TextEditingController();
late PlansCompanion plan; late PlansCompanion plan;
final daysNode = FocusNode(); List<String?> names = [];
final workoutsNode = FocusNode();
Future<List<String?>> getDistinctNames() async {
final names = await (db.gymSets.selectOnly(distinct: true)
..addColumns([db.gymSets.name]))
.get();
return names.map((name) => name.read(db.gymSets.name)).toList();
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getDistinctNames().then((value) {
setState(() {
names = value;
});
});
plan = widget.plan; plan = widget.plan;
_daysController.text = plan.days.value;
_workoutsController.text = plan.workouts.value;
if (plan.id.present)
workoutsNode.requestFocus();
else
daysNode.requestFocus();
} }
@override @override
dispose() { dispose() {
daysNode.dispose();
workoutsNode.dispose();
super.dispose(); super.dispose();
} }
Future<bool?> _showConfirmationDialog(BuildContext context) {
return showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Confirm Delete'),
content: const Text('Are you sure you want to delete this plan?'),
actions: <Widget>[
ElevatedButton(
child: const Text('Yes'),
onPressed: () {
Navigator.pop(context, true); // showDialog() returns true
},
),
ElevatedButton(
child: const Text('No'),
onPressed: () {
Navigator.pop(context, false); // showDialog() returns false
},
),
],
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (widget.plan.id.present) if (widget.plan.id.present)
actions.add(IconButton( actions.add(IconButton(
onPressed: () async { onPressed: () async {
bool? confirm = await _showConfirmationDialog(context);
if (!confirm!) return;
await db.plans.deleteOne(widget.plan); await db.plans.deleteOne(widget.plan);
if (!mounted) return; if (!mounted) return;
Navigator.pop(context); Navigator.pop(context);
@ -59,38 +92,11 @@ class _EditPlanPageState extends State<EditPlanPage> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: material.Column( child: material.Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: getChildren,
Days(onChanged: (days) {
setState(() {
days = days;
});
}),
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( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
if (_daysController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter days')));
daysNode.requestFocus();
return;
}
if (plan.id.present) if (plan.id.present)
await db.update(db.plans).replace(plan); await db.update(db.plans).replace(plan);
else else
@ -102,4 +108,32 @@ class _EditPlanPageState extends State<EditPlanPage> {
), ),
)); ));
} }
List<Widget> get getChildren {
return [
Expanded(
child: Days(
onChanged: (days) {
setState(() {
plan = plan.copyWith(days: Value(days));
});
},
days: plan.days.value,
),
),
names.isNotEmpty
? Expanded(
child: Exercises(
onChanged: (exercises) {
setState(() {
plan = plan.copyWith(exercises: Value(exercises));
});
},
exercises: plan.exercises.value,
names: names,
),
)
: const CircularProgressIndicator(),
];
}
} }

60
lib/exercises.dart Normal file
View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
class Exercises extends StatefulWidget {
final ValueChanged<String> onChanged;
final String? exercises;
final List<String?> names;
const Exercises(
{required this.onChanged,
required this.exercises,
super.key,
required this.names});
@override
createState() => _ExercisesState();
}
class _ExercisesState extends State<Exercises> {
late List<bool> _selections;
@override
initState() {
super.initState();
final exercises = widget.exercises?.split(',');
_selections =
widget.names.map((name) => exercises?.contains(name) ?? false).toList();
}
String _getExercises() {
List<String> exercises = [];
for (int i = 0; i < _selections.length; i++)
if (_selections[i]) exercises.add(widget.names[i] ?? '');
return exercises.join(",");
}
void _updateSelections(int index) {
setState(() {
_selections[index] = !_selections[index];
widget.onChanged(_getExercises());
});
}
@override
Widget build(BuildContext context) {
return Column(children: [
Text('Exercises', style: Theme.of(context).textTheme.headlineSmall),
Expanded(
child: ListView(
children: List.generate(widget.names.length, (index) {
return SwitchListTile(
title: Text(widget.names[index] ?? ''),
value: _selections[index],
onChanged: (value) => _updateSelections(index),
);
}),
),
),
]);
}
}

61
lib/plan_list.dart Normal file
View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:fmassive/database.dart';
import 'package:fmassive/edit_plan.dart';
import 'package:fmassive/main.dart';
import 'package:moor_flutter/moor_flutter.dart';
class PlanList extends StatelessWidget {
const PlanList({
super.key,
required this.plans,
});
final List<Plan> plans;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: plans.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(plans[index].days.replaceAll(',', ', ')),
subtitle: Text(plans[index].exercises),
onLongPress: () => showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Delete set'),
content: Text(
'Are you sure you want to delete ${plans[index].days}?'),
actions: <Widget>[
ElevatedButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
ElevatedButton(
child: const Text('Delete'),
onPressed: () async {
final navigator = Navigator.of(context);
await db.plans.deleteOne(plans[index]);
navigator.pop();
},
),
],
);
},
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EditPlanPage(plan: plans[index].toCompanion(false)),
),
);
});
},
);
}
}

View File

@ -3,5 +3,5 @@ import 'package:moor/moor.dart';
class Plans extends Table { class Plans extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get days => text()(); TextColumn get days => text()();
TextColumn get workouts => text()(); TextColumn get exercises => text()();
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:fmassive/database.dart'; import 'package:fmassive/database.dart';
import 'package:fmassive/edit_plan.dart'; import 'package:fmassive/edit_plan.dart';
import 'package:fmassive/main.dart'; import 'package:fmassive/main.dart';
import 'package:fmassive/plan_list.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
class PlansPage extends StatelessWidget { class PlansPage extends StatelessWidget {
@ -65,27 +66,20 @@ class _PlansPageState extends State<_PlansPage> {
builder: (context, snapshot) { builder: (context, snapshot) {
final plans = snapshot.data; final plans = snapshot.data;
if (snapshot.hasError)
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Error: ${snapshot.error}',
style: Theme.of(context).textTheme.headlineSmall,
)),
);
if (plans == null) if (plans == null)
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
return ListView.builder( return PlanList(plans: plans);
itemCount: plans.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(plans[index].days.replaceAll(',', ', ')),
subtitle:
Text(plans[index].workouts.replaceAll(',', ', ')),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditPlanPage(
plan: plans[index].toCompanion(false)),
),
);
});
},
);
}), }),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
@ -93,8 +87,8 @@ class _PlansPageState extends State<_PlansPage> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const EditPlanPage( builder: (context) => const EditPlanPage(
plan: plan: PlansCompanion(
PlansCompanion(days: Value(''), workouts: Value(''))), days: Value(''), exercises: Value(''))),
), ),
); );
}, },

View File

@ -95,11 +95,9 @@ class _SettingsPageState extends State<_SettingsPage> {
await file.copy(to); await file.copy(to);
print('Migrating...'); print('Migrating...');
db = MyDatabase(); db = MyDatabase();
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final migrator = db.createMigrator(); final migrator = db.createMigrator();
await migrator.createAll(); await migrator.createAll();
await db.customInsert('''
INSERT INTO gym_sets(id, name, reps, weight, created, unit, hidden, image, sets, minutes, seconds, steps)
SELECT id, name, reps, weight, created, unit, hidden, image, sets, minutes, seconds, steps FROM sets''');
print('Migrated.'); print('Migrated.');
}, },
child: const Text("Import"))); child: const Text("Import")));