diff --git a/CHANGELOG.md b/CHANGELOG.md index c5658c31..6aeac528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v5.1.0 (50100003) +#### NEW + +- `[External Modules]` Ability to add and view external modules + #### FIXES - `[UI/UX]` (ListView) Vertical padding did not match horizontal padding diff --git a/assets/localization/en.json b/assets/localization/en.json index 0ef20c77..ef21d4eb 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -4,7 +4,11 @@ "settings.AccountHelp": "LunaSea Account", "settings.AccountHelpHint1": "LunaSea offers a free account to backup your configuration to the cloud, with additional features coming in the future!", "settings.AddHeader": "Add Header", + "settings.AddModule": "Add Module", + "settings.AddModuleFailed": "Failed to Add Module", + "settings.AddModuleSuccess": "Module Added", "settings.AddProfile": "Add Profile", + "settings.AllFieldsAreRequired": "All fields are required", "settings.AmoledTheme": "AMOLED Theme", "settings.AmoledThemeBorders": "AMOLED Theme Borders", "settings.AmoledThemeBordersDescription": "Add Subtle Borders Across the UI", @@ -67,14 +71,19 @@ "settings.DeleteHeaderHint1": "Are you sure you want to delete this header?", "settings.DeleteIndexer": "Delete Indexer", "settings.DeleteIndexerHint1": "Are you sure you want to delete this indexer?", + "settings.DeleteModule": "Delete Module", + "settings.DeleteModuleHint1": "Are you sure you want to delete this external module?", + "settings.DeleteModuleSuccess": "Module Deleted", "settings.DeleteProfile": "Delete Profile", "settings.DismissBanners": "Dismiss Banners", "settings.DismissBannersHint1": "Are you sure you want to dismiss all tooltip banners?", "settings.DismissBannersHint2": "The tooltip banners will give you tips and hints for features available within LunaSea.", + "settings.DisplayName": "Display Name", "settings.Donations": "Donations", "settings.DonationsDescription": "Donate to the Developer", "settings.Drawer": "Drawer", "settings.DrawerDescription": "Customize the Drawer", + "settings.EditModule": "Edit Module", "settings.Email": "Email", "settings.EmailSentFailure": "Failed to Reset Password", "settings.EmailSentSuccess": "Email Sent", @@ -120,9 +129,11 @@ "settings.MACAddressHint4": "Each hexidecimal octet should be separated by a colon", "settings.MACAddressValidation": "Invalid MAC Address", "settings.MinimumCharacters": "Minimum of {} characters", + "settings.ModuleNotFound": "Module Not Found", "settings.Notifications": "Notifications", "settings.NotificationsDescription": "Setup Webhooks for Push Notifications", "settings.NoBackupsFound": "No Backups Found", + "settings.NoExternalModulesFound": "No External Modules Found", "settings.NoHeadersAdded": "No Headers Added", "settings.OpenLinksIn": "Open Links In…", "settings.Password": "Password", @@ -177,9 +188,11 @@ "lunasea.Disable": "Disable", "lunasea.Disabled": "Disabled", "lunasea.Dismiss": "Dismiss", + "lunasea.ExternalModules": "External Modules", "lunasea.GoBack": "Go Back", "lunasea.GoToSettings": "Go to Settings", "lunasea.IncorrectEncryptionKey": "Incorrect encryption key", + "lunasea.Module": "Module", "lunasea.ModuleIsNotEnabled": "{} Is Not Enabled", "lunasea.NotSet": "Not Set", "lunasea.NoModulesEnabled": "No Modules Enabled", diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index 742d3e46..150de445 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -33,6 +33,7 @@ class Database { /// Open all Hive boxes for reading. Future _openBoxes() async { await Hive.openBox('alerts'); + await Hive.openBox('external_modules'); await Hive.openBox('indexers'); await Hive.openBox('logs'); await Hive.openBox('lunasea'); @@ -50,6 +51,8 @@ class Database { //Get boxes static Box get alertsBox => Hive.box('alerts'); + static Box get externalModulesBox => + Hive.box('external_modules'); static Box get indexersBox => Hive.box('indexers'); static Box get logsBox => @@ -60,6 +63,8 @@ class Database { //Clear boxes void clearAlertsBox() => alertsBox.deleteAll(alertsBox.keys); + void clearExternalModulesBox() => + externalModulesBox.deleteAll(externalModulesBox.keys); void clearIndexersBox() => indexersBox.deleteAll(indexersBox.keys); void clearLogsBox() => logsBox.deleteAll(logsBox.keys); void clearLunaSeaBox() => lunaSeaBox.deleteAll(lunaSeaBox.keys); @@ -68,6 +73,7 @@ class Database { void clearAllBoxes({ bool clearEverything = false, }) { + clearExternalModulesBox(); clearLunaSeaBox(); clearProfilesBox(); clearIndexersBox(); diff --git a/lib/core/database/luna_database.dart b/lib/core/database/luna_database.dart index f8738500..3afe528d 100644 --- a/lib/core/database/luna_database.dart +++ b/lib/core/database/luna_database.dart @@ -4,6 +4,7 @@ import 'package:lunasea/core.dart'; class LunaDatabase extends LunaModuleDatabase { @override void registerAdapters() { + Hive.registerAdapter(ExternalModuleHiveObjectAdapter()); Hive.registerAdapter(IndexerHiveObjectAdapter()); Hive.registerAdapter(ProfileHiveObjectAdapter()); Hive.registerAdapter(LunaLogHiveObjectAdapter()); diff --git a/lib/core/models/configuration.dart b/lib/core/models/configuration.dart index 518cc4f1..b5de3052 100644 --- a/lib/core/models/configuration.dart +++ b/lib/core/models/configuration.dart @@ -1,2 +1,3 @@ +export 'configuration/external_module.dart'; export 'configuration/indexer.dart'; export 'configuration/profile.dart'; diff --git a/lib/core/models/configuration/external_module.dart b/lib/core/models/configuration/external_module.dart new file mode 100644 index 00000000..12d4075e --- /dev/null +++ b/lib/core/models/configuration/external_module.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +part 'external_module.g.dart'; + +/// Hive database object containing all information on an indexer +@HiveType(typeId: 26, adapterName: 'ExternalModuleHiveObjectAdapter') +class ExternalModuleHiveObject extends HiveObject { + /// Create a new [ExternalModuleHiveObject] object with all fields set to empty values('', false, 0, {}, etc.) + factory ExternalModuleHiveObject.empty() => ExternalModuleHiveObject( + displayName: '', + host: '', + ); + + /// Create a new [ExternalModuleHiveObject] from another [ExternalModuleHiveObject] (deep-copy). + factory ExternalModuleHiveObject.fromExternalModuleHiveObject( + ExternalModuleHiveObject indexer) => + ExternalModuleHiveObject( + displayName: indexer.displayName, + host: indexer.host, + ); + + /// Create a new [ExternalModuleHiveObject] from a map where the keys map 1-to-1 except for "key", which was the old key for apiKey. + /// + /// - Does _not_ do type checking, and will throw an error if the type is invalid. + /// - If the key is null, sets to the "empty" value + factory ExternalModuleHiveObject.fromMap(Map module) => + ExternalModuleHiveObject( + displayName: module['displayName'] ?? '', + host: module['host'] ?? '', + ); + + ExternalModuleHiveObject({ + @required this.displayName, + @required this.host, + }); + + @override + String toString() => toMap().toString(); + + Map toMap() => { + "displayName": displayName ?? '', + "host": host ?? '', + }; + + @HiveField(0) + String displayName; + @HiveField(1) + String host; +} diff --git a/lib/core/models/configuration/external_module.g.dart b/lib/core/models/configuration/external_module.g.dart new file mode 100644 index 00000000..0c5f2d3e --- /dev/null +++ b/lib/core/models/configuration/external_module.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'external_module.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ExternalModuleHiveObjectAdapter + extends TypeAdapter { + @override + final int typeId = 26; + + @override + ExternalModuleHiveObject read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ExternalModuleHiveObject( + displayName: fields[0] as String, + host: fields[1] as String, + ); + } + + @override + void write(BinaryWriter writer, ExternalModuleHiveObject obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.displayName) + ..writeByte(1) + ..write(obj.host); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ExternalModuleHiveObjectAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/core/models/configuration/profile.dart b/lib/core/models/configuration/profile.dart index d81ab581..a86a6a18 100644 --- a/lib/core/models/configuration/profile.dart +++ b/lib/core/models/configuration/profile.dart @@ -428,7 +428,7 @@ class ProfileHiveObject extends HiveObject { }; bool get anythingEnabled { - for (LunaModule module in LunaModule.DASHBOARD.allExternalModules()) { + for (LunaModule module in LunaModule.DASHBOARD.allIntegratedModules()) { if (module.isEnabled) return true; } return false; diff --git a/lib/core/modules.dart b/lib/core/modules.dart index c70d1e82..f22f4655 100644 --- a/lib/core/modules.dart +++ b/lib/core/modules.dart @@ -7,6 +7,7 @@ import 'package:quick_actions/quick_actions.dart'; part 'modules.g.dart'; const _DASHBOARD_KEY = 'dashboard'; +const _EXTERNAL_MODULES_KEY = 'externalmodules'; const _LIDARR_KEY = 'lidarr'; const _NZBGET_KEY = 'nzbget'; const _OVERSEERR_KEY = 'overseerr'; @@ -22,6 +23,8 @@ const _WAKE_ON_LAN_KEY = 'wake_on_lan'; enum LunaModule { @HiveField(0) DASHBOARD, + @HiveField(11) + EXTERNAL_MODULES, @HiveField(1) LIDARR, @HiveField(2) @@ -45,10 +48,10 @@ enum LunaModule { } extension LunaModuleExtension on LunaModule { - /// Returns a list of only active/enabled _external_ modules. + /// Returns a list of only _integrated_ modules. /// - /// External modules are modules that are not entirely internal (dashboard, settings, etc.) - List allExternalModules() { + /// Integrated modules are modules that are not entirely internal (dashboard, settings, etc.). + List allIntegratedModules() { List modules = LunaModule.values.toList() ..remove(LunaModule.DASHBOARD) ..remove(LunaModule.SETTINGS); @@ -84,6 +87,8 @@ extension LunaModuleExtension on LunaModule { return LunaModule.TAUTULLI; case _WAKE_ON_LAN_KEY: return LunaModule.WAKE_ON_LAN; + case _EXTERNAL_MODULES_KEY: + return LunaModule.EXTERNAL_MODULES; } return null; } @@ -113,6 +118,8 @@ extension LunaModuleExtension on LunaModule { return _TAUTULLI_KEY; case LunaModule.WAKE_ON_LAN: return _WAKE_ON_LAN_KEY; + case LunaModule.EXTERNAL_MODULES: + return _EXTERNAL_MODULES_KEY; } throw Exception('Invalid LunaModule'); } @@ -142,6 +149,8 @@ extension LunaModuleExtension on LunaModule { return Database.currentProfileObject?.tautulliEnabled ?? false; case LunaModule.WAKE_ON_LAN: return Database.currentProfileObject?.wakeOnLANEnabled ?? false; + case LunaModule.EXTERNAL_MODULES: + return Database.externalModulesBox?.isNotEmpty ?? false; } throw Exception('Invalid LunaModule'); } @@ -175,6 +184,10 @@ extension LunaModuleExtension on LunaModule { return Database.currentProfileObject.getTautulli(); case LunaModule.WAKE_ON_LAN: return Database.currentProfileObject.getWakeOnLAN(); + case LunaModule.EXTERNAL_MODULES: + return Database.externalModulesBox + .toMap() + .map((key, value) => MapEntry(key.toString(), value.toMap())); } throw Exception('Invalid LunaModule'); } @@ -204,6 +217,8 @@ extension LunaModuleExtension on LunaModule { return 'Overseerr'; case LunaModule.WAKE_ON_LAN: return 'Wake on LAN'; + case LunaModule.EXTERNAL_MODULES: + return 'lunasea.ExternalModules'.tr(); } throw Exception('Invalid LunaModule'); } @@ -233,6 +248,8 @@ extension LunaModuleExtension on LunaModule { return OverseerrHomeRouter().route(); case LunaModule.WAKE_ON_LAN: return null; + case LunaModule.EXTERNAL_MODULES: + return ExternalModulesHomeRouter().route(); } throw Exception('Invalid LunaModule'); } @@ -262,6 +279,8 @@ extension LunaModuleExtension on LunaModule { return LunaIcons.overseerr; case LunaModule.WAKE_ON_LAN: return Icons.settings_remote_rounded; + case LunaModule.EXTERNAL_MODULES: + return Icons.settings_ethernet_rounded; } throw Exception('Invalid LunaModule'); } @@ -291,6 +310,8 @@ extension LunaModuleExtension on LunaModule { return OverseerrDatabase(); case LunaModule.TAUTULLI: return TautulliDatabase(); + case LunaModule.EXTERNAL_MODULES: + return null; } throw Exception('Invalid LunaModule'); } @@ -320,6 +341,8 @@ extension LunaModuleExtension on LunaModule { return context.read(); case LunaModule.TAUTULLI: return context.read(); + case LunaModule.EXTERNAL_MODULES: + return null; } throw Exception('Invalid LunaModule'); } @@ -349,6 +372,8 @@ extension LunaModuleExtension on LunaModule { return Color(0xFF6366F1); case LunaModule.WAKE_ON_LAN: return LunaColours.accent; + case LunaModule.EXTERNAL_MODULES: + return LunaColours.accent; } throw Exception('Invalid LunaModule'); } @@ -378,6 +403,8 @@ extension LunaModuleExtension on LunaModule { return 'https://overseerr.dev'; case LunaModule.WAKE_ON_LAN: return null; + case LunaModule.EXTERNAL_MODULES: + return null; } throw Exception('Invalid LunaModule'); } @@ -407,6 +434,8 @@ extension LunaModuleExtension on LunaModule { return 'https://github.com/sct/overseerr'; case LunaModule.WAKE_ON_LAN: return null; + case LunaModule.EXTERNAL_MODULES: + return null; } throw Exception('Invalid LunaModule'); } @@ -436,6 +465,8 @@ extension LunaModuleExtension on LunaModule { return 'Manage Requests for New Content'; case LunaModule.WAKE_ON_LAN: return 'Wake Your Machine'; + case LunaModule.EXTERNAL_MODULES: + return 'Access External Modules'; } throw Exception('Invalid LunaModule'); } @@ -467,6 +498,8 @@ extension LunaModuleExtension on LunaModule { return 'Overseerr is a free and open source software application for managing requests for your media library. It integrates with your existing services, such as Sonarr, Radarr, and Plex!'; case LunaModule.WAKE_ON_LAN: return 'Wake on LAN is an industry standard protocol for waking computers up from a very low power mode remotely by sending a specially constructed packet to the machine.'; + case LunaModule.EXTERNAL_MODULES: + return 'LunaSea allows you to add additional modules that are not currently supported, allowing you to open the module\'s web GUI without having to leave LunaSea!'; } throw Exception('Invalid LunaModule'); } @@ -496,6 +529,8 @@ extension LunaModuleExtension on LunaModule { return ShortcutItem(type: key, localizedTitle: name); case LunaModule.WAKE_ON_LAN: return null; + case LunaModule.EXTERNAL_MODULES: + return ShortcutItem(type: key, localizedTitle: name); } throw Exception('Invalid LunaModule'); } @@ -525,6 +560,8 @@ extension LunaModuleExtension on LunaModule { return true; case LunaModule.WAKE_ON_LAN: return false; + case LunaModule.EXTERNAL_MODULES: + return false; } throw Exception('Invalid LunaModule'); } @@ -554,6 +591,8 @@ extension LunaModuleExtension on LunaModule { return; case LunaModule.WAKE_ON_LAN: return; + case LunaModule.EXTERNAL_MODULES: + return; } throw Exception('Invalid LunaModule'); } @@ -583,6 +622,8 @@ extension LunaModuleExtension on LunaModule { return TautulliWebhooks().handle(data); case LunaModule.WAKE_ON_LAN: return; + case LunaModule.EXTERNAL_MODULES: + return; } throw Exception('Invalid LunaModule'); } @@ -612,13 +653,17 @@ extension LunaModuleExtension on LunaModule { return SettingsConfigurationTautulliRouter(); case LunaModule.WAKE_ON_LAN: return SettingsConfigurationWakeOnLANRouter(); + case LunaModule.EXTERNAL_MODULES: + return SettingsConfigurationExternalModulesRouter(); } throw Exception('Invalid LunaModule'); } Future launch() async { if (route != null) - LunaState.navigatorKey.currentState - .pushNamedAndRemoveUntil(route, (Route route) => false); + LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil( + route, + (Route route) => false, + ); } } diff --git a/lib/core/modules.g.dart b/lib/core/modules.g.dart index 5df16ea8..80dec941 100644 --- a/lib/core/modules.g.dart +++ b/lib/core/modules.g.dart @@ -15,6 +15,8 @@ class LunaModuleAdapter extends TypeAdapter { switch (reader.readByte()) { case 0: return LunaModule.DASHBOARD; + case 11: + return LunaModule.EXTERNAL_MODULES; case 1: return LunaModule.LIDARR; case 2: @@ -46,6 +48,9 @@ class LunaModuleAdapter extends TypeAdapter { case LunaModule.DASHBOARD: writer.writeByte(0); break; + case LunaModule.EXTERNAL_MODULES: + writer.writeByte(11); + break; case LunaModule.LIDARR: writer.writeByte(1); break; diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index b34280e7..6cfcd73c 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -9,14 +9,16 @@ class LunaRouter { /// Calls `defineAllRoutes()` on all module routers that implement [LunaModuleRouter]. void initialize() { - router.notFoundHandler = - Handler(handlerFunc: (context, params) => LunaInvalidRoute()); + router.notFoundHandler = Handler( + handlerFunc: (context, params) => LunaInvalidRoute(), + ); DashboardRouter().defineAllRoutes(router); - SettingsRouter().defineAllRoutes(router); - SearchRouter().defineAllRoutes(router); - RadarrRouter().defineAllRoutes(router); - SonarrRouter().defineAllRoutes(router); + ExternalModulesRouter().defineAllRoutes(router); OverseerrRouter().defineAllRoutes(router); + RadarrRouter().defineAllRoutes(router); + SearchRouter().defineAllRoutes(router); + SettingsRouter().defineAllRoutes(router); + SonarrRouter().defineAllRoutes(router); TautulliRouter().defineAllRoutes(router); } diff --git a/lib/core/system/configuration.dart b/lib/core/system/configuration.dart index c8220222..3b1f527b 100644 --- a/lib/core/system/configuration.dart +++ b/lib/core/system/configuration.dart @@ -3,6 +3,11 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; class LunaConfiguration { + static const String _EXTERNAL_MODULES_KEY = 'external_modules'; + static const String _INDEXERS_KEY = 'indexers'; + static const String _LUNASEA_KEY = 'lunasea'; + static const String _PROFILES_KEY = 'profiles'; + /// Returns a list of all profiles converted to a map. List> _getProfiles() { List> _data = []; @@ -32,10 +37,24 @@ class LunaConfiguration { for (Map indexer in data) box.add(IndexerHiveObject.fromMap(indexer)); } + /// Returns a list of all external modules converted to a map. + List> _getExternalModules() { + List> _data = []; + for (var key in Database.externalModulesBox.keys) + _data.add(Database.externalModulesBox.get(key).toMap()); + return _data; + } + + /// Given a list of map objects, creates or updates external modules for each object. + void _setExternalModules(List data) { + Box box = Database.externalModulesBox; + for (Map module in data) box.add(ExternalModuleHiveObject.fromMap(module)); + } + /// Import the entire configuration from a JSON-encoded string (typically read through a `.lunasea` backup file). /// /// - Clears all boxes - /// - Calls `_setProfiles()` and `_setIndexers()` + /// - Calls [_setProfiles], [_setIndexers], and [_setExternalModules]. /// - Calls `import()` on all module databases, which implement [LunaDatabase]. /// - Resets the application state /// @@ -44,16 +63,22 @@ class LunaConfiguration { Map config = json.decode(data); Database().clearAllBoxes(); try { - if (config['profiles'] != null) _setProfiles(config['profiles']); - if (config['indexers'] != null) _setIndexers(config['indexers']); - if (config['lunasea'] != null) LunaDatabase().import(config['lunasea']); + if (config[_PROFILES_KEY] != null) _setProfiles(config[_PROFILES_KEY]); + if (config[_INDEXERS_KEY] != null) _setIndexers(config[_INDEXERS_KEY]); + if (config[_EXTERNAL_MODULES_KEY] != null) + _setExternalModules(config[_EXTERNAL_MODULES_KEY]); + if (config[_LUNASEA_KEY] != null) + LunaDatabase().import(config[_LUNASEA_KEY]); LunaModule.values.forEach((module) { if (config[module.key] != null) module.database?.import(config[module.key]); }); } catch (error, stack) { LunaLogger().error( - 'Failed to import configuration, resetting to default', error, stack); + 'Failed to import configuration, resetting to default', + error, + stack, + ); Database().setDefaults(); } // Reset the entire app's state @@ -66,9 +91,10 @@ class LunaConfiguration { /// - Calls `export()` on all module databases, which implement [LunaModuleDatabase]. String export() { Map config = { - "profiles": _getProfiles(), - "indexers": _getIndexers(), - "lunasea": LunaDatabase().export(), + _EXTERNAL_MODULES_KEY: _getExternalModules(), + _INDEXERS_KEY: _getIndexers(), + _PROFILES_KEY: _getProfiles(), + _LUNASEA_KEY: LunaDatabase().export(), }; LunaModule.values.forEach((module) { if (module.database != null) diff --git a/lib/core/ui/drawer/drawer.dart b/lib/core/ui/drawer/drawer.dart index d383afa4..c1db166f 100644 --- a/lib/core/ui/drawer/drawer.dart +++ b/lib/core/ui/drawer/drawer.dart @@ -12,10 +12,10 @@ class LunaDrawer extends StatelessWidget { static List moduleOrderedList() { LunaDatabaseValue dbValue = LunaDatabaseValue.DRAWER_MANUAL_ORDER; List _modules = (dbValue.data as List)?.cast(); - _modules ??= LunaModule.DASHBOARD.allExternalModules(); + _modules ??= LunaModule.DASHBOARD.allIntegratedModules(); // Add any modules that were added after the user set their drawer order preference _modules.addAll( - LunaModule.DASHBOARD.allExternalModules() + LunaModule.DASHBOARD.allIntegratedModules() ..retainWhere( (module) => !_modules.contains(module), ), @@ -53,7 +53,7 @@ class LunaDrawer extends StatelessWidget { } List _getAlphabeticalOrder(BuildContext context) { - List _modules = LunaModule.DASHBOARD.allExternalModules() + List _modules = LunaModule.DASHBOARD.allIntegratedModules() ..sort((a, b) => a.name.toLowerCase().compareTo( b.name.toLowerCase(), )); diff --git a/lib/modules.dart b/lib/modules.dart index dec0e4e1..50166691 100644 --- a/lib/modules.dart +++ b/lib/modules.dart @@ -1,4 +1,5 @@ export 'modules/dashboard.dart'; +export 'modules/external_modules.dart'; export 'modules/lidarr.dart'; export 'modules/nzbget.dart'; export 'modules/overseerr.dart'; diff --git a/lib/modules/dashboard/routes/modules/route.dart b/lib/modules/dashboard/routes/modules/route.dart index 35d75c45..d7282b81 100644 --- a/lib/modules/dashboard/routes/modules/route.dart +++ b/lib/modules/dashboard/routes/modules/route.dart @@ -43,7 +43,7 @@ class _State extends State List _buildAlphabeticalList() { List modules = []; int index = 0; - LunaModule.DASHBOARD.allExternalModules() + LunaModule.DASHBOARD.allIntegratedModules() ..sort((a, b) => a.name.toLowerCase().compareTo( b.name.toLowerCase(), )) diff --git a/lib/modules/external_modules.dart b/lib/modules/external_modules.dart new file mode 100644 index 00000000..80848422 --- /dev/null +++ b/lib/modules/external_modules.dart @@ -0,0 +1,2 @@ +export 'external_modules/core.dart'; +export 'external_modules/routes.dart'; diff --git a/lib/modules/external_modules/core.dart b/lib/modules/external_modules/core.dart new file mode 100644 index 00000000..2b7a0cc7 --- /dev/null +++ b/lib/modules/external_modules/core.dart @@ -0,0 +1 @@ +export 'core/router.dart'; diff --git a/lib/modules/external_modules/core/router.dart b/lib/modules/external_modules/core/router.dart new file mode 100644 index 00000000..c729bc5d --- /dev/null +++ b/lib/modules/external_modules/core/router.dart @@ -0,0 +1,44 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/external_modules.dart'; + +class ExternalModulesRouter extends LunaModuleRouter { + @override + void defineAllRoutes(FluroRouter router) { + ExternalModulesHomeRouter().defineRoute(router); + } +} + +abstract class ExternalModulesPageRouter extends LunaPageRouter { + ExternalModulesPageRouter(String route) : super(route); + + @override + void noParameterRouteDefinition( + FluroRouter router, { + bool homeRoute = false, + }) { + router.define( + fullRoute, + handler: Handler( + handlerFunc: (context, params) => widget(), + ), + transitionType: LunaRouter.transitionType, + ); + } + + @override + void withParameterRouteDefinition( + FluroRouter router, + Widget Function(BuildContext, Map>) handlerFunc, { + bool homeRoute = false, + }) { + router.define( + fullRoute, + handler: Handler( + handlerFunc: (context, params) => handlerFunc(context, params), + ), + transitionType: LunaRouter.transitionType, + ); + } +} diff --git a/lib/modules/external_modules/routes.dart b/lib/modules/external_modules/routes.dart new file mode 100644 index 00000000..6fb2a89e --- /dev/null +++ b/lib/modules/external_modules/routes.dart @@ -0,0 +1 @@ +export 'routes/external_modules.dart'; diff --git a/lib/modules/external_modules/routes/external_modules.dart b/lib/modules/external_modules/routes/external_modules.dart new file mode 100644 index 00000000..b5a6e2ec --- /dev/null +++ b/lib/modules/external_modules/routes/external_modules.dart @@ -0,0 +1,2 @@ +export 'external_modules/route.dart'; +export 'external_modules/widgets.dart'; diff --git a/lib/modules/external_modules/routes/external_modules/route.dart b/lib/modules/external_modules/routes/external_modules/route.dart new file mode 100644 index 00000000..2b9f4bb1 --- /dev/null +++ b/lib/modules/external_modules/routes/external_modules/route.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/external_modules.dart'; + +class ExternalModulesHomeRouter extends ExternalModulesPageRouter { + ExternalModulesHomeRouter() : super('/externalmodules'); + + @override + Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar(), + drawer: _drawer(), + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + useDrawer: true, + title: LunaModule.EXTERNAL_MODULES.name, + scrollControllers: [scrollController], + ); + } + + Widget _drawer() => LunaDrawer(page: LunaModule.EXTERNAL_MODULES.key); + + Widget _body() { + if ((Database.externalModulesBox.length ?? 0) == 0) { + return LunaMessage.moduleNotEnabled( + context: context, + module: LunaModule.EXTERNAL_MODULES.name, + ); + } + return LunaListView( + controller: scrollController, + children: _list, + ); + } + + List get _list { + List list = List.generate( + Database.externalModulesBox.length, + (index) => ExternalModulesModuleTile( + module: Database.externalModulesBox.getAt(index), + ), + ); + list.sort( + (a, b) => a.module.displayName + .toLowerCase() + .compareTo(b.module.displayName.toLowerCase()), + ); + return list; + } +} diff --git a/lib/modules/external_modules/routes/external_modules/widgets.dart b/lib/modules/external_modules/routes/external_modules/widgets.dart new file mode 100644 index 00000000..9821d502 --- /dev/null +++ b/lib/modules/external_modules/routes/external_modules/widgets.dart @@ -0,0 +1 @@ +export 'widgets/module_tile.dart'; diff --git a/lib/modules/external_modules/routes/external_modules/widgets/module_tile.dart b/lib/modules/external_modules/routes/external_modules/widgets/module_tile.dart new file mode 100644 index 00000000..f1a4ab94 --- /dev/null +++ b/lib/modules/external_modules/routes/external_modules/widgets/module_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class ExternalModulesModuleTile extends StatelessWidget { + final ExternalModuleHiveObject module; + + ExternalModulesModuleTile({ + Key key, + @required this.module, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: module.displayName), + subtitle: LunaText.subtitle(text: module.host), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => module.host.lunaOpenGenericLink(), + ); + } +} diff --git a/lib/modules/settings/core/dialogs.dart b/lib/modules/settings/core/dialogs.dart index 86c8a326..c60a15f3 100644 --- a/lib/modules/settings/core/dialogs.dart +++ b/lib/modules/settings/core/dialogs.dart @@ -135,6 +135,70 @@ class SettingsDialogs { return Tuple2(_flag, _textController.text); } + Future> editExternalModuleHost( + BuildContext context, { + String prefill = '', + }) async { + bool _flag = false; + final _formKey = GlobalKey(); + final _textController = TextEditingController()..text = prefill; + + void _setValues(bool flag) { + if (_formKey.currentState.validate()) { + _flag = flag; + Navigator.of(context).pop(); + } + } + + await LunaDialog.dialog( + context: context, + title: 'settings.Host'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Set'.tr(), + onPressed: () => _setValues(true), + ), + ], + content: [ + LunaDialog.textContent( + text: '${LunaUI.TEXT_BULLET}\t${'settings.HostHint1'.tr()}', + textAlign: TextAlign.left, + ), + LunaDialog.textContent( + text: '${LunaUI.TEXT_BULLET}\t${'settings.HostHint2'.tr()}', + textAlign: TextAlign.left, + ), + LunaDialog.textContent( + text: '${LunaUI.TEXT_BULLET}\t${'settings.HostHint3'.tr()}', + textAlign: TextAlign.left, + ), + LunaDialog.textContent( + text: '${LunaUI.TEXT_BULLET}\t${'settings.HostHint4'.tr()}', + textAlign: TextAlign.left, + ), + Form( + key: _formKey, + child: LunaDialog.textFormInput( + controller: _textController, + title: 'settings.Host'.tr(), + keyboardType: TextInputType.url, + onSubmitted: (_) => _setValues(true), + validator: (value) { + // Allow empty value + if (value == '') return null; + // Test for https:// or http:// + RegExp exp = RegExp(r"^(http|https)://", caseSensitive: false); + if (exp.hasMatch(value)) return null; + return 'settings.HostValidation'.tr(); + }, + ), + ), + ], + contentPadding: LunaDialog.inputTextDialogContentPadding(), + ); + return Tuple2(_flag, _textController.text); + } + Future deleteIndexer(BuildContext context) async { bool _flag = false; @@ -161,6 +225,32 @@ class SettingsDialogs { return _flag; } + Future deleteExternalModule(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'settings.DeleteModule'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Delete'.tr(), + textColor: LunaColours.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + LunaDialog.textContent(text: 'settings.DeleteModuleHint1'.tr()), + ], + contentPadding: LunaDialog.textDialogContentPadding(), + ); + return _flag; + } + Future deleteHeader(BuildContext context) async { bool _flag = false; diff --git a/lib/modules/settings/core/pages/headers.dart b/lib/modules/settings/core/pages/headers.dart index a760c486..5c316290 100644 --- a/lib/modules/settings/core/pages/headers.dart +++ b/lib/modules/settings/core/pages/headers.dart @@ -98,6 +98,8 @@ class _State extends State with LunaScrollControllerMixin { switch (widget.module) { case LunaModule.DASHBOARD: throw Exception('Dashboard does not have a headers page'); + case LunaModule.EXTERNAL_MODULES: + throw Exception('External modules do not have a headers page'); case LunaModule.LIDARR: return Database.currentProfileObject.lidarrHeaders; case LunaModule.RADARR: @@ -126,6 +128,8 @@ class _State extends State with LunaScrollControllerMixin { switch (widget.module) { case LunaModule.DASHBOARD: throw Exception('Dashboard does not have a headers page'); + case LunaModule.EXTERNAL_MODULES: + throw Exception('External modules do not have a headers page'); case LunaModule.LIDARR: return; case LunaModule.RADARR: diff --git a/lib/modules/settings/core/router.dart b/lib/modules/settings/core/router.dart index 9f75f623..efedc186 100644 --- a/lib/modules/settings/core/router.dart +++ b/lib/modules/settings/core/router.dart @@ -19,6 +19,10 @@ class SettingsRouter extends LunaModuleRouter { // Configuration/Dashboard SettingsConfigurationDashboardRouter().defineRoute(router); SettingsConfigurationDashboardCalendarSettingsRouter().defineRoute(router); + // Configuration/External Modules + SettingsConfigurationExternalModulesRouter().defineRoute(router); + SettingsConfigurationExternalModulesAddRouter().defineRoute(router); + SettingsConfigurationExternalModulesEditRouter().defineRoute(router); // Configuration/Lidarr SettingsConfigurationLidarrRouter().defineRoute(router); SettingsConfigurationLidarrConnectionDetailsRouter().defineRoute(router); diff --git a/lib/modules/settings/routes.dart b/lib/modules/settings/routes.dart index 9bf39430..27ab036c 100644 --- a/lib/modules/settings/routes.dart +++ b/lib/modules/settings/routes.dart @@ -3,6 +3,7 @@ export 'routes/configuration.dart'; export 'routes/configuration_appearance.dart'; export 'routes/configuration_dashboard.dart'; export 'routes/configuration_drawer.dart'; +export 'routes/configuration_externalmodules.dart'; export 'routes/configuration_lidarr.dart'; export 'routes/configuration_localization.dart'; export 'routes/configuration_nzbget.dart'; diff --git a/lib/modules/settings/routes/configuration_externalmodules.dart b/lib/modules/settings/routes/configuration_externalmodules.dart new file mode 100644 index 00000000..cedd9d8b --- /dev/null +++ b/lib/modules/settings/routes/configuration_externalmodules.dart @@ -0,0 +1,2 @@ +export 'configuration_externalmodules/pages.dart'; +export 'configuration_externalmodules/route.dart'; diff --git a/lib/modules/settings/routes/configuration_externalmodules/pages.dart b/lib/modules/settings/routes/configuration_externalmodules/pages.dart new file mode 100644 index 00000000..80c7d899 --- /dev/null +++ b/lib/modules/settings/routes/configuration_externalmodules/pages.dart @@ -0,0 +1,2 @@ +export 'pages/add_module.dart'; +export 'pages/edit_module.dart'; diff --git a/lib/modules/settings/routes/configuration_externalmodules/pages/add_module.dart b/lib/modules/settings/routes/configuration_externalmodules/pages/add_module.dart new file mode 100644 index 00000000..e34db46b --- /dev/null +++ b/lib/modules/settings/routes/configuration_externalmodules/pages/add_module.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsConfigurationExternalModulesAddRouter extends SettingsPageRouter { + SettingsConfigurationExternalModulesAddRouter() + : super('/settings/configuration/externalmodules/add'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final ExternalModuleHiveObject _module = ExternalModuleHiveObject.empty(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar(), + body: _body(), + bottomNavigationBar: _bottomNavigationBar(), + ); + } + + Widget _appBar() { + return LunaAppBar( + scrollControllers: [scrollController], + title: 'settings.AddModule'.tr(), + ); + } + + Widget _bottomNavigationBar() { + return LunaBottomActionBar( + actions: [ + LunaButton.text( + text: 'settings.AddModule'.tr(), + icon: Icons.add_rounded, + onTap: () async { + if (_module.displayName.isEmpty || _module.host.isEmpty) { + showLunaErrorSnackBar( + title: 'settings.AddModuleFailed'.tr(), + message: 'settings.AllFieldsAreRequired'.tr(), + ); + } else { + Database.externalModulesBox.add(_module); + showLunaSuccessSnackBar( + title: 'settings.AddModuleSuccess'.tr(), + message: _module.displayName, + ); + Navigator.of(context).pop(); + } + }, + ), + ], + ); + } + + Widget _body() { + return LunaListView( + controller: scrollController, + children: [ + _displayNameTile(), + _hostTile(), + ], + ); + } + + Widget _displayNameTile() { + return LunaListTile( + context: context, + title: LunaText.title(text: 'settings.DisplayName'.tr()), + subtitle: LunaText.subtitle( + text: _module.displayName == null || _module.displayName.isEmpty + ? 'lunasea.NotSet'.tr() + : _module.displayName, + ), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async { + Tuple2 values = await LunaDialogs().editText( + context, + 'settings.DisplayName'.tr(), + prefill: _module.displayName, + ); + if (values.item1 && mounted) + setState(() => _module.displayName = values.item2); + }, + ); + } + + Widget _hostTile() { + return LunaListTile( + context: context, + title: LunaText.title(text: 'settings.Host'.tr()), + subtitle: LunaText.subtitle( + text: _module.host == null || _module.host.isEmpty + ? 'lunasea.NotSet'.tr() + : _module.host, + ), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async { + Tuple2 values = + await SettingsDialogs().editExternalModuleHost( + context, + prefill: _module.host, + ); + if (values.item1 && mounted) + setState(() => _module.host = values.item2); + }, + ); + } +} diff --git a/lib/modules/settings/routes/configuration_externalmodules/pages/edit_module.dart b/lib/modules/settings/routes/configuration_externalmodules/pages/edit_module.dart new file mode 100644 index 00000000..3acf1d65 --- /dev/null +++ b/lib/modules/settings/routes/configuration_externalmodules/pages/edit_module.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsConfigurationExternalModulesEditRouter + extends SettingsPageRouter { + SettingsConfigurationExternalModulesEditRouter() + : super('/settings/configuration/externalmodules/edit/:moduleid'); + + @override + _Widget widget({ + @required int moduleId, + }) => + _Widget(moduleId: moduleId); + + @override + Future navigateTo( + BuildContext context, { + @required int moduleId, + }) async => + LunaRouter.router.navigateTo(context, route(moduleId: moduleId)); + + @override + String route({ + @required int moduleId, + }) => + super.fullRoute.replaceFirst(':moduleid', moduleId?.toString() ?? -1); + + @override + void defineRoute(FluroRouter router) { + super.withParameterRouteDefinition( + router, + (context, params) { + int moduleId = (params['moduleid']?.isNotEmpty ?? false) + ? (int.tryParse(params['moduleid'][0]) ?? -1) + : -1; + return _Widget(moduleId: moduleId); + }, + ); + } +} + +class _Widget extends StatefulWidget { + final int moduleId; + + _Widget({ + Key key, + @required this.moduleId, + }) : super(key: key); + + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + ExternalModuleHiveObject _module; + + @override + Widget build(BuildContext context) { + if (widget.moduleId < 0 || + !Database.externalModulesBox.containsKey(widget.moduleId)) { + return LunaInvalidRoute( + title: 'settings.EditModule'.tr(), + message: 'settings.ModuleNotFound'.tr(), + ); + } + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar(), + body: _body(), + bottomNavigationBar: _bottomNavigationBar(), + ); + } + + Widget _appBar() { + return LunaAppBar( + scrollControllers: [scrollController], + title: 'settings.EditModule'.tr(), + ); + } + + Widget _bottomNavigationBar() { + return LunaBottomActionBar( + actions: [ + LunaButton.text( + text: 'settings.DeleteModule'.tr(), + icon: Icons.delete_rounded, + color: LunaColours.red, + onTap: () async { + bool result = await SettingsDialogs().deleteExternalModule(context); + if (result) { + showLunaSuccessSnackBar( + title: 'settings.DeleteModuleSuccess'.tr(), + message: _module.displayName); + _module.delete(); + Navigator.of(context).pop(); + } + }, + ), + ], + ); + } + + Widget _body() { + return ValueListenableBuilder( + valueListenable: Database.externalModulesBox.listenable( + keys: [widget.moduleId], + ), + builder: (context, _, __) { + if (!Database.externalModulesBox.containsKey(widget.moduleId)) + return Container(); + _module = Database.externalModulesBox.get(widget.moduleId); + return LunaListView( + controller: scrollController, + children: [ + _displayNameTile(), + _hostTile(), + ], + ); + }, + ); + } + + Widget _displayNameTile() { + return LunaListTile( + context: context, + title: LunaText.title(text: 'settings.DisplayName'.tr()), + subtitle: LunaText.subtitle( + text: _module.displayName == null || _module.displayName.isEmpty + ? 'lunasea.NotSet'.tr() + : _module.displayName, + ), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async { + Tuple2 values = await LunaDialogs().editText( + context, + 'settings.DisplayName'.tr(), + prefill: _module.displayName, + ); + if (values.item1) _module.displayName = values.item2; + _module.save(); + }, + ); + } + + Widget _hostTile() { + return LunaListTile( + context: context, + title: LunaText.title(text: 'settings.Host'.tr()), + subtitle: LunaText.subtitle( + text: _module.host == null || _module.host.isEmpty + ? 'lunasea.NotSet'.tr() + : _module.host, + ), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async { + Tuple2 values = + await SettingsDialogs().editExternalModuleHost( + context, + prefill: _module.host, + ); + if (values.item1) _module.host = values.item2; + _module.save(); + }, + ); + } +} diff --git a/lib/modules/settings/routes/configuration_externalmodules/route.dart b/lib/modules/settings/routes/configuration_externalmodules/route.dart new file mode 100644 index 00000000..8ba73195 --- /dev/null +++ b/lib/modules/settings/routes/configuration_externalmodules/route.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsConfigurationExternalModulesRouter extends SettingsPageRouter { + SettingsConfigurationExternalModulesRouter() + : super('/settings/configuration/externalmodules'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar(), + body: _body(), + bottomNavigationBar: _bottomNavigationBar(), + ); + } + + Widget _appBar() { + return LunaAppBar( + scrollControllers: [scrollController], + title: LunaModule.EXTERNAL_MODULES.name, + actions: [ + LunaIconButton( + icon: Icons.help_outline, + onPressed: () async { + SettingsDialogs() + .moduleInformation(context, LunaModule.EXTERNAL_MODULES); + }, + ), + ], + ); + } + + Widget _bottomNavigationBar() { + return LunaBottomActionBar( + actions: [ + LunaButton.text( + text: 'settings.AddModule'.tr(), + icon: Icons.add_rounded, + onTap: () async => SettingsConfigurationExternalModulesAddRouter() + .navigateTo(context), + ), + ], + ); + } + + Widget _body() { + return ValueListenableBuilder( + valueListenable: Database.externalModulesBox.listenable(), + builder: (context, box, _) => LunaListView( + controller: scrollController, + children: [ + ..._moduleSection(), + ], + ), + ); + } + + List _moduleSection() => [ + if (Database.externalModulesBox.isEmpty) + LunaMessage(text: 'settings.NoExternalModulesFound'.tr()), + ..._modules, + ]; + + List get _modules { + List modules = + Database.externalModulesBox.values.toList(); + modules.sort((a, b) => + a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); + List list = List.generate( + modules.length, + (index) => _moduleTile(modules[index], modules[index].key), + ); + return list; + } + + Widget _moduleTile(ExternalModuleHiveObject module, int index) { + return LunaListTile( + context: context, + title: LunaText.title(text: module.displayName), + subtitle: LunaText.subtitle(text: module.host), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded), + onTap: () async => + SettingsConfigurationExternalModulesEditRouter().navigateTo( + context, + moduleId: index, + ), + ); + } +} diff --git a/lib/modules/settings/routes/configuration_search/route.dart b/lib/modules/settings/routes/configuration_search/route.dart index 6778d77a..d2be7250 100644 --- a/lib/modules/settings/routes/configuration_search/route.dart +++ b/lib/modules/settings/routes/configuration_search/route.dart @@ -66,14 +66,14 @@ class _State extends State<_Widget> with LunaScrollControllerMixin { builder: (context, box, _) => LunaListView( controller: scrollController, children: [ - ..._indexerSection, + ..._indexerSection(), ..._customization(), ], ), ); } - List get _indexerSection => [ + List _indexerSection() => [ if (Database.indexersBox.isEmpty) LunaMessage(text: 'No Indexers Found'), ..._indexers, @@ -83,9 +83,10 @@ class _State extends State<_Widget> with LunaScrollControllerMixin { List indexers = Database.indexersBox.values.toList(); indexers.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); - List list = List.generate(indexers.length, (index) { - return _indexerTile(indexers[index], indexers[index].key); - }); + List list = List.generate( + indexers.length, + (index) => _indexerTile(indexers[index], indexers[index].key), + ); return list; } diff --git a/localization/lunasea/en.json b/localization/lunasea/en.json index c0914426..78b93ffe 100644 --- a/localization/lunasea/en.json +++ b/localization/lunasea/en.json @@ -12,9 +12,11 @@ "lunasea.Disable": "Disable", "lunasea.Disabled": "Disabled", "lunasea.Dismiss": "Dismiss", + "lunasea.ExternalModules": "External Modules", "lunasea.GoBack": "Go Back", "lunasea.GoToSettings": "Go to Settings", "lunasea.IncorrectEncryptionKey": "Incorrect encryption key", + "lunasea.Module": "Module", "lunasea.ModuleIsNotEnabled": "{} Is Not Enabled", "lunasea.NotSet": "Not Set", "lunasea.NoModulesEnabled": "No Modules Enabled", diff --git a/localization/settings/en.json b/localization/settings/en.json index 56f39410..91458ffb 100644 --- a/localization/settings/en.json +++ b/localization/settings/en.json @@ -4,7 +4,11 @@ "settings.AccountHelp": "LunaSea Account", "settings.AccountHelpHint1": "LunaSea offers a free account to backup your configuration to the cloud, with additional features coming in the future!", "settings.AddHeader": "Add Header", + "settings.AddModule": "Add Module", + "settings.AddModuleFailed": "Failed to Add Module", + "settings.AddModuleSuccess": "Module Added", "settings.AddProfile": "Add Profile", + "settings.AllFieldsAreRequired": "All fields are required", "settings.AmoledTheme": "AMOLED Theme", "settings.AmoledThemeBorders": "AMOLED Theme Borders", "settings.AmoledThemeBordersDescription": "Add Subtle Borders Across the UI", @@ -67,14 +71,19 @@ "settings.DeleteHeaderHint1": "Are you sure you want to delete this header?", "settings.DeleteIndexer": "Delete Indexer", "settings.DeleteIndexerHint1": "Are you sure you want to delete this indexer?", + "settings.DeleteModule": "Delete Module", + "settings.DeleteModuleHint1": "Are you sure you want to delete this external module?", + "settings.DeleteModuleSuccess": "Module Deleted", "settings.DeleteProfile": "Delete Profile", "settings.DismissBanners": "Dismiss Banners", "settings.DismissBannersHint1": "Are you sure you want to dismiss all tooltip banners?", "settings.DismissBannersHint2": "The tooltip banners will give you tips and hints for features available within LunaSea.", + "settings.DisplayName": "Display Name", "settings.Donations": "Donations", "settings.DonationsDescription": "Donate to the Developer", "settings.Drawer": "Drawer", "settings.DrawerDescription": "Customize the Drawer", + "settings.EditModule": "Edit Module", "settings.Email": "Email", "settings.EmailSentFailure": "Failed to Reset Password", "settings.EmailSentSuccess": "Email Sent", @@ -120,9 +129,11 @@ "settings.MACAddressHint4": "Each hexidecimal octet should be separated by a colon", "settings.MACAddressValidation": "Invalid MAC Address", "settings.MinimumCharacters": "Minimum of {} characters", + "settings.ModuleNotFound": "Module Not Found", "settings.Notifications": "Notifications", "settings.NotificationsDescription": "Setup Webhooks for Push Notifications", "settings.NoBackupsFound": "No Backups Found", + "settings.NoExternalModulesFound": "No External Modules Found", "settings.NoHeadersAdded": "No Headers Added", "settings.OpenLinksIn": "Open Links In…", "settings.Password": "Password",