mirror of
https://github.com/chenasraf/mudblock.git
synced 2026-05-17 17:48:05 +00:00
feat: will pop scope dialog
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
43
lib/dialogs/confirmation_dialog.dart
Normal file
43
lib/dialogs/confirmation_dialog.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user