feat: button set page wip

This commit is contained in:
2023-10-05 23:53:48 +03:00
parent 8113f5eb3b
commit 1ec3a84a15
10 changed files with 536 additions and 53 deletions

View File

@@ -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<String> matches)
customInvoke;
@override
void invoke(GameStore store, Automation parent, List<String> matches) {
customInvoke(store, parent, matches);
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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<GameButton> 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<GameButton> 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<GameButton> 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<String, dynamic> json) {
return GameButtonLabelData(
label: json['label'],

View File

@@ -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<GameButtonData?> buttons;
final int? crossAxisCount;
final Alignment alignment;
final double spacing;
final String group;
String name;
GameButtonSetType type;
List<GameButtonData?> 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<String, dynamic> json) => GameButtonSetData(
factory GameButtonSetData.fromJson(Map<String, dynamic> 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<GameButtonData?>? 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:

View File

@@ -155,18 +155,33 @@ class MUDProfile {
id, 'aliases/${alias.id}', alias.toJson());
}
Future<void> deleteAlias(Alias alias) async {
debugPrint('MUDProfile.deleteAlias: $id/aliases/${alias.id}');
return ProfileStorage.deleteProfileFile(id, 'aliases/${alias.id}');
}
Future<void> saveTrigger(Trigger trigger) async {
debugPrint('MUDProfile.saveTrigger: $id/triggers/${trigger.id}');
return ProfileStorage.writeProfileFile(
id, 'triggers/${trigger.id}', trigger.toJson());
}
Future<void> deleteTrigger(Trigger trigger) async {
debugPrint('MUDProfile.deleteTrigger: $id/triggers/${trigger.id}');
return ProfileStorage.deleteProfileFile(id, 'triggers/${trigger.id}');
}
Future<void> saveButtonSet(GameButtonSetData buttonSet) async {
debugPrint('MUDProfile.saveButtonSet: $id/button_sets/${buttonSet.id}');
return ProfileStorage.writeProfileFile(
id, 'button_sets/${buttonSet.id}', buttonSet.toJson());
}
Future<void> deleteButtonSet(GameButtonSetData buttonSet) async {
debugPrint('MUDProfile.deleteButtonSet: $id/button_sets/${buttonSet.id}');
return ProfileStorage.deleteProfileFile(id, 'button_sets/${buttonSet.id}');
}
Future<void> saveVariable(List<Variable> current, Variable update) async {
debugPrint('MUDProfile.saveVariable: $id/vars');
final existing = current.indexWhere(
@@ -184,6 +199,21 @@ class MUDProfile {
);
}
Future<void> deleteVariable(List<Variable> 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,
}

View File

@@ -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 = <String, Widget Function(BuildContext)>{
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);

View File

@@ -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,

View File

@@ -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<GameButtonSetPage> createState() => _GameButtonSetPageState();
}
class _GameButtonSetPageState extends State<GameButtonSetPage> {
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<ButtonSetEditor> createState() => _ButtonSetEditorState();
}
class _ButtonSetEditorState extends State<ButtonSetEditor> {
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<ButtonEditorDialog> createState() => _ButtonEditorDialogState();
}
class _ButtonEditorDialogState extends State<ButtonEditorDialog> {
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"),
),
),
],
),
),
),
);
}
}

View File

@@ -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,

View File

@@ -109,7 +109,7 @@ class HomePageState extends State<HomePage>
for (final buttonSet in store.buttonSets)
Padding(
padding: const EdgeInsets.all(8.0),
child: GameButtonSet(buttonSet: buttonSet),
child: GameButtonSet(data: buttonSet),
)
],
),

View File

@@ -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,