Compare commits

...

5 Commits

10 changed files with 276 additions and 113 deletions

View File

@ -1,6 +1,6 @@
import 'dart:io';
import 'package:fmassive/gym_set.dart';
import 'package:fmassive/gym_sets.dart';
import 'package:fmassive/main.dart';
import 'package:fmassive/plans.dart';
import 'package:moor/ffi.dart';
@ -22,12 +22,18 @@ class MyDatabase extends _$MyDatabase {
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async {
print('Creating...');
await m.createAll();
var data = await (db.select(db.settings)..limit(1)).get();
if (data.isEmpty) await db.into(db.settings).insert(defaultSettings);
},
onUpgrade: (Migrator m, int from, int to) async {
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 db.customStatement('''
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> {
final int id;
final String days;
final String workouts;
Plan({required this.id, required this.days, required this.workouts});
final String exercises;
Plan({required this.id, required this.days, required this.exercises});
factory Plan.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
@ -1258,8 +1258,8 @@ class Plan extends DataClass implements Insertable<Plan> {
.mapFromDatabaseResponse(data['${effectivePrefix}id'])!,
days: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}days'])!,
workouts: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}workouts'])!,
exercises: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}exercises'])!,
);
}
@override
@ -1267,7 +1267,7 @@ class Plan extends DataClass implements Insertable<Plan> {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['days'] = Variable<String>(days);
map['workouts'] = Variable<String>(workouts);
map['exercises'] = Variable<String>(exercises);
return map;
}
@ -1275,7 +1275,7 @@ class Plan extends DataClass implements Insertable<Plan> {
return PlansCompanion(
id: Value(id),
days: Value(days),
workouts: Value(workouts),
exercises: Value(exercises),
);
}
@ -1285,7 +1285,7 @@ class Plan extends DataClass implements Insertable<Plan> {
return Plan(
id: serializer.fromJson<int>(json['id']),
days: serializer.fromJson<String>(json['days']),
workouts: serializer.fromJson<String>(json['workouts']),
exercises: serializer.fromJson<String>(json['exercises']),
);
}
@override
@ -1294,69 +1294,69 @@ class Plan extends DataClass implements Insertable<Plan> {
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'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,
days: days ?? this.days,
workouts: workouts ?? this.workouts,
exercises: exercises ?? this.exercises,
);
@override
String toString() {
return (StringBuffer('Plan(')
..write('id: $id, ')
..write('days: $days, ')
..write('workouts: $workouts')
..write('exercises: $exercises')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, days, workouts);
int get hashCode => Object.hash(id, days, exercises);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Plan &&
other.id == this.id &&
other.days == this.days &&
other.workouts == this.workouts);
other.exercises == this.exercises);
}
class PlansCompanion extends UpdateCompanion<Plan> {
final Value<int> id;
final Value<String> days;
final Value<String> workouts;
final Value<String> exercises;
const PlansCompanion({
this.id = const Value.absent(),
this.days = const Value.absent(),
this.workouts = const Value.absent(),
this.exercises = const Value.absent(),
});
PlansCompanion.insert({
this.id = const Value.absent(),
required String days,
required String workouts,
required String exercises,
}) : days = Value(days),
workouts = Value(workouts);
exercises = Value(exercises);
static Insertable<Plan> custom({
Expression<int>? id,
Expression<String>? days,
Expression<String>? workouts,
Expression<String>? exercises,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (days != null) 'days': days,
if (workouts != null) 'workouts': workouts,
if (exercises != null) 'exercises': exercises,
});
}
PlansCompanion copyWith(
{Value<int>? id, Value<String>? days, Value<String>? workouts}) {
{Value<int>? id, Value<String>? days, Value<String>? exercises}) {
return PlansCompanion(
id: id ?? this.id,
days: days ?? this.days,
workouts: workouts ?? this.workouts,
exercises: exercises ?? this.exercises,
);
}
@ -1369,8 +1369,8 @@ class PlansCompanion extends UpdateCompanion<Plan> {
if (days.present) {
map['days'] = Variable<String>(days.value);
}
if (workouts.present) {
map['workouts'] = Variable<String>(workouts.value);
if (exercises.present) {
map['exercises'] = Variable<String>(exercises.value);
}
return map;
}
@ -1380,7 +1380,7 @@ class PlansCompanion extends UpdateCompanion<Plan> {
return (StringBuffer('PlansCompanion(')
..write('id: $id, ')
..write('days: $days, ')
..write('workouts: $workouts')
..write('exercises: $exercises')
..write(')'))
.toString();
}
@ -1403,13 +1403,13 @@ class $PlansTable extends Plans with TableInfo<$PlansTable, Plan> {
late final GeneratedColumn<String?> days = GeneratedColumn<String?>(
'days', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _workoutsMeta = const VerificationMeta('workouts');
final VerificationMeta _exercisesMeta = const VerificationMeta('exercises');
@override
late final GeneratedColumn<String?> workouts = GeneratedColumn<String?>(
'workouts', aliasedName, false,
late final GeneratedColumn<String?> exercises = GeneratedColumn<String?>(
'exercises', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [id, days, workouts];
List<GeneratedColumn> get $columns => [id, days, exercises];
@override
String get aliasedName => _alias ?? 'plans';
@override
@ -1428,11 +1428,11 @@ class $PlansTable extends Plans with TableInfo<$PlansTable, Plan> {
} else if (isInserting) {
context.missing(_daysMeta);
}
if (data.containsKey('workouts')) {
context.handle(_workoutsMeta,
workouts.isAcceptableOrUnknown(data['workouts']!, _workoutsMeta));
if (data.containsKey('exercises')) {
context.handle(_exercisesMeta,
exercises.isAcceptableOrUnknown(data['exercises']!, _exercisesMeta));
} else if (isInserting) {
context.missing(_workoutsMeta);
context.missing(_exercisesMeta);
}
return context;
}

View File

@ -2,15 +2,15 @@ import 'package:flutter/material.dart';
class Days extends StatefulWidget {
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
createState() => _DaysState();
}
class _DaysState extends State<Days> {
final List<bool> _selections = List.generate(7, (_) => false);
final List<String> _days = [
'Monday',
'Tuesday',
@ -20,14 +20,19 @@ class _DaysState extends State<Days> {
'Saturday',
'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() {
List<String> selectedDays = [];
for (int i = 0; i < _selections.length; i++) {
if (_selections[i]) {
selectedDays.add(_days[i]);
}
}
for (int i = 0; i < _selections.length; i++)
if (_selections[i]) selectedDays.add(_days[i]);
return selectedDays.join(",");
}
@ -40,14 +45,19 @@ class _DaysState extends State<Days> {
@override
Widget build(BuildContext context) {
return Column(
children: List.generate(7, (index) {
return SwitchListTile(
title: Text(_days[index]),
value: _selections[index],
onChanged: (value) => _updateSelections(index),
);
}),
);
return Column(children: [
Text('Days', style: Theme.of(context).textTheme.headlineSmall),
Expanded(
child: ListView(
children: List.generate(7, (index) {
return SwitchListTile(
title: Text(_days[index]),
value: _selections[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:fmassive/database.dart';
import 'package:fmassive/days.dart';
import 'package:fmassive/exercises.dart';
import 'package:fmassive/main.dart';
import 'package:moor_flutter/moor_flutter.dart';
@ -15,37 +16,69 @@ class EditPlanPage extends StatefulWidget {
}
class _EditPlanPageState extends State<EditPlanPage> {
final TextEditingController _daysController = TextEditingController();
final TextEditingController _workoutsController = TextEditingController();
late PlansCompanion plan;
final daysNode = FocusNode();
final workoutsNode = FocusNode();
List<String?> names = [];
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
void initState() {
super.initState();
getDistinctNames().then((value) {
setState(() {
names = value;
});
});
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();
}
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
Widget build(BuildContext context) {
List<Widget> actions = [];
if (widget.plan.id.present)
actions.add(IconButton(
onPressed: () async {
bool? confirm = await _showConfirmationDialog(context);
if (!confirm!) return;
await db.plans.deleteOne(widget.plan);
if (!mounted) return;
Navigator.pop(context);
@ -59,38 +92,11 @@ class _EditPlanPageState extends State<EditPlanPage> {
padding: const EdgeInsets.all(16.0),
child: material.Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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));
});
},
),
],
children: getChildren,
),
),
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).replace(plan);
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 {
IntColumn get id => integer().autoIncrement()();
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/edit_plan.dart';
import 'package:fmassive/main.dart';
import 'package:fmassive/plan_list.dart';
import 'package:moor/moor.dart';
class PlansPage extends StatelessWidget {
@ -65,27 +66,20 @@ class _PlansPageState extends State<_PlansPage> {
builder: (context, snapshot) {
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)
return const Center(child: CircularProgressIndicator());
return ListView.builder(
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)),
),
);
});
},
);
return PlanList(plans: plans);
}),
floatingActionButton: FloatingActionButton(
onPressed: () async {
@ -93,8 +87,8 @@ class _PlansPageState extends State<_PlansPage> {
context,
MaterialPageRoute(
builder: (context) => const EditPlanPage(
plan:
PlansCompanion(days: Value(''), workouts: Value(''))),
plan: PlansCompanion(
days: Value(''), exercises: Value(''))),
),
);
},

View File

@ -95,11 +95,9 @@ class _SettingsPageState extends State<_SettingsPage> {
await file.copy(to);
print('Migrating...');
db = MyDatabase();
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final migrator = db.createMigrator();
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.');
},
child: const Text("Import")));