feat: aliases crud wip

This commit is contained in:
2023-09-26 02:21:24 +03:00
parent 8d1f0ffbd5
commit ea19723c80
13 changed files with 450 additions and 57 deletions

View File

@@ -11,9 +11,9 @@ enum MUDActionTarget {
}
class MUDAction {
final String content;
final MUDActionTarget sendTo;
const MUDAction(this.content, {this.sendTo = MUDActionTarget.world});
String content;
MUDActionTarget sendTo;
MUDAction(this.content, {this.sendTo = MUDActionTarget.world});
void invoke(BuildContext context, List<String> matches) {
final store = Provider.of<GameStore>(context, listen: false);
@@ -41,5 +41,17 @@ class MUDAction {
break;
}
}
factory MUDAction.empty() => MUDAction('');
factory MUDAction.fromJson(Map<String, dynamic> json) => MUDAction(
json['content'],
sendTo: MUDActionTarget.values[json['sendTo']],
);
Map<String, dynamic> toJson() => {
'content': content,
'sendTo': sendTo.index,
};
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/widgets.dart';
import '../string_utils.dart';
import 'action.dart';
class Alias {
@@ -10,7 +11,7 @@ class Alias {
bool isCaseSensitive;
bool isRemovedFromBuffer;
bool isTemporary;
int invokeCount = 0;
int invokeCount;
MUDAction action;
Alias({
@@ -22,8 +23,45 @@ class Alias {
this.isCaseSensitive = false,
this.isRemovedFromBuffer = false,
this.isTemporary = false,
this.invokeCount = 0,
});
factory Alias.empty() => Alias(
id: uuid(),
pattern: '',
enabled: true,
isRegex: false,
isCaseSensitive: false,
isRemovedFromBuffer: false,
isTemporary: false,
invokeCount: 0,
action: MUDAction.empty(),
);
factory Alias.fromJson(Map<String, dynamic> json) => Alias(
id: json['id'],
pattern: json['pattern'],
enabled: json['enabled'],
isRegex: json['isRegex'],
isCaseSensitive: json['isCaseSensitive'],
isRemovedFromBuffer: json['isRemovedFromBuffer'],
isTemporary: json['isTemporary'],
invokeCount: json['invokeCount'],
action: MUDAction.fromJson(json['action']),
);
Map<String, dynamic> toJson() => {
'id': id,
'pattern': pattern,
'enabled': enabled,
'isRegex': isRegex,
'isCaseSensitive': isCaseSensitive,
'isRemovedFromBuffer': isRemovedFromBuffer,
'isTemporary': isTemporary,
'invokeCount': invokeCount,
'action': action.toJson(),
};
bool matches(String line) {
if (isRegex) {
final regex = RegExp(pattern, caseSensitive: isCaseSensitive);
@@ -46,7 +84,8 @@ class Alias {
return [];
}
if (isRegex) {
final regex = RegExp(pattern, caseSensitive: isCaseSensitive, unicode: true);
final regex =
RegExp(pattern, caseSensitive: isCaseSensitive, unicode: true);
final rMatches = regex.allMatches(str);
final matches = <String>[];
for (var i = 0; i < rMatches.length; i++) {
@@ -72,5 +111,28 @@ class Alias {
return matches;
}
}
Alias copyWith({
String? id,
String? pattern,
bool? enabled,
bool? isRegex,
bool? isCaseSensitive,
bool? isRemovedFromBuffer,
bool? isTemporary,
int? invokeCount = 0,
MUDAction? action,
}) =>
Alias(
id: id ?? this.id,
pattern: pattern ?? this.pattern,
enabled: enabled ?? this.enabled,
isRegex: isRegex ?? this.isRegex,
isCaseSensitive: isCaseSensitive ?? this.isCaseSensitive,
isRemovedFromBuffer: isRemovedFromBuffer ?? this.isRemovedFromBuffer,
isTemporary: isTemporary ?? this.isTemporary,
invokeCount: invokeCount ?? this.invokeCount,
action: action ?? this.action,
);
}

View File

@@ -1,3 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:mudblock/core/storage.dart';
import 'alias.dart';
import 'trigger.dart';
class MUDProfile {
String id;
String name;
@@ -10,5 +16,27 @@ class MUDProfile {
required this.host,
required this.port,
});
Future<List<Trigger>> loadTriggers() async {
debugPrint('MUDProfile.loadTriggers: $id');
final triggers = await ProfileStorage.listProfileFiles(id, 'triggers');
return triggers.values.map((e) => Trigger.fromJson(e)).toList();
}
Future<List<Alias>> loadAliases() async {
debugPrint('MUDProfile.loadAliases: $id');
final aliases = await ProfileStorage.listProfileFiles(id, 'aliases');
return aliases.values.map((e) => Alias.fromJson(e)).toList();
}
Future<void> saveAlias(Alias alias) async {
debugPrint('MUDProfile.saveAlias: $id/aliases/${alias.id}');
return ProfileStorage.writeProfileFile(id, 'aliases/${alias.id}', alias.toJson());
}
Future<void> saveTrigger(Trigger trigger) async {
debugPrint('MUDProfile.saveTrigger: $id/triggers/${trigger.id}');
return ProfileStorage.writeProfileFile(id, 'triggers/${trigger.id}', trigger.toJson());
}
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/widgets.dart';
import 'action.dart';
import 'alias.dart';
class Trigger extends Alias {
@@ -12,6 +11,43 @@ class Trigger extends Alias {
super.isCaseSensitive = false,
super.isRemovedFromBuffer = false,
super.isTemporary = false,
super.invokeCount = 0,
});
factory Trigger.fromJson(Map<String, dynamic> json) => Trigger(
id: json['id'],
pattern: json['pattern'],
enabled: json['enabled'],
isRegex: json['isRegex'],
isCaseSensitive: json['isCaseSensitive'],
isRemovedFromBuffer: json['isRemovedFromBuffer'],
isTemporary: json['isTemporary'],
invokeCount: json['invokeCount'],
action: MUDAction.fromJson(json['action']),
);
@override
Trigger copyWith({
String? id,
String? pattern,
bool? enabled,
bool? isRegex,
bool? isCaseSensitive,
bool? isRemovedFromBuffer,
bool? isTemporary,
int? invokeCount = 0,
MUDAction? action,
}) =>
Trigger(
id: id ?? this.id,
pattern: pattern ?? this.pattern,
enabled: enabled ?? this.enabled,
isRegex: isRegex ?? this.isRegex,
isCaseSensitive: isCaseSensitive ?? this.isCaseSensitive,
isRemovedFromBuffer: isRemovedFromBuffer ?? this.isRemovedFromBuffer,
isTemporary: isTemporary ?? this.isTemporary,
invokeCount: invokeCount ?? this.invokeCount,
action: action ?? this.action,
);
}

80
lib/core/storage.dart Normal file
View File

@@ -0,0 +1,80 @@
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<Map<String, Map<String, dynamic>>> listAllProfiles() async {
return FileStorage.readDirectory('profiles');
}
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

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
@@ -40,8 +39,6 @@ class GameStore extends ChangeNotifier {
GameStore init() {
debugPrint('GameStore.init');
scrollController = ScrollController();
loadTriggers();
loadAliases();
return this;
}
@@ -61,51 +58,25 @@ class GameStore extends ChangeNotifier {
onData: onData,
onError: onError,
);
await Future.wait([
loadTriggers(),
loadAliases(),
]);
_client.connect();
}
void loadTriggers() {
Future<void> loadTriggers() async {
debugPrint('loadTriggers');
final list = await currentProfile.loadTriggers();
triggers.clear();
triggers.addAll(
[
Trigger(
id: 'test',
pattern: r'^You are in the ([^.]+)\. This is the ([^.]+)\.',
action: const MUDAction(
'Hello, %1, the %2!',
sendTo: MUDActionTarget.world,
),
isRegex: true,
),
Trigger(
id: 'test2',
pattern: r'^exits: ([\w\s]+)',
action: const MUDAction(
'I see exits: %1',
sendTo: MUDActionTarget.world,
),
isRegex: true,
),
],
);
triggers.addAll(list);
debugPrint('triggers: ${triggers.length}');
}
void loadAliases() {
Future<void> loadAliases() async {
final list = await currentProfile.loadAliases();
aliases.clear();
aliases.addAll(
[
Alias(
id: 'hello',
pattern: r'^hello|^hi',
action: const MUDAction(
'Hello, world!',
sendTo: MUDActionTarget.world,
),
isRegex: true,
),
],
);
aliases.addAll(list);
debugPrint('aliases: ${aliases.length}');
}
@@ -300,9 +271,30 @@ class GameStore extends ChangeNotifier {
input.text = content;
selectInput();
}
static consumer(
Widget Function(BuildContext context, GameStore value, Widget? child) builder,
) {
return Consumer<GameStore>(
builder: builder,
);
}
static provider({
required Widget child,
}) {
return ChangeNotifierProvider<GameStore>.value(
value: gameStore,
child: child,
);
}
}
mixin GameStoreMixin<T extends StatefulWidget> on State<T> {
mixin GameStoreMixin {
GameStore storeOf(BuildContext context) => Provider.of<GameStore>(context, listen: false);
}
mixin GameStoreStateMixin<T extends StatefulWidget> on State<T> {
GameStore get store => Provider.of<GameStore>(context, listen: false);
}

View File

@@ -0,0 +1,8 @@
import 'package:uuid/uuid.dart';
const _uuid = Uuid();
String uuid() {
return _uuid.v4();
}

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:mudblock/core/consts.dart';
import 'package:mudblock/core/store.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'core/features/alias.dart';
import 'core/storage/shared_prefs.dart';
import 'core/store.dart';
import 'pages/alias_page.dart';
import 'pages/alias_list_page.dart';
import 'pages/home_page.dart';
import 'pages/main_scaffold.dart';
import 'pages/profile_select_page.dart';
@@ -43,14 +45,26 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
builder: (context, child) {
return GameStore.provider(child: child!);
},
initialRoute: '/home',
routes: {
'/select-profile': (context) => const ProfileSelectPage(),
'/aliases': (context) => GameStore.consumer(
(context, store, child) {
return const AliasListPage();
},
),
'/alias': (context) {
final alias = ModalRoute.of(context)!.settings.arguments as Alias?;
return AliasPage(alias: alias);
},
'/home': (context) => MainScaffold(
builder: (context, _) {
return HomePage(key: homeKey);
}
),
builder: (context, _) {
return HomePage(key: homeKey);
},
),
},
);
}

View File

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:mudblock/core/store.dart';
import 'package:provider/provider.dart';
import '../core/features/alias.dart';
class AliasListPage extends StatelessWidget with GameStoreMixin {
const AliasListPage({super.key});
@override
Widget build(BuildContext context) {
var store = storeOf(context);
return Scaffold(
appBar: AppBar(
title: const Text('Aliases'),
),
body: Consumer<GameStore>(
builder: (context, store, child) {
final aliases = store.aliases;
return ListView.builder(
itemCount: aliases.length,
itemBuilder: (context, item) => ListTile(
title: Text(aliases[item].pattern),
subtitle: Text(aliases[item].action.content),
onTap: () {
Navigator.pushNamed(context, '/alias',
arguments: aliases[item]);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final alias = await Navigator.pushNamed(context, '/alias');
if (alias != null) {
store.currentProfile.saveAlias(alias as Alias);
}
},
child: const Icon(Icons.add),
),
);
}
}

82
lib/pages/alias_page.dart Normal file
View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../core/features/alias.dart';
class AliasPage extends StatefulWidget {
const AliasPage({super.key, required this.alias});
final Alias? alias;
@override
State<AliasPage> createState() => _AliasPageState();
}
class _AliasPageState extends State<AliasPage> {
late final Alias alias;
@override
void initState() {
alias = widget.alias?.copyWith() ?? Alias.empty();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Alias'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: TextEditingController(text: alias.pattern),
decoration: const InputDecoration(
labelText: 'Pattern',
),
onChanged: (value) {
alias.pattern = value;
},
),
),
SizedBox(
width: 300,
child: CheckboxListTile(
title: const Text('Regular Expression'),
value: alias.isRegex,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) {
alias.isRegex = value ?? false;
},
),
),
],
),
TextField(
controller: TextEditingController(text: alias.action.content),
decoration: const InputDecoration(
labelText: 'Action',
),
onChanged: (value) {
alias.action.content = value;
},
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pop(context, alias);
},
child: const Icon(Icons.save),
),
);
}
}

View File

@@ -18,7 +18,7 @@ class HomePage extends StatefulWidget {
}
class HomePageState extends State<HomePage>
with GameStoreMixin, WindowListener {
with GameStoreStateMixin, WindowListener {
@override
void initState() {
super.initState();
@@ -128,8 +128,8 @@ class HomePageState extends State<HomePage>
IconButton(
icon: const Icon(Icons.bug_report),
onPressed: () {
store.loadTriggers();
store.loadAliases();
Navigator.pushNamed(context, '/aliases');
},
),
],

View File

@@ -65,6 +65,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.3"
ctelnet:
dependency: "direct main"
description:
@@ -184,7 +192,7 @@ packages:
source: hosted
version: "1.0.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
@@ -340,6 +348,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@@ -380,6 +396,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7
url: "https://pub.dev"
source: hosted
version: "4.1.0"
vector_math:
dependency: transitive
description:

View File

@@ -41,6 +41,8 @@ dependencies:
shared_preferences: ^2.2.1
easy_debounce: ^2.0.3
localstore: ^1.3.5
path: ^1.8.3
uuid: ^4.1.0
dev_dependencies:
flutter_test: