From 4496592f676f8c87c42bd8f2e6be5fd0d816b2f2 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Tue, 31 Oct 2023 02:28:23 +0200 Subject: [PATCH] refactor: alias/trigger refactors - combined automation page - built in triggers - help - motd - triggers/aliases when disconnected --- lib/core/features/action.dart | 24 +++ lib/core/features/alias.dart | 13 +- lib/core/features/builtin_command.dart | 65 +++++-- lib/core/features/trigger.dart | 30 ++++ lib/core/routes.dart | 18 +- lib/core/store.dart | 60 ++++--- lib/pages/alias_page.dart | 203 ++-------------------- lib/pages/automation_page.dart | 228 +++++++++++++++++++++++++ lib/pages/home_page.dart | 84 +++++---- lib/pages/home_scaffold.dart | 5 + lib/pages/trigger_page.dart | 203 ++-------------------- 11 files changed, 466 insertions(+), 467 deletions(-) create mode 100644 lib/pages/automation_page.dart diff --git a/lib/core/features/action.dart b/lib/core/features/action.dart index f9389fb..719cfdc 100644 --- a/lib/core/features/action.dart +++ b/lib/core/features/action.dart @@ -10,6 +10,9 @@ enum MUDActionTarget { execute, script, input, + output, + immediate, + none, } class MUDAction { @@ -67,6 +70,17 @@ class MUDAction { store.input.text = content; store.setInput(content); break; + case MUDActionTarget.output: + debugPrint('ActionSendTo.output: $content'); + store.echoOwn(content); + break; + case MUDActionTarget.immediate: + debugPrint('ActionSendTo.immediate: $content'); + store.send(content); + break; + case MUDActionTarget.none: + debugPrint('ActionSendTo.none: $content'); + break; } debugPrint('MUDAction.invoke: done'); } @@ -89,6 +103,9 @@ class MUDAction { static String doVariableReplacements(GameStore store, String content) { debugPrint('MUDAction._doSpecialReplacements: $content'); + if (!store.connected) { + return content; + } content = content .replaceAll('%PASSWORD', store.currentProfile.password) .replaceAll('%USERNAME', store.currentProfile.username); @@ -106,6 +123,13 @@ class NativeMUDAction extends MUDAction { NativeMUDAction(this.customInvoke) : super('-- native code --', target: MUDActionTarget.script); + factory NativeMUDAction.echoSystem(String content) => NativeMUDAction((store, matches) { + store.echoSystem(content); + }); + factory NativeMUDAction.echo(String content) => NativeMUDAction((store, matches) { + store.echo(content); + }); + final void Function(GameStore store, List matches) customInvoke; diff --git a/lib/core/features/alias.dart b/lib/core/features/alias.dart index fb22ba7..21f8420 100644 --- a/lib/core/features/alias.dart +++ b/lib/core/features/alias.dart @@ -109,7 +109,7 @@ class AliasProcessResult { bool lineRemoved; bool processed; - AliasProcessResult({ this.lineRemoved = false, this.processed = false}); + AliasProcessResult({this.lineRemoved = false, this.processed = false}); } String _key(String str) => 'builtin-alias-$str'; @@ -129,11 +129,12 @@ final builtInAliases = [ Alias( id: _key('help'), pattern: 'mudhelp', - action: NativeMUDAction( - (store, matches) { - store.echo(BuiltinCommand.help()); - }, - ), + action: NativeMUDAction.echoSystem(BuiltinCommand.help()), + ), + Alias( + id: _key('motd'), + pattern: 'mudmotd', + action: NativeMUDAction.echoSystem(BuiltinCommand.motd()), ), ]; diff --git a/lib/core/features/builtin_command.dart b/lib/core/features/builtin_command.dart index 5d6010e..81db13b 100644 --- a/lib/core/features/builtin_command.dart +++ b/lib/core/features/builtin_command.dart @@ -1,20 +1,61 @@ +// ignore_for_file: prefer_interpolation_to_compose_strings + +import 'package:mudblock/core/string_utils.dart'; + class BuiltinCommand { + static const sep = + '--------------------------------------------------------------------------------'; static help() { - const sep = - '--------------------------------------------------------------------------------'; final year = DateTime.now().year; + final years = [2023]; + if (!years.contains(year)) { + years.add(year); + } return ''' -$sep -Mudblock - Help -$sep + {_mudblockhelp} + $sep + Mudblock - Help + $sep -Mudblock is a cross-platform MUD client. -See more information at https://github.com/chenasraf/mudblock + Mudblock is a cross-platform MUD client. + See more information at https://github.com/chenasraf/mudblock -$sep -Developed by Chen Asraf -Copyright © $year - All Rights Reserved -$sep -'''; + $sep + Developed by Chen Asraf + Copyright © ${years.join('-')} - All Rights Reserved + $sep + + To get started, tap the hamburger menu at the top right corner and + select a profile. + + You can create, delete or update the profiles as you wish. + + Each profile hosts its own settings, aliases, triggers, variables, buttons, and + other similar features. + + If you've never played an MUD, try one of the sample MUDs to get started! + $sep + {/_mudblockhelp} + ''' + .trimMultiline() + + '\n'; + } + + static motd() { + return ''' + {_mudblockmotd} + $sep + Welcome to MudBlock! + $sep + To get started, tap the hamburger menu at the top right corner and + select a profile. + + For more help, type "mudhelp" + $sep + {/_mudblockmotd} + ''' + .trimMultiline() + + '\n'; } } + diff --git a/lib/core/features/trigger.dart b/lib/core/features/trigger.dart index e5ed4ce..4603076 100644 --- a/lib/core/features/trigger.dart +++ b/lib/core/features/trigger.dart @@ -62,6 +62,7 @@ class Trigger extends Automation { if (trigger.matches(str)) { trigger.invokeEffect(store, str); if (trigger.isRemovedFromBuffer) { + debugPrint('TriggerProcessResult: lineRemoved = true'); showLine = false; } if (trigger.autoDisable) { @@ -107,3 +108,32 @@ class TriggerProcessResult { TriggerProcessResult({this.lineRemoved = false}); } +String _key(String str) => 'builtin-trigger-$str'; + +final builtInTriggers = [ + Trigger( + id: _key('mudhelpflag-start'), + pattern: '{_mudblockhelp}', + action: MUDAction.empty(), + isRemovedFromBuffer: true, + ), + Trigger( + id: _key('mudhelpflag-end'), + pattern: '{/_mudblockhelp}', + action: MUDAction.empty(), + isRemovedFromBuffer: true, + ), + Trigger( + id: _key('mudmotdflag-start'), + pattern: '{_mudblockmotd}', + action: MUDAction.empty(), + isRemovedFromBuffer: true, + ), + Trigger( + id: _key('mudmotdflag-end'), + pattern: '{/_mudblockmotd}', + action: MUDAction.empty(), + isRemovedFromBuffer: true, + ), +]; + diff --git a/lib/core/routes.dart b/lib/core/routes.dart index fae8cdc..08c2414 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -71,7 +71,14 @@ final routes = { ), Paths.alias: (context) { final alias = ModalRoute.of(context)!.settings.arguments as Alias?; - return AliasPage(alias: alias); + return GameStore.consumer( + builder: (context, store, child) => AliasPage( + alias: alias, + onSave: (alias) async { + store.currentProfile.saveAlias(alias); + }, + ), + ); }, // triggers @@ -88,7 +95,14 @@ final routes = { ), Paths.trigger: (context) { final trigger = ModalRoute.of(context)!.settings.arguments as Trigger?; - return TriggerPage(trigger: trigger); + return GameStore.consumer( + builder: (context, store, child) => TriggerPage( + trigger: trigger, + onSave: (trigger) async { + store.currentProfile.saveTrigger(trigger); + }, + ), + ); }, // variables diff --git a/lib/core/store.dart b/lib/core/store.dart index 0b07584..f5492ba 100644 --- a/lib/core/store.dart +++ b/lib/core/store.dart @@ -16,6 +16,7 @@ import '../core/string_utils.dart'; import 'consts.dart'; import 'features/action.dart'; import 'features/alias.dart'; +import 'features/builtin_command.dart'; import 'features/profile.dart'; import 'features/trigger.dart'; import 'routes.dart'; @@ -45,11 +46,14 @@ class GameStore extends ChangeNotifier { /// accepts csp but NOT double csp RegExp get outgoingMsgSplitPattern => - RegExp("(? + RegExp("(? _currentProfile!; List get aliases => builtInAliases + (_currentProfile?.aliases ?? []); - List get triggers => currentProfile.triggers; + List get triggers => + builtInTriggers + (_currentProfile?.triggers ?? []); get connected => _clientReady && _client.connected; @@ -62,13 +66,7 @@ class GameStore extends ChangeNotifier { } void appStart(BuildContext context) async { - echoSystem(''' - Welcome to MudBlock! - To get started, tap the hamburger menu at the top right corner and - select a profile. - ''' - .trimMultiline()); - // For more help, type "mudhelp" + echoSystem(BuiltinCommand.motd()); } void selectProfileAndConnect(BuildContext context) async { @@ -162,9 +160,12 @@ class GameStore extends ChangeNotifier { } } - void processLines(String text) { + void processLines(String text, {int? color}) { final lines = text.split(incomingMsgSplitPattern); - for (final (i, line) in lines.indexed) { + for (var (i, line) in lines.indexed) { + if (color != null && !line.startsWith('$esc[')) { + line = '$esc[${color}m$line'; + } onLine(line, newLine: i != lines.length - 1); } } @@ -269,12 +270,12 @@ class GameStore extends ChangeNotifier { /// echoOwn - same as echo, but with predefined color void echoOwn(String line) { - echo('$esc[93m$line'); + processLines(line, color: 93); } /// echoSystem - same as echo, but with predefined color void echoSystem(String line) { - echo('$esc[96m$line'); + processLines(line, color: 96); } /// echoError - same as echo, but with predefined color @@ -284,18 +285,27 @@ class GameStore extends ChangeNotifier { /// sendBytes - raw send bytes - DOES NOT split by outgoingMsgSplitPattern, no processing void sendBytes(List bytes) { + if (!_clientReady || !_client.connected) { + return; + } debugPrint('Sending bytes: $bytes'); _client.sendBytes(bytes); } /// sendString - send string - DOES NOT split by outgoingMsgSplitPattern, no processing void sendString(String line) { + if (!_clientReady || !_client.connected) { + return; + } debugPrint('Sending string: $line'); _client.send(line + newline); } /// send - raw send string - no processing, DOES split by outgoingMsgSplitPattern void send(String text) { + if (!_clientReady || !_client.connected) { + return; + } for (var line in _splitCsp(text)) { if (isCompressed) { debugPrint('Sending compressed bytes: $line'); @@ -313,7 +323,8 @@ class GameStore extends ChangeNotifier { line = MUDAction.doVariableReplacements(this, line); debugPrint('processing aliases for: $line'); var result = Alias.processLine(this, aliases, line); - if (!result.lineRemoved && currentProfile.settings.echoCommands) { + if (!result.lineRemoved && + (_currentProfile == null || currentProfile.settings.echoCommands)) { echoOwn(line); } if (!result.processed) { @@ -323,22 +334,23 @@ class GameStore extends ChangeNotifier { } List _splitCsp(String line) { + final pattern = _currentProfile != null + ? outgoingMsgSplitPattern + : _outgoingMsgSplitPattern(';'); + final csp = _currentProfile != null + ? currentProfile.settings.commandSeparator + : ';'; return line - .split(outgoingMsgSplitPattern) - .map( - (l) => l.replaceAll( - '$commandSeparator$commandSeparator', - commandSeparator, - ), - ) + .split(pattern) + .map((l) => l.replaceAll('$csp$csp', csp)) .toList(); } /// submitInput - echo input, process aliases and triggers, then send, scroll to end, select input void submitAsInput(String text) { - if (!_clientReady || !_client.connected) { - return; - } + // if (!_clientReady || !_client.connected) { + // return; + // } execute(text); scrollToEnd(); selectInput(); diff --git a/lib/pages/alias_page.dart b/lib/pages/alias_page.dart index e68c1d7..2b70472 100644 --- a/lib/pages/alias_page.dart +++ b/lib/pages/alias_page.dart @@ -1,202 +1,25 @@ import 'package:flutter/material.dart'; -import '../core/features/action.dart'; import '../core/features/alias.dart'; -import '../core/platform_utils.dart'; +import 'automation_page.dart'; -class AliasPage extends StatefulWidget { - const AliasPage({super.key, required this.alias}); +class AliasPage extends StatelessWidget { + const AliasPage({ + super.key, + this.alias, + required this.onSave, + }); final Alias? alias; - - @override - State createState() => _AliasPageState(); -} - -class _AliasPageState extends State { - late final Alias alias; - - @override - void initState() { - alias = widget.alias?.copyWith() ?? Alias.empty(); - super.initState(); - } + final void Function(Alias) onSave; @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Alias'), - actions: [ - Switch.adaptive( - value: alias.enabled, - onChanged: (value) async { - alias.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) { - final patternTextField = TextField( - controller: TextEditingController(text: alias.pattern), - decoration: const InputDecoration( - labelText: 'Pattern', - helperText: 'The pattern to match your input against', - ), - onChanged: (value) { - alias.pattern = value; - }, - ); - final regexCheckbox = CheckboxListTile( - title: const Text('Regular Expression'), - // subtitle: const Text( - // 'Whether the pattern is a regular expression', - // ), - value: alias.isRegex, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - alias.isRegex = value ?? false; - }); - }, - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (PlatformUtils.isDesktop) - Row( - children: [ - Expanded( - child: patternTextField, - ), - SizedBox( - width: 270, - child: regexCheckbox, - ), - ], - ), - if (!PlatformUtils.isDesktop) ...[ - patternTextField, - regexCheckbox, - ], - TextField( - controller: TextEditingController(text: alias.label), - decoration: const InputDecoration( - labelText: 'Label', - helperText: 'Can be used to identify the alias in scripts', - ), - onChanged: (value) { - alias.label = value; - }, - ), - TextField( - controller: - TextEditingController(text: alias.action.content), - minLines: PlatformUtils.isDesktop ? 20 : 5, - maxLines: PlatformUtils.isDesktop ? 50 : 20, - decoration: const InputDecoration( - labelText: 'Action', - floatingLabelBehavior: FloatingLabelBehavior.always, - ), - onChanged: (value) { - alias.action.content = value; - }, - ), - Padding( - padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), - child: Text( - 'Send To', - style: Theme.of(context).textTheme.bodySmall, - ), - ), - DropdownMenu( - initialSelection: alias.action.target, - onSelected: (value) { - if (value is MUDActionTarget) { - alias.action.target = value; - } - }, - dropdownMenuEntries: const [ - DropdownMenuEntry( - value: MUDActionTarget.world, - label: 'World', - ), - DropdownMenuEntry( - value: MUDActionTarget.execute, - label: 'Execute', - ), - DropdownMenuEntry( - value: MUDActionTarget.script, - label: 'Script', - ), - DropdownMenuEntry( - value: MUDActionTarget.input, - label: 'Command Input', - ), - ], - ), - CheckboxListTile( - title: const Text('Case Sensitive'), - subtitle: const Text( - 'If checked, the pattern will only match if the case matches.', - ), - value: alias.isCaseSensitive, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - alias.isCaseSensitive = value ?? false; - }); - }, - ), - CheckboxListTile( - title: const Text('Remove From Output'), - subtitle: const Text( - 'If checked, your input line will be removed from the buffer when it is matched.', - ), - value: alias.isRemovedFromBuffer, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - alias.isRemovedFromBuffer = value ?? false; - }); - }, - ), - CheckboxListTile( - title: const Text('Auto Disable'), - subtitle: const Text( - 'If checked, the alias will be disabled after it is matched once.', - ), - value: alias.autoDisable, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - alias.autoDisable = value ?? false; - }); - }, - ), - ], - ); - }, - ), - ), - ), - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.pop(context, alias); - }, - child: const Icon(Icons.save), - ), + return AutomationPage( + automation: alias, + title: 'Alias', + onSave: onSave, + create: (alias) => alias?.copyWith() ?? Alias.empty(), ); } } - diff --git a/lib/pages/automation_page.dart b/lib/pages/automation_page.dart new file mode 100644 index 0000000..4108e4f --- /dev/null +++ b/lib/pages/automation_page.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; + +import '../core/features/action.dart'; +import '../core/features/alias.dart'; +import '../core/features/automation.dart'; +import '../core/platform_utils.dart'; + +class AutomationPage extends StatefulWidget { + const AutomationPage({ + super.key, + required this.automation, + required this.create, + required this.title, + required this.onSave, + }); + + final T? automation; + final T Function(T? automation) create; + final void Function(T automation) onSave; + final String title; + + @override + State createState() => _AutomationPageState(); +} + +class _AutomationPageState + extends State> { + late final T automation; + + @override + void initState() { + automation = widget.create(widget.automation); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + actions: [ + Switch.adaptive( + value: automation.enabled, + onChanged: (value) async { + automation.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) { + var automationMatcheeStr = + T == Alias ? 'your input' : 'the output'; + var automationTypeStr = T == Alias ? 'alias' : 'trigger'; + final patternTextField = TextField( + controller: TextEditingController(text: automation.pattern), + decoration: InputDecoration( + labelText: 'Pattern', + helperText: + 'The pattern to match $automationMatcheeStr against', + ), + onChanged: (value) { + automation.pattern = value; + }, + ); + final regexCheckbox = CheckboxListTile( + title: const Text('Regular Expression'), + // subtitle: const Text( + // 'Whether the pattern is a regular expression', + // ), + value: automation.isRegex, + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) { + setState(() { + automation.isRegex = value ?? false; + }); + }, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (PlatformUtils.isDesktop) + Row( + children: [ + Expanded( + child: patternTextField, + ), + SizedBox( + width: 270, + child: regexCheckbox, + ), + ], + ), + if (!PlatformUtils.isDesktop) ...[ + patternTextField, + regexCheckbox, + ], + TextField( + controller: + TextEditingController(text: automation.label), + decoration: InputDecoration( + labelText: 'Label', + helperText: + 'Can be used to identify the $automationTypeStr in scripts', + ), + onChanged: (value) { + automation.label = value; + }, + ), + TextField( + controller: TextEditingController( + text: automation.action.content, + ), + minLines: PlatformUtils.isDesktop ? 20 : 5, + maxLines: PlatformUtils.isDesktop ? 50 : 20, + decoration: const InputDecoration( + labelText: 'Action', + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + onChanged: (value) { + automation.action.content = value; + }, + ), + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Text( + 'Send To', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + DropdownMenu( + initialSelection: automation.action.target, + onSelected: (value) { + if (value is MUDActionTarget) { + automation.action.target = value; + } + }, + dropdownMenuEntries: const [ + DropdownMenuEntry( + value: MUDActionTarget.world, + label: 'World', + ), + DropdownMenuEntry( + value: MUDActionTarget.execute, + label: 'Execute', + ), + DropdownMenuEntry( + value: MUDActionTarget.script, + label: 'Script', + ), + DropdownMenuEntry( + value: MUDActionTarget.input, + label: 'Command Input', + ), + DropdownMenuEntry( + value: MUDActionTarget.immediate, + label: 'Immediate', + ), + DropdownMenuEntry( + value: MUDActionTarget.output, + label: 'Output', + ), + ], + ), + CheckboxListTile( + title: const Text('Case Sensitive'), + subtitle: const Text( + 'If checked, the pattern will only match if the case matches.', + ), + value: automation.isCaseSensitive, + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) { + setState(() { + automation.isCaseSensitive = value ?? false; + }); + }, + ), + CheckboxListTile( + title: const Text('Remove From Output'), + subtitle: Text( + 'If checked, $automationMatcheeStr line will be removed from the buffer when it is matched.', + ), + value: automation.isRemovedFromBuffer, + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) { + setState(() { + automation.isRemovedFromBuffer = value ?? false; + }); + }, + ), + CheckboxListTile( + title: const Text('Auto Disable'), + subtitle: Text( + 'If checked, the $automationTypeStr will be disabled after it is matched once.', + ), + value: automation.autoDisable, + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) { + setState(() { + automation.autoDisable = value ?? false; + }); + }, + ), + ], + ); + }, + ), + ), + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.pop(context, automation); + }, + child: const Icon(Icons.save), + ), + ); + } +} + diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 3805b57..5cc6f1b 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -34,16 +34,16 @@ class HomePageState extends State super.dispose(); } + final consoleStyle = const TextStyle( + color: Colors.white, + fontFamily: 'Fira Code', + fontSize: 16, + height: 1, + ); + TextStyle get inputStyle => consoleStyle.copyWith(color: Colors.grey); + @override Widget build(BuildContext context) { - const consoleStyle = TextStyle( - color: Colors.white, - fontFamily: 'Fira Code', - fontSize: 16, - height: 1, - ); - final inputStyle = consoleStyle.copyWith(color: Colors.grey); - return Shortcuts( shortcuts: numpadKeysIntentMap, child: Actions( @@ -78,42 +78,7 @@ class HomePageState extends State child: Padding( padding: const EdgeInsets.all(8.0), child: Focus( - child: SelectableText.rich( - TextSpan( - children: [ - for (final line in lines) ...[ - for (final segment - in ColorUtils.split(line)) - TextSpan( - text: segment.text, - style: consoleStyle.copyWith( - color: Color( - segment.themedFgColor), - backgroundColor: Color( - segment.themedBgColor), - fontWeight: segment.bold - ? FontWeight.w900 - : FontWeight.w400, - fontStyle: segment.italic - ? FontStyle.italic - : null, - decoration: segment.underline - ? TextDecoration.underline - : null, - ), - ), - // const TextSpan( - // text: newline, - // style: - // consoleStyle, // .copyWith(fontSize: 1), - // ), - ], - ], - ), - enableInteractiveSelection: true, - selectionWidthStyle: BoxWidthStyle.tight, - selectionHeightStyle: BoxHeightStyle.max, - ), + child: _buildGameOutput(lines), ), ), ), @@ -156,6 +121,37 @@ class HomePageState extends State ); } + SelectableText _buildGameOutput(List lines) { + return SelectableText.rich( + TextSpan( + children: [ + for (final line in lines) ...[ + for (final segment in ColorUtils.split(line)) + TextSpan( + text: segment.text, + style: consoleStyle.copyWith( + color: Color(segment.themedFgColor), + backgroundColor: Color(segment.themedBgColor), + fontWeight: segment.bold ? FontWeight.w900 : FontWeight.w400, + fontStyle: segment.italic ? FontStyle.italic : null, + decoration: + segment.underline ? TextDecoration.underline : null, + ), + ), + // const TextSpan( + // text: newline, + // style: + // consoleStyle, // .copyWith(fontSize: 1), + // ), + ], + ], + ), + enableInteractiveSelection: true, + selectionWidthStyle: BoxWidthStyle.tight, + selectionHeightStyle: BoxHeightStyle.max, + ); + } + @override onWindowBlur() { debugPrint("Window blurred"); diff --git a/lib/pages/home_scaffold.dart b/lib/pages/home_scaffold.dart index 5b53dee..afa978a 100644 --- a/lib/pages/home_scaffold.dart +++ b/lib/pages/home_scaffold.dart @@ -106,6 +106,11 @@ class HomeScaffold extends StatelessWidget with GameStoreMixin { onTap: () => Navigator.pushNamed(context, Paths.shortcuts), ), const Divider(), + ListTile( + title: const Text('Profiles'), + leading: const Icon(Icons.cable), + onTap: () => Navigator.pushNamed(context, Paths.profiles), + ), ListTile( title: const Text('Settings'), leading: const Icon(Icons.settings), diff --git a/lib/pages/trigger_page.dart b/lib/pages/trigger_page.dart index 532d7b8..02d73ad 100644 --- a/lib/pages/trigger_page.dart +++ b/lib/pages/trigger_page.dart @@ -1,201 +1,26 @@ import 'package:flutter/material.dart'; -import '../core/features/action.dart'; import '../core/features/trigger.dart'; -import '../core/platform_utils.dart'; +import 'automation_page.dart'; -class TriggerPage extends StatefulWidget { - const TriggerPage({super.key, required this.trigger}); +class TriggerPage extends StatelessWidget { + const TriggerPage({ + super.key, + this.trigger, + required this.onSave, + }); final Trigger? trigger; - - @override - State createState() => _TriggerPageState(); -} - -class _TriggerPageState extends State { - late final Trigger trigger; - - @override - void initState() { - trigger = widget.trigger?.copyWith() ?? Trigger.empty(); - super.initState(); - } + final void Function(Trigger) onSave; @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Trigger'), - actions: [ - Switch.adaptive( - value: trigger.enabled, - onChanged: (value) { - trigger.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) { - final patternTextField = TextField( - controller: TextEditingController(text: trigger.pattern), - decoration: const InputDecoration( - labelText: 'Pattern', - helperText: 'The pattern to match your input against', - ), - onChanged: (value) { - trigger.pattern = value; - }, - ); - final regexCheckbox = CheckboxListTile( - title: const Text('Regular Expression'), - // subtitle: const Text( - // 'Whether the pattern is a regular expression', - // ), - value: trigger.isRegex, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - trigger.isRegex = value ?? false; - }); - }, - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (PlatformUtils.isDesktop) - Row( - children: [ - Expanded( - child: patternTextField, - ), - SizedBox( - width: 270, - child: regexCheckbox, - ), - ], - ), - if (!PlatformUtils.isDesktop) ...[ - patternTextField, - regexCheckbox, - ], - TextField( - controller: TextEditingController(text: trigger.label), - decoration: const InputDecoration( - labelText: 'Label', - helperText: 'Can be used to identify the trigger in scripts', - ), - onChanged: (value) { - trigger.label = value; - }, - ), - TextField( - controller: - TextEditingController(text: trigger.action.content), - minLines: PlatformUtils.isDesktop ? 20 : 5, - maxLines: PlatformUtils.isDesktop ? 50 : 20, - decoration: const InputDecoration( - labelText: 'Action', - floatingLabelBehavior: FloatingLabelBehavior.always, - ), - onChanged: (value) { - trigger.action.content = value; - }, - ), - Padding( - padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), - child: Text( - 'Send To', - style: Theme.of(context).textTheme.bodySmall, - ), - ), - DropdownMenu( - initialSelection: trigger.action.target, - onSelected: (value) { - if (value is MUDActionTarget) { - trigger.action.target = value; - } - }, - dropdownMenuEntries: const [ - DropdownMenuEntry( - value: MUDActionTarget.world, - label: 'World', - ), - DropdownMenuEntry( - value: MUDActionTarget.execute, - label: 'Execute', - ), - DropdownMenuEntry( - value: MUDActionTarget.script, - label: 'Script', - ), - DropdownMenuEntry( - value: MUDActionTarget.input, - label: 'Command Input', - ), - ], - ), - CheckboxListTile( - title: const Text('Case Sensitive'), - subtitle: const Text( - 'If checked, the pattern will only match if the case matches.', - ), - value: trigger.isCaseSensitive, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - trigger.isCaseSensitive = value ?? false; - }); - }, - ), - CheckboxListTile( - title: const Text('Remove From Output'), - subtitle: const Text( - 'If checked, the output line will be removed from the buffer when it is matched.', - ), - value: trigger.isRemovedFromBuffer, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - trigger.isRemovedFromBuffer = value ?? false; - }); - }, - ), - CheckboxListTile( - title: const Text('Auto Disable'), - subtitle: const Text( - 'If checked, the trigger will be disabled after it is matched once.', - ), - value: trigger.autoDisable, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) { - setState(() { - trigger.autoDisable = value ?? false; - }); - }, - ), - ], - ); - }, - ), - ), - ), - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.pop(context, trigger); - }, - child: const Icon(Icons.save), - ), + return AutomationPage( + automation: trigger, + title: 'Trigger', + onSave: onSave, + create: (trigger) => trigger?.copyWith() ?? Trigger.empty(), ); } } +