diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 96d46b3..0ca3675 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,4 +30,6 @@ android:name="flutterEmbedding" android:value="2" /> + + diff --git a/lib/core/features/action.dart b/lib/core/features/action.dart index f26001a..135195b 100644 --- a/lib/core/features/action.dart +++ b/lib/core/features/action.dart @@ -66,6 +66,8 @@ class MUDAction { }; String _doSpecialReplacements(GameStore store, String content) { + debugPrint('MUDAction._doSpecialReplacements: $content'); + debugPrint("password: ${store.currentProfile.password}"); return content .replaceAll('%PASSWORD', store.currentProfile.password) .replaceAll('%USERNAME', store.currentProfile.username) diff --git a/lib/core/features/profile.dart b/lib/core/features/profile.dart index e341215..6d2124e 100644 --- a/lib/core/features/profile.dart +++ b/lib/core/features/profile.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; + import 'package:encrypt/encrypt.dart' as enc; import 'package:flutter/foundation.dart'; +import '../consts.dart'; import '../secrets.dart'; import '../storage.dart'; import '../string_utils.dart'; @@ -52,15 +55,17 @@ class MUDProfile { password: password ?? this.password, ); - factory MUDProfile.fromJson(Map json) => MUDProfile( - id: json['id'], - name: json['name'], - host: json['host'], - port: json['port'], - mccpEnabled: json['mccpEnabled'], - username: json['username'], - password: decrypt(json['password']), - ); + factory MUDProfile.fromJson(Map json) { + return MUDProfile( + id: json['id'], + name: json['name'], + host: json['host'], + port: json['port'], + mccpEnabled: json['mccpEnabled'], + username: json['username'], + password: decrypt(json['password']), + ); + } Map toJson() => { 'id': id, @@ -74,48 +79,68 @@ class MUDProfile { static Future save(MUDProfile profile) async { debugPrint('MUDProfile.save: ${profile.id}'); - return ProfileStorage.writeProfileFile(profile.id, profile.id, profile.toJson()); + return ProfileStorage.writeProfileFile( + profile.id, profile.id, jsonEncode(profile.toJson())); } Future> loadTriggers() async { debugPrint('MUDProfile.loadTriggers: $id'); final triggers = await ProfileStorage.listProfileFiles(id, 'triggers'); - return triggers.values.map((e) => Trigger.fromJson(e)).toList(); + final triggerFiles = []; + for (final trigger in triggers) { + debugPrint('MUDProfile.loadTriggers: $id/triggers/$trigger'); + final triggerFile = + await ProfileStorage.readProfileFile(id, 'triggers/$trigger'); + if (triggerFile != null) { + triggerFiles.add(triggerFile); + } + } + return triggerFiles.map((e) => Trigger.fromJson(jsonDecode(e))).toList(); } Future> loadAliases() async { debugPrint('MUDProfile.loadAliases: $id'); final aliases = await ProfileStorage.listProfileFiles(id, 'aliases'); - return aliases.values.map((e) => Alias.fromJson(e)).toList(); + return aliases.map((e) => Alias.fromJson(jsonDecode(e))).toList(); } Future saveAlias(Alias alias) async { debugPrint('MUDProfile.saveAlias: $id/aliases/${alias.id}'); return ProfileStorage.writeProfileFile( - id, 'aliases/${alias.id}', alias.toJson()); + id, 'aliases/${alias.id}', jsonEncode(alias.toJson())); } Future saveTrigger(Trigger trigger) async { debugPrint('MUDProfile.saveTrigger: $id/triggers/${trigger.id}'); return ProfileStorage.writeProfileFile( - id, 'triggers/${trigger.id}', trigger.toJson()); + id, 'triggers/${trigger.id}', jsonEncode(trigger.toJson())); } + static final encKey = enc.Key.fromUtf8(pwdKey); + static final encrypter = enc.Encrypter(enc.AES(encKey, padding: null)); + static final iv = enc.IV.fromLength(16); + static String encrypt(String password) { if (password.isEmpty) { return ''; } - final key = enc.Key.fromUtf8(pwdKey); - final encrypter = enc.Encrypter(enc.AES(key)); - final encrypted = encrypter.encrypt(password, iv: enc.IV.fromLength(16)); + final encrypted = encrypter.encrypt(password, iv: iv); + // debugPrint('MUDProfile.encrypt: $password -> ${encrypted.base64}'); return encrypted.base64; } - static String decrypt(String json) { - final key = enc.Key.fromUtf8(pwdKey); - final encrypter = enc.Encrypter(enc.AES(key)); - final encrypted = enc.Encrypted.fromBase64(json); - return encrypter.decrypt(encrypted, iv: enc.IV.fromLength(16)); + static String decrypt(String password) { + if (password.isEmpty) { + return ''; + } + try { + // debugPrint('MUDProfile.decrypt: $password'); + final encrypted = enc.Encrypted.fromBase64(password); + return encrypter.decrypt(encrypted, iv: iv); + } catch (e, stack) { + debugPrint('MUDProfile.decrypt: $e$lf$stack'); + return password; + } } } diff --git a/lib/core/platform_utils.dart b/lib/core/platform_utils.dart index 4334db9..da844f8 100644 --- a/lib/core/platform_utils.dart +++ b/lib/core/platform_utils.dart @@ -1,8 +1,34 @@ import 'dart:io'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + class PlatformUtils { static get isDesktop => Platform.isMacOS || Platform.isWindows || Platform.isLinux; static get isMobile => !isDesktop; + + static Future getStorageBasePath() async { + switch (Platform.operatingSystem) { + case 'macos': + case 'linux': + final username = Platform.environment['USER'] ?? Platform.environment['USERNAME']; + return '/Users/$username/.config/mudblock'; + case 'windows': + final username = Platform.environment['USERNAME']; + return 'C:\\Users\\$username\\AppData\\Roaming\\mudblock'; + case 'android': + final base = await getExternalStorageDirectory(); + if (base == null) { + throw UnsupportedError('External storage not available'); + } + return path.join(base.path, 'mudblock'); + case 'ios': + final base = await getApplicationDocumentsDirectory(); + return path.join(base.path, 'mudblock'); + default: + throw UnsupportedError('Unsupported platform'); + } + } } diff --git a/lib/core/secrets.example.dart b/lib/core/secrets.example.dart new file mode 100644 index 0000000..6c01470 --- /dev/null +++ b/lib/core/secrets.example.dart @@ -0,0 +1,5 @@ +// can be generated using `hexdump -vn16 -e'4/4 "%08X" 1 "\n"' /dev/urandom` +// DO NOT LOSE THIS KEY! +// copy this file to secrets.dart (which is in .gitignore) +// in order to properly encrypt/decrypt passwords +const pwdKey = "..."; diff --git a/lib/core/storage.dart b/lib/core/storage.dart index 5b2373b..c84520a 100644 --- a/lib/core/storage.dart +++ b/lib/core/storage.dart @@ -1,55 +1,88 @@ +import 'dart:io'; + 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; +import 'platform_utils.dart'; - static Future?> readFile(String filename) async { +class FileStorage { + @deprecated + static final Localstore _store = Localstore.instance; + static late final String _base; + + static Future init() async { + _base = await PlatformUtils.getStorageBasePath(); + debugPrint('Storage base: $_base'); + } + + static Future 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 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) { + debugPrint('File does not exist: $filename'); + return null; + } + return file.readAsString(); } static Future writeFile( - String filename, Map data) async { + 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 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); } static Future 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 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(); } - static Future>> readDirectory( + static Future> readDirectory( String collection, ) async { - final docs = await _store.collection(collection).get(); - debugPrint('Listing collection: $collection, ${docs?.length} docs'); - return (docs ?? {}).cast>(); + // final docs = await _store.collection(collection).get(); + // debugPrint('Listing collection: $collection, ${docs?.length} docs'); + // return (docs ?? {}).cast(); + final dir = Directory(path.join(_base, collection)); + var exists = await dir.exists(); + if (!exists) { + debugPrint('Directory does not exist: $collection'); + return []; + } + // TODO use absolute paths? + return dir.list().map((e) => path.basename(e.path)).toList(); } static Future deleteDirectory(String collection) async { debugPrint('Clearing collection: $collection'); - await _store.collection(collection).delete(); + // await _store.collection(collection).delete(); + final dir = Directory(path.join(_base, collection)); + await dir.delete(recursive: true); } } class ProfileStorage { - static Future?> readProfileFile( + static Future readProfileFile( String profile, String filename) async { return FileStorage.readFile('profiles/$profile/$filename'); } static Future writeProfileFile( - String profile, String filename, Map data) async { + String profile, String filename, String data) async { await FileStorage.writeFile('profiles/$profile/$filename', data); } @@ -61,12 +94,11 @@ class ProfileStorage { await FileStorage.deleteFile('profiles/$profile/$filename'); } - static Future>> listAllProfiles() async { - final list = await FileStorage.readDirectory('profiles'); - return list.values.toList(); + static Future> listAllProfiles() async { + return FileStorage.readDirectory('profiles'); } - static Future>> listProfileFiles( + static Future> listProfileFiles( String profile, [ String? directory, ]) async { diff --git a/lib/core/storage.old.dart b/lib/core/storage.old.dart new file mode 100644 index 0000000..5b2373b --- /dev/null +++ b/lib/core/storage.old.dart @@ -0,0 +1,81 @@ +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?> 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 writeFile( + String filename, Map 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 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>> readDirectory( + String collection, + ) async { + final docs = await _store.collection(collection).get(); + debugPrint('Listing collection: $collection, ${docs?.length} docs'); + return (docs ?? {}).cast>(); + } + + static Future deleteDirectory(String collection) async { + debugPrint('Clearing collection: $collection'); + await _store.collection(collection).delete(); + } +} + +class ProfileStorage { + static Future?> readProfileFile( + String profile, String filename) async { + return FileStorage.readFile('profiles/$profile/$filename'); + } + + static Future writeProfileFile( + String profile, String filename, Map data) async { + await FileStorage.writeFile('profiles/$profile/$filename', data); + } + + static Future deleteProfile(String profile) async { + await FileStorage.deleteDirectory('profiles/$profile'); + } + + static Future deleteProfileFile(String profile, String filename) async { + await FileStorage.deleteFile('profiles/$profile/$filename'); + } + + static Future>> listAllProfiles() async { + final list = await FileStorage.readDirectory('profiles'); + return list.values.toList(); + } + + static Future>> listProfileFiles( + String profile, [ + String? directory, + ]) async { + return FileStorage.readDirectory( + 'profiles/$profile${directory != null ? '/$directory' : ''}'); + } + + static Future deleteAllProfiles() async { + await FileStorage.deleteDirectory('profiles'); + } +} + diff --git a/lib/core/store.dart b/lib/core/store.dart index 756b5b9..8417be7 100644 --- a/lib/core/store.dart +++ b/lib/core/store.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:math'; @@ -36,7 +37,7 @@ class GameStore extends ChangeNotifier { MUDProfile? _currentProfile; MUDProfile get currentProfile => _currentProfile!; - GameStore init() { + Future init() async { debugPrint('GameStore.init'); fillStockProfiles(); return this; @@ -97,6 +98,9 @@ class GameStore extends ChangeNotifier { if (trigger.isRemovedFromBuffer) { showLine = false; } + if (trigger.isTemporary) { + trigger.enabled = false; + } } } return showLine; @@ -113,6 +117,9 @@ class GameStore extends ChangeNotifier { alias.invokeEffect(this, str); sendLine = false; } + if (alias.isTemporary) { + alias.enabled = false; + } } return sendLine; } @@ -309,21 +316,32 @@ class GameStore extends ChangeNotifier { void loadProfiles() async { final list = await ProfileStorage.listAllProfiles(); profiles.clear(); - profiles.addAll(list.map((e) => MUDProfile.fromJson(e))); - _currentProfile = profiles.firstWhere((e) => e.id == currentProfile.id); + debugPrint('loading profiles: $list'); + for (final name in list) { + final profile = await ProfileStorage.readProfileFile(name, name); + profiles.add(MUDProfile.fromJson(jsonDecode(profile!))); + } + if (_currentProfile != null) { + _currentProfile = profiles.firstWhere((e) => e.id == currentProfile.id); + } notifyListeners(); } void fillStockProfiles() async { final list = await ProfileStorage.listAllProfiles(); + debugPrint('existing profiles: $list'); if (list.isEmpty) { for (final profile in profilePresets) { await MUDProfile.save(profile); profiles.add(profile); } } else { - profiles.addAll(list.map((e) => MUDProfile.fromJson(e))); + for (final name in list) { + final profile = await ProfileStorage.readProfileFile(name, name); + profiles.add(MUDProfile.fromJson(jsonDecode(profile!))); + } } + debugPrint('profiles: ${profiles.map((e) => [e.name, e.password])}'); notifyListeners(); } } @@ -337,5 +355,5 @@ mixin GameStoreStateMixin on State { GameStore get store => Provider.of(context, listen: false); } -final gameStore = GameStore().init(); +final gameStore = GameStore(); diff --git a/lib/main.dart b/lib/main.dart index 7a64f6d..009cd55 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:window_manager/window_manager.dart'; import 'core/platform_utils.dart'; import 'core/routes.dart'; +import 'core/storage.dart'; import 'core/storage/shared_prefs.dart'; import 'core/store.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await getPrefs(); + final status = await Permission.storage.status; + if (!status.isGranted) { + await Permission.storage.request(); + } + await FileStorage.init(); + await gameStore.init(); if (PlatformUtils.isDesktop) { await windowManager.ensureInitialized(); diff --git a/pubspec.lock b/pubspec.lock index 5ef58e2..c6a2685 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -241,7 +241,7 @@ packages: source: hosted version: "1.8.3" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa @@ -288,6 +288,46 @@ 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: diff --git a/pubspec.yaml b/pubspec.yaml index ea5caad..e3076a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: uuid: ^3.0.7 meta: ^1.9.1 lua_dardo: ^0.0.5 + path_provider: ^2.1.1 + permission_handler: ^11.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d6b86fa..fc50e10 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index bfa52f4..383263c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows screen_retriever window_manager )