From 8113f5eb3b2fcf4bce4e0195ba293f9f55add547 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Thu, 5 Oct 2023 16:50:50 +0300 Subject: [PATCH] feat: button set list page --- android/app/src/main/AndroidManifest.xml | 7 +- lib/core/features/automation.dart | 22 +- lib/core/features/game_button.dart | 310 +++++++++++++++-------- lib/core/features/game_button_set.dart | 155 +++++++++--- lib/core/features/profile.dart | 22 ++ lib/core/routes.dart | 9 +- lib/core/store.dart | 12 + lib/main.dart | 24 +- lib/pages/button_sets_list_page.dart | 78 ++++++ lib/pages/generic_list_page.dart | 29 ++- lib/pages/home_page.dart | 27 +- lib/pages/home_scaffold.dart | 5 + 12 files changed, 496 insertions(+), 204 deletions(-) create mode 100644 lib/pages/button_sets_list_page.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4f2ac1b..7a2aa5a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,7 +30,8 @@ android:name="flutterEmbedding" android:value="2" /> - - - + + + + diff --git a/lib/core/features/automation.dart b/lib/core/features/automation.dart index c6d33c5..1e531b6 100644 --- a/lib/core/features/automation.dart +++ b/lib/core/features/automation.dart @@ -74,24 +74,21 @@ class Automation { }; bool matches(String line) { - late RegExp regex; - if (isRegex) { - regex = RegExp(pattern, caseSensitive: isCaseSensitive); - } else { - regex = _strToRegExp(pattern); - } - return regex.hasMatch(line); + return actualRegex.hasMatch(line); } + String get actualPattern => isRegex ? pattern : _strToRegExp(pattern).pattern; + RegExp get actualRegex => isRegex ? RegExp(pattern, caseSensitive: isCaseSensitive) : _strToRegExp(pattern); + RegExp _strToRegExp(String pattern) { final updatedPattern = pattern - .replaceAll('*', '(.*?)') .replaceAll(r'\', r'\') .replaceAll(r'(', r'\(') .replaceAll(r')', r'\)') .replaceAll(r'[', r'\[') .replaceAll(r']', r'\]') - .replaceAll(r'/', r'\/'); + .replaceAll(r'/', r'\/') + .replaceAll('*', '(.*?)'); final regex = RegExp("^$updatedPattern\$", caseSensitive: isCaseSensitive); return regex; } @@ -105,11 +102,7 @@ class Automation { if (!matches(str)) { return []; } - if (!isRegex) { - pattern = _strToRegExp(pattern).pattern; - } - final regex = - RegExp(pattern, caseSensitive: isCaseSensitive, unicode: true); + final regex = actualRegex; final foundMatches = regex.allMatches(str); final results = []; for (var i = 0; i < foundMatches.length; i++) { @@ -146,3 +139,4 @@ class Automation { action: action ?? this.action, ); } + diff --git a/lib/core/features/game_button.dart b/lib/core/features/game_button.dart index eba20da..78daf38 100644 --- a/lib/core/features/game_button.dart +++ b/lib/core/features/game_button.dart @@ -5,15 +5,15 @@ import '../store.dart'; import 'action.dart'; import 'automation.dart'; -class GameButton extends StatefulWidget { - const GameButton({ - super.key, - required this.icon, +class GameButtonData { + const GameButtonData({ + required this.id, + required this.label, required this.pressAction, - this.iconUp, - this.iconDown, - this.iconLeft, - this.iconRight, + this.labelUp, + this.labelDown, + this.labelLeft, + this.labelRight, this.color, this.size = defaultSize, this.longPressAction, @@ -25,11 +25,12 @@ class GameButton extends StatefulWidget { static const defaultSize = 60.0; - final GameButtonIcon icon; - final GameButtonIcon? iconUp; - final GameButtonIcon? iconDown; - final GameButtonIcon? iconLeft; - final GameButtonIcon? iconRight; + final String id; + final GameButtonLabelData label; + final GameButtonLabelData? labelUp; + final GameButtonLabelData? labelDown; + final GameButtonLabelData? labelLeft; + final GameButtonLabelData? labelRight; final Color? color; final double? size; final MUDAction pressAction; @@ -39,6 +40,69 @@ class GameButton extends StatefulWidget { final MUDAction? swipeLeftAction; final MUDAction? swipeRightAction; + factory GameButtonData.fromJson(Map json) { + return GameButtonData( + id: json['id'] as String, + label: GameButtonLabelData.fromJson(json['label']), + pressAction: MUDAction.fromJson(json['pressAction']), + labelUp: json['labelUp'] == null + ? null + : GameButtonLabelData.fromJson(json['labelUp']), + labelDown: json['labelDown'] == null + ? null + : GameButtonLabelData.fromJson(json['labelDown']), + labelLeft: json['labelLeft'] == null + ? null + : GameButtonLabelData.fromJson(json['labelLeft']), + labelRight: json['labelRight'] == null + ? null + : GameButtonLabelData.fromJson(json['labelRight']), + color: json['color'] == null ? null : Color(json['color'] as int), + size: json['size'] == null ? null : json['size']!.toDouble(), + longPressAction: json['longPressAction'] == null + ? null + : MUDAction.fromJson(json['longPressAction']), + swipeUpAction: json['swipeUpAction'] == null + ? null + : MUDAction.fromJson(json['swipeUpAction']), + swipeDownAction: json['swipeDownAction'] == null + ? null + : MUDAction.fromJson(json['swipeDownAction']), + swipeLeftAction: json['swipeLeftAction'] == null + ? null + : MUDAction.fromJson(json['swipeLeftAction']), + swipeRightAction: json['swipeRightAction'] == null + ? null + : MUDAction.fromJson(json['swipeRightAction']), + ); + } + + Map toJson() => { + 'id': id, + 'label': label.toJson(), + 'pressAction': pressAction.toJson(), + 'labelUp': labelUp?.toJson(), + 'labelDown': labelDown?.toJson(), + 'labelLeft': labelLeft?.toJson(), + 'labelRight': labelRight?.toJson(), + 'color': color?.value, + 'size': size, + 'longPressAction': longPressAction?.toJson(), + 'swipeUpAction': swipeUpAction?.toJson(), + 'swipeDownAction': swipeDownAction?.toJson(), + 'swipeLeftAction': swipeLeftAction?.toJson(), + 'swipeRightAction': swipeRightAction?.toJson(), + }; +} + +class GameButton extends StatefulWidget { + const GameButton({ + super.key, + required this.data, + }); + + final GameButtonData data; + @override State createState() => _GameButtonState(); } @@ -49,6 +113,9 @@ class _GameButtonState extends State with GameStoreStateMixin { Offset? _dragStart; Offset? _dragEnd; + // + GameButtonData get data => widget.data; + @override void initState() { _direction = GameButtonDirection.none; @@ -58,15 +125,15 @@ class _GameButtonState extends State with GameStoreStateMixin { @override Widget build(BuildContext context) { final curIcon = _currentDirectionIcon(context); - return Container( - width: widget.size, - height: widget.size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: _color(context), - ), - child: _listener( - context: context, + return _listener( + context: context, + child: Container( + width: data.size, + height: data.size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: _color(context), + ), child: IconTheme( data: _iconTheme(curIcon, context), child: Icon(curIcon.icon), @@ -76,7 +143,7 @@ class _GameButtonState extends State with GameStoreStateMixin { } double get _swipeMinDist => - (widget.size ?? + (data.size ?? IconTheme.of(context).size ?? const IconThemeData.fallback().size!) / 2; @@ -90,37 +157,40 @@ class _GameButtonState extends State with GameStoreStateMixin { child: child, ); } + return GestureDetector( onTap: _onPressed, onLongPress: _onLongPress, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - onHorizontalDragUpdate: _onHorizontalDragUpdate, - onHorizontalDragEnd: _onHorizontalDragEnd, + onVerticalDragStart: _onDragStart, + onVerticalDragUpdate: _onDragUpdate, + onVerticalDragEnd: _onDragEnd, + onHorizontalDragStart: _onDragStart, + onHorizontalDragUpdate: _onDragUpdate, + onHorizontalDragEnd: _onDragEnd, child: child, ); } - GameButtonIcon _currentDirectionIcon(BuildContext context) { + GameButtonLabelData _currentDirectionIcon(BuildContext context) { switch (_direction) { case GameButtonDirection.up: - return widget.iconUp ?? widget.icon; + return data.labelUp ?? data.label; case GameButtonDirection.down: - return widget.iconDown ?? widget.icon; + return data.labelDown ?? data.label; case GameButtonDirection.left: - return widget.iconLeft ?? widget.icon; + return data.labelLeft ?? data.label; case GameButtonDirection.right: - return widget.iconRight ?? widget.icon; + return data.labelRight ?? data.label; default: - return widget.icon; + return data.label; } } - IconThemeData _iconTheme(GameButtonIcon icon, BuildContext context) => + IconThemeData _iconTheme(GameButtonLabelData icon, BuildContext context) => icon.iconTheme ?? IconTheme.of(context); Color _color(BuildContext context) => - widget.color ?? + data.color ?? Theme.of(context).buttonTheme.colorScheme?.background ?? Colors.grey; @@ -129,53 +199,29 @@ class _GameButtonState extends State with GameStoreStateMixin { } void _onLongPress() { - _callAction(widget.longPressAction); + _callAction(data.longPressAction); } - void _onVerticalDragUpdate(DragUpdateDetails details) { - if (details.delta.dy < 0) { - setState(() { - _direction = GameButtonDirection.up; - }); - } else if (details.delta.dy > 0) { - setState(() { - _direction = GameButtonDirection.down; - }); - } - } - - void _onHorizontalDragUpdate(DragUpdateDetails details) { - if (details.delta.dx < 0) { - setState(() { - _direction = GameButtonDirection.left; - }); - } else if (details.delta.dx > 0) { - setState(() { - _direction = GameButtonDirection.right; - }); - } - } - - void _onVerticalDragEnd(DragEndDetails details) { - if (_direction == GameButtonDirection.up) { - _callAction(widget.swipeUpAction); - } else if (_direction == GameButtonDirection.down) { - _callAction(widget.swipeDownAction); - } + void _onDragStart(DragStartDetails details) { + _dragStart = details.globalPosition; setState(() { _direction = GameButtonDirection.none; }); } - void _onHorizontalDragEnd(DragEndDetails details) { - if (_direction == GameButtonDirection.left) { - _callAction(widget.swipeLeftAction); - } else if (_direction == GameButtonDirection.right) { - _callAction(widget.swipeRightAction); + void _onDragUpdate(DragUpdateDetails details) { + final pos = details.globalPosition; + final direction = _getRelativeDragDirection(_dragStart!, pos); + + if (direction != _direction) { + setState(() { + _direction = direction; + }); } - setState(() { - _direction = GameButtonDirection.none; - }); + } + + void _onDragEnd(DragEndDetails details) { + // _dragEnd = details.; } void _onPointerUp(PointerUpEvent event) { @@ -194,27 +240,7 @@ class _GameButtonState extends State with GameStoreStateMixin { void _onPointerMove(PointerMoveEvent event) { _dragEnd = event.position; - final diff = _dragStart! - _dragEnd!; - GameButtonDirection direction = _direction; - - if (diff.distance > _swipeMinDist) { - // detect primary direction - if (diff.dx.abs() > diff.dy.abs()) { - if (diff.dx > _swipeMinDist) { - direction = GameButtonDirection.left; - } else { - direction = GameButtonDirection.right; - } - } else { - if (diff.dy > _swipeMinDist) { - direction = GameButtonDirection.up; - } else { - direction = GameButtonDirection.down; - } - } - } else { - direction = GameButtonDirection.none; - } + final direction = _getRelativeDragDirection(_dragStart!, _dragEnd!); if (direction != _direction) { setState(() { @@ -223,36 +249,106 @@ class _GameButtonState extends State with GameStoreStateMixin { } } - MUDAction _actionForDirection(GameButtonDirection direction) { + GameButtonDirection _getRelativeDragDirection(Offset start, Offset current) { + final diff = start - current; + GameButtonDirection direction = _direction; + + if (diff.distance > _swipeMinDist) { + // detect primary direction + // horizontal + if (diff.dx.abs() > diff.dy.abs()) { + if (diff.dx > _swipeMinDist) { + direction = GameButtonDirection.left; + } else { + direction = GameButtonDirection.right; + } + // vertical + } else { + if (diff.dy > _swipeMinDist) { + direction = GameButtonDirection.up; + } else { + direction = GameButtonDirection.down; + } + } + // pos is within button - no direction + } else { + direction = GameButtonDirection.none; + } + return direction; + } + + MUDAction _directionalAction(GameButtonDirection direction) { switch (direction) { case GameButtonDirection.up: - return widget.swipeUpAction ?? widget.pressAction; + return data.swipeUpAction ?? data.pressAction; case GameButtonDirection.down: - return widget.swipeDownAction ?? widget.pressAction; + return data.swipeDownAction ?? data.pressAction; case GameButtonDirection.left: - return widget.swipeLeftAction ?? widget.pressAction; + return data.swipeLeftAction ?? data.pressAction; case GameButtonDirection.right: - return widget.swipeRightAction ?? widget.pressAction; + return data.swipeRightAction ?? data.pressAction; default: - return widget.pressAction; + return data.pressAction; } } void _callAction(MUDAction? action) { - (action ?? _actionForDirection(GameButtonDirection.none)) - .invoke(store, parentAutomation, []); + final act = action ?? _directionalAction(GameButtonDirection.none); + act.invoke(store, parentAutomation, []); } void _callCurrentDirection() { - _callAction(_actionForDirection(_direction)); + _callAction(_directionalAction(_direction)); } } -class GameButtonIcon { - final IconData icon; +class GameButtonLabelData { + final String? label; + final IconData? icon; final IconThemeData? iconTheme; - GameButtonIcon(this.icon, {this.iconTheme}); + GameButtonLabelData({ + this.label, + this.icon, + this.iconTheme, + }) : assert(label != null || icon != null); + + factory GameButtonLabelData.fromJson(Map json) { + return GameButtonLabelData( + label: json['label'], + icon: json['icon'] != null + ? IconData( + json['icon'], + fontFamily: json['iconFontFamily'] ?? 'MaterialIcons', + ) + : null, + iconTheme: json['iconTheme'] != null + ? _iconThemeDataFromJson(json['iconTheme']) + : null, + ); + } + + Map toJson() { + return { + 'label': label, + 'icon': icon?.codePoint, + 'iconFontFamily': icon?.fontFamily, + 'iconTheme': iconTheme != null ? _iconThemeDataToJson(iconTheme!) : null, + }; + } + + static IconThemeData _iconThemeDataFromJson(Map json) => + IconThemeData( + color: json['color'] != null ? Color(json['color']) : null, + opacity: json['opacity'] ?? 1.0, + size: json['size'] ?? 24.0, + ); + + static Map _iconThemeDataToJson(IconThemeData data) => { + 'color': data.color?.value, + 'opacity': data.opacity, + 'size': data.size, + }; } enum GameButtonDirection { diff --git a/lib/core/features/game_button_set.dart b/lib/core/features/game_button_set.dart index 6a5877c..421fe75 100644 --- a/lib/core/features/game_button_set.dart +++ b/lib/core/features/game_button_set.dart @@ -2,81 +2,142 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import '../string_utils.dart'; import 'action.dart'; import 'game_button.dart'; -class GameButtonsView extends StatelessWidget { - const GameButtonsView({ +class GameButtonSet extends StatelessWidget { + const GameButtonSet({ super.key, - required this.gameButtonSet, + required this.buttonSet, }); - final GameButtonSet gameButtonSet; + final GameButtonSetData buttonSet; @override Widget build(BuildContext context) { - final buttons = gameButtonSet.buttons + return Align( + alignment: buttonSet.alignment, + child: IconTheme( + data: IconTheme.of(context).copyWith(size: 32), + child: Builder( + builder: (context) { + final containerSize = buttonSet.size; + return SizedBox( + width: containerSize.width, + height: containerSize.height, + child: _buildButtonContainer(context), + ); + }, + ), + ), + ); + } + + Widget _buildButtonContainer(BuildContext context) { + final type = buttonSet.type; + final crossAxisCount = buttonSet.crossAxisCount; + final buttonWidgets = buttonSet.buttons .map( (button) => Padding( - padding: EdgeInsets.all(gameButtonSet.spacing ?? 8) / 2, - child: button ?? Container(), + padding: EdgeInsets.all(buttonSet.spacing / 2), + child: button != null ? GameButton(data: button) : Container(), ), ) .toList(); - final type = gameButtonSet.type; - final crossAxisCount = gameButtonSet.crossAxisCount; - switch (type) { case GameButtonSetType.row: return Row( mainAxisAlignment: MainAxisAlignment.center, - children: buttons, + children: buttonWidgets, ); case GameButtonSetType.column: return Column( mainAxisAlignment: MainAxisAlignment.center, - children: buttons, + children: buttonWidgets, ); case GameButtonSetType.grid: return GridView.count( crossAxisCount: crossAxisCount ?? 3, - children: buttons, + children: buttonWidgets, ); } } } -class GameButtonSet { +class GameButtonSetData { + final String id; + final String name; final GameButtonSetType type; - final List buttons; + final List buttons; final int? crossAxisCount; final Alignment alignment; - final double? spacing; + final double spacing; + final String group; - const GameButtonSet({ + const GameButtonSetData({ + required this.id, required this.type, + required this.name, required this.buttons, this.crossAxisCount, this.alignment = Alignment.center, this.spacing = 8, + this.group = '', }); Size get size => Size(calculateWidth(), calculateHeight()); + factory GameButtonSetData.fromJson(Map json) => GameButtonSetData( + id: json['id'] as String, + name: json['name'] as String, + type: GameButtonSetType.values.firstWhere( + (type) => type.name == json['type'], + ), + buttons: (json['buttons'] as List) + .map( + (button) => button == null + ? null + : GameButtonData.fromJson(button as Map), + ) + .toList(), + crossAxisCount: json['crossAxisCount'] as int?, + alignment: Alignment( + json['alignment']['x'] as double, + json['alignment']['y'] as double, + ), + spacing: json['spacing'] as double, + group: json['group'] as String? ?? '', + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'type': type.name, + 'buttons': buttons.map((button) => button?.toJson()).toList(), + 'crossAxisCount': crossAxisCount, + 'alignment': { + 'x': alignment.x, + 'y': alignment.y, + }, + 'spacing': spacing, + 'group': group, + }; + double calculateWidth() { switch (type) { case GameButtonSetType.row: return buttons.fold( 0, (sum, button) => - sum + (button?.size ?? GameButton.defaultSize)) + - (buttons.length - 1) * (spacing ?? 8); + sum + (button?.size ?? GameButtonData.defaultSize)) + + (buttons.length - 1) * spacing; case GameButtonSetType.column: return buttons.fold( 0, (sum, button) => - max(sum, button?.size ?? GameButton.defaultSize)) + - (buttons.length - 1) * (spacing ?? 8); + max(sum, button?.size ?? GameButtonData.defaultSize)) + + (buttons.length - 1) * spacing; case GameButtonSetType.grid: final rowCount = buttons.length ~/ (crossAxisCount ?? 3); final rowWidths = List.generate( @@ -88,11 +149,12 @@ class GameButtonSet { ) .fold( 0, - (sum, button) => sum + (button?.size ?? GameButton.defaultSize), + (sum, button) => + sum + (button?.size ?? GameButtonData.defaultSize), ), ); return rowWidths.fold(0, (sum, width) => max(sum, width)) + - (rowCount - 1) * (spacing ?? 8); + (rowCount - 1) * spacing; } } @@ -102,14 +164,14 @@ class GameButtonSet { return buttons.fold( 0, (sum, button) => - max(sum, button?.size ?? GameButton.defaultSize)) + - (buttons.length - 1) * (spacing ?? 8); + max(sum, button?.size ?? GameButtonData.defaultSize)) + + (buttons.length - 1) * spacing; case GameButtonSetType.column: return buttons.fold( 0, (sum, button) => - sum + (button?.size ?? GameButton.defaultSize)) + - (buttons.length - 1) * (spacing ?? 8); + sum + (button?.size ?? GameButtonData.defaultSize)) + + (buttons.length - 1) * spacing; case GameButtonSetType.grid: final rowCount = buttons.length ~/ (crossAxisCount ?? 3); final rowHeights = List.generate( @@ -121,11 +183,12 @@ class GameButtonSet { ) .fold( 0, - (sum, button) => sum + (button?.size ?? GameButton.defaultSize), + (sum, button) => + sum + (button?.size ?? GameButtonData.defaultSize), ), ); return rowHeights.fold(0, (sum, height) => max(sum, height)) + - (rowCount - 1) * (spacing ?? 8); + (rowCount - 1) * spacing; } } } @@ -136,35 +199,43 @@ enum GameButtonSetType { grid, } -final movementPreset = GameButtonSet( +final movementPreset = GameButtonSetData( + id: uuid(), + name: 'Movement', type: GameButtonSetType.grid, crossAxisCount: 3, + alignment: Alignment.bottomRight, buttons: [ null, - GameButton( - icon: GameButtonIcon(Icons.keyboard_arrow_up), + GameButtonData( + id: uuid(), + label: GameButtonLabelData(icon: Icons.keyboard_arrow_up), pressAction: MUDAction('north'), ), null, - GameButton( - icon: GameButtonIcon(Icons.keyboard_arrow_left), + GameButtonData( + id: uuid(), + label: GameButtonLabelData(icon: Icons.keyboard_arrow_left), pressAction: MUDAction('west'), ), - GameButton( - icon: GameButtonIcon(Icons.visibility_outlined), - iconUp: GameButtonIcon(Icons.exit_to_app), - iconDown: GameButtonIcon(Icons.exit_to_app), + GameButtonData( + id: uuid(), + label: GameButtonLabelData(icon: Icons.visibility_outlined), + labelUp: GameButtonLabelData(icon: Icons.exit_to_app), + labelDown: GameButtonLabelData(icon: Icons.exit_to_app), pressAction: MUDAction('look'), swipeUpAction: MUDAction('exits'), swipeDownAction: MUDAction('exits'), ), - GameButton( - icon: GameButtonIcon(Icons.keyboard_arrow_right), + GameButtonData( + id: uuid(), + label: GameButtonLabelData(icon: Icons.keyboard_arrow_right), pressAction: MUDAction('east'), ), null, - GameButton( - icon: GameButtonIcon(Icons.keyboard_arrow_down), + GameButtonData( + id: uuid(), + label: GameButtonLabelData(icon: Icons.keyboard_arrow_down), pressAction: MUDAction('south'), ), null, diff --git a/lib/core/features/profile.dart b/lib/core/features/profile.dart index aefcd0f..980dda6 100644 --- a/lib/core/features/profile.dart +++ b/lib/core/features/profile.dart @@ -6,6 +6,7 @@ import '../secrets.dart'; import '../storage.dart'; import '../string_utils.dart'; import 'alias.dart'; +import 'game_button_set.dart'; import 'trigger.dart'; import 'variable.dart'; @@ -133,6 +134,21 @@ class MUDProfile { .toList(); } + Future> loadButtonSets() async { + debugPrint('MUDProfile.loadButtonSets: $id'); + final buttonSets = await ProfileStorage.listProfileFiles(id, 'button_sets'); + final buttonSetFiles = >[]; + for (final buttonSet in buttonSets) { + debugPrint('MUDProfile.loadButtonSets: $id/buttonSets/$buttonSet'); + final buttonSetFile = + await ProfileStorage.readProfileFile(id, 'button_sets/$buttonSet'); + if (buttonSetFile != null) { + buttonSetFiles.add(buttonSetFile); + } + } + return buttonSetFiles.map((e) => GameButtonSetData.fromJson(e)).toList(); + } + Future saveAlias(Alias alias) async { debugPrint('MUDProfile.saveAlias: $id/aliases/${alias.id}'); return ProfileStorage.writeProfileFile( @@ -145,6 +161,12 @@ class MUDProfile { id, 'triggers/${trigger.id}', trigger.toJson()); } + Future saveButtonSet(GameButtonSetData buttonSet) async { + debugPrint('MUDProfile.saveButtonSet: $id/button_sets/${buttonSet.id}'); + return ProfileStorage.writeProfileFile( + id, 'button_sets/${buttonSet.id}', buttonSet.toJson()); + } + Future saveVariable(List current, Variable update) async { debugPrint('MUDProfile.saveVariable: $id/vars'); final existing = current.indexWhere( diff --git a/lib/core/routes.dart b/lib/core/routes.dart index 59bdc9e..649c1d4 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:mudblock/core/consts.dart'; import '../core/features/alias.dart'; import '../core/features/trigger.dart'; import '../core/store.dart'; import '../pages/alias_list_page.dart'; import '../pages/alias_page.dart'; +import '../pages/button_sets_list_page.dart'; import '../pages/home_page.dart'; import '../pages/home_scaffold.dart'; import '../pages/profile_page.dart'; @@ -14,6 +14,7 @@ import '../pages/trigger_list_page.dart'; import '../pages/trigger_page.dart'; import '../pages/variable_list_page.dart'; import '../pages/variable_page.dart'; +import 'consts.dart'; import 'features/profile.dart'; import 'features/variable.dart'; @@ -72,9 +73,15 @@ final routes = { final variable = ModalRoute.of(context)!.settings.arguments as Variable?; return VariablePage(variable: variable); }, + Paths.buttons: (context) => GameStore.consumer( + builder: (context, store, child) { + return const ButtonSetListPage(); + }, + ), Paths.home: (context) => HomeScaffold( builder: (context, _) { return HomePage(key: homeKey); }, ), }; + diff --git a/lib/core/store.dart b/lib/core/store.dart index 2a77cc1..fa88021 100644 --- a/lib/core/store.dart +++ b/lib/core/store.dart @@ -13,6 +13,7 @@ import 'package:provider/provider.dart'; import 'color_utils.dart'; import 'consts.dart'; import 'features/alias.dart'; +import 'features/game_button_set.dart'; import 'features/profile.dart'; import 'features/trigger.dart'; import 'features/variable.dart'; @@ -37,9 +38,11 @@ class GameStore extends ChangeNotifier { bool _clientReady = false; // features + // TODO - move to MUDProfile and make that reactive final List triggers = []; final List aliases = []; final Map variables = {}; + final List buttonSets = []; MUDProfile get currentProfile => _currentProfile!; @@ -72,6 +75,7 @@ class GameStore extends ChangeNotifier { loadTriggers(), loadAliases(), loadVariables(), + loadButtonSets(), ]); _client.connect(); } @@ -101,6 +105,14 @@ class GameStore extends ChangeNotifier { debugPrint('Variables: ${variables.length}'); } + Future loadButtonSets() async { + final list = await currentProfile.loadButtonSets(); + buttonSets.clear(); + buttonSets.addAll(list); + notifyListeners(); + debugPrint('ButtonSets: ${buttonSets.length}'); + } + bool processTriggers(String line) { bool showLine = true; final str = ColorUtils.stripColor(line); diff --git a/lib/main.dart b/lib/main.dart index 2b13309..e24da9b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; @@ -39,17 +41,29 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { + final theme = ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ); return MaterialApp( title: 'Mudblock', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), + theme: theme, builder: (context, child) { - return GameStore.provider(child: child!); + return GameStore.provider( + child: Container( + color: theme.colorScheme.background, + child: Padding( + padding: PlatformUtils.isDesktop + ? EdgeInsets.only(top: Platform.isMacOS ? 28.0 : 32.0) + : EdgeInsets.zero, + child: child!, + ), + ), + ); }, initialRoute: Paths.home, routes: routes, ); } } + diff --git a/lib/pages/button_sets_list_page.dart b/lib/pages/button_sets_list_page.dart new file mode 100644 index 0000000..13d5fd3 --- /dev/null +++ b/lib/pages/button_sets_list_page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +import '../core/features/game_button_set.dart'; +import '../core/routes.dart'; +import '../core/store.dart'; +import 'generic_list_page.dart'; + +class ButtonSetListPage extends StatelessWidget with GameStoreMixin { + const ButtonSetListPage({super.key}); + + @override + Widget build(BuildContext context) { + return GenericListPage( + title: const Text('ButtonSets'), + save: save, + items: storeOf(context).buttonSets, + detailsPath: Paths.buttonSet, + displayName: (buttonSet) => buttonSet.name, + searchTags: (buttonSet) => [ + buttonSet.group, + ], + actions: [ + DropdownButton( + items: const [ + DropdownMenuItem( + value: 'navigation_preset', + child: Text('Create Navigation set'), + ), + ], + onChanged: (value) { + switch (value) { + case 'navigation_preset': + // Navigator.pushNamed( + // context, + // Paths.buttonSet, + // arguments: movementPreset, + // ); + save(storeOf(context), movementPreset); + break; + } + }, + ), + ], + itemBuilder: (context, store, buttonSet) { + return ListTile( + key: Key(buttonSet.id), + title: Text(buttonSet.name), + // TODO change/remove + subtitle: Text(buttonSet.name), + // leading: Switch.adaptive( + // value: buttonSet.enabled, + // onChanged: (value) { + // buttonSet.enabled = value; + // save(store, buttonSet); + // }, + // ), + onTap: () async { + final updated = await Navigator.pushNamed( + context, + Paths.buttonSet, + arguments: buttonSet, + ); + if (updated != null) { + await save(store, updated as GameButtonSetData); + } + }, + ); + }, + ); + } + + Future save(GameStore store, GameButtonSetData updated) async { + await store.currentProfile.saveButtonSet(updated); + // TODO - stop re-loading all triggers, only replace the one that changed + await store.loadButtonSets(); + } +} + diff --git a/lib/pages/generic_list_page.dart b/lib/pages/generic_list_page.dart index d74e413..48d349f 100755 --- a/lib/pages/generic_list_page.dart +++ b/lib/pages/generic_list_page.dart @@ -11,6 +11,7 @@ class GenericListPage extends StatefulWidget with GameStoreMixin { required this.displayName, required this.searchTags, required this.itemBuilder, + this.actions, }); final List items; @@ -21,6 +22,7 @@ class GenericListPage extends StatefulWidget with GameStoreMixin { itemBuilder; final String Function(T item) displayName; final List Function(T item) searchTags; + final List? actions; @override State> createState() => _GenericListPageState(); @@ -29,6 +31,7 @@ class GenericListPage extends StatefulWidget with GameStoreMixin { class _GenericListPageState extends State> with GameStoreStateMixin { List _filteredItems = []; + String _searchTerms = ''; @override void initState() { @@ -41,19 +44,23 @@ class _GenericListPageState extends State> return Scaffold( appBar: AppBar( title: widget.title, + actions: widget.actions, ), body: GameStore.consumer( builder: (context, store, child) { debugPrint('Generic list rebuild'); return Column( children: [ - TextField( - decoration: const InputDecoration( - hintText: 'Search', + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + decoration: const InputDecoration( + hintText: 'Search', + ), + onChanged: (value) { + _search(value); + }, ), - onChanged: (value) { - _search(value); - }, ), ListView.builder( itemCount: _filteredItems.length, @@ -72,7 +79,8 @@ class _GenericListPageState extends State> onPressed: () async { final item = await Navigator.pushNamed(context, widget.detailsPath); if (item != null) { - widget.save(store, item as T); + await widget.save(store, item as T); + _search(_searchTerms); } }, ), @@ -81,11 +89,12 @@ class _GenericListPageState extends State> void _search(String value) { setState(() { + _searchTerms = value; _filteredItems = widget.items.where((item) { final tags = widget.searchTags(item); - final displayName = widget.displayName(item); - return displayName.contains(value) || - tags.any((tag) => tag.contains(value)); + final displayName = widget.displayName(item).toLowerCase(); + return displayName.contains(value.toLowerCase()) || + tags.any((tag) => tag.contains(value.toLowerCase())); }).toList(); }); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 935ff7b..7b84ee3 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -106,28 +106,11 @@ class HomePageState extends State ), ), ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.bottomRight, - child: IconTheme( - data: IconTheme.of(context).copyWith(size: 32), - child: Builder( - builder: (context) { - final buttonSet = movementPreset; - final size = buttonSet.size; - return SizedBox( - width: size.width, - height: size.height, - child: GameButtonsView( - gameButtonSet: movementPreset, - ), - ); - }, - ), - ), - ), - ) + for (final buttonSet in store.buttonSets) + Padding( + padding: const EdgeInsets.all(8.0), + child: GameButtonSet(buttonSet: buttonSet), + ) ], ), ); diff --git a/lib/pages/home_scaffold.dart b/lib/pages/home_scaffold.dart index 345ee66..9062a13 100644 --- a/lib/pages/home_scaffold.dart +++ b/lib/pages/home_scaffold.dart @@ -25,9 +25,14 @@ class HomeScaffold extends StatelessWidget with GameStoreMixin { child: Padding( padding: PlatformUtils.isDesktop ? const EdgeInsets.only(top: 60) + // ? const EdgeInsets.only(top: 0) : EdgeInsets.zero, child: ListView( children: [ + ListTile( + title: const Text('Button Sets'), + onTap: () => Navigator.pushNamed(context, Paths.buttons), + ), ListTile( title: const Text('Aliases'), onTap: () => Navigator.pushNamed(context, Paths.aliases),