feat: will pop scope dialog

This commit is contained in:
2023-10-31 02:24:48 +02:00
parent a826c815f0
commit 8cca134673
6 changed files with 270 additions and 121 deletions

View File

@@ -5,12 +5,36 @@ import 'package:flutter/material.dart';
import 'widget_utils.dart';
class DialogUtils {
static DialogButtons deleteButtons(
BuildContext context, {
required FutureOr<void> Function() onDelete,
bool dismissOnDelete = true,
}) {
return DialogButtons.two(
confirm: ElevatedButton(
child: const Text('Delete'),
onPressed: () {
onDelete();
if (dismissOnDelete) {
Navigator.of(context).pop();
}
},
),
dismiss: TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
);
}
static DialogButtons saveButtons(
BuildContext context, {
required FutureOr<void> Function() onSave,
bool dismissOnSave = true,
}) {
return DialogButtons.confirm(
return DialogButtons.two(
confirm: ElevatedButton(
child: const Text('Save'),
onPressed: () {
@@ -33,7 +57,7 @@ class DialogUtils {
BuildContext context, {
required FutureOr<void> Function() onAdd,
}) {
return DialogButtons.single(
return DialogButtons.one(
child: ElevatedButton(
child: const Text('Add'),
onPressed: () {
@@ -43,29 +67,75 @@ class DialogUtils {
),
);
}
static yesNoButtons(
BuildContext context, {
required void Function() onYes,
required void Function() onNo,
bool dismissOnYes = true,
bool dismissOnNo = true,
bool revesed = false,
}) {
return DialogButtons.two(
confirm: ElevatedButton(
child: const Text('Yes'),
onPressed: () {
onYes();
if (dismissOnYes) {
Navigator.of(context).pop();
}
},
),
dismiss: TextButton(
child: const Text('No'),
onPressed: () {
onNo();
if (dismissOnNo) {
Navigator.of(context).pop();
}
},
),
reversed: revesed,
);
}
}
class DialogButtons {
final List<Widget> children;
final double spacing;
final bool reversed;
DialogButtons({required this.children, this.spacing = 8});
DialogButtons({
required this.children,
this.spacing = 8,
this.reversed = false,
});
DialogButtons.single({required Widget child})
DialogButtons.one({required Widget child})
: children = [child],
spacing = 0;
spacing = 0,
reversed = false;
DialogButtons.confirm(
{required Widget confirm, required Widget dismiss, this.spacing = 8})
: children = [dismiss, confirm];
DialogButtons.two({
required Widget confirm,
required Widget dismiss,
this.spacing = 8,
this.reversed = false,
}) : children = [dismiss, confirm];
Widget row() {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: spacing > 0
? WidgetUtils.join(children, SizedBox(width: spacing))
: children,
children: spacing > 0 ? WidgetUtils.gap(children, spacing) : children,
);
}
List<Widget> actions({bool includeSpacing = false}) {
final widgets = reversed ? children.reversed.toList() : children;
if (!includeSpacing || spacing == 0) {
return widgets;
}
return WidgetUtils.gap(widgets, spacing);
}
}

View File

@@ -161,7 +161,7 @@ class MUDProfile extends PluginBase {
if (!storage.initialized) {
await storage.init();
}
return storage.deleteFile(id);
return storage.deleteDirectory('.');
}
static final encKey = enc.Key.fromUtf8(pwdKey);

View File

@@ -87,7 +87,7 @@ class FileStorage<T> implements IStorage<T> {
@override
Future<void> deleteDirectory(String directory) async {
debugPrint('Clearing directory: $directory');
final dir = Directory(path.join(base, directory));
final dir = Directory(directory == '.' ? base : path.join(base, directory));
await dir.delete(recursive: true);
}

View File

@@ -1,7 +1,12 @@
import 'package:flutter/material.dart';
class WidgetUtils {
static join(List<Widget> widgets, Widget separator) =>
widgets.expand((widget) => [separator, widget]).toList()..removeAt(0);
static join(List<Widget> widgets, Widget separator) => widgets.length == 1
? widgets
: widgets.expand((widget) => [separator, widget]).toList()
..removeAt(0);
static gap(List<Widget> widgets, double gap) =>
join(widgets, SizedBox(width: gap, height: gap));
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import '../core/dialog_utils.dart';
class WillConfirmPopScope extends StatelessWidget {
const WillConfirmPopScope({
super.key,
required this.child,
required this.dirty,
});
final Widget child;
final bool dirty;
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (!dirty) {
return true;
}
final res = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Are you sure?'),
content: const Text(
'Do you want to go back? Your changes would be lost'),
actions: DialogUtils.yesNoButtons(
context,
onYes: () => Navigator.of(context).pop(true),
onNo: () => Navigator.of(context).pop(false),
dismissOnYes: false,
dismissOnNo: false,
).actions(),
),
);
return res ?? false;
},
child: child,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../core/features/profile.dart';
import '../dialogs/confirmation_dialog.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key, required this.profile});
@@ -12,128 +13,158 @@ class ProfilePage extends StatefulWidget {
}
class _ProfilePageState extends State<ProfilePage> {
late final MUDProfile profile;
late MUDProfile profile;
late bool isNew;
bool dirty = false;
final nameController = TextEditingController();
@override
void initState() {
profile = widget.profile?.copyWith() ?? MUDProfile.empty();
nameController.text = profile.name;
isNew = widget.profile == null;
super.initState();
}
void setDirty() {
if (dirty) {
return;
}
setState(() {
dirty = true;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
),
body: Align(
alignment: Alignment.topCenter,
child: SizedBox(
width: 1200,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: TextEditingController(text: profile.name),
decoration: const InputDecoration(
labelText: 'Profile name',
helperText: 'The name of the profile',
),
onChanged: (value) {
profile.name = value;
},
),
TextField(
controller: TextEditingController(text: profile.host),
decoration: const InputDecoration(
labelText: 'Host',
),
onChanged: (value) {
profile.host = value;
},
),
TextField(
controller:
TextEditingController(text: profile.port.toString()),
decoration: const InputDecoration(
labelText: 'Port',
),
onChanged: (value) {
profile.port = int.tryParse(value) ?? profile.port;
},
),
CheckboxListTile(
title: const Text('Enable MCCP Compression'),
subtitle: const Text(
'Enables MCCP Compression for this profile (if available).',
),
value: profile.mccpEnabled,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) {
setState(() {
profile.mccpEnabled = value ?? false;
});
},
),
const SizedBox(height: 32),
Text(
'Authentication',
style: Theme.of(context).textTheme.titleMedium,
),
TextField(
controller: TextEditingController(text: profile.username),
decoration: const InputDecoration(
labelText: 'Username',
),
onChanged: (value) {
profile.username = value;
},
),
TextField(
controller: TextEditingController(text: profile.password),
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
onChanged: (value) {
profile.password = value;
},
),
const SizedBox(height: 24),
DropdownMenu(
label: const Text('Authentication Method'),
initialSelection: profile.authMethod,
onSelected: (value) {
setState(() {
profile.authMethod = value as AuthMethod;
});
},
dropdownMenuEntries: const [
DropdownMenuEntry(
value: AuthMethod.none,
label: 'None',
return WillConfirmPopScope(
dirty: dirty,
child: Scaffold(
appBar: AppBar(
title: const Text('Profile'),
),
body: Align(
alignment: Alignment.topCenter,
child: SizedBox(
width: 1200,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Profile name',
helperText: 'The name of the profile',
),
DropdownMenuEntry(
value: AuthMethod.diku,
label: 'Diku-style',
onChanged: (value) {
setDirty();
profile.name = value;
if (isNew) {
profile = profile.copyWith(
id: value.replaceAll(RegExp(r'\W+'), '_'),
);
}
},
),
TextField(
controller: TextEditingController(text: profile.host),
decoration: const InputDecoration(
labelText: 'Host',
),
],
),
],
onChanged: (value) {
setDirty();
profile.host = value;
},
),
TextField(
controller:
TextEditingController(text: profile.port.toString()),
decoration: const InputDecoration(
labelText: 'Port',
),
onChanged: (value) {
setDirty();
profile.port = int.tryParse(value) ?? profile.port;
},
),
CheckboxListTile(
title: const Text('Enable MCCP Compression'),
subtitle: const Text(
'Enables MCCP Compression for this profile (if available).',
),
value: profile.mccpEnabled,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) {
setDirty();
setState(() {
profile.mccpEnabled = value ?? false;
});
},
),
const SizedBox(height: 32),
Text(
'Authentication',
style: Theme.of(context).textTheme.titleMedium,
),
TextField(
controller: TextEditingController(text: profile.username),
decoration: const InputDecoration(
labelText: 'Username',
),
onChanged: (value) {
setDirty();
profile.username = value;
},
),
TextField(
controller: TextEditingController(text: profile.password),
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
onChanged: (value) {
setDirty();
profile.password = value;
},
),
const SizedBox(height: 24),
DropdownMenu(
label: const Text('Authentication Method'),
initialSelection: profile.authMethod,
onSelected: (value) {
setDirty();
setState(() {
profile.authMethod = value as AuthMethod;
});
},
dropdownMenuEntries: const [
DropdownMenuEntry(
value: AuthMethod.none,
label: 'None',
),
DropdownMenuEntry(
value: AuthMethod.diku,
label: 'Diku-style',
),
],
),
],
),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pop(context, profile);
},
child: const Icon(Icons.save),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pop(context, profile);
},
child: const Icon(Icons.save),
),
),
);
}
}