From b4df26abb689e3dcef466ec7d9531876895e7d25 Mon Sep 17 00:00:00 2001 From: Jagandeep Brar Date: Thu, 27 Aug 2020 09:26:05 -0500 Subject: [PATCH] [TestFlight] v4.0.0+400003 (#246) NEW - [Quick Actions] Enable home screen quick actions to quickly launch into up to 4 modules - [Tautulli/User] Implemented user pages including profile, history, synced, and IP addresses - [Settings/Resources] Added link to join TestFlight TWEAKS - [Home/Modules] Slightly adjusted design of quick links - [Settings] Slightly adjusted design of module tiles FIXES - [Home/Modules] Search tile would not show up after adding first indexer until a route change - [Tautulli/Activity] Tautulli could show an incorrect transcode container --- .../{config.yaml => config.yml} | 0 ios/Podfile.lock | 6 + ios/Runner.xcodeproj/project.pbxproj | 2 + ios/Runner/AppDelegate.swift | 10 + lib/core.dart | 2 + lib/core/constants.dart | 4 +- lib/core/database/adapters/lunasea.dart | 21 ++ lib/core/extensions/duration.dart | 22 ++ lib/core/homescreen_actions.dart | 50 ++++ lib/core/module_map.dart | 19 ++ lib/core/routes.dart | 2 + lib/core/ui/navigation/drawer.dart | 22 +- lib/modules/home/core.dart | 1 + lib/modules/home/core/constants.dart | 23 ++ lib/modules/home/routes/home.dart | 6 + lib/modules/home/routes/quick_access.dart | 42 ++-- lib/modules/home/widgets/summary_tile.dart | 5 +- lib/modules/lidarr/core/constants.dart | 23 +- lib/modules/nzbget/core/constants.dart | 23 +- lib/modules/radarr/core/constants.dart | 21 +- lib/modules/sabnzbd/core/constants.dart | 21 +- lib/modules/search/core/constants.dart | 23 +- lib/modules/settings/core/constants.dart | 15 +- lib/modules/settings/routes/modules.dart | 114 ++------- .../routes/modules/general_lunasea.dart | 61 +---- .../general_lunasea/general_lunasea.dart | 60 +++++ .../general_lunasea_quick_actions.dart | 58 +++++ lib/modules/settings/routes/system.dart | 8 + .../settings/widgets/modules/lunasea.dart | 1 + .../modules/lunasea/quick_actions_tile.dart | 15 ++ lib/modules/sonarr/core/constants.dart | 21 +- lib/modules/tautulli/core/constants.dart | 34 ++- lib/modules/tautulli/core/state.dart | 85 ++++++- lib/modules/tautulli/modules.dart | 12 + .../tautulli/modules/activity/route.dart | 12 +- .../activity/widgets/activity_tile.dart | 12 +- .../activity_details/widgets/information.dart | 3 +- lib/modules/tautulli/modules/graphs.dart | 2 + .../tautulli/modules/graphs/route.dart | 0 .../tautulli/modules/graphs/widgets.dart | 0 .../tautulli/modules/history/widgets.dart | 1 + .../modules/history/widgets/history_tile.dart | 159 +++++++++++++ lib/modules/tautulli/modules/libraries.dart | 2 + .../tautulli/modules/libraries/route.dart | 0 .../tautulli/modules/libraries/widgets.dart | 0 .../tautulli/modules/recently_added.dart | 2 + .../modules/recently_added/route.dart | 0 .../modules/recently_added/widgets.dart | 0 lib/modules/tautulli/modules/statistics.dart | 2 + .../tautulli/modules/statistics/route.dart | 0 .../tautulli/modules/statistics/widgets.dart | 0 .../tautulli/modules/synced_items.dart | 2 + .../tautulli/modules/synced_items/route.dart | 25 ++ .../modules/synced_items/widgets.dart | 1 + .../widgets/synced_item_tile.dart | 65 ++++++ .../tautulli/widgets/navigation_bar.dart | 2 +- .../tautulli/modules/user_details.dart | 2 + .../tautulli/modules/user_details/route.dart | 70 ++++++ .../modules/user_details/widgets.dart | 5 + .../modules/user_details/widgets/history.dart | 91 ++++++++ .../user_details/widgets/ip_addresses.dart | 121 ++++++++++ .../user_details/widgets/navigation_bar.dart | 51 +++++ .../modules/user_details/widgets/profile.dart | 216 ++++++++++++++++++ .../user_details/widgets/synced_items.dart | 88 +++++++ lib/modules/tautulli/modules/users/route.dart | 16 +- .../modules/users/widgets/user_tile.dart | 26 +-- lib/modules/wake_on_lan/core/constants.dart | 15 +- pubspec.lock | 27 ++- pubspec.yaml | 9 +- 69 files changed, 1550 insertions(+), 309 deletions(-) rename .github/ISSUE_TEMPLATE/{config.yaml => config.yml} (100%) create mode 100644 lib/core/homescreen_actions.dart create mode 100644 lib/core/module_map.dart create mode 100644 lib/modules/home/core/constants.dart create mode 100644 lib/modules/settings/routes/modules/general_lunasea/general_lunasea.dart create mode 100644 lib/modules/settings/routes/modules/general_lunasea/general_lunasea_quick_actions.dart create mode 100644 lib/modules/settings/widgets/modules/lunasea/quick_actions_tile.dart create mode 100644 lib/modules/tautulli/modules/graphs.dart create mode 100644 lib/modules/tautulli/modules/graphs/route.dart create mode 100644 lib/modules/tautulli/modules/graphs/widgets.dart create mode 100644 lib/modules/tautulli/modules/history/widgets/history_tile.dart create mode 100644 lib/modules/tautulli/modules/libraries.dart create mode 100644 lib/modules/tautulli/modules/libraries/route.dart create mode 100644 lib/modules/tautulli/modules/libraries/widgets.dart create mode 100644 lib/modules/tautulli/modules/recently_added.dart create mode 100644 lib/modules/tautulli/modules/recently_added/route.dart create mode 100644 lib/modules/tautulli/modules/recently_added/widgets.dart create mode 100644 lib/modules/tautulli/modules/statistics.dart create mode 100644 lib/modules/tautulli/modules/statistics/route.dart create mode 100644 lib/modules/tautulli/modules/statistics/widgets.dart create mode 100644 lib/modules/tautulli/modules/synced_items.dart create mode 100644 lib/modules/tautulli/modules/synced_items/route.dart create mode 100644 lib/modules/tautulli/modules/synced_items/widgets.dart create mode 100644 lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart create mode 100644 lib/modules/tautulli/modules/user_details.dart create mode 100644 lib/modules/tautulli/modules/user_details/route.dart create mode 100644 lib/modules/tautulli/modules/user_details/widgets.dart create mode 100644 lib/modules/tautulli/modules/user_details/widgets/history.dart create mode 100644 lib/modules/tautulli/modules/user_details/widgets/ip_addresses.dart create mode 100644 lib/modules/tautulli/modules/user_details/widgets/navigation_bar.dart create mode 100644 lib/modules/tautulli/modules/user_details/widgets/profile.dart create mode 100644 lib/modules/tautulli/modules/user_details/widgets/synced_items.dart diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yaml rename to .github/ISSUE_TEMPLATE/config.yml diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 084cc9e4..50b8ee6b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -49,6 +49,8 @@ PODS: - Flutter - "permission_handler (5.0.1+1)": - Flutter + - quick_actions (0.0.1): + - Flutter - Reachability (3.2) - SDWebImage (5.8.0): - SDWebImage/Core (= 5.8.0) @@ -70,6 +72,7 @@ DEPENDENCIES: - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - permission_handler (from `.symlinks/plugins/permission_handler/ios`) + - quick_actions (from `.symlinks/plugins/quick_actions/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) @@ -98,6 +101,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider/ios" permission_handler: :path: ".symlinks/plugins/permission_handler/ios" + quick_actions: + :path: ".symlinks/plugins/quick_actions/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher: @@ -115,6 +120,7 @@ SPEC CHECKSUMS: package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6 + quick_actions: 6cb2390c4dab0e737c94573c27e18d9666710720 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SDWebImage: 84000f962cbfa70c07f19d2234cbfcf5d779b5dc SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1438cdcf..ceb5dbeb 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -261,6 +261,7 @@ "${BUILT_PRODUCTS_DIR}/in_app_purchase/in_app_purchase.framework", "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/quick_actions/quick_actions.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", ); @@ -279,6 +280,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/quick_actions.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", ); diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 0437245c..6338b1b1 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -13,4 +13,14 @@ import Flutter GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + @available(iOS 9.0, *) + override func application( + _ application: UIApplication, + performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void + ) { + let controller = window.rootViewController as? FlutterViewController + let channel = FlutterMethodChannel(name: "plugins.flutter.io/quick_actions", binaryMessenger: controller! as! FlutterBinaryMessenger) + channel.invokeMethod("launch", arguments: shortcutItem.type) + } } diff --git a/lib/core.dart b/lib/core.dart index 79943e7c..230e58f4 100644 --- a/lib/core.dart +++ b/lib/core.dart @@ -3,9 +3,11 @@ export 'core/configuration.dart'; export 'core/database.dart'; export 'core/dialogs.dart'; export 'core/extensions.dart'; +export 'core/homescreen_actions.dart'; export 'core/in_app_purchases.dart'; export 'core/logger.dart'; export 'core/module_flags.dart'; +export 'core/module_map.dart'; export 'core/providers.dart'; export 'core/routes.dart'; export 'core/state.dart'; diff --git a/lib/core/constants.dart b/lib/core/constants.dart index a3c1aa07..ee6ecf8d 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lunasea/core/module_map.dart'; import 'package:lunasea/modules.dart' show LidarrConstants, RadarrConstants, @@ -15,7 +16,7 @@ class Constants { static const APPLICATION_NAME = "LunaSea"; static const SENTRY_DSN = "https://511f76efcf714ecfb5ed6b26b5819bd6@o426090.ingest.sentry.io/5367513"; //Services - static const Map MODULE_MAP = { + static const Map MODULE_MAP = { LidarrConstants.MODULE_KEY: LidarrConstants.MODULE_MAP, RadarrConstants.MODULE_KEY: RadarrConstants.MODULE_MAP, SonarrConstants.MODULE_KEY: SonarrConstants.MODULE_MAP, @@ -77,6 +78,7 @@ class Constants { static const URL_CHANGELOG = 'https://docs.lunasea.app/development/changelog'; static const URL_DISCORD = 'https://discord.com/invite/8MH2N3h'; static const URL_SENTRY = 'https://sentry.io'; + static const URL_TESTFLIGHT = 'https://testflight.apple.com/join/WWXaybra'; static const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15'; //Automation static const Map historyReasonMessages = { diff --git a/lib/core/database/adapters/lunasea.dart b/lib/core/database/adapters/lunasea.dart index 9a593ba9..68bbbbf0 100644 --- a/lib/core/database/adapters/lunasea.dart +++ b/lib/core/database/adapters/lunasea.dart @@ -19,6 +19,13 @@ enum LunaSeaDatabaseValue { DRAWER_EXPAND_AUTOMATION, DRAWER_EXPAND_CLIENTS, DRAWER_EXPAND_MONITORING, + QUICK_ACTIONS_LIDARR, + QUICK_ACTIONS_RADARR, + QUICK_ACTIONS_SONARR, + QUICK_ACTIONS_NZBGET, + QUICK_ACTIONS_SABNZBD, + QUICK_ACTIONS_TAUTULLI, + QUICK_ACTIONS_SEARCH, } extension LunaSeaDatabaseValueExtension on LunaSeaDatabaseValue { @@ -33,6 +40,13 @@ extension LunaSeaDatabaseValueExtension on LunaSeaDatabaseValue { case LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION: return 'LUNASEA_DRAWER_EXPAND_AUTOMATION'; case LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS: return 'LUNASEA_DRAWER_EXPAND_CLIENTS'; case LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING: return 'LUNASEA_DRAWER_EXPAND_MONITORING'; + case LunaSeaDatabaseValue.QUICK_ACTIONS_LIDARR: return 'LUNASEA_QUICK_ACTIONS_LIDARR'; + case LunaSeaDatabaseValue.QUICK_ACTIONS_RADARR: return 'LUNASEA_QUICK_ACTIONS_RADARR'; + case LunaSeaDatabaseValue.QUICK_ACTIONS_SONARR: return 'LUNASEA_QUICK_ACTIONS_SONARR'; + case LunaSeaDatabaseValue.QUICK_ACTIONS_NZBGET: return 'LUNASEA_QUICK_ACTIONS_NZBGET'; + case LunaSeaDatabaseValue.QUICK_ACTIONS_SABNZBD: return 'LUNASEA_QUICK_ACTIONS_SABNZBD'; + case LunaSeaDatabaseValue.QUICK_ACTIONS_TAUTULLI: return 'LUNASEA_QUICK_ACTIONS_TAUTULLI'; + case LunaSeaDatabaseValue.QUICK_ACTIONS_SEARCH: return 'LUNASEA_QUICK_ACTIONS_SEARCH'; } throw Exception('key not found'); } @@ -49,6 +63,13 @@ extension LunaSeaDatabaseValueExtension on LunaSeaDatabaseValue { case LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION: return _box.get(this.key, defaultValue: true); case LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS: return _box.get(this.key, defaultValue: true); case LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING: return _box.get(this.key, defaultValue: true); + case LunaSeaDatabaseValue.QUICK_ACTIONS_LIDARR: return _box.get(this.key, defaultValue: false); + case LunaSeaDatabaseValue.QUICK_ACTIONS_RADARR: return _box.get(this.key, defaultValue: false); + case LunaSeaDatabaseValue.QUICK_ACTIONS_SONARR: return _box.get(this.key, defaultValue: false); + case LunaSeaDatabaseValue.QUICK_ACTIONS_NZBGET: return _box.get(this.key, defaultValue: false); + case LunaSeaDatabaseValue.QUICK_ACTIONS_SABNZBD: return _box.get(this.key, defaultValue: false); + case LunaSeaDatabaseValue.QUICK_ACTIONS_TAUTULLI: return _box.get(this.key, defaultValue: false); + case LunaSeaDatabaseValue.QUICK_ACTIONS_SEARCH: return _box.get(this.key, defaultValue: false); } throw Exception('data not found'); } diff --git a/lib/core/extensions/duration.dart b/lib/core/extensions/duration.dart index f6641719..bae20937 100644 --- a/lib/core/extensions/duration.dart +++ b/lib/core/extensions/duration.dart @@ -1,4 +1,7 @@ +import 'package:lunasea/core.dart'; + extension DurationExtension on Duration { + /// Creates a HH:MM:SS timestamp //ignore: non_constant_identifier_names String lsDuration_timestamp() { String hours = this.inHours.toString().padLeft(2, '0'); @@ -10,4 +13,23 @@ extension DurationExtension on Duration { '$seconds', ].join(); } + + + /// Creates a full timestamp, akin to 1 Day, 23 Hours, 10 Minutes + //ignore: non_constant_identifier_names + String lsDuration_fullTimestamp() { + String days, hours, minutes; + if(this.inDays == 1) days = '1 Day '; + if(this.inDays > 1) days = '${this.inDays.toString()} Days '; + if(this.inHours == 1) hours = '1 Hour '; + if(this.inHours > 1) hours = '${(this.inHours%24).toString()} Hours '; + if(this.inMinutes == 1) minutes = '1 Minute '; + if(this.inMinutes > 1) minutes = '${(this.inMinutes%60).toString()} Minutes '; + return [ + if(days != null) days, + if(hours != null) hours, + if(minutes != null) minutes, + if(minutes == null) Constants.TEXT_EMDASH, + ].join(); + } } diff --git a/lib/core/homescreen_actions.dart b/lib/core/homescreen_actions.dart new file mode 100644 index 00000000..559b22c7 --- /dev/null +++ b/lib/core/homescreen_actions.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:quick_actions/quick_actions.dart'; +import 'package:lunasea/modules.dart'; +export 'package:quick_actions/quick_actions.dart' show ShortcutItem; + +class HomescreenActions { + static final QuickActions _quickActions = QuickActions(); + + HomescreenActions._(); + + static void initialize(BuildContext context) { + _quickActions.initialize((action) => _handler(context, action)); + setShortcutItems(); + } + + static void setShortcutItems() { + _quickActions.setShortcutItems([ + if(LunaSeaDatabaseValue.QUICK_ACTIONS_SEARCH.data) SearchConstants.MODULE_QUICK_ACTION, + if(LunaSeaDatabaseValue.QUICK_ACTIONS_LIDARR.data) LidarrConstants.MODULE_QUICK_ACTION, + if(LunaSeaDatabaseValue.QUICK_ACTIONS_RADARR.data) RadarrConstants.MODULE_QUICK_ACTION, + if(LunaSeaDatabaseValue.QUICK_ACTIONS_SONARR.data) SonarrConstants.MODULE_QUICK_ACTION, + if(LunaSeaDatabaseValue.QUICK_ACTIONS_NZBGET.data) NZBGetConstants.MODULE_QUICK_ACTION, + if(LunaSeaDatabaseValue.QUICK_ACTIONS_SABNZBD.data) SABnzbdConstants.MODULE_QUICK_ACTION, + if(LunaSeaDatabaseValue.QUICK_ACTIONS_TAUTULLI.data) TautulliConstants.MODULE_QUICK_ACTION, + ]); + } + + static void _handler(BuildContext context, String action) { + if(action != null) { + switch(action) { + case SearchConstants.MODULE_KEY: _pushSearch(context); break; + case LidarrConstants.MODULE_KEY: _pushLidarr(context); break; + case RadarrConstants.MODULE_KEY: _pushRadarr(context); break; + case SonarrConstants.MODULE_KEY: _pushSonarr(context); break; + case NZBGetConstants.MODULE_KEY: _pushNZBGet(context); break; + case SABnzbdConstants.MODULE_KEY: _pushSABnzbd(context); break; + case TautulliConstants.MODULE_KEY: _pushTautulli(context); break; + } + } + } + + static void _pushSearch(BuildContext context) => Navigator.of(context).pushNamed(Search.ROUTE_NAME); + static void _pushLidarr(BuildContext context) => Navigator.of(context).pushNamed(Lidarr.ROUTE_NAME); + static void _pushRadarr(BuildContext context) => Navigator.of(context).pushNamed(Radarr.ROUTE_NAME); + static void _pushSonarr(BuildContext context) => Navigator.of(context).pushNamed(Sonarr.ROUTE_NAME); + static void _pushNZBGet(BuildContext context) => Navigator.of(context).pushNamed(NZBGet.ROUTE_NAME); + static void _pushSABnzbd(BuildContext context) => Navigator.of(context).pushNamed(SABnzbd.ROUTE_NAME); + static void _pushTautulli(BuildContext context) => Navigator.of(context).pushNamed(Tautulli.ROUTE_NAME); +} diff --git a/lib/core/module_map.dart b/lib/core/module_map.dart new file mode 100644 index 00000000..f51888a8 --- /dev/null +++ b/lib/core/module_map.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class ModuleMap { + final String name; + final String description; + final String settingsDescription; + final String route; + final IconData icon; + final Color color; + + const ModuleMap({ + @required this.name, + @required this.description, + @required this.settingsDescription, + @required this.route, + @required this.icon, + @required this.color, + }); +} diff --git a/lib/core/routes.dart b/lib/core/routes.dart index 18eb2cea..9bd87cb4 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -62,6 +62,8 @@ class Routes { SettingsModulesLunaSea.ROUTE_NAME: (context) => SettingsModulesLunaSea(), SettingsModulesSearch.ROUTE_NAME: (context) => SettingsModulesSearch(), SettingsModulesWakeOnLAN.ROUTE_NAME: (context) => SettingsModulesWakeOnLAN(), + // /settings/modules/lunasea/* + SettingsModulesLunaSeaQuickActions.ROUTE_NAME: (context) => SettingsModulesLunaSeaQuickActions(), // /settings/modules/indexers/* SettingsModulesSearchAdd.ROUTE_NAME: (context) => SettingsModulesSearchAdd(), SettingsModulesSearchEdit.ROUTE_NAME: (context) => SettingsModulesSearchEdit(), diff --git a/lib/core/ui/navigation/drawer.dart b/lib/core/ui/navigation/drawer.dart index 6c851b03..d50af2ad 100644 --- a/lib/core/ui/navigation/drawer.dart +++ b/lib/core/ui/navigation/drawer.dart @@ -120,9 +120,9 @@ class LSDrawer extends StatelessWidget { Database.currentProfileObject.enabledAutomationModules.length, (index) => _buildEntry( context: context, - route: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationModules[index]]['route'], - icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationModules[index]]['icon'], - title: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationModules[index]]['name'], + route: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationModules[index]].route, + icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationModules[index]].icon, + title: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationModules[index]].name, padLeft: true, ), ), @@ -140,9 +140,9 @@ class LSDrawer extends StatelessWidget { Database.currentProfileObject.enabledClientModules.length, (index) => _buildEntry( context: context, - route: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientModules[index]]['route'], - icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientModules[index]]['icon'], - title: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientModules[index]]['name'], + route: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientModules[index]].route, + icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientModules[index]].icon, + title: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientModules[index]].name, padLeft: true, ), ), @@ -160,9 +160,9 @@ class LSDrawer extends StatelessWidget { Database.currentProfileObject.enabledMonitoringModules.length, (index) => _buildEntry( context: context, - route: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]]['route'], - icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]]['icon'], - title: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]]['name'], + route: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]].route, + icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]].icon, + title: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]].name, padLeft: true, ), ), @@ -211,9 +211,9 @@ class LSDrawer extends StatelessWidget { @required BuildContext context, }) { return ListTile( - leading: LSIcon(icon: Constants.MODULE_MAP['wake_on_lan']['icon']), + leading: LSIcon(icon: Constants.MODULE_MAP[WakeOnLANConstants.MODULE_KEY].icon), title: Text( - Constants.MODULE_MAP['wake_on_lan']['name'], + Constants.MODULE_MAP[WakeOnLANConstants.MODULE_KEY].name, style: TextStyle( color: Colors.white, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, diff --git a/lib/modules/home/core.dart b/lib/modules/home/core.dart index fb66fe12..ca505565 100644 --- a/lib/modules/home/core.dart +++ b/lib/modules/home/core.dart @@ -1,5 +1,6 @@ export 'core/api.dart'; export 'core/adapters.dart'; +export 'core/constants.dart'; export 'core/database.dart'; export 'core/state.dart'; export 'core/dialogs.dart'; diff --git a/lib/modules/home/core/constants.dart b/lib/modules/home/core/constants.dart new file mode 100644 index 00000000..314842a2 --- /dev/null +++ b/lib/modules/home/core/constants.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class HomeConstants { + HomeConstants._(); + + static const MODULE_KEY = 'home'; + + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Home', + description: 'Home', + settingsDescription: 'Configure the Home Screen', + icon: CustomIcons.home, + route: '/', + color: Color(Constants.ACCENT_COLOR), + ); + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); +} diff --git a/lib/modules/home/routes/home.dart b/lib/modules/home/routes/home.dart index b073125d..290fe1bc 100644 --- a/lib/modules/home/routes/home.dart +++ b/lib/modules/home/routes/home.dart @@ -16,6 +16,12 @@ class _State extends State { final List _refreshKeys = [GlobalKey()]; String _profileState = Database.currentProfileObject.toString(); + @override + void initState() { + super.initState(); + HomescreenActions.initialize(context); + } + @override Widget build(BuildContext context) => WillPopScope( onWillPop: _willPopScope, diff --git a/lib/modules/home/routes/quick_access.dart b/lib/modules/home/routes/quick_access.dart index 71764273..d0e1202f 100644 --- a/lib/modules/home/routes/quick_access.dart +++ b/lib/modules/home/routes/quick_access.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; +import 'package:lunasea/modules.dart'; import 'package:lunasea/modules/home.dart'; class HomeQuickAccess extends StatefulWidget { @@ -17,9 +18,12 @@ class _State extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - return widget.profile.anythingEnabled || Database.indexersBox.length > 0 - ? _body - : LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false); + return ValueListenableBuilder( + valueListenable: Database.indexersBox.listenable(), + builder: (context, box, _) => widget.profile.anythingEnabled || Database.indexersBox.length > 0 + ? _body + : LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false), + ); } Widget get _body { @@ -30,30 +34,30 @@ class _State extends State with AutomaticKeepAliveClientMixin { itemBuilder: (context, index) { //Check if indexer search should be included at the top if(index == 0 && _hasIndexers) return HomeSummaryTile( - title: Constants.MODULE_MAP['search']['name'], - subtitle: Constants.MODULE_MAP['search']['desc'], - icon: Constants.MODULE_MAP['search']['icon'], + title: Constants.MODULE_MAP[SearchConstants.MODULE_KEY].name, + subtitle: Constants.MODULE_MAP[SearchConstants.MODULE_KEY].description, + icon: Constants.MODULE_MAP[SearchConstants.MODULE_KEY].icon, index: index, - route: Constants.MODULE_MAP['search']['route'], - color: Constants.MODULE_MAP['search']['color'], + route: Constants.MODULE_MAP[SearchConstants.MODULE_KEY].route, + color: Constants.MODULE_MAP[SearchConstants.MODULE_KEY].color, ); if(index == _serviceCount-1) return HomeSummaryTile( - title: Constants.MODULE_MAP['settings']['name'], - subtitle: Constants.MODULE_MAP['settings']['desc'], - icon: Constants.MODULE_MAP['settings']['icon'], + title: Constants.MODULE_MAP[SettingsConstants.MODULE_KEY].name, + subtitle: Constants.MODULE_MAP[SettingsConstants.MODULE_KEY].description, + icon: Constants.MODULE_MAP[SettingsConstants.MODULE_KEY].icon, index: index, - route: Constants.MODULE_MAP['settings']['route'], - color: Constants.MODULE_MAP['settings']['color'], + route: Constants.MODULE_MAP[SettingsConstants.MODULE_KEY].route, + color: Constants.MODULE_MAP[SettingsConstants.MODULE_KEY].color, justPush: true, ); - Map data = Constants.MODULE_MAP[widget.profile.enabledModules[_hasIndexers ? index-1 : index]]; + ModuleMap data = Constants.MODULE_MAP[widget.profile.enabledModules[_hasIndexers ? index-1 : index]]; if(data != null) return HomeSummaryTile( - title: data['name'], - subtitle: data['desc'], - icon: data['icon'], + title: data.name, + subtitle: data.description, + icon: data.icon, index: index, - route: data['route'], - color: data['color'], + route: data.route, + color: data.color, ); return Container(); }, diff --git a/lib/modules/home/widgets/summary_tile.dart b/lib/modules/home/widgets/summary_tile.dart index 8b1bde4e..e026af94 100644 --- a/lib/modules/home/widgets/summary_tile.dart +++ b/lib/modules/home/widgets/summary_tile.dart @@ -27,15 +27,12 @@ class HomeSummaryTile extends StatelessWidget { builder: (context, box, _) => LSCardTile( title: LSTitle(text: title), subtitle: LSSubtitle(text: subtitle), - leading: LSIconButton( + trailing: LSIconButton( icon: icon, color: HomeDatabaseValue.MODULES_BRAND_COLOURS.data ? color : LSColors.list(index), ), - trailing: LSIconButton( - icon: Icons.arrow_forward_ios, - ), onTap: () async => justPush ? Navigator.of(context).pushNamed(route) : Navigator.of(context).pushNamedAndRemoveUntil(route, (Route route) => false), diff --git a/lib/modules/lidarr/core/constants.dart b/lib/modules/lidarr/core/constants.dart index ea1e8362..a0d4d81e 100644 --- a/lib/modules/lidarr/core/constants.dart +++ b/lib/modules/lidarr/core/constants.dart @@ -4,15 +4,16 @@ import 'package:lunasea/core.dart'; class LidarrConstants { LidarrConstants._(); - static const String MODULE_KEY = 'lidarr'; + static const MODULE_KEY = 'lidarr'; - static const Map MODULE_MAP = { - 'name': 'Lidarr', - 'desc': 'Manage Music', - 'icon': CustomIcons.music, - 'route': '/lidarr', - 'color': Color(0xFF159552), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Lidarr', + description: 'Manage Music', + settingsDescription: 'Configure Lidarr', + icon: CustomIcons.music, + route: '/lidarr', + color: Color(0xFF159552), + ); static const Map EVENT_TYPE_MESSAGES = { 'trackFileRenamed': 'Track File Renamed', @@ -23,4 +24,10 @@ class LidarrConstants { 'downloadFailed': 'Download Failed', 'grabbed': 'Grabbed From', }; + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); } \ No newline at end of file diff --git a/lib/modules/nzbget/core/constants.dart b/lib/modules/nzbget/core/constants.dart index 99526842..df4a4cc1 100644 --- a/lib/modules/nzbget/core/constants.dart +++ b/lib/modules/nzbget/core/constants.dart @@ -4,13 +4,20 @@ import 'package:lunasea/core.dart'; class NZBGetConstants { NZBGetConstants._(); - static const String MODULE_KEY = 'nzbget'; + static const MODULE_KEY = 'nzbget'; - static const Map MODULE_MAP = { - 'name': 'NZBGet', - 'desc': 'Manage Usenet Downloads', - 'icon': CustomIcons.nzbget, - 'route': '/nzbget', - 'color': Color(0xFF42D535), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'NZBGet', + description: 'Manage Usenet Downloads', + settingsDescription: 'Configure NZBGet', + icon: CustomIcons.nzbget, + route: '/nzbget', + color: Color(0xFF42D535), + ); + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); } diff --git a/lib/modules/radarr/core/constants.dart b/lib/modules/radarr/core/constants.dart index 303a8244..c629f433 100644 --- a/lib/modules/radarr/core/constants.dart +++ b/lib/modules/radarr/core/constants.dart @@ -7,13 +7,20 @@ class RadarrConstants { static const String MODULE_KEY = 'radarr'; - static const Map MODULE_MAP = { - 'name': 'Radarr', - 'desc': 'Manage Movies', - 'icon': CustomIcons.movies, - 'route': '/radarr', - 'color': Color(0xFFFEC333), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Radarr', + description: 'Manage Movies', + settingsDescription: 'Configure Radarr', + icon: CustomIcons.movies, + route: '/radarr', + color: Color(0xFFFEC333), + ); + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); static const Map EVENT_TYPE_MESSAGES = { 'movieFileRenamed': 'Movie File Renamed', diff --git a/lib/modules/sabnzbd/core/constants.dart b/lib/modules/sabnzbd/core/constants.dart index 48b2029d..9f0ae34c 100644 --- a/lib/modules/sabnzbd/core/constants.dart +++ b/lib/modules/sabnzbd/core/constants.dart @@ -6,11 +6,18 @@ class SABnzbdConstants { static const String MODULE_KEY = 'sabnzbd'; - static const Map MODULE_MAP = { - 'name': 'SABnzbd', - 'desc': 'Manage Usenet Downloads', - 'icon': CustomIcons.sabnzbd, - 'route': '/sabnzbd', - 'color': Color(0xFFFECC2B), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'SABnzbd', + description: 'Manage Usenet Downloads', + settingsDescription: 'Configure SABnzbd', + icon: CustomIcons.sabnzbd, + route: '/sabnzbd', + color: Color(0xFFFECC2B), + ); + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); } diff --git a/lib/modules/search/core/constants.dart b/lib/modules/search/core/constants.dart index 66b82ec2..19ba5964 100644 --- a/lib/modules/search/core/constants.dart +++ b/lib/modules/search/core/constants.dart @@ -1,16 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:lunasea/core/constants.dart'; +import 'package:lunasea/core.dart'; class SearchConstants { SearchConstants._(); static const String MODULE_KEY = 'search'; - static const Map MODULE_MAP = { - 'name': 'Search', - 'desc': 'Search Newznab Indexers', - 'icon': Icons.search, - 'route': '/search', - 'color': Color(Constants.ACCENT_COLOR), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Search', + description: 'Search Newznab Indexers', + settingsDescription: 'Configure Search', + icon: Icons.search, + route: '/search', + color: Color(Constants.ACCENT_COLOR), + ); + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); } diff --git a/lib/modules/settings/core/constants.dart b/lib/modules/settings/core/constants.dart index 4dfad4c9..89e21217 100644 --- a/lib/modules/settings/core/constants.dart +++ b/lib/modules/settings/core/constants.dart @@ -6,11 +6,12 @@ class SettingsConstants { static const String MODULE_KEY = 'settings'; - static const Map MODULE_MAP = { - 'name': 'Settings', - 'desc': 'Update Configuration', - 'icon': CustomIcons.settings, - 'route': '/settings', - 'color': Color(Constants.ACCENT_COLOR), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Settings', + description: 'Update Configuration', + settingsDescription: '', + icon: CustomIcons.settings, + route: '/settings', + color: Color(Constants.ACCENT_COLOR), + ); } diff --git a/lib/modules/settings/routes/modules.dart b/lib/modules/settings/routes/modules.dart index d94ce15e..1d8756da 100644 --- a/lib/modules/settings/routes/modules.dart +++ b/lib/modules/settings/routes/modules.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/modules.dart'; class SettingsModules extends StatefulWidget { static const ROUTE_NAME = '/settings/modules'; @@ -38,43 +38,12 @@ class _State extends State with AutomaticKeepAliveClientMixin { LSCardTile( title: LSTitle(text: Constants.APPLICATION_NAME), subtitle: LSSubtitle(text: 'Configure Global Options'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: CustomIcons.code, - color: LSColors.list(0), - ), + trailing: LSIconButton(icon: CustomIcons.code), onTap: () async => Navigator.of(context).pushNamed(SettingsModulesLunaSea.ROUTE_NAME), ), - LSCardTile( - title: LSTitle(text: 'Home'), - subtitle: LSSubtitle(text: 'Configure the Home Screen'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: CustomIcons.home, - color: LSColors.list(1), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesHome.ROUTE_NAME), - ), - if(ModuleFlags.SEARCH) LSCardTile( - title: LSTitle(text: 'Search'), - subtitle: LSSubtitle(text: 'Configure Search'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: Icons.search, - color: LSColors.list(2), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesSearch.ROUTE_NAME), - ), - if(ModuleFlags.WAKE_ON_LAN) LSCardTile( - title: LSTitle(text: Constants.MODULE_MAP['wake_on_lan']['name']), - subtitle: LSSubtitle(text: 'Configure Wake on LAN'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: Constants.MODULE_MAP['wake_on_lan']['icon'], - color: LSColors.list(3), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesWakeOnLAN.ROUTE_NAME), - ), + _tileFromModuleMap(HomeConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesHome.ROUTE_NAME)), + if(ModuleFlags.SEARCH) _tileFromModuleMap(SearchConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesSearch.ROUTE_NAME)), + if(ModuleFlags.WAKE_ON_LAN) _tileFromModuleMap(WakeOnLANConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesWakeOnLAN.ROUTE_NAME)), ]; List get _automation => [ @@ -82,36 +51,9 @@ class _State extends State with AutomaticKeepAliveClientMixin { text: 'Automation', subtitle: 'Configure and customize automation modules', ), - if(ModuleFlags.LIDARR) LSCardTile( - title: LSTitle(text: 'Lidarr'), - subtitle: LSSubtitle(text: 'Configure Lidarr'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: CustomIcons.music, - color: LSColors.list(4), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesLidarr.ROUTE_NAME), - ), - if(ModuleFlags.RADARR) LSCardTile( - title: LSTitle(text: 'Radarr'), - subtitle: LSSubtitle(text: 'Configure Radarr'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: CustomIcons.movies, - color: LSColors.list(5), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesRadarr.ROUTE_NAME), - ), - if(ModuleFlags.SONARR) LSCardTile( - title: LSTitle(text: 'Sonarr'), - subtitle: LSSubtitle(text: 'Configure Sonarr'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: CustomIcons.television, - color: LSColors.list(6), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesSonarr.ROUTE_NAME), - ), + if(ModuleFlags.LIDARR) _tileFromModuleMap(LidarrConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesLidarr.ROUTE_NAME)), + if(ModuleFlags.RADARR) _tileFromModuleMap(RadarrConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesRadarr.ROUTE_NAME)), + if(ModuleFlags.SONARR) _tileFromModuleMap(SonarrConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesSonarr.ROUTE_NAME)), ]; List get _clients => [ @@ -119,26 +61,8 @@ class _State extends State with AutomaticKeepAliveClientMixin { text: 'Clients', subtitle: 'Configure and customize client modules', ), - if(ModuleFlags.NZBGET) LSCardTile( - title: LSTitle(text: 'NZBGet'), - subtitle: LSSubtitle(text: 'Configure NZBGet'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: CustomIcons.nzbget, - color: LSColors.list(7), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesNZBGet.ROUTE_NAME), - ), - if(ModuleFlags.SABNZBD) LSCardTile( - title: LSTitle(text: 'SABnzbd'), - subtitle: LSSubtitle(text: 'Configure SABnzbd'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: CustomIcons.sabnzbd, - color: LSColors.list(8), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesSABnzbd.ROUTE_NAME), - ), + if(ModuleFlags.NZBGET) _tileFromModuleMap(NZBGetConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesNZBGet.ROUTE_NAME)), + if(ModuleFlags.SABNZBD) _tileFromModuleMap(SABnzbdConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesSABnzbd.ROUTE_NAME)), ]; List get _monitoring => [ @@ -146,15 +70,13 @@ class _State extends State with AutomaticKeepAliveClientMixin { text: 'Monitoring', subtitle: 'Configure and customize monitoring modules', ), - if(ModuleFlags.TAUTULLI) LSCardTile( - title: LSTitle(text: Constants.MODULE_MAP['tautulli']['name']), - subtitle: LSSubtitle(text: Constants.MODULE_MAP['tautulli']['settings_desc']), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - leading: LSIconButton( - icon: Constants.MODULE_MAP['tautulli']['icon'], - color: LSColors.list(9), - ), - onTap: () async => Navigator.of(context).pushNamed(SettingsModulesTautulli.ROUTE_NAME), - ), + if(ModuleFlags.TAUTULLI) _tileFromModuleMap(TautulliConstants.MODULE_MAP, () async => Navigator.of(context).pushNamed(SettingsModulesTautulli.ROUTE_NAME)), ]; + + Widget _tileFromModuleMap(ModuleMap map, Function onTap) => LSCardTile( + title: LSTitle(text: map.name), + subtitle: LSSubtitle(text: map.settingsDescription), + trailing: LSIconButton(icon: map.icon), + onTap: onTap, + ); } diff --git a/lib/modules/settings/routes/modules/general_lunasea.dart b/lib/modules/settings/routes/modules/general_lunasea.dart index 8f0dae0a..77fb85ff 100644 --- a/lib/modules/settings/routes/modules/general_lunasea.dart +++ b/lib/modules/settings/routes/modules/general_lunasea.dart @@ -1,59 +1,2 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/settings.dart'; - -class SettingsModulesLunaSea extends StatefulWidget { - static const ROUTE_NAME = '/settings/modules/lunasea'; - - @override - State createState() => _State(); -} - -class _State extends State { - final _scaffoldKey = GlobalKey(); - - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - body: _body, - appBar: _appBar, - ); - - Widget get _appBar => LSAppBar(title: Constants.APPLICATION_NAME); - - Widget get _body => LSListView( - children: [ - ..._appearance, - ..._drawer, - if(Platform.isIOS) ..._other, - ], - ); - - List get _appearance => [ - LSHeader( - text: 'Appearance', - subtitle: 'Customize the appearance of ${Constants.APPLICATION_NAME} to fit your needs', - ), - SettingsModulesLunaSeaAMOLEDTile(), - SettingsModulesLunaSeaAMOLEDBorderTile(), - ]; - - List get _other => [ - LSHeader( - text: 'Other', - subtitle: 'All other customizable options', - ), - SettingsModulesLunaSeaBrowserTile(), - ]; - - List get _drawer => [ - LSHeader( - text: 'Drawer', - subtitle: 'Customize the drawer to fit your needs', - ), - SettingsDrawerExpandAutomationTile(), - SettingsDrawerExpandClientsTile(), - SettingsDrawerExpandMonitoringTile(), - ]; -} +export 'general_lunasea/general_lunasea.dart'; +export 'general_lunasea/general_lunasea_quick_actions.dart'; diff --git a/lib/modules/settings/routes/modules/general_lunasea/general_lunasea.dart b/lib/modules/settings/routes/modules/general_lunasea/general_lunasea.dart new file mode 100644 index 00000000..4fb87de0 --- /dev/null +++ b/lib/modules/settings/routes/modules/general_lunasea/general_lunasea.dart @@ -0,0 +1,60 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsModulesLunaSea extends StatefulWidget { + static const ROUTE_NAME = '/settings/modules/lunasea'; + + @override + State createState() => _State(); +} + +class _State extends State { + final _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) => Scaffold( + key: _scaffoldKey, + body: _body, + appBar: _appBar, + ); + + Widget get _appBar => LSAppBar(title: Constants.APPLICATION_NAME); + + Widget get _body => LSListView( + children: [ + ..._appearance, + ..._drawer, + ..._other, + ], + ); + + List get _appearance => [ + LSHeader( + text: 'Appearance', + subtitle: 'Customize the appearance of ${Constants.APPLICATION_NAME} to fit your needs', + ), + SettingsModulesLunaSeaAMOLEDTile(), + SettingsModulesLunaSeaAMOLEDBorderTile(), + ]; + + List get _other => [ + LSHeader( + text: 'Other', + subtitle: 'All other customizable options', + ), + SettingsModulesQuickActionsTile(), + if(Platform.isIOS) SettingsModulesLunaSeaBrowserTile(), + ]; + + List get _drawer => [ + LSHeader( + text: 'Drawer', + subtitle: 'Customize the drawer to fit your needs', + ), + SettingsDrawerExpandAutomationTile(), + SettingsDrawerExpandClientsTile(), + SettingsDrawerExpandMonitoringTile(), + ]; +} diff --git a/lib/modules/settings/routes/modules/general_lunasea/general_lunasea_quick_actions.dart b/lib/modules/settings/routes/modules/general_lunasea/general_lunasea_quick_actions.dart new file mode 100644 index 00000000..1a470a1e --- /dev/null +++ b/lib/modules/settings/routes/modules/general_lunasea/general_lunasea_quick_actions.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class SettingsModulesLunaSeaQuickActions extends StatefulWidget { + static const ROUTE_NAME = '/settings/modules/lunasea/quick_actions'; + + @override + State createState() => _State(); +} + +class _State extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) => Scaffold( + key: _scaffoldKey, + appBar: _appBar, + body: _body, + ); + + Widget get _appBar => LSAppBar(title: 'Quick Actions'); + + Widget get _body => LSListView( + children: [ + LSHeader( + text: 'Modules', + subtitle: 'Only four quick actions will appear, and are added in the order below. Enabling more than four modules will have no effect.', + ), + _tile(title: 'Search', action: LunaSeaDatabaseValue.QUICK_ACTIONS_SEARCH), + LSDivider(), + _tile(title: 'Lidarr', action: LunaSeaDatabaseValue.QUICK_ACTIONS_LIDARR), + _tile(title: 'Radarr', action: LunaSeaDatabaseValue.QUICK_ACTIONS_RADARR), + _tile(title: 'Sonarr', action: LunaSeaDatabaseValue.QUICK_ACTIONS_SONARR), + LSDivider(), + _tile(title: 'NZBGet', action: LunaSeaDatabaseValue.QUICK_ACTIONS_NZBGET), + _tile(title: 'SABnzbd', action: LunaSeaDatabaseValue.QUICK_ACTIONS_SABNZBD), + LSDivider(), + _tile(title: 'Tautulli', action: LunaSeaDatabaseValue.QUICK_ACTIONS_TAUTULLI), + ], + ); + + Widget _tile({ + @required String title, + @required LunaSeaDatabaseValue action, + }) => LSCardTile( + title: LSTitle(text: title), + trailing: ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [action.key]), + builder: (context, box, _) => Switch( + value: action.data, + onChanged: (value) { + action.put(value); + HomescreenActions.setShortcutItems(); + } + ), + ), + ); +} diff --git a/lib/modules/settings/routes/system.dart b/lib/modules/settings/routes/system.dart index 68f9730d..417c8366 100644 --- a/lib/modules/settings/routes/system.dart +++ b/lib/modules/settings/routes/system.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:lunasea/modules/settings/widgets/system/enable_sentry.dart'; import 'package:package_info/package_info.dart'; @@ -120,6 +122,12 @@ class _State extends State with AutomaticKeepAliveClientMixin { trailing: LSIconButton(icon: CustomIcons.reddit), onTap: () async => await Constants.URL_REDDIT.lsLinks_OpenLink(), ), + if(Platform.isIOS) LSCardTile( + title: LSTitle(text: 'TestFlight'), + subtitle: LSSubtitle(text: 'Join the TestFlight Beta'), + trailing: LSIconButton(icon: Icons.developer_board), + onTap: () async => await Constants.URL_TESTFLIGHT.lsLinks_OpenLink() + ), LSCardTile( title: LSTitle(text: 'Website'), subtitle: LSSubtitle(text: 'Visit LunaSea\'s Website'), diff --git a/lib/modules/settings/widgets/modules/lunasea.dart b/lib/modules/settings/widgets/modules/lunasea.dart index 320659e3..1c398d44 100644 --- a/lib/modules/settings/widgets/modules/lunasea.dart +++ b/lib/modules/settings/widgets/modules/lunasea.dart @@ -4,3 +4,4 @@ export 'lunasea/drawer_expand_automation_tile.dart'; export 'lunasea/drawer_expand_clients_tile.dart'; export 'lunasea/drawer_expand_monitoring_tile.dart'; export 'lunasea/other_browser_tile.dart'; +export 'lunasea/quick_actions_tile.dart'; diff --git a/lib/modules/settings/widgets/modules/lunasea/quick_actions_tile.dart b/lib/modules/settings/widgets/modules/lunasea/quick_actions_tile.dart new file mode 100644 index 00000000..a0244843 --- /dev/null +++ b/lib/modules/settings/widgets/modules/lunasea/quick_actions_tile.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsModulesQuickActionsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Quick Actions'), + subtitle: LSSubtitle(text: 'Enable Quick Actions on the Home Screen'), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => Navigator.of(context).pushNamed(SettingsModulesLunaSeaQuickActions.ROUTE_NAME); +} diff --git a/lib/modules/sonarr/core/constants.dart b/lib/modules/sonarr/core/constants.dart index 010c3bee..12bd19fa 100644 --- a/lib/modules/sonarr/core/constants.dart +++ b/lib/modules/sonarr/core/constants.dart @@ -7,13 +7,20 @@ class SonarrConstants { static const String MODULE_KEY = 'sonarr'; - static const Map MODULE_MAP = { - 'name': 'Sonarr', - 'desc': 'Manage Television Series', - 'icon': CustomIcons.television, - 'route': '/sonarr', - 'color': Color(0xFF3FC6F4), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Sonarr', + description: 'Manage Television Series', + settingsDescription: 'Configure Sonarr', + icon: CustomIcons.television, + route: '/sonarr', + color: Color(0xFF3FC6F4), + ); + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); static const Map EVENT_TYPE_MESSAGES = { 'episodeFileRenamed': 'Episode File Renamed', diff --git a/lib/modules/tautulli/core/constants.dart b/lib/modules/tautulli/core/constants.dart index a349e592..e5a5f74a 100644 --- a/lib/modules/tautulli/core/constants.dart +++ b/lib/modules/tautulli/core/constants.dart @@ -7,23 +7,35 @@ class TautulliConstants { static const String MODULE_KEY = 'tautulli'; - static const Map MODULE_MAP = { - 'name': 'Tautulli', - 'desc': 'View Plex Activity', - 'settings_desc': 'Configure Tautulli', - 'icon': CustomIcons.tautulli, - 'route': '/tautulli', - 'color': Color(0xFFDBA23A), - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Tautulli', + description: 'View Plex Activity', + settingsDescription: 'Configure Tautulli', + icon: CustomIcons.tautulli, + route: '/tautulli', + color: Color(0xFFDBA23A), + ); + + //ignore: non_constant_identifier_names + static final ShortcutItem MODULE_QUICK_ACTION = ShortcutItem( + type: MODULE_KEY, + localizedTitle: MODULE_MAP.name, + ); // ignore: non_constant_identifier_names static final Map MODULE_ROUTES = { Tautulli.ROUTE_NAME: (context) => Tautulli(), + // Activity TautulliActivityRoute.ROUTE_NAME: (context) => TautulliActivityRoute(), - TautulliHistoryRoute.ROUTE_NAME: (context) => TautulliHistoryRoute(), - TautulliUsersRoute.ROUTE_NAME: (context) => TautulliUsersRoute(), - TautulliMoreRoute.ROUTE_NAME: (context) => TautulliMoreRoute(), TautulliActivityDetailsRoute.ROUTE_NAME: (context) => TautulliActivityDetailsRoute(), + // History + TautulliHistoryRoute.ROUTE_NAME: (context) => TautulliHistoryRoute(), + // Users + TautulliUsersRoute.ROUTE_NAME: (context) => TautulliUsersRoute(), + TautulliUserDetailsRoute.ROUTE_NAME: (context) => TautulliUserDetailsRoute(), + // More + TautulliMoreRoute.ROUTE_NAME: (context) => TautulliMoreRoute(), TautulliLogsRoute.ROUTE_NAME: (context) => TautulliLogsRoute(), + TautulliSyncedItemsRoute.ROUTE_NAME: (context) => TautulliSyncedItemsRoute(), }; } diff --git a/lib/modules/tautulli/core/state.dart b/lib/modules/tautulli/core/state.dart index c4a60319..0e47007e 100644 --- a/lib/modules/tautulli/core/state.dart +++ b/lib/modules/tautulli/core/state.dart @@ -19,6 +19,7 @@ class TautulliState extends ChangeNotifier { resetUsers(); if(initialize) { _navigationIndex = TautulliDatabaseValue.NAVIGATION_INDEX.data; + _userDetailsNavigationIndex = 0; } notifyListeners(); } @@ -84,6 +85,14 @@ class TautulliState extends ChangeNotifier { notifyListeners(); } + int _userDetailsNavigationIndex; + int get userDetailsNavigationIndex => _userDetailsNavigationIndex; + set userDetailsNavigationIndex(int userDetailsNavigationIndex) { + assert(userDetailsNavigationIndex != null); + _userDetailsNavigationIndex = userDetailsNavigationIndex; + notifyListeners(); + } + /*********** * ACTIVITY * ************/ @@ -115,6 +124,7 @@ class TautulliState extends ChangeNotifier { /// - Setting the initial state of the future to an instance of the API call void resetActivity() { cancelActivityTimer(); + _activity = null; if(_api != null) { _activity = _api.activity.getActivity(); createActivityTimer(); @@ -126,7 +136,7 @@ class TautulliState extends ChangeNotifier { * USERS * ********/ - /// Storing the user data + /// Storing the user table Future _users; Future get users => _users; set users(Future users) { @@ -135,9 +145,78 @@ class TautulliState extends ChangeNotifier { notifyListeners(); } + /// Storing individual user profile data + Map> _userProfile; + Map> get userProfile => _userProfile; + void setUserProfile(int userId, Future data) { + assert(userId != null); + assert(data != null); + _userProfile[userId] = data; + notifyListeners(); + } + + /// Storing individual user synced items + Map>> _userSyncedItems; + Map>> get userSyncedItems => _userSyncedItems; + void setUserSyncedItems(int userId, Future> data) { + assert(userId != null); + assert(data != null); + _userSyncedItems[userId] = data; + notifyListeners(); + } + + /// Storing individual user IP addresses + Map> _userIPs; + Map> get userIPs => _userIPs; + void setUserIPs(int userId, Future data) { + assert(userId != null); + assert(data != null); + _userIPs[userId] = data; + notifyListeners(); + } + + /// Storing individual user watch time stats + Map>> _userWatchStats; + Map>> get userWatchStats => _userWatchStats; + void setUserWatchStats(int userId, Future> data) { + assert(userId != null); + assert(data != null); + _userWatchStats[userId] = data; + notifyListeners(); + } + + /// Storing individual user player stats + Map>> _userPlayerStats; + Map>> get userPlayerStats => _userPlayerStats; + void setUserPlayerStats(int userId, Future> data) { + assert(userId != null); + assert(data != null); + _userPlayerStats[userId] = data; + notifyListeners(); + } + + Map> _userHistory; + Map> get userHistory => _userHistory; + void setUserHistory(int userId, Future data) { + assert(userId != null); + assert(data != null); + _userHistory[userId] = data; + notifyListeners(); + } + /// Reset the users by: /// - Setting the intial state of the future to an instance of the API call + /// - Resets individual user data maps void resetUsers() { + // Clear + _users = null; + _userProfile = {}; + _userSyncedItems = {}; + _userIPs = {}; + _userWatchStats = {}; + _userPlayerStats = {}; + _userHistory = {}; + // Reset user table if(_api != null) { _users = _api.users.getUsersTable( length: 250, @@ -148,6 +227,10 @@ class TautulliState extends ChangeNotifier { notifyListeners(); } + /********** + * HISTORY * + **********/ + /********* * IMAGES * *********/ diff --git a/lib/modules/tautulli/modules.dart b/lib/modules/tautulli/modules.dart index e662e1a1..dbde34c5 100644 --- a/lib/modules/tautulli/modules.dart +++ b/lib/modules/tautulli/modules.dart @@ -1,7 +1,19 @@ export 'modules/activity.dart'; export 'modules/activity_details.dart'; +export 'modules/graphs.dart'; export 'modules/history.dart'; +export 'modules/libraries.dart'; export 'modules/logs.dart'; +export 'modules/logs_logins.dart'; +export 'modules/logs_newsletters.dart'; +export 'modules/logs_notifications.dart'; +export 'modules/logs_plex_media_scanner.dart'; +export 'modules/logs_tautulli.dart'; +export 'modules/logs_plex_media_server.dart'; export 'modules/more.dart'; +export 'modules/recently_added.dart'; +export 'modules/statistics.dart'; +export 'modules/synced_items.dart'; export 'modules/tautulli.dart'; export 'modules/users.dart'; +export 'modules/user_details.dart'; diff --git a/lib/modules/tautulli/modules/activity/route.dart b/lib/modules/tautulli/modules/activity/route.dart index 6dd40aba..4ca51980 100644 --- a/lib/modules/tautulli/modules/activity/route.dart +++ b/lib/modules/tautulli/modules/activity/route.dart @@ -43,7 +43,7 @@ class _State extends State with AutomaticKeepAliveClientM selector: (_, state) => state.activity, builder: (context, activity, _) => FutureBuilder( future: activity, - builder: (context, snapshot) { + builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { Logger.error( @@ -55,18 +55,18 @@ class _State extends State with AutomaticKeepAliveClientM uploadToSentry: !(snapshot.error is DioError), ); } - return LSErrorMessage(onTapHandler: () async => _refresh()); + return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); } - if(snapshot.hasData) return (snapshot.data as TautulliActivity).streamCount == 0 + if(snapshot.hasData) return snapshot.data.streamCount == 0 ? _noActivity() - : _list((snapshot.data as TautulliActivity)); + : _activity(snapshot.data); return LSLoader(); }, ), ), ); - Widget _list(TautulliActivity activity) => LSListView( + Widget _activity(TautulliActivity activity) => LSListView( children: [ TautulliActivityStatus(activity: activity), ...List.generate( @@ -80,6 +80,6 @@ class _State extends State with AutomaticKeepAliveClientM text: 'No Active Streams', showButton: true, buttonText: 'Refresh', - onTapHandler: () async => _refresh(), + onTapHandler: () async => _refreshKey.currentState.show(), ); } \ No newline at end of file diff --git a/lib/modules/tautulli/modules/activity/widgets/activity_tile.dart b/lib/modules/tautulli/modules/activity/widgets/activity_tile.dart index 2b31f67c..961cec9b 100644 --- a/lib/modules/tautulli/modules/activity/widgets/activity_tile.dart +++ b/lib/modules/tautulli/modules/activity/widgets/activity_tile.dart @@ -28,11 +28,13 @@ class TautulliActivityTile extends StatelessWidget { borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), onTap: () async => _enterDetails(context), ), - decoration: LSCardBackground( - uri: Provider.of(context, listen: false).getImageURLFromPath(session.art), - headers: Provider.of(context, listen: false).headers, - darken: true, - ), + decoration: session.art != null && session.art.isNotEmpty + ? LSCardBackground( + uri: Provider.of(context, listen: false).getImageURLFromPath(session.art), + headers: Provider.of(context, listen: false).headers, + darken: true, + ) + : null, ); Widget _body(BuildContext context) => Row( diff --git a/lib/modules/tautulli/modules/activity_details/widgets/information.dart b/lib/modules/tautulli/modules/activity_details/widgets/information.dart index 81649507..1abad9fb 100644 --- a/lib/modules/tautulli/modules/activity_details/widgets/information.dart +++ b/lib/modules/tautulli/modules/activity_details/widgets/information.dart @@ -96,7 +96,8 @@ class TautulliActivityDetailsInformation extends StatelessWidget { session.streamContainerDecision.name, ' (', session.container.toUpperCase(), - if(session.transcodeContainer != null && session.transcodeContainer.isNotEmpty) '${Constants.TEXT_RARROW} ${session.transcodeContainer.toUpperCase()}', + if(session.streamContainerDecision != null && session.streamContainerDecision != TautulliTranscodeDecision.DIRECT_PLAY) + ' ${Constants.TEXT_RARROW} ${session.streamContainer.toUpperCase()}', ')', ].join(), ); diff --git a/lib/modules/tautulli/modules/graphs.dart b/lib/modules/tautulli/modules/graphs.dart new file mode 100644 index 00000000..c0fc6131 --- /dev/null +++ b/lib/modules/tautulli/modules/graphs.dart @@ -0,0 +1,2 @@ +export 'graphs/route.dart'; +export 'graphs/widgets.dart'; diff --git a/lib/modules/tautulli/modules/graphs/route.dart b/lib/modules/tautulli/modules/graphs/route.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/graphs/widgets.dart b/lib/modules/tautulli/modules/graphs/widgets.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/history/widgets.dart b/lib/modules/tautulli/modules/history/widgets.dart index e69de29b..54a3049e 100644 --- a/lib/modules/tautulli/modules/history/widgets.dart +++ b/lib/modules/tautulli/modules/history/widgets.dart @@ -0,0 +1 @@ +export 'widgets/history_tile.dart'; diff --git a/lib/modules/tautulli/modules/history/widgets/history_tile.dart b/lib/modules/tautulli/modules/history/widgets/history_tile.dart new file mode 100644 index 00000000..67abc8b6 --- /dev/null +++ b/lib/modules/tautulli/modules/history/widgets/history_tile.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliHistoryTile extends StatelessWidget { + final TautulliHistoryRecord history; + final double _imageDimension = 83.0; + final double _padding = 8.0; + + TautulliHistoryTile({ + Key key, + @required this.history, + }) : super(key: key); + + @override + Widget build(BuildContext context) => LSCard( + child: InkWell( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + child: Row( + children: [ + _poster(context), + _details, + ], + ), + onTap: () async => _onTap(context), + ), + decoration: LSCardBackground( + darken: true, + uri: Provider.of(context, listen: false).getImageURLFromRatingKey( + history.grandparentRatingKey ?? history.parentRatingKey ?? history.ratingKey ?? '', + ), + headers: Provider.of(context, listen: false).headers.cast(), + ), + ); + + Widget _poster(BuildContext context) => CachedNetworkImage( + fadeInDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + fadeOutDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + imageUrl: Provider.of(context, listen: false).getImageURLFromPath(history.thumb), + httpHeaders: Provider.of(context, listen: false).headers.cast(), + imageBuilder: (context, imageProvider) => Container( + height: _imageDimension, + width: _imageDimension/1.5, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + placeholder: (context, url) => _placeholder, + errorWidget: (context, url, error) => _placeholder, + ); + + Widget get _placeholder => Container( + height: _imageDimension, + width: _imageDimension/1.5, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + image: DecorationImage( + image: AssetImage('assets/images/sonarr/noseriesposter.png'), + fit: BoxFit.cover, + ), + ), + ); + + Widget get _details => Expanded( + child: Padding( + child: Container( + child: Column( + children: [ + _title, + _subtitle, + _date, + _userInfo, + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + ), + height: (_imageDimension-(_padding*2)), + ), + padding: EdgeInsets.all(_padding), + ), + ); + + Widget get _title => LSTitle( + text: history.grandparentTitle == null || history.grandparentTitle.isEmpty + ? history.parentTitle == null || history.parentTitle.isEmpty + ? history.title == null || history.title.isEmpty + ? 'Unknown Title' + : history.title + : history.parentTitle + : history.grandparentTitle, + maxLines: 1, + ); + + Widget get _subtitle => RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + children: [ + // Television + if(history.mediaType == TautulliMediaType.EPISODE) TextSpan(text: 'S${history.parentMediaIndex}\t•\tE${history.mediaIndex}'), + if(history.mediaType == TautulliMediaType.EPISODE) TextSpan(text: '\t—\t${history.title}'), + // Movie + if(history.mediaType == TautulliMediaType.MOVIE) TextSpan(text: history.year.toString()), + // Music + if(history.mediaType == TautulliMediaType.TRACK) TextSpan(text: history.title), + if(history.mediaType == TautulliMediaType.TRACK) TextSpan(text: '\t—\t${history.parentTitle}'), + // Live + if(history.mediaType == TautulliMediaType.LIVE) TextSpan(text: history.title), + ], + ), + softWrap: false, + maxLines: 1, + overflow: TextOverflow.fade, + ); + + Widget get _userInfo { + IconData _icon; + switch(history.watchedStatus) { + case TautulliWatchedStatus.UNWATCHED: _icon = Icons.radio_button_unchecked; break; + case TautulliWatchedStatus.PARTIALLY_WATCHED: _icon = Icons.radio_button_checked; break; + case TautulliWatchedStatus.WATCHED: _icon = Icons.check_circle; break; + } + return Row( + children: [ + Padding( + child: LSIcon( + icon: _icon, + size: Constants.UI_FONT_SIZE_SUBHEADER, + ), + padding: EdgeInsets.only(right: 6.0), + ), + Expanded( + child: LSSubtitle( + text: history.friendlyName ?? 'Unknown User', + maxLines: 1, + ), + ) + ], + ); + } + + Widget get _date => LSSubtitle(text: DateTime.now().lsDateTime_ageString(history.date)); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'History details have not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/libraries.dart b/lib/modules/tautulli/modules/libraries.dart new file mode 100644 index 00000000..713d0467 --- /dev/null +++ b/lib/modules/tautulli/modules/libraries.dart @@ -0,0 +1,2 @@ +export 'libraries/route.dart'; +export 'libraries/widgets.dart'; diff --git a/lib/modules/tautulli/modules/libraries/route.dart b/lib/modules/tautulli/modules/libraries/route.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/libraries/widgets.dart b/lib/modules/tautulli/modules/libraries/widgets.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/recently_added.dart b/lib/modules/tautulli/modules/recently_added.dart new file mode 100644 index 00000000..e2a3e515 --- /dev/null +++ b/lib/modules/tautulli/modules/recently_added.dart @@ -0,0 +1,2 @@ +export 'recently_added/route.dart'; +export 'recently_added/widgets.dart'; diff --git a/lib/modules/tautulli/modules/recently_added/route.dart b/lib/modules/tautulli/modules/recently_added/route.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/recently_added/widgets.dart b/lib/modules/tautulli/modules/recently_added/widgets.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/statistics.dart b/lib/modules/tautulli/modules/statistics.dart new file mode 100644 index 00000000..9975695b --- /dev/null +++ b/lib/modules/tautulli/modules/statistics.dart @@ -0,0 +1,2 @@ +export 'statistics/route.dart'; +export 'statistics/widgets.dart'; diff --git a/lib/modules/tautulli/modules/statistics/route.dart b/lib/modules/tautulli/modules/statistics/route.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/statistics/widgets.dart b/lib/modules/tautulli/modules/statistics/widgets.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/synced_items.dart b/lib/modules/tautulli/modules/synced_items.dart new file mode 100644 index 00000000..d2f01cc4 --- /dev/null +++ b/lib/modules/tautulli/modules/synced_items.dart @@ -0,0 +1,2 @@ +export 'synced_items/route.dart'; +export 'synced_items/widgets.dart'; diff --git a/lib/modules/tautulli/modules/synced_items/route.dart b/lib/modules/tautulli/modules/synced_items/route.dart new file mode 100644 index 00000000..23cb8ece --- /dev/null +++ b/lib/modules/tautulli/modules/synced_items/route.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliSyncedItemsRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/synced_items'; + + TautulliSyncedItemsRoute({ + Key key, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) => Scaffold( + key: _scaffoldKey, + appBar: _appBar, + ); + + Widget get _appBar => LSAppBar(title: 'Synced Items'); +} diff --git a/lib/modules/tautulli/modules/synced_items/widgets.dart b/lib/modules/tautulli/modules/synced_items/widgets.dart new file mode 100644 index 00000000..2127e5a5 --- /dev/null +++ b/lib/modules/tautulli/modules/synced_items/widgets.dart @@ -0,0 +1 @@ +export 'widgets/synced_item_tile.dart'; diff --git a/lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart b/lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart new file mode 100644 index 00000000..63bfc0cb --- /dev/null +++ b/lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliSyncedItemTile extends StatelessWidget { + final TautulliSyncedItem syncedItem; + + TautulliSyncedItemTile({ + Key key, + @required this.syncedItem, + }): super(key: key); + + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: syncedItem.syncTitle), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + children: [ + TextSpan( + text: (syncedItem.state ?? 'Unknown').lsLanguage_Capitalize(), + style: TextStyle( + color: LSColors.accent, + fontWeight: FontWeight.bold, + ), + ), + TextSpan(text: '\t${Constants.TEXT_EMDASH}\t'), + TextSpan(text: (syncedItem.metadataType ?? 'Unknown').lsLanguage_Capitalize()), + TextSpan(text: '\t${Constants.TEXT_BULLET}\t'), + TextSpan(text: (syncedItem.itemCompleteCount ?? 0) == 1 + ? '1 Item' + : '${(syncedItem.itemCompleteCount ?? 0)} Items', + ), + TextSpan(text: '\t${Constants.TEXT_BULLET}\t'), + TextSpan(text: (syncedItem.totalSize ?? 0).lsBytes_BytesToString(decimals: 1)), + TextSpan(text: '\n'), + TextSpan(text: syncedItem.user ?? 'Unknown User'), + TextSpan(text: '\n'), + TextSpan(text: syncedItem.platform ?? 'Unknown Platform'), + TextSpan(text: '\t${Constants.TEXT_BULLET}\t'), + TextSpan(text: syncedItem.deviceName ?? 'Unknown Device'), + ], + ), + ), + decoration: syncedItem.ratingKey != null + ? LSCardBackground( + uri: Provider.of(context, listen: false).getImageURLFromRatingKey(syncedItem.ratingKey), + headers: Provider.of(context, listen: false).headers, + ) + : null, + onTap: () async => _onTap(context), + padContent: true, + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'Library data has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart index 525706d7..fa81c0ae 100644 --- a/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart @@ -31,7 +31,7 @@ class TautulliNavigationBar extends StatefulWidget { class _State extends State { @override Widget build(BuildContext context) => Selector( - selector: (_, model) => model.navigationIndex, + selector: (_, state) => state.navigationIndex, builder: (context, index, _) => LSBottomNavigationBar( index: index, icons: TautulliNavigationBar.icons, diff --git a/lib/modules/tautulli/modules/user_details.dart b/lib/modules/tautulli/modules/user_details.dart new file mode 100644 index 00000000..e7680930 --- /dev/null +++ b/lib/modules/tautulli/modules/user_details.dart @@ -0,0 +1,2 @@ +export 'user_details/route.dart'; +export 'user_details/widgets.dart'; diff --git a/lib/modules/tautulli/modules/user_details/route.dart b/lib/modules/tautulli/modules/user_details/route.dart new file mode 100644 index 00000000..ac690534 --- /dev/null +++ b/lib/modules/tautulli/modules/user_details/route.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliUserDetailsRouteArguments { + final TautulliTableUser user; + + TautulliUserDetailsRouteArguments({ + @required this.user, + }); +} + +class TautulliUserDetailsRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/user/details'; + + TautulliUserDetailsRoute({ + Key key, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + TautulliUserDetailsRouteArguments _arguments; + PageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController(initialPage: Provider.of(context, listen: false).userDetailsNavigationIndex); + SchedulerBinding.instance.scheduleFrameCallback((_) { + setState(() => _arguments = ModalRoute.of(context).settings.arguments); + }); + } + + @override + Widget build(BuildContext context) => _arguments == null + ? Scaffold() + : Scaffold( + key: _scaffoldKey, + appBar: _appBar, + bottomNavigationBar: _bottomNavigationBar, + body: _body, + ); + + Widget get _appBar => LSAppBar( + title: _arguments.user.friendlyName, + ); + + Widget get _bottomNavigationBar => TautulliUserDetailsNavigationBar(pageController: _pageController); + + List get _tabs => [ + TautulliUserDetailsProfile(user: _arguments.user), + TautulliUserDetailsHistory(user: _arguments.user), + TautulliUserDetailsSyncedItems(user: _arguments.user), + TautulliUserDetailsIPAddresses(user: _arguments.user), + ]; + + Widget get _body => PageView( + controller: _pageController, + children: _tabs, + onPageChanged: _onPageChanged, + ); + + void _onPageChanged(int index) => Provider.of(context, listen: false).userDetailsNavigationIndex = index; +} diff --git a/lib/modules/tautulli/modules/user_details/widgets.dart b/lib/modules/tautulli/modules/user_details/widgets.dart new file mode 100644 index 00000000..c4c5cfea --- /dev/null +++ b/lib/modules/tautulli/modules/user_details/widgets.dart @@ -0,0 +1,5 @@ +export 'widgets/history.dart'; +export 'widgets/ip_addresses.dart'; +export 'widgets/navigation_bar.dart'; +export 'widgets/profile.dart'; +export 'widgets/synced_items.dart'; diff --git a/lib/modules/tautulli/modules/user_details/widgets/history.dart b/lib/modules/tautulli/modules/user_details/widgets/history.dart new file mode 100644 index 00000000..4963498d --- /dev/null +++ b/lib/modules/tautulli/modules/user_details/widgets/history.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliUserDetailsHistory extends StatefulWidget { + final TautulliTableUser user; + + TautulliUserDetailsHistory({ + Key key, + @required this.user, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + } + + Future _refresh() async { + TautulliState _state = Provider.of(context, listen: false); + _state.setUserHistory( + widget.user.userId, + _state.api.history.getHistory( + userId: widget.user.userId, + length: 100, + ), + ); + await _state.userHistory[widget.user.userId]; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Widget get _body => LSRefreshIndicator( + refreshKey: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: Provider.of(context).userHistory[widget.user.userId], + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + Logger.error( + 'TautulliUserDetailsHistory', + '_body', + 'Unable to fetch Tautulli user history: ${widget.user.userId}', + snapshot.error, + null, + uploadToSentry: !(snapshot.error is DioError), + ); + } + return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + } + if(snapshot.hasData) return snapshot.data.records.length == 0 + ? _noHistory() + : _history(snapshot.data); + return LSLoader(); + }, + ), + ); + + Widget _history(TautulliHistory history) => LSListViewBuilder( + itemCount: history.records.length, + itemBuilder: (context, index) => TautulliHistoryTile(history: history.records[index]), + ); + + Widget _noHistory() => LSGenericMessage( + text: 'No History Found', + showButton: true, + buttonText: 'Refresh', + onTapHandler: () async => _refreshKey.currentState.show(), + ); +} diff --git a/lib/modules/tautulli/modules/user_details/widgets/ip_addresses.dart b/lib/modules/tautulli/modules/user_details/widgets/ip_addresses.dart new file mode 100644 index 00000000..ccc5b5ed --- /dev/null +++ b/lib/modules/tautulli/modules/user_details/widgets/ip_addresses.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliUserDetailsIPAddresses extends StatefulWidget { + final TautulliTableUser user; + + TautulliUserDetailsIPAddresses({ + Key key, + @required this.user, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + } + + Future _refresh() async { + TautulliState _state = Provider.of(context, listen: false); + _state.setUserIPs( + widget.user.userId, + _state.api.users.getUserIPs(userId: widget.user.userId), + ); + await _state.userIPs[widget.user.userId]; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Widget get _body => LSRefreshIndicator( + refreshKey: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: Provider.of(context).userIPs[widget.user.userId], + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + Logger.error( + 'TautulliUserDetailsIPAddresses', + '_body', + 'Unable to fetch Tautulli user IP addresses: ${widget.user.userId}', + snapshot.error, + null, + uploadToSentry: !(snapshot.error is DioError), + ); + } + return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + } + if(snapshot.hasData) return snapshot.data.ips.length == 0 + ? _noIPs() + : _ips(snapshot.data); + return LSLoader(); + }, + ), + ); + + Widget _ips(TautulliUserIPs ips) => LSListViewBuilder( + itemCount: ips.ips.length, + itemBuilder: (context, index) => _tile(ips.ips[index]), + ); + + Widget _noIPs() => LSGenericMessage( + text: 'No IPs Found', + showButton: true, + buttonText: 'Refresh', + onTapHandler: () async => _refreshKey.currentState.show(), + ); + + Widget _tile(TautulliUserIPRecord record) => LSCardTile( + title: LSTitle(text: record.ipAddress), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + children: [ + TextSpan(text: DateTime.now().lsDateTime_ageString(record.lastSeen)), + TextSpan(text: '\t${Constants.TEXT_EMDASH}\t'), + TextSpan(text: record.playCount == 1 + ? '1 Play' + : '${record.playCount} Plays', + ), + TextSpan(text: '\n'), + TextSpan(text: record.lastPlayed), + ], + ), + maxLines: 2, + softWrap: false, + overflow: TextOverflow.fade, + ), + decoration: record.thumb != null && record.thumb.isNotEmpty + ? LSCardBackground( + uri: Provider.of(context, listen: false).getImageURLFromPath(record.thumb ?? ''), + headers: Provider.of(context, listen: false).headers, + darken: true, + ) + : null, + padContent: true, + ); +} diff --git a/lib/modules/tautulli/modules/user_details/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/user_details/widgets/navigation_bar.dart new file mode 100644 index 00000000..835f03e5 --- /dev/null +++ b/lib/modules/tautulli/modules/user_details/widgets/navigation_bar.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliUserDetailsNavigationBar extends StatefulWidget { + static const List icons = [ + CustomIcons.user, + CustomIcons.history, + Icons.sync, + Icons.computer, + ]; + + static const List titles = [ + 'Profile', + 'History', + 'Synced', + 'IPs', + ]; + + final PageController pageController; + + TautulliUserDetailsNavigationBar({ + Key key, + @required this.pageController, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) => Selector( + selector: (_, state) => state.userDetailsNavigationIndex, + builder: (context, index, _) => LSBottomNavigationBar( + index: index, + icons: TautulliUserDetailsNavigationBar.icons, + titles: TautulliUserDetailsNavigationBar.titles, + onTap: _navOnTap, + ), + ); + + Future _navOnTap(int index) async { + await widget.pageController.animateToPage( + index, + duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + curve: Curves.easeOutSine, + ); + Provider.of(context, listen: false).userDetailsNavigationIndex = index; + } +} \ No newline at end of file diff --git a/lib/modules/tautulli/modules/user_details/widgets/profile.dart b/lib/modules/tautulli/modules/user_details/widgets/profile.dart new file mode 100644 index 00000000..b5159a72 --- /dev/null +++ b/lib/modules/tautulli/modules/user_details/widgets/profile.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliUserDetailsProfile extends StatefulWidget { + final TautulliTableUser user; + + TautulliUserDetailsProfile({ + Key key, + @required this.user, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + // Tracks the initial load to ensure the futures have been initialized + bool _initialLoad = false; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + } + + Future _refresh() async { + TautulliState _state = Provider.of(context, listen: false); + // Initial load or refresh of the user profile data + _state.setUserProfile( + widget.user.userId, + _state.api.users.getUser(userId: widget.user.userId), + ); + // Initial load or refresh of the user watch stats + _state.setUserWatchStats( + widget.user.userId, + _state.api.users.getUserWatchTimeStats(userId: widget.user.userId, queryDays: [1, 7, 30, 0]), + ); + // Initial load or refresh of the user player stats + _state.setUserPlayerStats( + widget.user.userId, + _state.api.users.getUserPlayerStats(userId: widget.user.userId), + ); + setState(() => _initialLoad = true); + // This await keeps the refresh indicator showing until the data is loaded + await Future.wait([ + _state.userProfile[widget.user.userId], + _state.userWatchStats[widget.user.userId], + _state.userPlayerStats[widget.user.userId], + ]); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Widget get _body => _initialLoad + ? LSRefreshIndicator( + refreshKey: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: Future.wait([ + Provider.of(context).userProfile[widget.user.userId], + Provider.of(context).userWatchStats[widget.user.userId], + Provider.of(context).userPlayerStats[widget.user.userId], + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + Logger.error( + 'TautulliUserDetailsProfile', + '_body', + 'Unable to fetch Tautulli user: ${widget.user.userId}', + snapshot.error, + null, + uploadToSentry: !(snapshot.error is DioError), + ); + } + return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + } + if(snapshot.hasData) return _list( + user: snapshot.data[0], + watchtime: snapshot.data[1], + player: snapshot.data[2], + ); + return LSLoader(); + }, + ), + ) + : LSLoader(); + + Widget _list({ + @required TautulliUser user, + @required List watchtime, + @required List player, + }) => LSListView( + children: [ + ..._profile(user), + ..._globalStats(watchtime), + if(player.length > 0) ..._playerStats(player), + ], + ); + + List _profile(TautulliUser user) => _block( + title: 'Profile', + children: [ + _content('email', user.email), + _content('last seen', widget.user.lastSeen != null + ? DateTime.now().lsDateTime_ageString(widget.user.lastSeen) + : 'Never', + ), + _content('', ''), + _content('title', widget.user.lastPlayed ?? 'None'), + _content('platform', widget.user.platform ?? 'None'), + _content('player', widget.user.player ?? 'None'), + _content('location', widget.user.ipAddress ?? 'None'), + ], + ); + + List _globalStats(List watchtime) => _block( + title: 'Global Stats', + children: List.generate( + watchtime.length, + (index) => _content( + _globalStatsTitle(watchtime[index].queryDays), + _globalStatsContent(watchtime[index].totalPlays, watchtime[index].totalTime), + ), + ), + ); + + String _globalStatsTitle(int days) { + if(days == 0) return 'All Time'; + if(days == 1) return '24 Hours'; + return '$days Days'; + } + + String _globalStatsContent(int plays, Duration duration) { + String _plays = plays == 1 ? '1 Play': '$plays Plays'; + return '$_plays\n${duration.lsDuration_fullTimestamp()}'; + } + + List _playerStats(List player) => [ + LSHeader(text: 'Player Stats'), + ...List.generate( + player.length, + (index) => LSCard( + child: Padding( + child: Column( + children: [ + _content('player', player[index].playerName), + _content('platform', player[index].platform), + _content('plays', player[index].totalPlays == 1 ? '1 Play' : '${player[index].totalPlays} Plays'), + ], + ), + padding: EdgeInsets.symmetric(vertical: 8.0), + ), + ), + ), + ]; + + List _block({ + @required String title, + @required List children, + }) => [ + LSHeader(text: title), + LSCard( + child: Padding( + child: Column( + children: children, + ), + padding: EdgeInsets.symmetric(vertical: 8.0), + ), + ), + ]; + + Widget _content(String header, String body) => Padding( + child: Row( + children: [ + Expanded( + child: Text( + header.toUpperCase(), + textAlign: TextAlign.end, + style: TextStyle( + color: Colors.white70, + ), + ), + flex: 2, + ), + Container(width: 16.0, height: 0.0), + Expanded( + child: Text( + body, + textAlign: TextAlign.start, + style: TextStyle(), + ), + flex: 5, + ), + ], + crossAxisAlignment: CrossAxisAlignment.start, + ), + padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + ); +} diff --git a/lib/modules/tautulli/modules/user_details/widgets/synced_items.dart b/lib/modules/tautulli/modules/user_details/widgets/synced_items.dart new file mode 100644 index 00000000..3df5ae73 --- /dev/null +++ b/lib/modules/tautulli/modules/user_details/widgets/synced_items.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliUserDetailsSyncedItems extends StatefulWidget { + final TautulliTableUser user; + + TautulliUserDetailsSyncedItems({ + Key key, + @required this.user, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + } + + Future _refresh() async { + TautulliState _state = Provider.of(context, listen: false); + _state.setUserSyncedItems( + widget.user.userId, + _state.api.libraries.getSyncedItems(userId: widget.user.userId), + ); + await _state.userSyncedItems[widget.user.userId]; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Widget get _body => LSRefreshIndicator( + refreshKey: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: Provider.of(context).userSyncedItems[widget.user.userId], + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + Logger.error( + 'TautulliUserDetailsSyncedItems', + '_body', + 'Unable to fetch Tautulli user synced items: ${widget.user.userId}', + snapshot.error, + null, + uploadToSentry: !(snapshot.error is DioError), + ); + } + return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); + } + if(snapshot.hasData) return snapshot.data.length == 0 + ? _noSyncedItems() + : _syncedItems(snapshot.data); + return LSLoader(); + }, + ), + ); + + Widget _syncedItems(List items) => LSListViewBuilder( + itemCount: items.length, + itemBuilder: (context, index) => TautulliSyncedItemTile(syncedItem: items[index]), + ); + + Widget _noSyncedItems() => LSGenericMessage( + text: 'No Synced Items Found', + showButton: true, + buttonText: 'Refresh', + onTapHandler: () async => _refreshKey.currentState.show(), + ); +} diff --git a/lib/modules/tautulli/modules/users/route.dart b/lib/modules/tautulli/modules/users/route.dart index 96dcb21c..4bda330f 100644 --- a/lib/modules/tautulli/modules/users/route.dart +++ b/lib/modules/tautulli/modules/users/route.dart @@ -43,7 +43,7 @@ class _State extends State with AutomaticKeepAliveClientMixi selector: (_, state) => state.users, builder: (context, users, _) => FutureBuilder( future: users, - builder: (context, snapshot) { + builder: (context, AsyncSnapshot snapshot) { if(snapshot.hasError) { if(snapshot.connectionState != ConnectionState.waiting) { Logger.error( @@ -55,21 +55,21 @@ class _State extends State with AutomaticKeepAliveClientMixi uploadToSentry: !(snapshot.error is DioError), ); } - return LSErrorMessage(onTapHandler: () async => _refresh()); + return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show()); } - if(snapshot.hasData) return (snapshot.data as TautulliUsersTable).users.length == 0 + if(snapshot.hasData) return snapshot.data.users.length == 0 ? _noUsers() - : _list((snapshot.data as TautulliUsersTable)); + : _users(snapshot.data); return LSLoader(); }, ), ), ); - Widget _list(TautulliUsersTable users) => LSListViewBuilder( - itemCount: users.users.length, - itemBuilder: (context, index) => TautulliUserTile(user: users.users[index]), - ); + Widget _users(TautulliUsersTable users) => LSListViewBuilder( + itemCount: users.users.length, + itemBuilder: (context, index) => TautulliUserTile(user: users.users[index]), + ); Widget _noUsers() => LSGenericMessage( text: 'No Users Found', diff --git a/lib/modules/tautulli/modules/users/widgets/user_tile.dart b/lib/modules/tautulli/modules/users/widgets/user_tile.dart index 41be7dda..9275461f 100644 --- a/lib/modules/tautulli/modules/users/widgets/user_tile.dart +++ b/lib/modules/tautulli/modules/users/widgets/user_tile.dart @@ -28,11 +28,13 @@ class TautulliUserTile extends StatelessWidget { ), onTap: () async => _enterDetails(context), ), - decoration: LSCardBackground( - darken: true, - uri: Provider.of(context, listen: false).getImageURLFromPath(user.thumb ?? ''), - headers: Provider.of(context, listen: false).headers.cast(), - ), + decoration: user.thumb != null && user.thumb.isNotEmpty + ? LSCardBackground( + darken: true, + uri: Provider.of(context, listen: false).getImageURLFromPath(user.thumb), + headers: Provider.of(context, listen: false).headers.cast(), + ) + : null, ); Widget _userThumb(BuildContext context) => CachedNetworkImage( @@ -124,12 +126,8 @@ class TautulliUserTile extends StatelessWidget { ], ); - Future _enterDetails(BuildContext context) async { - LSSnackBar( - context: context, - title: 'Coming Soon!', - message: 'This feature has not yet been implemented', - type: SNACKBAR_TYPE.info, - ); - } -} \ No newline at end of file + Future _enterDetails(BuildContext context) async => Navigator.of(context).pushNamed( + TautulliUserDetailsRoute.ROUTE_NAME, + arguments: TautulliUserDetailsRouteArguments(user: user), + ); +} diff --git a/lib/modules/wake_on_lan/core/constants.dart b/lib/modules/wake_on_lan/core/constants.dart index 55f81d77..7da05b13 100644 --- a/lib/modules/wake_on_lan/core/constants.dart +++ b/lib/modules/wake_on_lan/core/constants.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; class WakeOnLANConstants { WakeOnLANConstants._(); static const MODULE_KEY = 'wake_on_lan'; - static const Map MODULE_MAP = { - 'name': 'Wake on LAN', - 'desc': 'Wake a Sleeping Machine', - 'icon': Icons.settings_remote, - 'route': null, - }; + static const ModuleMap MODULE_MAP = ModuleMap( + name: 'Wake on LAN', + description: 'Wake a Sleeping Machine', + settingsDescription: 'Configure Wake on LAN', + icon: Icons.settings_remote, + route: null, + color: Color(Constants.ACCENT_COLOR), + ); } diff --git a/pubspec.lock b/pubspec.lock index 7bdf59fb..4cfcaad8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,14 +105,14 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "2.3.0-rc" + version: "2.3.1" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0-nullsafety.2" charcode: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.13" + version: "1.15.0-nullsafety.2" color: dependency: transitive description: @@ -472,7 +472,7 @@ packages: name: in_app_purchase url: "https://pub.dartlang.org" source: hosted - version: "0.3.4+4" + version: "0.3.4+5" intl: dependency: "direct main" description: @@ -521,7 +521,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0-nullsafety.2" mime: dependency: transitive description: @@ -718,6 +718,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.5" + quick_actions: + dependency: "direct main" + description: + name: quick_actions + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0+8" quiver: dependency: transitive description: @@ -855,7 +862,7 @@ packages: name: tautulli url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" term_glyph: dependency: transitive description: @@ -890,7 +897,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0-nullsafety.2" url_launcher: dependency: "direct main" description: @@ -946,7 +953,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0-nullsafety.2" wake_on_lan: dependency: "direct main" description: @@ -1004,5 +1011,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.9.0-14.0.dev <3.0.0" - flutter: ">=1.19.0-2.0.pre <2.0.0" + dart: ">=2.10.0-0.0.dev <2.10.0" + flutter: ">=1.20.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9cb14ae9..78d65dac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: lunasea description: Self Hosted Manager -version: 4.0.0+400002 +version: 4.0.0+400003 publish_to: 'none' environment: sdk: ">=2.7.0 <3.0.0" dependencies: - cached_network_image: ^2.3.0-rc + cached_network_image: ^2.3.1 connectivity: ^0.4.9+2 convert: ^2.1.1 cupertino_icons: ^0.1.3 @@ -27,13 +27,14 @@ dependencies: google_nav_bar: ^3.0.0 hive: ^1.4.4 hive_flutter: ^0.3.1 - in_app_purchase: ^0.3.4+4 + in_app_purchase: ^0.3.4+5 intl: ^0.16.1 package_info: ^0.4.3 path_provider: ^1.6.14 percent_indicator: ^2.1.5 permission_handler: ^5.0.1+1 provider: ^4.3.2+1 + quick_actions: ^0.4.0+8 sentry: ^3.0.1 stack_trace: ^1.9.5 table_calendar: ^2.2.3 @@ -42,7 +43,7 @@ dependencies: uuid: ^2.2.0 xml_parser: ^0.1.2 # LunaTools Packages - tautulli: ^1.0.1 + tautulli: ^1.0.2 wake_on_lan: ^1.1.1+1 dev_dependencies: