mirror of
https://github.com/chenasraf/mudblock.git
synced 2026-05-18 01:48:57 +00:00
refactor: profile & storage refactoring
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}');
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() {});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user