feat: add toggles to aliases/triggers

refactor: remove deprecated localstore
This commit is contained in:
2023-09-29 13:12:27 +03:00
parent ba7599e82e
commit 86040e62d3
15 changed files with 156 additions and 185 deletions

View File

@@ -11,7 +11,7 @@ class Alias extends Automation {
super.isRegex = false,
super.isCaseSensitive = false,
super.isRemovedFromBuffer = false,
super.isTemporary = false,
super.autoDisable = false,
super.invokeCount = 0,
});
@@ -22,7 +22,7 @@ class Alias extends Automation {
isRegex: false,
isCaseSensitive: false,
isRemovedFromBuffer: false,
isTemporary: false,
autoDisable: false,
invokeCount: 0,
action: MUDAction.empty(),
);
@@ -34,7 +34,7 @@ class Alias extends Automation {
isRegex: json['isRegex'],
isCaseSensitive: json['isCaseSensitive'],
isRemovedFromBuffer: json['isRemovedFromBuffer'],
isTemporary: json['isTemporary'],
autoDisable: json['autoDisable'],
invokeCount: json['invokeCount'],
action: MUDAction.fromJson(json['action']),
);
@@ -47,7 +47,7 @@ class Alias extends Automation {
bool? isRegex,
bool? isCaseSensitive,
bool? isRemovedFromBuffer,
bool? isTemporary,
bool? autoDisable,
int? invokeCount = 0,
MUDAction? action,
}) =>
@@ -58,7 +58,7 @@ class Alias extends Automation {
isRegex: isRegex ?? this.isRegex,
isCaseSensitive: isCaseSensitive ?? this.isCaseSensitive,
isRemovedFromBuffer: isRemovedFromBuffer ?? this.isRemovedFromBuffer,
isTemporary: isTemporary ?? this.isTemporary,
autoDisable: autoDisable ?? this.autoDisable,
invokeCount: invokeCount ?? this.invokeCount,
action: action ?? this.action,
);

View File

@@ -1,4 +1,3 @@
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import '../store.dart';
@@ -12,8 +11,10 @@ class Automation {
bool isRegex;
bool isCaseSensitive;
bool isRemovedFromBuffer;
bool isTemporary;
bool autoDisable;
int invokeCount;
/// This is used to temporarily disable an automation after using it once when it has autoDisable set to true.
bool tempDisabled = false;
MUDAction action;
Automation({
@@ -24,10 +25,12 @@ class Automation {
this.isRegex = false,
this.isCaseSensitive = false,
this.isRemovedFromBuffer = false,
this.isTemporary = false,
this.autoDisable = false,
this.invokeCount = 0,
});
bool get isAvailable => enabled && !tempDisabled;
factory Automation.empty() => Automation(
id: uuid(),
pattern: '',
@@ -35,7 +38,7 @@ class Automation {
isRegex: false,
isCaseSensitive: false,
isRemovedFromBuffer: false,
isTemporary: false,
autoDisable: false,
invokeCount: 0,
action: MUDAction.empty(),
);
@@ -47,7 +50,7 @@ class Automation {
isRegex: json['isRegex'],
isCaseSensitive: json['isCaseSensitive'],
isRemovedFromBuffer: json['isRemovedFromBuffer'],
isTemporary: json['isTemporary'],
autoDisable: json['autoDisable'],
invokeCount: json['invokeCount'],
action: MUDAction.fromJson(json['action']),
);
@@ -59,7 +62,7 @@ class Automation {
'isRegex': isRegex,
'isCaseSensitive': isCaseSensitive,
'isRemovedFromBuffer': isRemovedFromBuffer,
'isTemporary': isTemporary,
'autoDisable': autoDisable,
'invokeCount': invokeCount,
'action': action.toJson(),
};
@@ -122,7 +125,7 @@ class Automation {
bool? isRegex,
bool? isCaseSensitive,
bool? isRemovedFromBuffer,
bool? isTemporary,
bool? autoDisable,
int? invokeCount = 0,
MUDAction? action,
}) =>
@@ -133,7 +136,7 @@ class Automation {
isRegex: isRegex ?? this.isRegex,
isCaseSensitive: isCaseSensitive ?? this.isCaseSensitive,
isRemovedFromBuffer: isRemovedFromBuffer ?? this.isRemovedFromBuffer,
isTemporary: isTemporary ?? this.isTemporary,
autoDisable: autoDisable ?? this.autoDisable,
invokeCount: invokeCount ?? this.invokeCount,
action: action ?? this.action,
);

View File

@@ -101,7 +101,16 @@ class MUDProfile {
Future<List<Alias>> loadAliases() async {
debugPrint('MUDProfile.loadAliases: $id');
final aliases = await ProfileStorage.listProfileFiles(id, 'aliases');
return aliases.map((e) => Alias.fromJson(jsonDecode(e))).toList();
final aliasFiles = <String>[];
for (final alias in aliases) {
debugPrint('MUDProfile.loadAliases: $id/aliases/$alias');
final aliasFile =
await ProfileStorage.readProfileFile(id, 'aliases/$alias');
if (aliasFile != null) {
aliasFiles.add(aliasFile);
}
}
return aliasFiles.map((e) => Alias.fromJson(jsonDecode(e))).toList();
}
Future<void> saveAlias(Alias alias) async {

View File

@@ -11,7 +11,7 @@ class Trigger extends Automation {
super.isRegex = false,
super.isCaseSensitive = false,
super.isRemovedFromBuffer = false,
super.isTemporary = false,
super.autoDisable = false,
super.invokeCount = 0,
});
@@ -22,7 +22,7 @@ class Trigger extends Automation {
isRegex: false,
isCaseSensitive: false,
isRemovedFromBuffer: false,
isTemporary: false,
autoDisable: false,
invokeCount: 0,
action: MUDAction.empty(),
);
@@ -34,7 +34,7 @@ class Trigger extends Automation {
isRegex: json['isRegex'],
isCaseSensitive: json['isCaseSensitive'],
isRemovedFromBuffer: json['isRemovedFromBuffer'],
isTemporary: json['isTemporary'],
autoDisable: json['autoDisable'],
invokeCount: json['invokeCount'],
action: MUDAction.fromJson(json['action']),
);
@@ -47,7 +47,7 @@ class Trigger extends Automation {
bool? isRegex,
bool? isCaseSensitive,
bool? isRemovedFromBuffer,
bool? isTemporary,
bool? autoDisable,
int? invokeCount = 0,
MUDAction? action,
}) =>
@@ -58,7 +58,7 @@ class Trigger extends Automation {
isRegex: isRegex ?? this.isRegex,
isCaseSensitive: isCaseSensitive ?? this.isCaseSensitive,
isRemovedFromBuffer: isRemovedFromBuffer ?? this.isRemovedFromBuffer,
isTemporary: isTemporary ?? this.isTemporary,
autoDisable: autoDisable ?? this.autoDisable,
invokeCount: invokeCount ?? this.invokeCount,
action: action ?? this.action,
);

View File

@@ -10,10 +10,12 @@ class PlatformUtils {
static Future<String> getStorageBasePath() async {
switch (Platform.operatingSystem) {
case 'macos':
case 'linux':
final username = Platform.environment['USER'] ?? Platform.environment['USERNAME'];
return '/Users/$username/.config/mudblock';
case 'macos':
final dir = await getApplicationDocumentsDirectory();
return dir.path;
case 'windows':
final username = Platform.environment['USERNAME'];
return 'C:\\Users\\$username\\AppData\\Roaming\\mudblock';

View File

@@ -1,19 +1,16 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:localstore/localstore.dart';
import 'package:path/path.dart' as path;
import 'platform_utils.dart';
class FileStorage {
@deprecated
static final Localstore _store = Localstore.instance;
static late final String _base;
static late final String base;
static Future<void> init() async {
_base = await PlatformUtils.getStorageBasePath();
debugPrint('Storage base: $_base');
base = await PlatformUtils.getStorageBasePath();
debugPrint('Storage base: $base');
}
static Future<String?> readFile(String filename) async {
@@ -21,7 +18,7 @@ class FileStorage {
// final collection = path.dirname(filename);
// filename = path.basename(filename);
// return _store.collection(collection).doc(filename).get();
final file = File(path.join(_base, filename));
final file = File(path.join(base, filename));
var exists = await file.exists();
if (!exists) {
debugPrint('File does not exist: $filename');
@@ -30,14 +27,13 @@ class FileStorage {
return file.readAsString();
}
static Future<void> writeFile(
String filename, String data) async {
static Future<void> writeFile(String filename, String data) async {
debugPrint(
'Setting file: $filename, data: ${data.toString().length} bytes');
// final collection = path.dirname(filename);
// filename = path.basename(filename);
// await _store.collection(collection).doc(filename).set(data);
final file = File(path.join(_base, filename));
final file = File(path.join(base, filename));
await file.create(recursive: true);
await file.writeAsString(data);
}
@@ -47,7 +43,7 @@ class FileStorage {
// final collection = path.dirname(filename);
// filename = path.basename(filename);
// await _store.collection(collection).doc(filename).delete();
final file = File(path.join(_base, filename));
final file = File(path.join(base, filename));
await file.delete();
}
@@ -57,7 +53,7 @@ class FileStorage {
// final docs = await _store.collection(collection).get();
// debugPrint('Listing collection: $collection, ${docs?.length} docs');
// return (docs ?? {}).cast<String, String>();
final dir = Directory(path.join(_base, collection));
final dir = Directory(path.join(base, collection));
var exists = await dir.exists();
if (!exists) {
debugPrint('Directory does not exist: $collection');
@@ -70,7 +66,7 @@ class FileStorage {
static Future<void> deleteDirectory(String collection) async {
debugPrint('Clearing collection: $collection');
// await _store.collection(collection).delete();
final dir = Directory(path.join(_base, collection));
final dir = Directory(path.join(base, collection));
await dir.delete(recursive: true);
}
}
@@ -95,7 +91,11 @@ class ProfileStorage {
}
static Future<List<String>> listAllProfiles() async {
return FileStorage.readDirectory('profiles');
final list = await FileStorage.readDirectory('profiles');
return list
.where((f) =>
Directory(path.join(FileStorage.base, 'profiles', f)).existsSync())
.toList();
}
static Future<List<String>> listProfileFiles(

View File

@@ -1,81 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:localstore/localstore.dart';
import 'package:path/path.dart' as path;
class FileStorage {
static final Localstore _store = Localstore.instance;
static Future<Map<String, dynamic>?> readFile(String filename) async {
debugPrint('Getting file: $filename');
final collection = path.dirname(filename);
filename = path.basename(filename);
return _store.collection(collection).doc(filename).get();
}
static Future<void> writeFile(
String filename, Map<String, dynamic> data) async {
debugPrint(
'Setting file: $filename, data: ${data.toString().length} bytes');
final collection = path.dirname(filename);
filename = path.basename(filename);
await _store.collection(collection).doc(filename).set(data);
}
static Future<void> deleteFile(String filename) async {
debugPrint('Deleting file: $filename');
final collection = path.dirname(filename);
filename = path.basename(filename);
await _store.collection(collection).doc(filename).delete();
}
static Future<Map<String, Map<String, dynamic>>> readDirectory(
String collection,
) async {
final docs = await _store.collection(collection).get();
debugPrint('Listing collection: $collection, ${docs?.length} docs');
return (docs ?? {}).cast<String, Map<String, dynamic>>();
}
static Future<void> deleteDirectory(String collection) async {
debugPrint('Clearing collection: $collection');
await _store.collection(collection).delete();
}
}
class ProfileStorage {
static Future<Map<String, dynamic>?> readProfileFile(
String profile, String filename) async {
return FileStorage.readFile('profiles/$profile/$filename');
}
static Future<void> writeProfileFile(
String profile, String filename, Map<String, dynamic> data) async {
await FileStorage.writeFile('profiles/$profile/$filename', data);
}
static Future<void> deleteProfile(String profile) async {
await FileStorage.deleteDirectory('profiles/$profile');
}
static Future<void> deleteProfileFile(String profile, String filename) async {
await FileStorage.deleteFile('profiles/$profile/$filename');
}
static Future<List<Map<String, dynamic>>> listAllProfiles() async {
final list = await FileStorage.readDirectory('profiles');
return list.values.toList();
}
static Future<Map<String, Map<String, dynamic>>> listProfileFiles(
String profile, [
String? directory,
]) async {
return FileStorage.readDirectory(
'profiles/$profile${directory != null ? '/$directory' : ''}');
}
static Future<void> deleteAllProfiles() async {
await FileStorage.deleteDirectory('profiles');
}
}

View File

@@ -35,6 +35,8 @@ class GameStore extends ChangeNotifier {
late StreamSubscription<List<int>> _decodedSub;
late final List<MUDProfile> profiles = [];
MUDProfile? _currentProfile;
bool _clientReady = false;
MUDProfile get currentProfile => _currentProfile!;
Future<GameStore> init() async {
@@ -90,7 +92,7 @@ class GameStore extends ChangeNotifier {
bool showLine = true;
final str = ColorUtils.stripColor(line);
for (final trigger in triggers) {
if (!trigger.enabled) {
if (!trigger.isAvailable) {
continue;
}
if (trigger.matches(str)) {
@@ -98,8 +100,8 @@ class GameStore extends ChangeNotifier {
if (trigger.isRemovedFromBuffer) {
showLine = false;
}
if (trigger.isTemporary) {
trigger.enabled = false;
if (trigger.autoDisable) {
trigger.tempDisabled = true;
}
}
}
@@ -110,21 +112,22 @@ class GameStore extends ChangeNotifier {
bool sendLine = true;
final str = line;
for (final alias in aliases) {
if (!alias.enabled) {
if (!alias.isAvailable) {
continue;
}
if (alias.matches(str)) {
alias.invokeEffect(this, str);
sendLine = false;
}
if (alias.isTemporary) {
alias.enabled = false;
if (alias.autoDisable) {
alias.tempDisabled = true;
}
}
return sendLine;
}
void _onConnect() {
_clientReady = true;
echo('Connected');
}
@@ -231,6 +234,7 @@ class GameStore extends ChangeNotifier {
}
void sendString(String line) {
debugPrint('sending string: $line');
_client.send(line + newline);
}
@@ -253,6 +257,9 @@ class GameStore extends ChangeNotifier {
}
void submitInput(String text) {
if (!_clientReady || !_client.connected) {
return;
}
echo(text);
execute(text);
scrollToEnd();

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:window_manager/window_manager.dart';
import 'core/platform_utils.dart';
@@ -11,10 +10,10 @@ import 'core/store.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await getPrefs();
final status = await Permission.manageExternalStorage.status;
if (!status.isGranted) {
await Permission.manageExternalStorage.request();
}
// final status = await Permission.manageExternalStorage.status;
// if (!status.isGranted) {
// await Permission.manageExternalStorage.request();
// }
await FileStorage.init();
await gameStore.init();
if (PlatformUtils.isDesktop) {

View File

@@ -19,37 +19,49 @@ class AliasListPage extends StatelessWidget with GameStoreMixin {
final aliases = store.aliases;
return ListView.builder(
itemCount: aliases.length,
itemBuilder: (context, item) => ListTile(
key: Key(aliases[item].id),
title: Text(aliases[item].pattern),
subtitle: Text(aliases[item].action.content),
onTap: () async {
final alias = await Navigator.pushNamed(
context,
Paths.alias,
arguments: aliases[item],
);
if (alias != null) {
await store.currentProfile.saveAlias(alias as Alias);
await store.loadAliases();
}
},
),
itemBuilder: (context, item) {
final alias = aliases[item];
return ListTile(
key: Key(alias.id),
title: Text(alias.pattern),
subtitle: Text(alias.action.content),
leading: Switch.adaptive(
value: alias.enabled,
onChanged: (value) {
alias.enabled = value;
save(store, alias); },
),
onTap: () async {
final updated = await Navigator.pushNamed(
context,
Paths.alias,
arguments: alias,
);
if (updated != null) {
await save(store, updated as Alias);
}
},
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final store = storeOf(context);
final store = storeOf(context);
final alias = await Navigator.pushNamed(context, Paths.alias);
if (alias != null) {
await store.currentProfile.saveAlias(alias as Alias);
await store.loadAliases();
}
save(store, alias as Alias); }
},
),
);
}
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.loadAliases();
}
}

View File

@@ -27,6 +27,14 @@ class _AliasPageState extends State<AliasPage> {
return Scaffold(
appBar: AppBar(
title: const Text('Alias'),
actions: [
Switch.adaptive(
value: alias.enabled,
onChanged: (value) async {
alias.enabled = value;
},
)
],
),
body: Align(
alignment: Alignment.topCenter,
@@ -138,7 +146,7 @@ class _AliasPageState extends State<AliasPage> {
},
),
CheckboxListTile(
title: const Text('Remove From Buffer'),
title: const Text('Remove From Output'),
subtitle: const Text(
'If checked, your input line will be removed from the buffer when it is matched.',
),
@@ -151,15 +159,15 @@ class _AliasPageState extends State<AliasPage> {
},
),
CheckboxListTile(
title: const Text('Temporary'),
title: const Text('Auto Disable'),
subtitle: const Text(
'If checked, the alias will be disabled after it is matched once.',
),
value: alias.isTemporary,
value: alias.autoDisable,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) {
setState(() {
alias.isTemporary = value ?? false;
alias.autoDisable = value ?? false;
});
},
),

View File

@@ -19,22 +19,30 @@ class TriggerListPage extends StatelessWidget with GameStoreMixin {
final triggers = store.triggers;
return ListView.builder(
itemCount: triggers.length,
itemBuilder: (context, item) => ListTile(
key: Key(triggers[item].id),
title: Text(triggers[item].pattern),
subtitle: Text(triggers[item].action.content),
onTap: () async {
final trigger = await Navigator.pushNamed(
context,
Paths.trigger,
arguments: triggers[item],
);
if (trigger != null) {
await store.currentProfile.saveTrigger(trigger as Trigger);
await store.loadTriggers();
}
},
),
itemBuilder: (context, item) {
final trigger = triggers[item];
return ListTile(
key: Key(trigger.id),
title: Text(trigger.pattern),
subtitle: Text(trigger.action.content),
leading: Switch.adaptive(
value: trigger.enabled,
onChanged: (value) {
trigger.enabled = value;
save(store, trigger); },
),
onTap: () async {
final updated = await Navigator.pushNamed(
context,
Paths.trigger,
arguments: trigger,
);
if (updated != null) {
await save(store, updated as Trigger);
}
},
);
},
);
},
),
@@ -44,12 +52,16 @@ class TriggerListPage extends StatelessWidget with GameStoreMixin {
final store = storeOf(context);
final trigger = await Navigator.pushNamed(context, Paths.trigger);
if (trigger != null) {
await store.currentProfile.saveTrigger(trigger as Trigger);
await store.loadTriggers();
}
save(store, trigger as Trigger); }
},
),
);
}
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.loadTriggers();
}
}

View File

@@ -27,6 +27,14 @@ class _TriggerPageState extends State<TriggerPage> {
return Scaffold(
appBar: AppBar(
title: const Text('Trigger'),
actions: [
Switch.adaptive(
value: trigger.enabled,
onChanged: (value) {
trigger.enabled = value;
},
)
],
),
body: Align(
alignment: Alignment.topCenter,
@@ -138,7 +146,7 @@ class _TriggerPageState extends State<TriggerPage> {
},
),
CheckboxListTile(
title: const Text('Remove From Buffer'),
title: const Text('Remove From Output'),
subtitle: const Text(
'If checked, the output line will be removed from the buffer when it is matched.',
),
@@ -151,15 +159,15 @@ class _TriggerPageState extends State<TriggerPage> {
},
),
CheckboxListTile(
title: const Text('Temporary'),
title: const Text('Auto Disable'),
subtitle: const Text(
'If checked, the trigger will be disabled after it is matched once.',
),
value: trigger.isTemporary,
value: trigger.autoDisable,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) {
setState(() {
trigger.isTemporary = value ?? false;
trigger.autoDisable = value ?? false;
});
},
),

View File

@@ -93,10 +93,10 @@ packages:
dependency: "direct main"
description:
name: ctelnet
sha256: "6e188a06fc683e04f217eaf8062919492447ce8cd2ddeabbf44992500f5025c3"
sha256: "06cba141577de338adc9436ad733f33668224b0cfcf046f8edd58fcde09aeed2"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
version: "0.1.4"
cupertino_icons:
dependency: "direct main"
description:
@@ -184,14 +184,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
localstore:
dependency: "direct main"
description:
name: localstore
sha256: "42a0afb7696cfab1b4bd7d08355b4ee01f975fd364553b28d51496eccaf11cce"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
lua_dardo:
dependency: "direct main"
description:

View File

@@ -28,7 +28,7 @@ environment:
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
ctelnet: ^0.1.3
ctelnet: ^0.1.4
flutter:
sdk: flutter
@@ -40,7 +40,7 @@ dependencies:
shared_preferences: ^2.2.1
easy_debounce: ^2.0.3
encrypt: ^5.0.3
localstore: ^1.3.5
# localstore: ^1.3.5
path: ^1.8.3
uuid: ^3.0.7
meta: ^1.9.1