feat: global variables support

This commit is contained in:
2023-09-29 14:39:19 +03:00
parent 86040e62d3
commit ee507e6e39
18 changed files with 502 additions and 178 deletions

View File

@@ -13,8 +13,8 @@ enum MUDActionTarget {
class MUDAction {
String content;
MUDActionTarget sendTo;
MUDAction(this.content, {this.sendTo = MUDActionTarget.world});
MUDActionTarget target;
MUDAction(this.content, {this.target = MUDActionTarget.world});
void invoke(GameStore store, List<String> matches) {
debugPrint('MUDAction.invoke: ${this.content}, $matches');
@@ -25,7 +25,7 @@ class MUDAction {
content = _doSpecialReplacements(store, content);
debugPrint('MUDAction.invoking: $content');
switch (sendTo) {
switch (target) {
case MUDActionTarget.world:
debugPrint('ActionSendTo.world: $content');
store.send(content);
@@ -57,22 +57,29 @@ class MUDAction {
factory MUDAction.fromJson(Map<String, dynamic> json) => MUDAction(
json['content'],
sendTo: MUDActionTarget.values[json['sendTo']],
// TODO generalize getting enum from string
target: MUDActionTarget.values.firstWhere(
(e) => e.name == json['target'],
orElse: () => MUDActionTarget.world,
),
);
Map<String, dynamic> toJson() => {
'content': content,
'sendTo': sendTo.index,
'target': target.name,
};
String _doSpecialReplacements(GameStore store, String content) {
debugPrint('MUDAction._doSpecialReplacements: $content');
debugPrint("password: ${store.currentProfile.password}");
return content
content = content
.replaceAll('%PASSWORD', store.currentProfile.password)
.replaceAll('%USERNAME', store.currentProfile.username)
//
;
for (final vari in store.variables) {
content = content.replaceAll('%${vari.name}', vari.value);
}
return content;
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:encrypt/encrypt.dart' as enc;
import 'package:flutter/foundation.dart';
@@ -9,6 +7,7 @@ import '../storage.dart';
import '../string_utils.dart';
import 'alias.dart';
import 'trigger.dart';
import 'variable.dart';
class MUDProfile {
String id;
@@ -18,6 +17,7 @@ class MUDProfile {
bool mccpEnabled;
String username;
String password;
AuthMethod authMethod;
MUDProfile({
required this.id,
@@ -27,6 +27,7 @@ class MUDProfile {
this.mccpEnabled = true,
this.username = '',
this.password = '',
this.authMethod = AuthMethod.none,
});
factory MUDProfile.empty() => MUDProfile(
@@ -44,6 +45,7 @@ class MUDProfile {
bool? mccpEnabled,
String? username,
String? password,
AuthMethod? authMethod,
}) =>
MUDProfile(
id: id ?? this.id,
@@ -53,6 +55,7 @@ class MUDProfile {
mccpEnabled: mccpEnabled ?? this.mccpEnabled,
username: username ?? this.username,
password: password ?? this.password,
authMethod: authMethod ?? this.authMethod,
);
factory MUDProfile.fromJson(Map<String, dynamic> json) {
@@ -64,6 +67,11 @@ class MUDProfile {
mccpEnabled: json['mccpEnabled'],
username: json['username'],
password: decrypt(json['password']),
// TODO generalize getting enum from string
authMethod: AuthMethod.values.firstWhere(
(e) => e.name == json['authMethod'],
orElse: () => AuthMethod.none,
),
);
}
@@ -75,18 +83,19 @@ class MUDProfile {
'mccpEnabled': mccpEnabled,
'username': username,
'password': encrypt(password),
'authMethod': authMethod.name,
};
static Future<void> save(MUDProfile profile) async {
debugPrint('MUDProfile.save: ${profile.id}');
return ProfileStorage.writeProfileFile(
profile.id, profile.id, jsonEncode(profile.toJson()));
profile.id, profile.id, (profile.toJson()));
}
Future<List<Trigger>> loadTriggers() async {
debugPrint('MUDProfile.loadTriggers: $id');
final triggers = await ProfileStorage.listProfileFiles(id, 'triggers');
final triggerFiles = <String>[];
final triggerFiles = <Map<String, dynamic>>[];
for (final trigger in triggers) {
debugPrint('MUDProfile.loadTriggers: $id/triggers/$trigger');
final triggerFile =
@@ -95,13 +104,13 @@ class MUDProfile {
triggerFiles.add(triggerFile);
}
}
return triggerFiles.map((e) => Trigger.fromJson(jsonDecode(e))).toList();
return triggerFiles.map((e) => Trigger.fromJson(e)).toList();
}
Future<List<Alias>> loadAliases() async {
debugPrint('MUDProfile.loadAliases: $id');
final aliases = await ProfileStorage.listProfileFiles(id, 'aliases');
final aliasFiles = <String>[];
final aliasFiles = <Map<String, dynamic>>[];
for (final alias in aliases) {
debugPrint('MUDProfile.loadAliases: $id/aliases/$alias');
final aliasFile =
@@ -110,19 +119,47 @@ class MUDProfile {
aliasFiles.add(aliasFile);
}
}
return aliasFiles.map((e) => Alias.fromJson(jsonDecode(e))).toList();
return aliasFiles.map((e) => Alias.fromJson(e)).toList();
}
Future<List<Variable>> loadVariables() async {
debugPrint('MUDProfile.loadVariables: $id');
final vars = await ProfileStorage.readProfileFile(id, 'vars');
if (vars == null) {
return [];
}
return (vars['vars'] as List<dynamic>)
.map((e) => Variable.fromJson(e))
.toList();
}
Future<void> saveAlias(Alias alias) async {
debugPrint('MUDProfile.saveAlias: $id/aliases/${alias.id}');
return ProfileStorage.writeProfileFile(
id, 'aliases/${alias.id}', jsonEncode(alias.toJson()));
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}', jsonEncode(trigger.toJson()));
id, 'triggers/${trigger.id}', trigger.toJson());
}
Future<void> saveVariable(List<Variable> current, Variable update) async {
debugPrint('MUDProfile.saveVariable: $id/vars');
final existing = current.indexWhere(
(v) => v.name == update.name,
);
if (existing >= 0) {
current[existing] = update;
} else {
current.add(update);
}
return ProfileStorage.writeProfileFile(
id,
'vars',
{'vars': current.map((v) => v.toJson()).toList()},
);
}
static final encKey = enc.Key.fromUtf8(pwdKey);
@@ -158,3 +195,8 @@ class MUDProfile {
}
}
enum AuthMethod {
none,
diku,
}

View File

@@ -0,0 +1,94 @@
class Variable<T> {
String name;
VariableType type;
String strValue;
Variable(this.name, this.type, this.strValue);
Variable.empty()
: type = VariableType.string,
name = '',
strValue = '';
Variable.string(this.name, this.strValue) : type = VariableType.string;
Variable.number(this.name, this.strValue) : type = VariableType.number;
Variable.boolean(this.name, this.strValue) : type = VariableType.boolean;
T get value {
switch (type) {
case VariableType.string:
return strValue as T;
case VariableType.number:
return double.parse(strValue) as T;
case VariableType.boolean:
return (strValue == 'true') as T;
}
}
set value(T value) {
switch (type) {
case VariableType.string:
strValue = value as String;
break;
case VariableType.number:
strValue = (value as double).toString();
break;
case VariableType.boolean:
strValue = (value as bool).toString();
break;
}
}
factory Variable.fromJson(Map<String, dynamic> json) {
// TODO generalize getting enum from string
switch (VariableType.values.firstWhere(
(e) => e.name == json['type'],
orElse: () => VariableType.string,
)) {
case VariableType.string:
return Variable.string(json['name'], json['value']);
case VariableType.number:
return Variable.number(json['name'], json['value']);
case VariableType.boolean:
return Variable.boolean(json['name'], json['value']);
}
}
Map<String, dynamic> toJson() => {
'name': name,
'type': type.name,
'value': strValue,
};
Variable copyWith<T>({
String? name,
VariableType? type,
String? strValue,
}) =>
Variable(
name ?? this.name,
type ?? this.type,
strValue ?? this.strValue,
);
}
class StringVariable extends Variable<String> {
StringVariable(String name, String value) : super.string(name, value);
StringVariable.fromJson(Map<String, dynamic> json)
: super(json['name'], VariableType.values[json['type']], json['value']);
}
class NumberVariable extends Variable<double> {
NumberVariable(String name, double value)
: super.number(name, value.toString());
NumberVariable.fromJson(Map<String, dynamic> json)
: super(json['name'], VariableType.values[json['type']], json['value']);
}
class BooleanVariable extends Variable<bool> {
BooleanVariable(String name, bool value)
: super.boolean(name, value.toString());
BooleanVariable.fromJson(Map<String, dynamic> json)
: super(json['name'], VariableType.values[json['type']], json['value']);
}
enum VariableType { string, number, boolean }

View File

@@ -12,12 +12,14 @@ final profilePresets = [
name: 'SimpleMUD',
host: 'smud.ourmmo.com',
port: 3000,
authMethod: AuthMethod.diku,
),
MUDProfile(
id: 'aardwolf',
name: 'Aardwolf',
host: 'aardmud.org',
port: 23,
authMethod: AuthMethod.diku,
),
MUDProfile(
id: 'batmud',

View File

@@ -12,16 +12,26 @@ import '../pages/profile_page.dart';
import '../pages/select_profile_page.dart';
import '../pages/trigger_list_page.dart';
import '../pages/trigger_page.dart';
import '../pages/variable_list_page.dart';
import '../pages/variable_page.dart';
import 'features/profile.dart';
import 'features/variable.dart';
class Paths {
static const home = '/';
static const profiles = '/profiles';
static const profile = '/profile';
static const aliases = '/aliases';
static const alias = '/alias';
static const triggers = '/triggers';
static const trigger = '/trigger';
static const home = '/home';
static const variables = '/variables';
static const variable = '/variable';
static const settings = '/settings';
}
@@ -49,6 +59,15 @@ final routes = <String, Widget Function(BuildContext)>{
final trigger = ModalRoute.of(context)!.settings.arguments as Trigger?;
return TriggerPage(trigger: trigger);
},
Paths.variables: (context) => GameStore.consumer(
builder: (context, store, child) {
return const VariableListPage();
},
),
Paths.variable: (context) {
final variable = ModalRoute.of(context)!.settings.arguments as Variable?;
return VariablePage(variable: variable);
},
Paths.home: (context) => HomeScaffold(
builder: (context, _) {
return HomePage(key: homeKey);

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
@@ -15,9 +16,6 @@ class FileStorage {
static Future<String?> readFile(String filename) async {
debugPrint('Getting file: $filename');
// final collection = path.dirname(filename);
// filename = path.basename(filename);
// return _store.collection(collection).doc(filename).get();
final file = File(path.join(base, filename));
var exists = await file.exists();
if (!exists) {
@@ -30,9 +28,6 @@ class FileStorage {
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));
await file.create(recursive: true);
await file.writeAsString(data);
@@ -40,9 +35,6 @@ class FileStorage {
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();
final file = File(path.join(base, filename));
await file.delete();
}
@@ -50,9 +42,6 @@ class FileStorage {
static Future<List<String>> readDirectory(
String collection,
) async {
// 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));
var exists = await dir.exists();
if (!exists) {
@@ -65,29 +54,30 @@ 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));
await dir.delete(recursive: true);
}
}
class ProfileStorage {
static Future<String?> readProfileFile(
static Future<Map<String, dynamic>?> readProfileFile(
String profile, String filename) async {
return FileStorage.readFile('profiles/$profile/$filename');
final data = await FileStorage.readFile('profiles/$profile/$filename.json');
return data != null ? jsonDecode(data) : null;
}
static Future<void> writeProfileFile(
String profile, String filename, String data) async {
await FileStorage.writeFile('profiles/$profile/$filename', data);
String profile, String filename, dynamic data) async {
data = jsonEncode(data);
await FileStorage.writeFile('profiles/$profile/$filename.json', data);
}
static Future<void> deleteProfile(String profile) async {
await FileStorage.deleteDirectory('profiles/$profile');
await FileStorage.deleteDirectory('profiles/$profile.json');
}
static Future<void> deleteProfileFile(String profile, String filename) async {
await FileStorage.deleteFile('profiles/$profile/$filename');
await FileStorage.deleteFile('profiles/$profile/$filename.json');
}
static Future<List<String>> listAllProfiles() async {
@@ -95,6 +85,7 @@ class ProfileStorage {
return list
.where((f) =>
Directory(path.join(FileStorage.base, 'profiles', f)).existsSync())
.map((f) => path.withoutExtension(f))
.toList();
}
@@ -102,8 +93,9 @@ class ProfileStorage {
String profile, [
String? directory,
]) async {
return FileStorage.readDirectory(
final list = await FileStorage.readDirectory(
'profiles/$profile${directory != null ? '/$directory' : ''}');
return list.map((f) => path.withoutExtension(f)).toList();
}
static Future<void> deleteAllProfiles() async {

View File

@@ -15,6 +15,7 @@ import 'consts.dart';
import 'features/alias.dart';
import 'features/profile.dart';
import 'features/trigger.dart';
import 'features/variable.dart';
const maxLines = 2000;
@@ -26,8 +27,6 @@ class GameStore extends ChangeNotifier {
final FocusNode inputFocus = FocusNode();
bool isCompressed = false;
final ZLibDecoder decoder = ZLibDecoder();
final List<Trigger> triggers = [];
final List<Alias> aliases = [];
final msgSplitPattern = RegExp("($cr$lf)|($lf$cr)|$cr|$lf");
final ZLibCodec _decoder = ZLibCodec();
final StreamController<List<int>> _rawStreamController = StreamController();
@@ -37,6 +36,11 @@ class GameStore extends ChangeNotifier {
MUDProfile? _currentProfile;
bool _clientReady = false;
// features
final List<Trigger> triggers = [];
final List<Alias> aliases = [];
final List<Variable> variables = [];
MUDProfile get currentProfile => _currentProfile!;
Future<GameStore> init() async {
@@ -67,6 +71,7 @@ class GameStore extends ChangeNotifier {
await Future.wait([
loadTriggers(),
loadAliases(),
loadVariables(),
]);
_client.connect();
}
@@ -88,6 +93,14 @@ class GameStore extends ChangeNotifier {
debugPrint('Aliases: ${aliases.length}');
}
Future<void> loadVariables() async {
final list = await currentProfile.loadVariables();
variables.clear();
variables.addAll(list);
notifyListeners();
debugPrint('Variables: ${variables.length}');
}
bool processTriggers(String line) {
bool showLine = true;
final str = ColorUtils.stripColor(line);
@@ -126,9 +139,18 @@ class GameStore extends ChangeNotifier {
return sendLine;
}
void _onConnect() {
Future<void> _onConnect() async {
_clientReady = true;
echo('Connected');
if (currentProfile.authMethod != AuthMethod.none && currentProfile.username.isNotEmpty && currentProfile.password.isNotEmpty) {
// _client.doo(90);
// sendBytes([Symbols.iac, Symbols.doo, 90]);
debugPrint('Sending username and password');
await Future.delayed(const Duration(milliseconds: 100));
send(currentProfile.username);
await Future.delayed(const Duration(milliseconds: 100));
send(currentProfile.password);
}
}
void requestMCCP() {
@@ -326,7 +348,7 @@ class GameStore extends ChangeNotifier {
debugPrint('loading profiles: $list');
for (final name in list) {
final profile = await ProfileStorage.readProfileFile(name, name);
profiles.add(MUDProfile.fromJson(jsonDecode(profile!)));
profiles.add(MUDProfile.fromJson(profile!));
}
if (_currentProfile != null) {
_currentProfile = profiles.firstWhere((e) => e.id == currentProfile.id);
@@ -348,7 +370,7 @@ class GameStore extends ChangeNotifier {
for (final name in list) {
final profile = await ProfileStorage.readProfileFile(name, name);
debugPrint('profile: $profile');
profiles.add(MUDProfile.fromJson(jsonDecode(profile!)));
profiles.add(MUDProfile.fromJson(profile!));
}
}
debugPrint('profiles: ${profiles.map((e) => [e.name, e.password])}');

View File

@@ -10,10 +10,6 @@ import 'core/store.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await getPrefs();
// final status = await Permission.manageExternalStorage.status;
// if (!status.isGranted) {
// await Permission.manageExternalStorage.request();
// }
await FileStorage.init();
await gameStore.init();
if (PlatformUtils.isDesktop) {
@@ -52,7 +48,7 @@ class MyApp extends StatelessWidget {
builder: (context, child) {
return GameStore.provider(child: child!);
},
initialRoute: '/home',
initialRoute: Paths.home,
routes: routes,
);
}

View File

@@ -107,10 +107,10 @@ class _AliasPageState extends State<AliasPage> {
),
),
DropdownMenu(
initialSelection: alias.action.sendTo,
initialSelection: alias.action.target,
onSelected: (value) {
if (value is MUDActionTarget) {
alias.action.sendTo = value;
alias.action.target = value;
}
},
dropdownMenuEntries: const [

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../core/platform_utils.dart';
import '../core/routes.dart';
import '../core/store.dart';
@@ -20,25 +21,39 @@ class HomeScaffold extends StatelessWidget {
builder: builder,
),
endDrawer: Drawer(
child: ListView(
children: [
ListTile(
title: const Text('Aliases'),
onTap: () => Navigator.pushNamed(context, Paths.aliases),
),
ListTile(
title: const Text('Triggers'),
onTap: () => Navigator.pushNamed(context, Paths.triggers),
),
ListTile(
title: const Text('Settings'),
onTap: () => Navigator.pushNamed(context, Paths.settings),
),
ListTile(
title: const Text('Disconnect'),
onTap: () => gameStore.connect(context),
),
],
child: Padding(
padding: PlatformUtils.isDesktop
? const EdgeInsets.only(top: 60)
: EdgeInsets.zero,
child: ListView(
children: [
ListTile(
title: const Text('Aliases'),
onTap: () => Navigator.pushNamed(context, Paths.aliases),
),
ListTile(
title: const Text('Triggers'),
onTap: () => Navigator.pushNamed(context, Paths.triggers),
),
ListTile(
title: const Text('Variables'),
onTap: () => Navigator.pushNamed(context, Paths.variables),
),
ListTile(
title: const Text('Settings'),
onTap: () => Navigator.pushNamed(context, Paths.settings),
),
ListTile(
title: const Text('Disconnect'),
onTap: () async {
await gameStore.disconnect();
if (context.mounted) {
gameStore.connect(context);
}
},
),
],
),
),
),
);

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../core/features/profile.dart';
import '../core/platform_utils.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key, required this.profile});
@@ -33,73 +32,99 @@ class _ProfilePageState extends State<ProfilePage> {
width: 1200,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: TextEditingController(text: profile.name),
decoration: const InputDecoration(
labelText: 'Profile name',
helperText: 'The name of the profile',
),
onChanged: (value) {
profile.name = value;
},
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: TextEditingController(text: profile.name),
decoration: const InputDecoration(
labelText: 'Profile name',
helperText: 'The name of the profile',
),
TextField(
controller: TextEditingController(text: profile.host),
decoration: const InputDecoration(
labelText: 'Host',
),
onChanged: (value) {
profile.host = value;
},
onChanged: (value) {
profile.name = value;
},
),
TextField(
controller: TextEditingController(text: profile.host),
decoration: const InputDecoration(
labelText: 'Host',
),
TextField(
controller:
TextEditingController(text: profile.port.toString()),
decoration: const InputDecoration(
labelText: 'Port',
),
onChanged: (value) {
profile.port = int.tryParse(value) ?? profile.port;
},
onChanged: (value) {
profile.host = value;
},
),
TextField(
controller:
TextEditingController(text: profile.port.toString()),
decoration: const InputDecoration(
labelText: 'Port',
),
CheckboxListTile(
title: const Text('Enable MCCP Compression'),
subtitle: const Text(
'Enables MCCP Compression for this profile (if available).',
),
value: profile.mccpEnabled,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) {
setState(() {
profile.mccpEnabled = value ?? false;
});
},
onChanged: (value) {
profile.port = int.tryParse(value) ?? profile.port;
},
),
CheckboxListTile(
title: const Text('Enable MCCP Compression'),
subtitle: const Text(
'Enables MCCP Compression for this profile (if available).',
),
TextField(
controller: TextEditingController(text: profile.username),
decoration: const InputDecoration(
labelText: 'Username',
),
onChanged: (value) {
profile.username = value;
},
value: profile.mccpEnabled,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) {
setState(() {
profile.mccpEnabled = value ?? false;
});
},
),
const SizedBox(height: 32),
Text(
'Authentication',
style: Theme.of(context).textTheme.titleMedium,
),
TextField(
controller: TextEditingController(text: profile.username),
decoration: const InputDecoration(
labelText: 'Username',
),
TextField(
controller: TextEditingController(text: profile.password),
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
onChanged: (value) {
profile.password = value;
},
onChanged: (value) {
profile.username = value;
},
),
TextField(
controller: TextEditingController(text: profile.password),
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
],
)),
onChanged: (value) {
profile.password = value;
},
),
const SizedBox(height: 24),
DropdownMenu(
label: const Text('Authentication Method'),
initialSelection: profile.authMethod,
onSelected: (value) {
setState(() {
profile.authMethod = value as AuthMethod;
});
},
dropdownMenuEntries: const [
DropdownMenuEntry(
value: AuthMethod.none,
label: 'None',
),
DropdownMenuEntry(
value: AuthMethod.diku,
label: 'Diku-style',
),
],
),
],
),
),
),
),
),

View File

@@ -107,10 +107,10 @@ class _TriggerPageState extends State<TriggerPage> {
),
),
DropdownMenu(
initialSelection: trigger.action.sendTo,
initialSelection: trigger.action.target,
onSelected: (value) {
if (value is MUDActionTarget) {
trigger.action.sendTo = value;
trigger.action.target = value;
}
},
dropdownMenuEntries: const [

View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:mudblock/core/store.dart';
import '../core/features/variable.dart';
import '../core/routes.dart';
class VariableListPage extends StatelessWidget with GameStoreMixin {
const VariableListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Variables'),
),
body: GameStore.consumer(
builder: (context, store, child) {
debugPrint('Variable list rebuild');
final variables = store.variables;
return ListView.builder(
itemCount: variables.length,
itemBuilder: (context, item) {
final variable = variables[item];
return ListTile(
key: Key(variable.name),
title: Text(variable.name),
subtitle: Text(variable.strValue),
onTap: () async {
final updated = await Navigator.pushNamed(
context,
Paths.variable,
arguments: variable,
);
if (updated != null) {
await save(store, updated as Variable);
}
},
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final store = storeOf(context);
final variable = await Navigator.pushNamed(context, Paths.variable);
if (variable != null) {
save(store, variable as Variable);
}
},
),
);
}
Future<void> save(GameStore store, Variable updated) async {
await store.currentProfile.saveVariable(store.variables, updated);
await store.loadVariables();
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import '../core/features/variable.dart';
class VariablePage extends StatefulWidget {
const VariablePage({super.key, required this.variable});
final Variable? variable;
@override
State<VariablePage> createState() => _VariablePageState();
}
class _VariablePageState extends State<VariablePage> {
late final Variable variable;
@override
void initState() {
variable = widget.variable?.copyWith() ?? Variable.empty();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Variable'),
),
body: Align(
alignment: Alignment.topCenter,
child: SizedBox(
width: 1200,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Builder(
builder: (context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: TextEditingController(text: variable.name),
decoration: const InputDecoration(
labelText: 'Name',
helperText: 'The name of the variable',
),
onChanged: (value) {
variable.name = value;
},
),
DropdownMenu(
initialSelection: variable.type,
onSelected: (value) {
variable.type = value as VariableType;
},
dropdownMenuEntries: VariableType.values
.map(
(e) => DropdownMenuEntry(
value: e,
label: e.name,
),
)
.toList(),
),
TextField(
controller:
TextEditingController(text: variable.strValue),
decoration: const InputDecoration(
labelText: 'Value',
helperText: 'The value of the variable',
),
onChanged: (value) {
variable.strValue = value;
},
),
],
);
},
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pop(context, variable);
},
child: const Icon(Icons.save),
),
);
}
}

View File

@@ -280,46 +280,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: ad65ba9af42a3d067203641de3fd9f547ded1410bad3b84400c2b4899faede70
url: "https://pub.dev"
source: hosted
version: "11.0.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: ace7d15a3d1a4a0b91c041d01e5405df221edb9de9116525efc773c74e6fc790
url: "https://pub.dev"
source: hosted
version: "11.0.5"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
url: "https://pub.dev"
source: hosted
version: "9.1.4"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2
url: "https://pub.dev"
source: hosted
version: "3.11.5"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
url: "https://pub.dev"
source: hosted
version: "0.1.3"
platform:
dependency: transitive
description:

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
version: 0.1.0+1
environment:
sdk: '>=3.1.2 <4.0.0'
@@ -46,7 +46,7 @@ dependencies:
meta: ^1.9.1
lua_dardo: ^0.0.5
path_provider: ^2.1.1
permission_handler: ^11.0.0
# permission_handler: ^11.0.0
dev_dependencies:
flutter_test:

View File

@@ -6,13 +6,10 @@
#include "generated_plugin_registrant.h"
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
WindowManagerPluginRegisterWithRegistrar(

View File

@@ -3,7 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
screen_retriever
window_manager
)