Add plans page + edit

This commit is contained in:
Brandon Presley 2023-04-13 18:58:26 +12:00
parent b1c7fc8882
commit 0f7d938ad7
7 changed files with 407 additions and 132 deletions

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:fmassive/gym_set.dart'; import 'package:fmassive/gym_set.dart';
import 'package:fmassive/main.dart'; import 'package:fmassive/main.dart';
import 'package:fmassive/plans.dart';
import 'package:moor/ffi.dart'; import 'package:moor/ffi.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -11,7 +12,7 @@ import 'settings.dart';
part 'database.g.dart'; part 'database.g.dart';
@UseMoor(tables: [Settings, GymSets]) @UseMoor(tables: [Settings, GymSets, Plans])
class MyDatabase extends _$MyDatabase { class MyDatabase extends _$MyDatabase {
MyDatabase() : super(_openConnection()); MyDatabase() : super(_openConnection());

View File

@ -1239,12 +1239,220 @@ 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});
factory Plan.fromData(Map<String, dynamic> 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<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['days'] = Variable<String>(days);
map['workouts'] = Variable<String>(workouts);
return map;
}
PlansCompanion toCompanion(bool nullToAbsent) {
return PlansCompanion(
id: Value(id),
days: Value(days),
workouts: Value(workouts),
);
}
factory Plan.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return Plan(
id: serializer.fromJson<int>(json['id']),
days: serializer.fromJson<String>(json['days']),
workouts: serializer.fromJson<String>(json['workouts']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'days': serializer.toJson<String>(days),
'workouts': serializer.toJson<String>(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<Plan> {
final Value<int> id;
final Value<String> days;
final Value<String> 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<Plan> custom({
Expression<int>? id,
Expression<String>? days,
Expression<String>? workouts,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (days != null) 'days': days,
if (workouts != null) 'workouts': workouts,
});
}
PlansCompanion copyWith(
{Value<int>? id, Value<String>? days, Value<String>? workouts}) {
return PlansCompanion(
id: id ?? this.id,
days: days ?? this.days,
workouts: workouts ?? this.workouts,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (days.present) {
map['days'] = Variable<String>(days.value);
}
if (workouts.present) {
map['workouts'] = Variable<String>(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<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _daysMeta = const VerificationMeta('days');
@override
late final GeneratedColumn<String?> days = GeneratedColumn<String?>(
'days', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _workoutsMeta = const VerificationMeta('workouts');
@override
late final GeneratedColumn<String?> workouts = GeneratedColumn<String?>(
'workouts', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [id, days, workouts];
@override
String get aliasedName => _alias ?? 'plans';
@override
String get actualTableName => 'plans';
@override
VerificationContext validateIntegrity(Insertable<Plan> 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<GeneratedColumn> get $primaryKey => {id};
@override
Plan map(Map<String, dynamic> 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 { abstract class _$MyDatabase extends GeneratedDatabase {
_$MyDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e); _$MyDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
late final $SettingsTable settings = $SettingsTable(this); late final $SettingsTable settings = $SettingsTable(this);
late final $GymSetsTable gymSets = $GymSetsTable(this); late final $GymSetsTable gymSets = $GymSetsTable(this);
late final $PlansTable plans = $PlansTable(this);
@override @override
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>(); Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
@override @override
List<DatabaseSchemaEntity> get allSchemaEntities => [settings, gymSets]; List<DatabaseSchemaEntity> get allSchemaEntities =>
[settings, gymSets, plans];
} }

113
lib/edit_plan.dart Normal file
View File

@ -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<EditPlanPage> {
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<Widget> 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),
),
));
}
}

View File

@ -54,7 +54,8 @@ class _EditGymSetPageState extends State<EditGymSetPage> {
}, },
icon: const Icon(Icons.delete))); icon: const Icon(Icons.delete)));
return Scaffold( return SafeArea(
child: Scaffold(
appBar: AppBar(title: const Text('Edit Gym Set'), actions: actions), appBar: AppBar(title: const Text('Edit Gym Set'), actions: actions),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@ -111,6 +112,12 @@ class _EditGymSetPageState extends State<EditGymSetPage> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { 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) if (gymSet.id.present)
await db.update(db.gymSets).write(gymSet); await db.update(db.gymSets).write(gymSet);
else { else {
@ -123,6 +130,6 @@ class _EditGymSetPageState extends State<EditGymSetPage> {
}, },
child: const Icon(Icons.check), child: const Icon(Icons.check),
), ),
); ));
} }
} }

View File

@ -51,7 +51,7 @@ class RootPage extends State<HomePage> {
case 'Best': case 'Best':
return const BestPage(); return const BestPage();
case 'Plans': case 'Plans':
return const PlansPage(); return PlansPage(search: search);
default: default:
return _HomePageWidget(search: search); return _HomePageWidget(search: search);
} }

7
lib/plans.dart Normal file
View File

@ -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()();
}

View File

@ -1,163 +1,102 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/material.dart' as material;
import 'package:fmassive/database.dart'; import 'package:fmassive/database.dart';
import 'package:fmassive/edit_plan.dart';
import 'package:fmassive/main.dart'; import 'package:fmassive/main.dart';
import 'package:fmassive/settings.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
class PlansPage extends StatelessWidget { class PlansPage extends StatelessWidget {
const PlansPage({super.key}); const PlansPage({super.key, required this.search});
final String search;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return Scaffold(
body: Center( body: Center(
child: _PlansPage(), child: _PlansPage(search: search),
), ),
); );
} }
} }
class _PlansPage extends StatefulWidget { 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 @override
createState() => _PlansPageState(); createState() => _PlansPageState();
} }
class _PlansPageState extends State<_PlansPage> { class _PlansPageState extends State<_PlansPage> {
late Stream<Setting> stream; bool showSearch = false;
late Stream<List<Plan>> stream;
final TextEditingController searchController = TextEditingController(); @override
initState() {
super.initState();
setStream();
}
void reset() async { void setStream() {
var data = await db.select(db.settings).get(); stream = (db.select(db.plans)
if (data.isEmpty) await db.into(db.settings).insert(defaultSettings); ..where((gymSet) => gymSet.days.contains(widget.search))
setState(() { ..limit(10, offset: 0))
if (data.isEmpty) return; .watch();
});
} }
@override @override
void initState() { didUpdateWidget(covariant _PlansPage oldWidget) {
super.initState(); super.didUpdateWidget(oldWidget);
stream = db.select(db.settings).watchSingle(); setStream();
}
void toggleSearch() {
setState(() {
showSearch = !showSearch;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: StreamBuilder<Setting>( body: StreamBuilder<List<Plan>>(
stream: stream, stream: stream,
builder: (context, snapshot) { builder: (context, snapshot) {
final settings = snapshot.data; final plans = snapshot.data;
if (settings == null) if (plans == null)
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
return SingleChildScrollView( return ListView.builder(
padding: const EdgeInsets.all(8.0), itemCount: plans.length,
child: material.Column( itemBuilder: (context, index) {
crossAxisAlignment: CrossAxisAlignment.start, return ListTile(
children: [ title: Text(plans[index].days),
SwitchListTile( subtitle: Text(plans[index].workouts),
title: const Text('Alarm'), onTap: () async {
value: settings.alarm, await Navigator.push(
onChanged: (value) { context,
db MaterialPageRoute(
.update(db.settings) builder: (context) => EditPlanPage(
.write(SettingsCompanion(alarm: Value(value))); plan: plans[index].toCompanion(false)),
}, ),
), );
SwitchListTile( });
title: const Text('Vibrate'), },
value: settings.vibrate, );
onChanged: (value) { }),
db floatingActionButton: FloatingActionButton(
.update(db.settings) onPressed: () async {
.write(SettingsCompanion(vibrate: Value(value))); await Navigator.push(
}, context,
), MaterialPageRoute(
SwitchListTile( builder: (context) => const EditPlanPage(
title: const Text('Notify'), plan:
value: settings.notify, PlansCompanion(days: Value(''), workouts: Value(''))),
onChanged: (value) { ),
db );
.update(db.settings) },
.write(SettingsCompanion(notify: Value(value))); child: const Icon(Icons.add)));
},
),
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)));
},
),
],
),
);
}),
);
} }
} }