From 1ec3a84a15153353ded710b16b69e080042c2bd0 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Thu, 5 Oct 2023 23:53:48 +0300 Subject: [PATCH] feat: button set page wip --- lib/core/features/action.dart | 13 ++ lib/core/features/game_button.dart | 103 ++++++++-- lib/core/features/game_button_set.dart | 96 ++++++--- lib/core/features/profile.dart | 31 +++ lib/core/routes.dart | 7 + lib/pages/alias_list_page.dart | 18 ++ lib/pages/button_set_page.dart | 262 +++++++++++++++++++++++++ lib/pages/button_sets_list_page.dart | 39 +++- lib/pages/home_page.dart | 2 +- lib/pages/trigger_list_page.dart | 18 ++ 10 files changed, 536 insertions(+), 53 deletions(-) create mode 100644 lib/pages/button_set_page.dart diff --git a/lib/core/features/action.dart b/lib/core/features/action.dart index 9867d16..cf61792 100644 --- a/lib/core/features/action.dart +++ b/lib/core/features/action.dart @@ -91,3 +91,16 @@ class MUDAction { } } +class NativeMUDAction extends MUDAction { + NativeMUDAction(this.customInvoke) + : super('-- native code --', target: MUDActionTarget.script); + + final void Function(GameStore store, Automation parent, List matches) + customInvoke; + + @override + void invoke(GameStore store, Automation parent, List matches) { + customInvoke(store, parent, matches); + } +} + diff --git a/lib/core/features/game_button.dart b/lib/core/features/game_button.dart index 78daf38..0a6638a 100644 --- a/lib/core/features/game_button.dart +++ b/lib/core/features/game_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../platform_utils.dart'; import '../store.dart'; +import '../string_utils.dart'; import 'action.dart'; import 'automation.dart'; @@ -40,6 +41,12 @@ class GameButtonData { final MUDAction? swipeLeftAction; final MUDAction? swipeRightAction; + factory GameButtonData.empty() => GameButtonData( + id: uuid(), + label: GameButtonLabelData.empty(), + pressAction: MUDAction.empty(), + ); + factory GameButtonData.fromJson(Map json) { return GameButtonData( id: json['id'] as String, @@ -77,6 +84,39 @@ class GameButtonData { ); } + GameButtonData copyWith({ + String? id, + GameButtonLabelData? label, + GameButtonLabelData? labelUp, + GameButtonLabelData? labelDown, + GameButtonLabelData? labelLeft, + GameButtonLabelData? labelRight, + Color? color, + double? size, + MUDAction? pressAction, + MUDAction? longPressAction, + MUDAction? swipeUpAction, + MUDAction? swipeDownAction, + MUDAction? swipeLeftAction, + MUDAction? swipeRightAction, + }) => + GameButtonData( + id: id ?? this.id, + label: label ?? this.label, + labelUp: labelUp ?? this.labelUp, + labelDown: labelDown ?? this.labelDown, + labelLeft: labelLeft ?? this.labelLeft, + labelRight: labelRight ?? this.labelRight, + color: color ?? this.color, + size: size ?? this.size, + pressAction: pressAction ?? this.pressAction, + longPressAction: longPressAction ?? this.longPressAction, + swipeUpAction: swipeUpAction ?? this.swipeUpAction, + swipeDownAction: swipeDownAction ?? this.swipeDownAction, + swipeLeftAction: swipeLeftAction ?? this.swipeLeftAction, + swipeRightAction: swipeRightAction ?? this.swipeRightAction, + ); + Map toJson() => { 'id': id, 'label': label.toJson(), @@ -93,6 +133,38 @@ class GameButtonData { 'swipeLeftAction': swipeLeftAction?.toJson(), 'swipeRightAction': swipeRightAction?.toJson(), }; + + MUDAction directionalAction(GameButtonDirection direction) { + switch (direction) { + case GameButtonDirection.up: + return swipeUpAction ?? pressAction; + case GameButtonDirection.down: + return swipeDownAction ?? pressAction; + case GameButtonDirection.left: + return swipeLeftAction ?? pressAction; + case GameButtonDirection.right: + return swipeRightAction ?? pressAction; + default: + return pressAction; + } + } + + MUDAction? actionForDirection(GameButtonDirection direction) { + switch (direction) { + case GameButtonDirection.none: + return pressAction; + case GameButtonDirection.up: + return swipeUpAction; + case GameButtonDirection.down: + return swipeDownAction; + case GameButtonDirection.left: + return swipeLeftAction; + case GameButtonDirection.right: + return swipeRightAction; + default: + return null; + } + } } class GameButton extends StatefulWidget { @@ -148,7 +220,10 @@ class _GameButtonState extends State with GameStoreStateMixin { const IconThemeData.fallback().size!) / 2; - Widget _listener({required BuildContext context, required Widget child}) { + Widget _listener({ + required BuildContext context, + required Widget child, + }) { if (PlatformUtils.isDesktop) { return Listener( onPointerDown: _onPointerDown, @@ -221,7 +296,10 @@ class _GameButtonState extends State with GameStoreStateMixin { } void _onDragEnd(DragEndDetails details) { - // _dragEnd = details.; + _callCurrentDirection(); + setState(() { + _direction = GameButtonDirection.none; + }); } void _onPointerUp(PointerUpEvent event) { @@ -277,28 +355,13 @@ class _GameButtonState extends State with GameStoreStateMixin { return direction; } - MUDAction _directionalAction(GameButtonDirection direction) { - switch (direction) { - case GameButtonDirection.up: - return data.swipeUpAction ?? data.pressAction; - case GameButtonDirection.down: - return data.swipeDownAction ?? data.pressAction; - case GameButtonDirection.left: - return data.swipeLeftAction ?? data.pressAction; - case GameButtonDirection.right: - return data.swipeRightAction ?? data.pressAction; - default: - return data.pressAction; - } - } - void _callAction(MUDAction? action) { - final act = action ?? _directionalAction(GameButtonDirection.none); + final act = action ?? data.directionalAction(GameButtonDirection.none); act.invoke(store, parentAutomation, []); } void _callCurrentDirection() { - _callAction(_directionalAction(_direction)); + _callAction(data.directionalAction(_direction)); } } @@ -313,6 +376,8 @@ class GameButtonLabelData { this.iconTheme, }) : assert(label != null || icon != null); + factory GameButtonLabelData.empty() => GameButtonLabelData(label: '?'); + factory GameButtonLabelData.fromJson(Map json) { return GameButtonLabelData( label: json['label'], diff --git a/lib/core/features/game_button_set.dart b/lib/core/features/game_button_set.dart index 421fe75..fb38692 100644 --- a/lib/core/features/game_button_set.dart +++ b/lib/core/features/game_button_set.dart @@ -9,20 +9,20 @@ import 'game_button.dart'; class GameButtonSet extends StatelessWidget { const GameButtonSet({ super.key, - required this.buttonSet, + required this.data, }); - final GameButtonSetData buttonSet; + final GameButtonSetData data; @override Widget build(BuildContext context) { return Align( - alignment: buttonSet.alignment, + alignment: data.alignment, child: IconTheme( data: IconTheme.of(context).copyWith(size: 32), child: Builder( builder: (context) { - final containerSize = buttonSet.size; + final containerSize = data.size; return SizedBox( width: containerSize.width, height: containerSize.height, @@ -35,16 +35,37 @@ class GameButtonSet extends StatelessWidget { } Widget _buildButtonContainer(BuildContext context) { - final type = buttonSet.type; - final crossAxisCount = buttonSet.crossAxisCount; - final buttonWidgets = buttonSet.buttons - .map( - (button) => Padding( - padding: EdgeInsets.all(buttonSet.spacing / 2), - child: button != null ? GameButton(data: button) : Container(), - ), - ) - .toList(); + return buildContainer( + context: context, + type: data.type, + crossAxisCount: data.crossAxisCount, + spacing: data.spacing, + count: data.buttons.length, + size: data.size, + alignment: data.alignment, + builder: (context, index) => data.buttons[index] != null + ? GameButton(data: data.buttons[index]!) + : Container(), + ); + } + + static Widget buildContainer({ + required BuildContext context, + required GameButtonSetType type, + required Widget Function(BuildContext context, int index) builder, + required int count, + required int? crossAxisCount, + required Alignment alignment, + required double spacing, + required Size size, + }) { + final buttonWidgets = List.generate( + count, + (index) => Padding( + padding: EdgeInsets.all(spacing / 2), + child: builder(context, index), + ), + ); switch (type) { case GameButtonSetType.row: return Row( @@ -67,15 +88,15 @@ class GameButtonSet extends StatelessWidget { class GameButtonSetData { final String id; - final String name; - final GameButtonSetType type; - final List buttons; - final int? crossAxisCount; - final Alignment alignment; - final double spacing; - final String group; + String name; + GameButtonSetType type; + List buttons; + int? crossAxisCount; + Alignment alignment; + double spacing; + String group; - const GameButtonSetData({ + GameButtonSetData({ required this.id, required this.type, required this.name, @@ -86,9 +107,17 @@ class GameButtonSetData { this.group = '', }); + factory GameButtonSetData.empty() => GameButtonSetData( + id: uuid(), + name: '', + type: GameButtonSetType.row, + buttons: [], + ); + Size get size => Size(calculateWidth(), calculateHeight()); - factory GameButtonSetData.fromJson(Map json) => GameButtonSetData( + factory GameButtonSetData.fromJson(Map json) => + GameButtonSetData( id: json['id'] as String, name: json['name'] as String, type: GameButtonSetType.values.firstWhere( @@ -124,6 +153,27 @@ class GameButtonSetData { 'group': group, }; + GameButtonSetData copyWith({ + String? id, + String? name, + GameButtonSetType? type, + List? buttons, + int? crossAxisCount, + Alignment? alignment, + double? spacing, + String? group, + }) => + GameButtonSetData( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + buttons: buttons ?? this.buttons, + crossAxisCount: crossAxisCount ?? this.crossAxisCount, + alignment: alignment ?? this.alignment, + spacing: spacing ?? this.spacing, + group: group ?? this.group, + ); + double calculateWidth() { switch (type) { case GameButtonSetType.row: diff --git a/lib/core/features/profile.dart b/lib/core/features/profile.dart index 980dda6..1274234 100644 --- a/lib/core/features/profile.dart +++ b/lib/core/features/profile.dart @@ -155,18 +155,33 @@ class MUDProfile { id, 'aliases/${alias.id}', alias.toJson()); } + Future deleteAlias(Alias alias) async { + debugPrint('MUDProfile.deleteAlias: $id/aliases/${alias.id}'); + return ProfileStorage.deleteProfileFile(id, 'aliases/${alias.id}'); + } + Future saveTrigger(Trigger trigger) async { debugPrint('MUDProfile.saveTrigger: $id/triggers/${trigger.id}'); return ProfileStorage.writeProfileFile( id, 'triggers/${trigger.id}', trigger.toJson()); } + Future deleteTrigger(Trigger trigger) async { + debugPrint('MUDProfile.deleteTrigger: $id/triggers/${trigger.id}'); + return ProfileStorage.deleteProfileFile(id, 'triggers/${trigger.id}'); + } + Future saveButtonSet(GameButtonSetData buttonSet) async { debugPrint('MUDProfile.saveButtonSet: $id/button_sets/${buttonSet.id}'); return ProfileStorage.writeProfileFile( id, 'button_sets/${buttonSet.id}', buttonSet.toJson()); } + Future deleteButtonSet(GameButtonSetData buttonSet) async { + debugPrint('MUDProfile.deleteButtonSet: $id/button_sets/${buttonSet.id}'); + return ProfileStorage.deleteProfileFile(id, 'button_sets/${buttonSet.id}'); + } + Future saveVariable(List current, Variable update) async { debugPrint('MUDProfile.saveVariable: $id/vars'); final existing = current.indexWhere( @@ -184,6 +199,21 @@ class MUDProfile { ); } + Future deleteVariable(List current, Variable update) async { + debugPrint('MUDProfile.deleteVariable: $id/vars'); + final existing = current.indexWhere( + (v) => v.name == update.name, + ); + if (existing >= 0) { + current.removeAt(existing); + } + return ProfileStorage.writeProfileFile( + id, + 'vars', + {'vars': current.map((v) => v.toJson()).toList()}, + ); + } + static final encKey = enc.Key.fromUtf8(pwdKey); static final encrypter = enc.Encrypter(enc.AES(encKey, padding: null)); @@ -221,3 +251,4 @@ enum AuthMethod { none, diku, } + diff --git a/lib/core/routes.dart b/lib/core/routes.dart index 649c1d4..5c9d0e2 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -5,6 +5,7 @@ import '../core/features/trigger.dart'; import '../core/store.dart'; import '../pages/alias_list_page.dart'; import '../pages/alias_page.dart'; +import '../pages/button_set_page.dart'; import '../pages/button_sets_list_page.dart'; import '../pages/home_page.dart'; import '../pages/home_scaffold.dart'; @@ -15,6 +16,7 @@ import '../pages/trigger_page.dart'; import '../pages/variable_list_page.dart'; import '../pages/variable_page.dart'; import 'consts.dart'; +import 'features/game_button_set.dart'; import 'features/profile.dart'; import 'features/variable.dart'; @@ -78,6 +80,11 @@ final routes = { return const ButtonSetListPage(); }, ), + Paths.buttonSet: (context) { + final buttonSet = + ModalRoute.of(context)!.settings.arguments as GameButtonSetData?; + return GameButtonSetPage(buttonSet: buttonSet); + }, Paths.home: (context) => HomeScaffold( builder: (context, _) { return HomePage(key: homeKey); diff --git a/lib/pages/alias_list_page.dart b/lib/pages/alias_list_page.dart index 1ceeb64..d020022 100644 --- a/lib/pages/alias_list_page.dart +++ b/lib/pages/alias_list_page.dart @@ -32,6 +32,24 @@ class AliasListPage extends StatelessWidget with GameStoreMixin { save(store, alias); }, ), + trailing: PopupMenuButton( + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: 'delete', + child: Text('Delete'), + ), + ]; + }, + onSelected: (value) { + switch (value) { + case 'delete': + store.currentProfile.deleteAlias(alias); + store.loadAliases(); + break; + } + }, + ), onTap: () async { final updated = await Navigator.pushNamed( context, diff --git a/lib/pages/button_set_page.dart b/lib/pages/button_set_page.dart new file mode 100644 index 0000000..80bf854 --- /dev/null +++ b/lib/pages/button_set_page.dart @@ -0,0 +1,262 @@ +import 'package:flutter/material.dart'; + +import '../core/features/action.dart'; +import '../core/features/game_button.dart'; +import '../core/features/game_button_set.dart'; +import '../core/platform_utils.dart'; + +class GameButtonSetPage extends StatefulWidget { + const GameButtonSetPage({super.key, required this.buttonSet}); + + final GameButtonSetData? buttonSet; + + @override + State createState() => _GameButtonSetPageState(); +} + +class _GameButtonSetPageState extends State { + late final GameButtonSetData buttonSet; + + @override + void initState() { + buttonSet = widget.buttonSet?.copyWith() ?? GameButtonSetData.empty(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Button Set'), + // actions: [ + // Switch.adaptive( + // value: buttonSet.enabled, + // onChanged: (value) { + // buttonSet.enabled = value; + // }, + // ) + // ], + ), + body: Align( + alignment: Alignment.topCenter, + child: SizedBox( + width: 1200, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Builder( + builder: (context) { + return ListView( + shrinkWrap: true, + children: [ + TextField( + decoration: const InputDecoration( + labelText: 'Name', + ), + controller: TextEditingController( + text: buttonSet.name, + ), + onChanged: (value) { + buttonSet.name = value; + }, + ), + const SizedBox(height: 16), + DropdownMenu( + label: const Text('Type'), + initialSelection: buttonSet.type, + dropdownMenuEntries: GameButtonSetType.values + .map( + (e) => DropdownMenuEntry( + value: e, + label: e.name, + ), + ) + .toList(), + onSelected: (value) { + buttonSet.type = value as GameButtonSetType; + }, + ), + const SizedBox(height: 16), + ButtonSetEditor(data: buttonSet), + ], + ); + }, + ), + ), + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.pop(context, buttonSet); + }, + child: const Icon(Icons.save), + ), + ); + } +} + +class ButtonSetEditor extends StatefulWidget { + const ButtonSetEditor({ + super.key, + required this.data, + }); + + final GameButtonSetData? data; + + @override + State createState() => _ButtonSetEditorState(); +} + +class _ButtonSetEditorState extends State { + late final GameButtonSetData data; + + @override + void initState() { + data = widget.data?.copyWith() ?? GameButtonSetData.empty(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return _buildContainer( + context, + (context, index) { + final data = this.data.buttons[index]; + return Container( + color: Colors.grey, + child: data != null + ? FakeGameButton( + label: data.label, + size: data.size ?? GameButtonData.defaultSize, + spacing: this.data.spacing, + onEdit: () { + showDialog( + context: context, + builder: (context) => ButtonEditorDialog(data: data), + ); + }, + ) + : Container(), + ); + }, + ); + } + + Widget _buildContainer(BuildContext context, + Widget Function(BuildContext context, int index) builder) { + final size = data.size; + return Center( + child: SizedBox( + width: size.width, + height: size.height, + child: GameButtonSet.buildContainer( + context: context, + type: data.type, + size: data.size, + count: data.buttons.length, + crossAxisCount: data.crossAxisCount, + spacing: data.spacing, + alignment: data.alignment, + builder: builder, + ), + ), + ); + } +} + +class FakeGameButton extends StatelessWidget { + const FakeGameButton({ + super.key, + required this.label, + required this.onEdit, + required this.size, + required this.spacing, + }); + + final GameButtonLabelData label; + final void Function() onEdit; + final double size; + final double spacing; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + offset: Offset(0, size), + itemBuilder: (context) => const [ + PopupMenuItem( + value: 'edit', + child: Text('Edit'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Delete'), + ), + ], + onSelected: (value) { + if (value == 'edit') { + onEdit(); + } + }, + child: GameButton( + data: GameButtonData.empty().copyWith( + label: label, + pressAction: MUDAction.empty(), + longPressAction: MUDAction.empty(), + swipeUpAction: MUDAction.empty(), + swipeDownAction: MUDAction.empty(), + swipeLeftAction: MUDAction.empty(), + swipeRightAction: MUDAction.empty(), + ), + ), + ); + } +} + +class ButtonEditorDialog extends StatefulWidget { + const ButtonEditorDialog({ + super.key, + this.data, + }); + + final GameButtonData? data; + + @override + State createState() => _ButtonEditorDialogState(); +} + +class _ButtonEditorDialogState extends State { + late final GameButtonData data; + + @override + void initState() { + data = widget.data?.copyWith() ?? GameButtonData.empty(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: SizedBox( + width: 600, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + shrinkWrap: true, + children: [ + for (final direction in GameButtonDirection.values) + TextFormField( + initialValue: + data.actionForDirection(direction)?.content ?? '', + decoration: InputDecoration( + label: Text("${direction.name} action"), + ), + ), + ], + ), + ), + ), + ); + } +} + diff --git a/lib/pages/button_sets_list_page.dart b/lib/pages/button_sets_list_page.dart index 13d5fd3..b17d4b3 100644 --- a/lib/pages/button_sets_list_page.dart +++ b/lib/pages/button_sets_list_page.dart @@ -20,14 +20,16 @@ class ButtonSetListPage extends StatelessWidget with GameStoreMixin { buttonSet.group, ], actions: [ - DropdownButton( - items: const [ - DropdownMenuItem( - value: 'navigation_preset', - child: Text('Create Navigation set'), - ), - ], - onChanged: (value) { + PopupMenuButton( + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: 'navigation_preset', + child: Text('Create: Navigation Preset'), + ), + ]; + }, + onSelected: (value) { switch (value) { case 'navigation_preset': // Navigator.pushNamed( @@ -45,8 +47,7 @@ class ButtonSetListPage extends StatelessWidget with GameStoreMixin { return ListTile( key: Key(buttonSet.id), title: Text(buttonSet.name), - // TODO change/remove - subtitle: Text(buttonSet.name), + // subtitle: Text(buttonSet.name), // leading: Switch.adaptive( // value: buttonSet.enabled, // onChanged: (value) { @@ -54,6 +55,24 @@ class ButtonSetListPage extends StatelessWidget with GameStoreMixin { // save(store, buttonSet); // }, // ), + trailing: PopupMenuButton( + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: 'delete', + child: Text('Delete'), + ), + ]; + }, + onSelected: (value) { + switch (value) { + case 'delete': + store.currentProfile.deleteButtonSet(buttonSet); + store.loadButtonSets(); + break; + } + }, + ), onTap: () async { final updated = await Navigator.pushNamed( context, diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 7b84ee3..3e5f3da 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -109,7 +109,7 @@ class HomePageState extends State for (final buttonSet in store.buttonSets) Padding( padding: const EdgeInsets.all(8.0), - child: GameButtonSet(buttonSet: buttonSet), + child: GameButtonSet(data: buttonSet), ) ], ), diff --git a/lib/pages/trigger_list_page.dart b/lib/pages/trigger_list_page.dart index 83998ea..eb46ef5 100644 --- a/lib/pages/trigger_list_page.dart +++ b/lib/pages/trigger_list_page.dart @@ -32,6 +32,24 @@ class TriggerListPage extends StatelessWidget with GameStoreMixin { save(store, trigger); }, ), + trailing: PopupMenuButton( + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: 'delete', + child: Text('Delete'), + ), + ]; + }, + onSelected: (value) { + switch (value) { + case 'delete': + store.currentProfile.deleteTrigger(trigger); + store.loadTriggers(); + break; + } + }, + ), onTap: () async { final updated = await Navigator.pushNamed( context,