refactor: profile & storage refactoring

This commit is contained in:
2023-10-14 01:02:18 +03:00
parent 7f2f8bdfe7
commit 23c96b5a3c
13 changed files with 326 additions and 190 deletions

View File

@@ -21,7 +21,7 @@ class KeyboardAction extends ContextAction<KeyboardIntent> 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);
}
}

View File

@@ -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<Future<void>> additionalLoaders() => [];
Future<List<Trigger>> 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<Settings> loadSettings() async {
debugPrint('MUDProfile.loadSettings: $id');
final settings = await ProfileStorage.readProfileFile(id, 'settings');
if (settings == null) {
return Settings.empty();
}
return Settings.fromJson(settings);
}
Future<void> 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<void> saveSettings(Settings settings) async {
debugPrint('MUDProfile.saveSettings: $id');
return ProfileStorage.writeProfileFile(id, 'settings', settings.toJson());
}
Future<void> deleteButtonSet(GameButtonSetData buttonSet) async {
debugPrint('MUDProfile.deleteButtonSet: $id/button_sets/${buttonSet.id}');

View File

@@ -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<Future<void>> additionalLoaders() => [
getKeyboardShortcuts(),
getSettings(),
];
Future<KeyboardShortcuts> 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<void> saveKeyboardShortcuts(KeyboardShortcuts shortcuts) async {
debugPrint('MUDProfile.saveKeyboardShortcuts: $id');
keyboardShortcuts = shortcuts;
notifyListeners();
return ProfileStorage.writeProfileFile(
id, 'keyboard_shortcuts', shortcuts.toJson());
}
Future<void> getKeyboardShortcuts() async {
final shortcuts = await loadKeyboardShortcuts();
keyboardShortcuts = shortcuts;
notifyListeners();
debugPrint('KeyboardShortcuts loaded');
}
Future<Settings> 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<void> getSettings() async {
final settings = await loadSettings();
this.settings = settings;
notifyListeners();
debugPrint('Settings loaded: ${settings.showTimestamps}');
}
Future<void> saveSettings(Settings settings) async {
debugPrint('MUDProfile.saveSettings');
this.settings = settings;
notifyListeners();
return ProfileStorage.writeProfileFile(id, 'settings', settings.toJson());
}
static Future<void> 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,
}

View File

@@ -46,41 +46,68 @@ class Paths {
}
final routes = <String, Widget Function(BuildContext)>{
// 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 = <String, Widget Function(BuildContext)>{
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();

View File

@@ -39,7 +39,7 @@ class FileStorage {
await file.delete();
}
static Future<List<String>> readDirectory(
static Future<List<String>> 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<void> deleteDirectory(String collection) async {
@@ -59,32 +61,63 @@ class FileStorage {
}
}
class JsonStorage {
static const encoder = JsonEncoder.withIndent(' ');
static const decoder = JsonDecoder();
static Future<Map<String, dynamic>?> readFile(String filename) async {
final data = await FileStorage.readFile('$filename.json');
return data != null ? decoder.convert(data) : null;
}
static Future<void> writeFile(
String filename, Map<String, dynamic> data) async {
final output = encoder.convert(data);
await FileStorage.writeFile('$filename.json', output);
}
static Future<void> deleteFile(String filename) async {
await FileStorage.deleteFile('$filename.json');
}
static Future<List<Map<String, dynamic>?>> readDirectory(
String collection,
) async {
final list = await FileStorage.listDirectoryFiles(collection);
return Future.wait(
list.map((f) => readFile('$collection/${path.withoutExtension(f)}')),
);
}
static Future<void> deleteDirectory(String collection) async {
await FileStorage.deleteDirectory(collection);
}
}
class ProfileStorage {
static const encoder = JsonEncoder.withIndent(' ');
static const decoder = JsonDecoder();
static Future<Map<String, dynamic>?> 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<void> 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<void> deleteProfile(String profile) async {
await FileStorage.deleteDirectory('profiles/$profile.json');
await JsonStorage.deleteDirectory('profiles/$profile');
}
static Future<void> deleteProfileFile(String profile, String filename) async {
await FileStorage.deleteFile('profiles/$profile/$filename.json');
await JsonStorage.deleteFile('profiles/$profile/$filename');
}
static Future<List<String>> 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<void> deleteAllProfiles() async {
await FileStorage.deleteDirectory('profiles');
await JsonStorage.deleteDirectory('profiles');
}
}

View File

@@ -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("(?<!$commandSeparator)$commandSeparator(?!$commandSeparator)");
// features
KeyboardShortcuts keyboardShortcuts = KeyboardShortcuts.empty();
Settings settings = Settings.empty();
MUDProfile get currentProfile => _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<MUDProfile?>(
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(<Future<dynamic>>[
currentProfile.load(),
loadKeyboardShortcuts(),
loadSettings(),
]);
await currentProfile.load();
_client.connect();
notifyListeners();
}
Future<KeyboardShortcuts> 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<void> saveKeyboardShortcuts(KeyboardShortcuts shortcuts) async {
debugPrint('MUDProfile.saveKeyboardShortcuts: ${currentProfile.id}');
return ProfileStorage.writeProfileFile(
currentProfile.id, 'keyboard_shortcuts', shortcuts.toJson());
}
Future<void> loadSettings() async {
final settings = await currentProfile.loadSettings();
this.settings = settings;
notifyListeners();
debugPrint('Settings loaded');
}
Future<void> 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<int> 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('<no changes>');
}
echoSystem('');
}
}
mixin GameStoreMixin {

View File

@@ -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<Alias> aliases;
final Future<void> Function(Alias) onSave;
final Future<void> 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<void> 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();
}
}

View File

@@ -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<GameButtonSetData> buttonSets;
final Future<void> Function(GameButtonSetData) onSave;
final Future<void> 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<void> 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();
}
}

View File

@@ -17,8 +17,8 @@ class GenericListPage<T> extends StatefulWidget with GameStoreMixin {
final List<T> items;
final Widget title;
final String detailsPath;
final Future<void> Function(GameStore store, T updated) save;
final Widget Function(BuildContext context, GameStore store, T item)
final Future<void> Function(T updated) save;
final Widget Function(BuildContext context, T item)
itemBuilder;
final String Function(T item) displayName;
final List<String> Function(T item) searchTags;
@@ -62,7 +62,7 @@ class _GenericListPageState<T> extends State<GenericListPage<T>>
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<T> extends State<GenericListPage<T>>
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(() {});
}
},

View File

@@ -25,7 +25,7 @@ class HomePageState extends State<HomePage>
void initState() {
super.initState();
windowManager.addListener(this);
Future.delayed(Duration.zero, () => store.connect(context));
Future.delayed(Duration.zero, () => store.showConnectionDialog(context));
}
@override

View File

@@ -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);
}
},
),

View File

@@ -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<SettingsPage> createState() => _SettingsPageState();
@@ -16,62 +23,58 @@ class _SettingsPageState extends State<SettingsPage> 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),
),
);
}
}

View File

@@ -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<Trigger> triggers;
final Future<void> Function(Trigger) onSave;
final Future<void> 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<void> 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();
}
}