diff --git a/lib/core/features/profile.dart b/lib/core/features/profile.dart index fb3936e..9f04dde 100644 --- a/lib/core/features/profile.dart +++ b/lib/core/features/profile.dart @@ -8,6 +8,7 @@ import '../storage.dart'; import '../string_utils.dart'; import 'alias.dart'; import 'game_button_set.dart'; +import 'settings.dart'; import 'trigger.dart'; import 'variable.dart'; @@ -160,6 +161,15 @@ class MUDProfile { return KeyboardShortcuts.fromJson(shortcuts); } + Future loadSettings() async { + debugPrint('MUDProfile.loadSettings: $id'); + final settings = await ProfileStorage.readProfileFile(id, 'settings'); + if (settings == null) { + return Settings.empty(); + } + return Settings.fromJson(settings); + } + Future saveAlias(Alias alias) async { debugPrint('MUDProfile.saveAlias: $id/aliases/${alias.id}'); return ProfileStorage.writeProfileFile( @@ -194,6 +204,11 @@ class MUDProfile { id, 'keyboard_shortcuts', shortcuts.toJson()); } + Future saveSettings(Settings settings) async { + debugPrint('MUDProfile.saveSettings: $id'); + return ProfileStorage.writeProfileFile(id, 'settings', settings.toJson()); + } + Future deleteButtonSet(GameButtonSetData buttonSet) async { debugPrint('MUDProfile.deleteButtonSet: $id/button_sets/${buttonSet.id}'); return ProfileStorage.deleteProfileFile(id, 'button_sets/${buttonSet.id}'); diff --git a/lib/core/features/settings.dart b/lib/core/features/settings.dart new file mode 100644 index 0000000..994d0cc --- /dev/null +++ b/lib/core/features/settings.dart @@ -0,0 +1,41 @@ +class Settings { + String commandSeparator; + bool echoCommands; + bool showTimestamps; + + Settings({ + required this.commandSeparator, + required this.echoCommands, + required this.showTimestamps, + }); + + factory Settings.empty() => Settings( + commandSeparator: ';', + echoCommands: true, + showTimestamps: false, + ); + + factory Settings.fromJson(Map json) => Settings( + commandSeparator: json['commandSeparator'] as String, + echoCommands: json['echoCommands'] as bool? ?? true, + showTimestamps: json['showTimestamps'] as bool? ?? false, + ); + + Map toJson() => { + 'commandSeparator': commandSeparator, + 'echoCommands': echoCommands, + 'showTimestamps': showTimestamps, + }; + + Settings copyWith({ + String? commandSeparator, + bool? echoCommands, + }) { + return Settings( + commandSeparator: commandSeparator ?? this.commandSeparator, + echoCommands: echoCommands ?? this.echoCommands, + showTimestamps: showTimestamps, + ); + } +} + diff --git a/lib/core/routes.dart b/lib/core/routes.dart index 083df6c..f486fa6 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:mudblock/pages/keyboard_shortcuts_page.dart'; import '../core/features/alias.dart'; import '../core/features/trigger.dart'; @@ -10,8 +9,10 @@ import '../pages/button_set_page.dart'; import '../pages/button_sets_list_page.dart'; import '../pages/home_page.dart'; import '../pages/home_scaffold.dart'; +import '../pages/keyboard_shortcuts_page.dart'; import '../pages/profile_page.dart'; import '../pages/select_profile_page.dart'; +import '../pages/settings_page.dart'; import '../pages/trigger_list_page.dart'; import '../pages/trigger_page.dart'; import '../pages/variable_list_page.dart'; @@ -98,6 +99,9 @@ final routes = { ); }); }, + Paths.settings: (context) { + return const SettingsPage(); + }, Paths.home: (context) => HomeScaffold( builder: (context, _) { return const HomePage(); diff --git a/lib/core/settings.dart b/lib/core/settings.dart deleted file mode 100644 index 5a43323..0000000 --- a/lib/core/settings.dart +++ /dev/null @@ -1,4 +0,0 @@ -class Settings { - String commandSeparator = ';'; - bool echoCommands = true; -} diff --git a/lib/core/store.dart b/lib/core/store.dart index 9a8535e..653a339 100644 --- a/lib/core/store.dart +++ b/lib/core/store.dart @@ -15,6 +15,7 @@ import 'features/action.dart'; import 'features/alias.dart'; import 'features/game_button_set.dart'; import 'features/profile.dart'; +import 'features/settings.dart'; import 'features/trigger.dart'; import 'features/variable.dart'; import 'keyboard_shortcuts.dart'; @@ -29,9 +30,8 @@ class GameStore extends ChangeNotifier { final FocusNode inputFocus = FocusNode(); bool isCompressed = false; final ZLibDecoder decoder = ZLibDecoder(); - final msgSplitPattern = RegExp("($cr$lf)|($lf$cr)|$cr|$lf"); + final incomingMsgSplitPattern = RegExp("($cr$lf)|($lf$cr)|$cr|$lf"); // accepts csp but NOT double csp - final outgoingMsgSplitPattern = RegExp("(?> _rawStreamController = StreamController(); late Stream> _decodedStream; @@ -40,12 +40,13 @@ class GameStore extends ChangeNotifier { MUDProfile? _currentProfile; bool _clientReady = false; - // TODO move to settings - /// command separator - static const csp = ";"; + String get commandSeparator => settings.commandSeparator; + RegExp get outgoingMsgSplitPattern => + RegExp("(? triggers = []; final List aliases = []; final Map variables = {}; @@ -87,6 +88,8 @@ class GameStore extends ChangeNotifier { loadVariables(), loadButtonSets(), loadKeyboardShortcuts(), + + loadSettings(), ]); _client.connect(); } @@ -131,6 +134,13 @@ class GameStore extends ChangeNotifier { debugPrint('KeyboardShortcuts loaded'); } + Future loadSettings() async { + final settings = await currentProfile.loadSettings(); + this.settings = settings; + notifyListeners(); + debugPrint('Settings loaded'); + } + bool processTriggers(String line) { bool showLine = true; final str = ColorUtils.stripColor(line); @@ -203,7 +213,7 @@ class GameStore extends ChangeNotifier { try { final data = Message(bytes); handleMCCPHandshake(data); - for (final line in data.text.split(msgSplitPattern)) { + for (final line in data.text.split(incomingMsgSplitPattern)) { onLine(line); } } catch (e, stack) { @@ -223,7 +233,7 @@ class GameStore extends ChangeNotifier { handleMCCPHandshake(data); } - for (final line in data.text.split(msgSplitPattern)) { + for (final line in data.text.split(incomingMsgSplitPattern)) { onLine(line); } } catch (e, stack) { @@ -331,7 +341,8 @@ class GameStore extends ChangeNotifier { List _splitCsp(String line) { return line .split(outgoingMsgSplitPattern) - .map((l) => l.replaceAll('$csp$csp', csp)) + .map((l) => l.replaceAll( + '$commandSeparator$commandSeparator', commandSeparator)) .toList(); } diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..10511b3 --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +import '../core/features/settings.dart'; +import '../core/store.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State with GameStoreStateMixin { + late Settings settings; + + @override + void initState() { + super.initState(); + debugPrint('SettingsPage.initState'); + settings = store.settings.copyWith(); + } + + @override + Widget build(BuildContext context) { + return GameStore.consumer( + builder: (context, store, child) { + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + ), + body: Center( + child: SizedBox( + width: 600, + child: ListView( + children: [ + TextFormField( + initialValue: settings.commandSeparator, + onChanged: (value) => settings.commandSeparator = value, + maxLength: 1, + decoration: const InputDecoration( + labelText: 'Command Separator', + helperText: + 'The character that separates commands. To send it literally, use it twice.', + ), + ), + const SizedBox(height: 16), + CheckboxListTile.adaptive( + value: settings.echoCommands, + onChanged: (value) => setState(() => settings.echoCommands = value!), + title: const Text('Echo Commands'), + subtitle: const Text( + 'Whether to echo commands to the screen as they are sent.'), + ), + CheckboxListTile.adaptive( + value: settings.showTimestamps, + onChanged: (value) => setState(() => settings.showTimestamps = value!), + title: const Text('Show Timestamps'), + subtitle: const Text( + 'Whether to show timestamps on messages received from the server.'), + ), + ], + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + store.currentProfile.saveSettings(settings); + store.loadSettings(); + Navigator.pop(context); + }, + child: const Icon(Icons.save), + ), + ); + }, + ); + } +} +