From 23c96b5a3cc6bfea4e4ffa15609d9d1c51b11fde Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Sat, 14 Oct 2023 01:02:18 +0300 Subject: [PATCH] refactor: profile & storage refactoring --- lib/core/features/keyboard_shortcuts.dart | 2 +- lib/core/features/plugin.dart | 18 +--- lib/core/features/profile.dart | 63 ++++++++++++- lib/core/routes.dart | 78 ++++++++++++---- lib/core/storage.dart | 58 +++++++++--- lib/core/store.dart | 92 +++++++++---------- lib/pages/alias_list_page.dart | 28 +++--- lib/pages/button_sets_list_page.dart | 30 ++++--- lib/pages/generic_list_page.dart | 8 +- lib/pages/home_page.dart | 2 +- lib/pages/home_scaffold.dart | 2 +- lib/pages/settings_page.dart | 105 +++++++++++----------- lib/pages/trigger_list_page.dart | 30 +++---- 13 files changed, 326 insertions(+), 190 deletions(-) diff --git a/lib/core/features/keyboard_shortcuts.dart b/lib/core/features/keyboard_shortcuts.dart index 0559364..a778530 100644 --- a/lib/core/features/keyboard_shortcuts.dart +++ b/lib/core/features/keyboard_shortcuts.dart @@ -21,7 +21,7 @@ class KeyboardAction extends ContextAction with GameStoreMixin { bool isEnabled(KeyboardIntent intent, [BuildContext? context]) { if (context == null) return false; final store = storeOf(context); - if (store.keyboardShortcuts.get(intent.key).isEmpty) return false; + if (store.currentProfile.keyboardShortcuts.get(intent.key).isEmpty) return false; return super.isEnabled(intent, context); } } diff --git a/lib/core/features/plugin.dart b/lib/core/features/plugin.dart index bece2c8..9c4d048 100644 --- a/lib/core/features/plugin.dart +++ b/lib/core/features/plugin.dart @@ -3,7 +3,6 @@ import 'package:flutter/foundation.dart'; import '../storage.dart'; import 'alias.dart'; import 'game_button_set.dart'; -import 'settings.dart'; import 'trigger.dart'; import 'variable.dart'; @@ -23,11 +22,14 @@ class PluginBase extends ChangeNotifier { getTriggers(), getVariables(), getButtonSets(), + ...additionalLoaders(), ]); notifyListeners(); } + List> additionalLoaders() => []; + Future> loadTriggers() async { debugPrint('MUDProfile.loadTriggers: $id'); final triggers = await ProfileStorage.listProfileFiles(id, 'triggers'); @@ -84,16 +86,6 @@ class PluginBase extends ChangeNotifier { return buttonSetFiles.map((e) => GameButtonSetData.fromJson(e)).toList(); } - - 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( @@ -122,10 +114,6 @@ class PluginBase extends ChangeNotifier { id, 'button_sets/${buttonSet.id}', buttonSet.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}'); diff --git a/lib/core/features/profile.dart b/lib/core/features/profile.dart index 5959ddb..1eb6e9d 100644 --- a/lib/core/features/profile.dart +++ b/lib/core/features/profile.dart @@ -5,7 +5,9 @@ import '../consts.dart'; import '../secrets.dart'; import '../storage.dart'; import '../string_utils.dart'; +import 'keyboard_shortcuts.dart'; import 'plugin.dart'; +import 'settings.dart'; class MUDProfile extends PluginBase { String name; @@ -16,6 +18,9 @@ class MUDProfile extends PluginBase { String password; AuthMethod authMethod; + Settings settings = Settings.empty(); + KeyboardShortcuts keyboardShortcuts = KeyboardShortcuts.empty(); + MUDProfile({ required String id, required this.name, @@ -83,13 +88,68 @@ class MUDProfile extends PluginBase { 'authMethod': authMethod.name, }; + @override + List> additionalLoaders() => [ + getKeyboardShortcuts(), + getSettings(), + ]; + + Future loadKeyboardShortcuts() async { + debugPrint('MUDProfile.loadKeyboardShortcuts: $id'); + final shortcuts = + await ProfileStorage.readProfileFile(id, 'keyboard_shortcuts'); + debugPrint('MUDProfile.loadKeyboardShortcuts: $shortcuts'); + if (shortcuts == null) { + return KeyboardShortcuts.empty(); + } + return KeyboardShortcuts.fromJson(shortcuts); + } + + Future saveKeyboardShortcuts(KeyboardShortcuts shortcuts) async { + debugPrint('MUDProfile.saveKeyboardShortcuts: $id'); + keyboardShortcuts = shortcuts; + notifyListeners(); + return ProfileStorage.writeProfileFile( + id, 'keyboard_shortcuts', shortcuts.toJson()); + } + + Future getKeyboardShortcuts() async { + final shortcuts = await loadKeyboardShortcuts(); + keyboardShortcuts = shortcuts; + notifyListeners(); + debugPrint('KeyboardShortcuts loaded'); + } + + Future loadSettings() async { + debugPrint('MUDProfile.loadSettings'); + final settings = await ProfileStorage.readProfileFile(id, 'settings'); + debugPrint('MUDProfile.loadSettings: $settings'); + if (settings == null) { + return Settings.empty(); + } + return Settings.fromJson(settings); + } + + Future getSettings() async { + final settings = await loadSettings(); + this.settings = settings; + notifyListeners(); + debugPrint('Settings loaded: ${settings.showTimestamps}'); + } + + Future saveSettings(Settings settings) async { + debugPrint('MUDProfile.saveSettings'); + this.settings = settings; + notifyListeners(); + return ProfileStorage.writeProfileFile(id, 'settings', settings.toJson()); + } + static Future save(MUDProfile profile) async { debugPrint('MUDProfile.save: ${profile.id}'); return ProfileStorage.writeProfileFile( profile.id, profile.id, (profile.toJson())); } - static final encKey = enc.Key.fromUtf8(pwdKey); static final encrypter = enc.Encrypter(enc.AES(encKey, padding: null)); @@ -127,3 +187,4 @@ enum AuthMethod { none, diku, } + diff --git a/lib/core/routes.dart b/lib/core/routes.dart index 11ebdcf..250be59 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -46,41 +46,68 @@ class Paths { } final routes = { + // profiles Paths.profiles: (context) => const SelectProfilePage(), Paths.profile: (context) { final profile = ModalRoute.of(context)!.settings.arguments as MUDProfile?; return ProfilePage(profile: profile); }, + + // aliases Paths.aliases: (context) => GameStore.consumer( - builder: (context, store, child) { - return const AliasListPage(); - }, + builder: (context, store, child) => AliasListPage( + aliases: store.currentProfile.aliases, + onSave: (alias) async { + store.currentProfile.saveAlias(alias); + }, + onDelete: (alias) async { + store.currentProfile.deleteAlias(alias); + }, + ), ), Paths.alias: (context) { final alias = ModalRoute.of(context)!.settings.arguments as Alias?; return AliasPage(alias: alias); }, + + // triggers Paths.triggers: (context) => GameStore.consumer( - builder: (context, store, child) { - return const TriggerListPage(); - }, + builder: (context, store, child) => TriggerListPage( + triggers: store.currentProfile.triggers, + onSave: (trigger) async { + store.currentProfile.saveTrigger(trigger); + }, + onDelete: (trigger) async { + store.currentProfile.deleteTrigger(trigger); + }, + ), ), Paths.trigger: (context) { final trigger = ModalRoute.of(context)!.settings.arguments as Trigger?; return TriggerPage(trigger: trigger); }, + + // variables Paths.variables: (context) => GameStore.consumer( - builder: (context, store, child) { - return const VariableListPage(); - }, + builder: (context, store, child) => const VariableListPage(), ), Paths.variable: (context) { final variable = ModalRoute.of(context)!.settings.arguments as Variable?; return VariablePage(variable: variable); }, + + // buttons Paths.buttons: (context) => GameStore.consumer( builder: (context, store, child) { - return const ButtonSetListPage(); + return ButtonSetListPage( + buttonSets: store.currentProfile.buttonSets, + onSave: (buttonSet) async { + store.currentProfile.saveButtonSet(buttonSet); + }, + onDelete: (buttonSet) async { + store.currentProfile.deleteButtonSet(buttonSet); + }, + ); }, ), Paths.buttonSet: (context) { @@ -88,20 +115,35 @@ final routes = { ModalRoute.of(context)!.settings.arguments as GameButtonSetData?; return GameButtonSetPage(buttonSet: buttonSet); }, + + // shortcuts Paths.shortcuts: (context) { - return GameStore.consumer(builder: (context, store, child) { - return KeyboardShortcutsPage( - shortcuts: store.keyboardShortcuts, + return GameStore.consumer( + builder: (context, store, child) => KeyboardShortcutsPage( + shortcuts: store.currentProfile.keyboardShortcuts, onSave: (shortcuts) { - store.keyboardShortcuts = shortcuts; - store.saveKeyboardShortcuts(shortcuts); + store.currentProfile.saveKeyboardShortcuts(shortcuts); }, - ); - }); + ), + ); }, + + // settings Paths.settings: (context) { - return const SettingsPage(); + return GameStore.consumer( + builder: (context, store, child) => SettingsPage( + settings: store.currentProfile.settings, + onSave: (settings) async { + Navigator.pop(context); + final old = store.currentProfile.settings.copyWith(); + await store.currentProfile.saveSettings(settings); + store.echoSettingsChanged(old, settings); + }, + ), + ); }, + + // home Paths.home: (context) => HomeScaffold( builder: (context, _) { return const HomePage(); diff --git a/lib/core/storage.dart b/lib/core/storage.dart index 41c41a0..b98c917 100644 --- a/lib/core/storage.dart +++ b/lib/core/storage.dart @@ -39,7 +39,7 @@ class FileStorage { await file.delete(); } - static Future> readDirectory( + static Future> listDirectoryFiles( String collection, ) async { final dir = Directory(path.join(base, collection)); @@ -49,7 +49,9 @@ class FileStorage { return []; } // TODO use absolute paths? - return dir.list().map((e) => path.basename(e.path)).toList(); + final list = await dir.list().map((e) => path.basename(e.path)).toList(); + debugPrint('Listing directory: $collection, $list'); + return list; } static Future deleteDirectory(String collection) async { @@ -59,32 +61,63 @@ class FileStorage { } } +class JsonStorage { + static const encoder = JsonEncoder.withIndent(' '); + static const decoder = JsonDecoder(); + + static Future?> readFile(String filename) async { + final data = await FileStorage.readFile('$filename.json'); + return data != null ? decoder.convert(data) : null; + } + + static Future writeFile( + String filename, Map data) async { + final output = encoder.convert(data); + await FileStorage.writeFile('$filename.json', output); + } + + static Future deleteFile(String filename) async { + await FileStorage.deleteFile('$filename.json'); + } + + static Future?>> readDirectory( + String collection, + ) async { + final list = await FileStorage.listDirectoryFiles(collection); + return Future.wait( + list.map((f) => readFile('$collection/${path.withoutExtension(f)}')), + ); + } + + static Future deleteDirectory(String collection) async { + await FileStorage.deleteDirectory(collection); + } +} + class ProfileStorage { static const encoder = JsonEncoder.withIndent(' '); static const decoder = JsonDecoder(); static Future?> readProfileFile( String profile, String filename) async { - final data = await FileStorage.readFile('profiles/$profile/$filename.json'); - return data != null ? decoder.convert(data) : null; + return JsonStorage.readFile('profiles/$profile/$filename'); } static Future writeProfileFile( String profile, String filename, dynamic data) async { - data = encoder.convert(data); - await FileStorage.writeFile('profiles/$profile/$filename.json', data); + await JsonStorage.writeFile('profiles/$profile/$filename', data); } static Future deleteProfile(String profile) async { - await FileStorage.deleteDirectory('profiles/$profile.json'); + await JsonStorage.deleteDirectory('profiles/$profile'); } static Future deleteProfileFile(String profile, String filename) async { - await FileStorage.deleteFile('profiles/$profile/$filename.json'); + await JsonStorage.deleteFile('profiles/$profile/$filename'); } static Future> listAllProfiles() async { - final list = await FileStorage.readDirectory('profiles'); + final list = await FileStorage.listDirectoryFiles('profiles'); return list .where((f) => Directory(path.join(FileStorage.base, 'profiles', f)).existsSync()) @@ -96,12 +129,13 @@ class ProfileStorage { String profile, [ String? directory, ]) async { - final list = await FileStorage.readDirectory( - 'profiles/$profile${directory != null ? '/$directory' : ''}'); + final dir = directory != null ? '$profile/$directory' : profile; + final list = await FileStorage.listDirectoryFiles('profiles/$dir'); return list.map((f) => path.withoutExtension(f)).toList(); } static Future deleteAllProfiles() async { - await FileStorage.deleteDirectory('profiles'); + await JsonStorage.deleteDirectory('profiles'); } } + diff --git a/lib/core/store.dart b/lib/core/store.dart index 8b85d8e..76f3f5e 100644 --- a/lib/core/store.dart +++ b/lib/core/store.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:ctelnet/ctelnet.dart'; import 'package:flutter/material.dart'; +import 'package:mudblock/core/features/settings.dart'; import 'package:mudblock/core/profile_presets.dart'; import 'package:mudblock/core/storage.dart'; import 'package:mudblock/pages/select_profile_page.dart'; @@ -15,7 +16,6 @@ import 'features/action.dart'; import 'features/alias.dart'; import 'features/keyboard_shortcuts.dart'; import 'features/profile.dart'; -import 'features/settings.dart'; const maxLines = 2000; @@ -37,14 +37,10 @@ class GameStore extends ChangeNotifier { MUDProfile? _currentProfile; bool _clientReady = false; - String get commandSeparator => settings.commandSeparator; + String get commandSeparator => currentProfile.settings.commandSeparator; RegExp get outgoingMsgSplitPattern => RegExp("(? _currentProfile!; get connected => _clientReady && _client.connected; @@ -55,7 +51,7 @@ class GameStore extends ChangeNotifier { return this; } - void connect(BuildContext context) async { + void showConnectionDialog(BuildContext context) async { final profile = await showDialog( context: context, builder: (context) { @@ -64,7 +60,9 @@ class GameStore extends ChangeNotifier { if (profile == null) { return; } + _currentProfile?.removeListener(notifyListeners); _currentProfile = profile; + currentProfile.addListener(notifyListeners); echo('Connecting...'); _client = CTelnetClient( host: currentProfile.host, @@ -74,46 +72,11 @@ class GameStore extends ChangeNotifier { onData: onData, onError: onError, ); - await Future.wait(>[ - currentProfile.load(), - loadKeyboardShortcuts(), - loadSettings(), - ]); + await currentProfile.load(); _client.connect(); notifyListeners(); } - Future getKeyboardShortcuts() async { - debugPrint('MUDProfile.loadKeyboardShortcuts: ${currentProfile.id}'); - // TODO use global storage (not profile specific) - final shortcuts = await ProfileStorage.readProfileFile( - currentProfile.id, 'keyboard_shortcuts'); - if (shortcuts == null) { - return KeyboardShortcuts.empty(); - } - return KeyboardShortcuts.fromJson(shortcuts); - } - - Future saveKeyboardShortcuts(KeyboardShortcuts shortcuts) async { - debugPrint('MUDProfile.saveKeyboardShortcuts: ${currentProfile.id}'); - return ProfileStorage.writeProfileFile( - currentProfile.id, 'keyboard_shortcuts', shortcuts.toJson()); - } - - Future loadSettings() async { - final settings = await currentProfile.loadSettings(); - this.settings = settings; - notifyListeners(); - debugPrint('Settings loaded'); - } - - Future loadKeyboardShortcuts() async { - final shortcuts = await getKeyboardShortcuts(); - keyboardShortcuts = shortcuts; - notifyListeners(); - debugPrint('KeyboardShortcuts loaded'); - } - bool processTriggers(String line) { bool showLine = true; final str = ColorUtils.stripColor(line); @@ -261,6 +224,9 @@ class GameStore extends ChangeNotifier { /// echo - echo to screen, DOES NOT split by msgSplitPattern, is not send to server void echo(String line) { + if (currentProfile.settings.showTimestamps) { + line = '[${DateTime.now().toIso8601String()}] $line'; + } _lines.add(line); notifyListeners(); scrollToEnd(); @@ -273,6 +239,13 @@ class GameStore extends ChangeNotifier { scrollToEnd(); } + /// echoSystem - same as echo, but with predefined color + void echoSystem(String line) { + _lines.add('$esc[92m$line'); + notifyListeners(); + scrollToEnd(); + } + /// sendBytes - raw send bytes - DOES NOT split by outgoingMsgSplitPattern, no processing void sendBytes(List bytes) { var output = bytes; @@ -324,7 +297,9 @@ class GameStore extends ChangeNotifier { if (!_clientReady || !_client.connected) { return; } - echoOwn(text); + if (currentProfile.settings.echoCommands) { + echoOwn(text); + } execute(text); scrollToEnd(); selectInput(); @@ -420,12 +395,39 @@ class GameStore extends ChangeNotifier { } void onShortcut(NumpadKey key, BuildContext context) { - final action = keyboardShortcuts.get(key); + final action = currentProfile.keyboardShortcuts.get(key); if (action.isNotEmpty) { submitInput(action); selectInput(); } } + + void echoSettingsChanged(Settings old, Settings updated) { + echoSystem('Settings updated:'); + var updateCount = 0; + if (updated.showTimestamps != old.showTimestamps) { + updateCount++; + echoSystem( + 'Timestamps are now ${updated.showTimestamps ? 'enabled' : 'disabled'}'); + } + if (updated.echoCommands != old.echoCommands) { + updateCount++; + echoSystem( + 'Echoing own commands is now ${updated.echoCommands ? 'enabled' : 'disabled'}'); + } + if (updated.commandSeparator != old.commandSeparator) { + updateCount++; + echoSystem( + 'Command separator is now "${updated.commandSeparator}". ' + 'To escape when sending, use it twice like so: ' + '"${updated.commandSeparator}${updated.commandSeparator}"', + ); + } + if (updateCount == 0) { + echoSystem(''); + } + echoSystem(''); + } } mixin GameStoreMixin { diff --git a/lib/pages/alias_list_page.dart b/lib/pages/alias_list_page.dart index 3350130..c32e869 100644 --- a/lib/pages/alias_list_page.dart +++ b/lib/pages/alias_list_page.dart @@ -6,13 +6,22 @@ import '../core/routes.dart'; import 'generic_list_page.dart'; class AliasListPage extends StatelessWidget with GameStoreMixin { - const AliasListPage({super.key}); + const AliasListPage({ + super.key, + required this.aliases, + required this.onSave, + required this.onDelete, + }); + + final List aliases; + final Future Function(Alias) onSave; + final Future Function(Alias) onDelete; @override Widget build(BuildContext context) { return GenericListPage( title: const Text('Aliases'), - save: save, + save: onSave, items: storeOf(context).currentProfile.aliases, detailsPath: Paths.alias, displayName: (alias) => alias.pattern, @@ -20,7 +29,7 @@ class AliasListPage extends StatelessWidget with GameStoreMixin { alias.action.content, alias.group, ], - itemBuilder: (context, store, alias) { + itemBuilder: (context, alias) { return ListTile( key: Key(alias.id), title: Text(alias.pattern), @@ -29,7 +38,7 @@ class AliasListPage extends StatelessWidget with GameStoreMixin { value: alias.enabled, onChanged: (value) { alias.enabled = value; - save(store, alias); + onSave(alias); }, ), trailing: PopupMenuButton( @@ -44,8 +53,7 @@ class AliasListPage extends StatelessWidget with GameStoreMixin { onSelected: (value) { switch (value) { case 'delete': - store.currentProfile.deleteAlias(alias); - store.currentProfile.loadAliases(); + onDelete(alias); break; } }, @@ -57,7 +65,7 @@ class AliasListPage extends StatelessWidget with GameStoreMixin { arguments: alias, ); if (updated != null) { - await save(store, updated as Alias); + await onSave(updated as Alias); } }, ); @@ -65,9 +73,5 @@ class AliasListPage extends StatelessWidget with GameStoreMixin { ); } - Future save(GameStore store, Alias updated) async { - await store.currentProfile.saveAlias(updated); - // TODO - stop re-loading all aliases, only replace the one that changed - await store.currentProfile.loadAliases(); - } } + diff --git a/lib/pages/button_sets_list_page.dart b/lib/pages/button_sets_list_page.dart index 4d14d82..8ec5613 100644 --- a/lib/pages/button_sets_list_page.dart +++ b/lib/pages/button_sets_list_page.dart @@ -6,14 +6,23 @@ import '../core/store.dart'; import 'generic_list_page.dart'; class ButtonSetListPage extends StatelessWidget with GameStoreMixin { - const ButtonSetListPage({super.key}); + const ButtonSetListPage({ + super.key, + required this.buttonSets, + required this.onSave, + required this.onDelete, + }); + + final List buttonSets; + final Future Function(GameButtonSetData) onSave; + final Future Function(GameButtonSetData) onDelete; @override Widget build(BuildContext context) { return GenericListPage( title: const Text('Button Sets'), - save: save, - items: storeOf(context).currentProfile.buttonSets, + save: onSave, + items: buttonSets, detailsPath: Paths.buttonSet, displayName: (buttonSet) => buttonSet.name, searchTags: (buttonSet) => [ @@ -47,7 +56,7 @@ class ButtonSetListPage extends StatelessWidget with GameStoreMixin { }, ), ], - itemBuilder: (context, store, buttonSet) { + itemBuilder: (context, buttonSet) { return ListTile( key: Key(buttonSet.id), title: Text(buttonSet.name), @@ -56,7 +65,7 @@ class ButtonSetListPage extends StatelessWidget with GameStoreMixin { value: buttonSet.enabled, onChanged: (value) { buttonSet.enabled = value; - save(store, buttonSet); + onSave(buttonSet); }, ), trailing: PopupMenuButton( @@ -71,8 +80,7 @@ class ButtonSetListPage extends StatelessWidget with GameStoreMixin { onSelected: (value) { switch (value) { case 'delete': - store.currentProfile.deleteButtonSet(buttonSet); - store.currentProfile.loadButtonSets(); + onDelete(buttonSet); break; } }, @@ -84,18 +92,12 @@ class ButtonSetListPage extends StatelessWidget with GameStoreMixin { arguments: buttonSet, ); if (updated != null) { - await save(store, updated as GameButtonSetData); + await onSave(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.currentProfile.loadButtonSets(); - } } diff --git a/lib/pages/generic_list_page.dart b/lib/pages/generic_list_page.dart index b9a7943..67254d0 100755 --- a/lib/pages/generic_list_page.dart +++ b/lib/pages/generic_list_page.dart @@ -17,8 +17,8 @@ class GenericListPage extends StatefulWidget with GameStoreMixin { final List items; final Widget title; final String detailsPath; - final Future Function(GameStore store, T updated) save; - final Widget Function(BuildContext context, GameStore store, T item) + final Future Function(T updated) save; + final Widget Function(BuildContext context, T item) itemBuilder; final String Function(T item) displayName; final List Function(T item) searchTags; @@ -62,7 +62,7 @@ class _GenericListPageState extends State> shrinkWrap: true, itemBuilder: (context, i) { final item = filteredItems[i]; - return widget.itemBuilder(context, store, item); + return widget.itemBuilder(context, item); }, ), ], @@ -74,7 +74,7 @@ class _GenericListPageState extends State> onPressed: () async { final item = await Navigator.pushNamed(context, widget.detailsPath); if (item != null) { - await widget.save(store, item as T); + await widget.save(item as T); setState(() {}); } }, diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index c5f3d37..1d7df7b 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -25,7 +25,7 @@ class HomePageState extends State void initState() { super.initState(); windowManager.addListener(this); - Future.delayed(Duration.zero, () => store.connect(context)); + Future.delayed(Duration.zero, () => store.showConnectionDialog(context)); } @override diff --git a/lib/pages/home_scaffold.dart b/lib/pages/home_scaffold.dart index 69ff815..8cabeb6 100644 --- a/lib/pages/home_scaffold.dart +++ b/lib/pages/home_scaffold.dart @@ -112,7 +112,7 @@ class HomeScaffold extends StatelessWidget with GameStoreMixin { Navigator.of(context).pop(); await gameStore.disconnect(); if (context.mounted) { - gameStore.connect(context); + gameStore.showConnectionDialog(context); } }, ), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 10511b3..ebf2e4b 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -4,7 +4,14 @@ import '../core/features/settings.dart'; import '../core/store.dart'; class SettingsPage extends StatefulWidget { - const SettingsPage({super.key}); + const SettingsPage({ + super.key, + required this.settings, + required this.onSave, + }); + + final Settings settings; + final void Function(Settings) onSave; @override State createState() => _SettingsPageState(); @@ -16,62 +23,58 @@ class _SettingsPageState extends State with GameStoreStateMixin { @override void initState() { super.initState(); - debugPrint('SettingsPage.initState'); - settings = store.settings.copyWith(); + settings = widget.settings.copyWith(); + debugPrint('SettingsPage.initState, ${settings.showTimestamps}'); } @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.'), - ), - ], + 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), - ), - ); - }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => widget.onSave(settings), + child: const Icon(Icons.save), + ), ); } } diff --git a/lib/pages/trigger_list_page.dart b/lib/pages/trigger_list_page.dart index 0a0bbe4..436538a 100644 --- a/lib/pages/trigger_list_page.dart +++ b/lib/pages/trigger_list_page.dart @@ -6,13 +6,22 @@ import '../core/routes.dart'; import 'generic_list_page.dart'; class TriggerListPage extends StatelessWidget with GameStoreMixin { - const TriggerListPage({super.key}); + const TriggerListPage({ + super.key, + required this.triggers, + required this.onSave, + required this.onDelete, + }); + + final List triggers; + final Future Function(Trigger) onSave; + final Future Function(Trigger) onDelete; @override Widget build(BuildContext context) { return GenericListPage( title: const Text('Triggers'), - save: save, + save: onSave, items: storeOf(context).currentProfile.triggers, detailsPath: Paths.trigger, displayName: (trigger) => trigger.pattern, @@ -20,7 +29,7 @@ class TriggerListPage extends StatelessWidget with GameStoreMixin { trigger.action.content, trigger.group, ], - itemBuilder: (context, store, trigger) { + itemBuilder: (context, trigger) { return ListTile( key: Key(trigger.id), title: Text(trigger.pattern), @@ -29,7 +38,7 @@ class TriggerListPage extends StatelessWidget with GameStoreMixin { value: trigger.enabled, onChanged: (value) { trigger.enabled = value; - save(store, trigger); + onSave(trigger); }, ), isThreeLine: true, @@ -45,9 +54,7 @@ class TriggerListPage extends StatelessWidget with GameStoreMixin { onSelected: (value) { switch (value) { case 'delete': - // TODO extract this to props - store.currentProfile.deleteTrigger(trigger); - store.currentProfile.loadTriggers(); + onDelete(trigger); break; } }, @@ -59,19 +66,12 @@ class TriggerListPage extends StatelessWidget with GameStoreMixin { arguments: trigger, ); if (updated != null) { - await save(store, updated as Trigger); + await onSave(updated as Trigger); } }, ); }, ); } - - // TODO extract this to props - Future save(GameStore store, Trigger updated) async { - await store.currentProfile.saveTrigger(updated); - // TODO - stop re-loading all triggers, only replace the one that changed - await store.currentProfile.loadTriggers(); - } }