mirror of
https://github.com/jagandeepbrar/lunasea.git
synced 2026-08-30 20:24:25 +00:00
[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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<String, ModuleMap> 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 = {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
50
lib/core/homescreen_actions.dart
Normal file
50
lib/core/homescreen_actions.dart
Normal file
@@ -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(<ShortcutItem>[
|
||||
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);
|
||||
}
|
||||
19
lib/core/module_map.dart
Normal file
19
lib/core/module_map.dart
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
23
lib/modules/home/core/constants.dart
Normal file
23
lib/modules/home/core/constants.dart
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,12 @@ class _State extends State<Home> {
|
||||
final List _refreshKeys = [GlobalKey<RefreshIndicatorState>()];
|
||||
String _profileState = Database.currentProfileObject.toString();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
HomescreenActions.initialize(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => WillPopScope(
|
||||
onWillPop: _willPopScope,
|
||||
|
||||
@@ -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<HomeQuickAccess> 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<HomeQuickAccess> 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();
|
||||
},
|
||||
|
||||
@@ -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<dynamic> route) => false),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<SettingsModules> 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<Widget> get _automation => [
|
||||
@@ -82,36 +51,9 @@ class _State extends State<SettingsModules> 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<Widget> get _clients => [
|
||||
@@ -119,26 +61,8 @@ class _State extends State<SettingsModules> 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<Widget> get _monitoring => [
|
||||
@@ -146,15 +70,13 @@ class _State extends State<SettingsModules> 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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<SettingsModulesLunaSea> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<SettingsModulesLunaSea> {
|
||||
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@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: <Widget>[
|
||||
..._appearance,
|
||||
..._drawer,
|
||||
if(Platform.isIOS) ..._other,
|
||||
],
|
||||
);
|
||||
|
||||
List<Widget> get _appearance => [
|
||||
LSHeader(
|
||||
text: 'Appearance',
|
||||
subtitle: 'Customize the appearance of ${Constants.APPLICATION_NAME} to fit your needs',
|
||||
),
|
||||
SettingsModulesLunaSeaAMOLEDTile(),
|
||||
SettingsModulesLunaSeaAMOLEDBorderTile(),
|
||||
];
|
||||
|
||||
List<Widget> get _other => [
|
||||
LSHeader(
|
||||
text: 'Other',
|
||||
subtitle: 'All other customizable options',
|
||||
),
|
||||
SettingsModulesLunaSeaBrowserTile(),
|
||||
];
|
||||
|
||||
List<Widget> 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';
|
||||
|
||||
@@ -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<SettingsModulesLunaSea> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<SettingsModulesLunaSea> {
|
||||
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@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: <Widget>[
|
||||
..._appearance,
|
||||
..._drawer,
|
||||
..._other,
|
||||
],
|
||||
);
|
||||
|
||||
List<Widget> get _appearance => [
|
||||
LSHeader(
|
||||
text: 'Appearance',
|
||||
subtitle: 'Customize the appearance of ${Constants.APPLICATION_NAME} to fit your needs',
|
||||
),
|
||||
SettingsModulesLunaSeaAMOLEDTile(),
|
||||
SettingsModulesLunaSeaAMOLEDBorderTile(),
|
||||
];
|
||||
|
||||
List<Widget> get _other => [
|
||||
LSHeader(
|
||||
text: 'Other',
|
||||
subtitle: 'All other customizable options',
|
||||
),
|
||||
SettingsModulesQuickActionsTile(),
|
||||
if(Platform.isIOS) SettingsModulesLunaSeaBrowserTile(),
|
||||
];
|
||||
|
||||
List<Widget> get _drawer => [
|
||||
LSHeader(
|
||||
text: 'Drawer',
|
||||
subtitle: 'Customize the drawer to fit your needs',
|
||||
),
|
||||
SettingsDrawerExpandAutomationTile(),
|
||||
SettingsDrawerExpandClientsTile(),
|
||||
SettingsDrawerExpandMonitoringTile(),
|
||||
];
|
||||
}
|
||||
@@ -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<SettingsModulesLunaSeaQuickActions> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<SettingsModulesLunaSeaQuickActions> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@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();
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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<SettingsSystem> 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'),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<void> _onTap(BuildContext context) async => Navigator.of(context).pushNamed(SettingsModulesLunaSeaQuickActions.ROUTE_NAME);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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<String, WidgetBuilder> MODULE_ROUTES = <String, WidgetBuilder>{
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<TautulliUsersTable> _users;
|
||||
Future<TautulliUsersTable> get users => _users;
|
||||
set users(Future<TautulliUsersTable> users) {
|
||||
@@ -135,9 +145,78 @@ class TautulliState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Storing individual user profile data
|
||||
Map<int, Future<TautulliUser>> _userProfile;
|
||||
Map<int, Future<TautulliUser>> get userProfile => _userProfile;
|
||||
void setUserProfile(int userId, Future<TautulliUser> data) {
|
||||
assert(userId != null);
|
||||
assert(data != null);
|
||||
_userProfile[userId] = data;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Storing individual user synced items
|
||||
Map<int, Future<List<TautulliSyncedItem>>> _userSyncedItems;
|
||||
Map<int, Future<List<TautulliSyncedItem>>> get userSyncedItems => _userSyncedItems;
|
||||
void setUserSyncedItems(int userId, Future<List<TautulliSyncedItem>> data) {
|
||||
assert(userId != null);
|
||||
assert(data != null);
|
||||
_userSyncedItems[userId] = data;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Storing individual user IP addresses
|
||||
Map<int, Future<TautulliUserIPs>> _userIPs;
|
||||
Map<int, Future<TautulliUserIPs>> get userIPs => _userIPs;
|
||||
void setUserIPs(int userId, Future<TautulliUserIPs> data) {
|
||||
assert(userId != null);
|
||||
assert(data != null);
|
||||
_userIPs[userId] = data;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Storing individual user watch time stats
|
||||
Map<int, Future<List<TautulliUserWatchTimeStats>>> _userWatchStats;
|
||||
Map<int, Future<List<TautulliUserWatchTimeStats>>> get userWatchStats => _userWatchStats;
|
||||
void setUserWatchStats(int userId, Future<List<TautulliUserWatchTimeStats>> data) {
|
||||
assert(userId != null);
|
||||
assert(data != null);
|
||||
_userWatchStats[userId] = data;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Storing individual user player stats
|
||||
Map<int, Future<List<TautulliUserPlayerStats>>> _userPlayerStats;
|
||||
Map<int, Future<List<TautulliUserPlayerStats>>> get userPlayerStats => _userPlayerStats;
|
||||
void setUserPlayerStats(int userId, Future<List<TautulliUserPlayerStats>> data) {
|
||||
assert(userId != null);
|
||||
assert(data != null);
|
||||
_userPlayerStats[userId] = data;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Map<int, Future<TautulliHistory>> _userHistory;
|
||||
Map<int, Future<TautulliHistory>> get userHistory => _userHistory;
|
||||
void setUserHistory(int userId, Future<TautulliHistory> 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 *
|
||||
*********/
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -43,7 +43,7 @@ class _State extends State<TautulliActivityRoute> with AutomaticKeepAliveClientM
|
||||
selector: (_, state) => state.activity,
|
||||
builder: (context, activity, _) => FutureBuilder(
|
||||
future: activity,
|
||||
builder: (context, snapshot) {
|
||||
builder: (context, AsyncSnapshot<TautulliActivity> snapshot) {
|
||||
if(snapshot.hasError) {
|
||||
if(snapshot.connectionState != ConnectionState.waiting) {
|
||||
Logger.error(
|
||||
@@ -55,18 +55,18 @@ class _State extends State<TautulliActivityRoute> 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<TautulliActivityRoute> with AutomaticKeepAliveClientM
|
||||
text: 'No Active Streams',
|
||||
showButton: true,
|
||||
buttonText: 'Refresh',
|
||||
onTapHandler: () async => _refresh(),
|
||||
onTapHandler: () async => _refreshKey.currentState.show(),
|
||||
);
|
||||
}
|
||||
@@ -28,11 +28,13 @@ class TautulliActivityTile extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
|
||||
onTap: () async => _enterDetails(context),
|
||||
),
|
||||
decoration: LSCardBackground(
|
||||
uri: Provider.of<TautulliState>(context, listen: false).getImageURLFromPath(session.art),
|
||||
headers: Provider.of<TautulliState>(context, listen: false).headers,
|
||||
darken: true,
|
||||
),
|
||||
decoration: session.art != null && session.art.isNotEmpty
|
||||
? LSCardBackground(
|
||||
uri: Provider.of<TautulliState>(context, listen: false).getImageURLFromPath(session.art),
|
||||
headers: Provider.of<TautulliState>(context, listen: false).headers,
|
||||
darken: true,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
Widget _body(BuildContext context) => Row(
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
2
lib/modules/tautulli/modules/graphs.dart
Normal file
2
lib/modules/tautulli/modules/graphs.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'graphs/route.dart';
|
||||
export 'graphs/widgets.dart';
|
||||
0
lib/modules/tautulli/modules/graphs/route.dart
Normal file
0
lib/modules/tautulli/modules/graphs/route.dart
Normal file
0
lib/modules/tautulli/modules/graphs/widgets.dart
Normal file
0
lib/modules/tautulli/modules/graphs/widgets.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'widgets/history_tile.dart';
|
||||
|
||||
159
lib/modules/tautulli/modules/history/widgets/history_tile.dart
Normal file
159
lib/modules/tautulli/modules/history/widgets/history_tile.dart
Normal file
@@ -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<TautulliState>(context, listen: false).getImageURLFromRatingKey(
|
||||
history.grandparentRatingKey ?? history.parentRatingKey ?? history.ratingKey ?? '',
|
||||
),
|
||||
headers: Provider.of<TautulliState>(context, listen: false).headers.cast<String, String>(),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _poster(BuildContext context) => CachedNetworkImage(
|
||||
fadeInDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED),
|
||||
fadeOutDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED),
|
||||
imageUrl: Provider.of<TautulliState>(context, listen: false).getImageURLFromPath(history.thumb),
|
||||
httpHeaders: Provider.of<TautulliState>(context, listen: false).headers.cast<String, String>(),
|
||||
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: <TextSpan>[
|
||||
// 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<void> _onTap(BuildContext context) async => LSSnackBar(
|
||||
context: context,
|
||||
title: 'Coming Soon!',
|
||||
message: 'History details have not yet been implemented',
|
||||
type: SNACKBAR_TYPE.info,
|
||||
);
|
||||
}
|
||||
2
lib/modules/tautulli/modules/libraries.dart
Normal file
2
lib/modules/tautulli/modules/libraries.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'libraries/route.dart';
|
||||
export 'libraries/widgets.dart';
|
||||
0
lib/modules/tautulli/modules/libraries/route.dart
Normal file
0
lib/modules/tautulli/modules/libraries/route.dart
Normal file
0
lib/modules/tautulli/modules/libraries/widgets.dart
Normal file
0
lib/modules/tautulli/modules/libraries/widgets.dart
Normal file
2
lib/modules/tautulli/modules/recently_added.dart
Normal file
2
lib/modules/tautulli/modules/recently_added.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'recently_added/route.dart';
|
||||
export 'recently_added/widgets.dart';
|
||||
2
lib/modules/tautulli/modules/statistics.dart
Normal file
2
lib/modules/tautulli/modules/statistics.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'statistics/route.dart';
|
||||
export 'statistics/widgets.dart';
|
||||
0
lib/modules/tautulli/modules/statistics/route.dart
Normal file
0
lib/modules/tautulli/modules/statistics/route.dart
Normal file
2
lib/modules/tautulli/modules/synced_items.dart
Normal file
2
lib/modules/tautulli/modules/synced_items.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'synced_items/route.dart';
|
||||
export 'synced_items/widgets.dart';
|
||||
25
lib/modules/tautulli/modules/synced_items/route.dart
Normal file
25
lib/modules/tautulli/modules/synced_items/route.dart
Normal file
@@ -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<TautulliSyncedItemsRoute> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<TautulliSyncedItemsRoute> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: _appBar,
|
||||
);
|
||||
|
||||
Widget get _appBar => LSAppBar(title: 'Synced Items');
|
||||
}
|
||||
1
lib/modules/tautulli/modules/synced_items/widgets.dart
Normal file
1
lib/modules/tautulli/modules/synced_items/widgets.dart
Normal file
@@ -0,0 +1 @@
|
||||
export '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<TautulliState>(context, listen: false).getImageURLFromRatingKey(syncedItem.ratingKey),
|
||||
headers: Provider.of<TautulliState>(context, listen: false).headers,
|
||||
)
|
||||
: null,
|
||||
onTap: () async => _onTap(context),
|
||||
padContent: true,
|
||||
);
|
||||
|
||||
Future<void> _onTap(BuildContext context) async => LSSnackBar(
|
||||
context: context,
|
||||
title: 'Coming Soon!',
|
||||
message: 'Library data has not yet been implemented',
|
||||
type: SNACKBAR_TYPE.info,
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class TautulliNavigationBar extends StatefulWidget {
|
||||
class _State extends State<TautulliNavigationBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Selector<TautulliState, int>(
|
||||
selector: (_, model) => model.navigationIndex,
|
||||
selector: (_, state) => state.navigationIndex,
|
||||
builder: (context, index, _) => LSBottomNavigationBar(
|
||||
index: index,
|
||||
icons: TautulliNavigationBar.icons,
|
||||
|
||||
2
lib/modules/tautulli/modules/user_details.dart
Normal file
2
lib/modules/tautulli/modules/user_details.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'user_details/route.dart';
|
||||
export 'user_details/widgets.dart';
|
||||
70
lib/modules/tautulli/modules/user_details/route.dart
Normal file
70
lib/modules/tautulli/modules/user_details/route.dart
Normal file
@@ -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<StatefulWidget> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<TautulliUserDetailsRoute> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
TautulliUserDetailsRouteArguments _arguments;
|
||||
PageController _pageController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(initialPage: Provider.of<TautulliState>(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<Widget> 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<TautulliState>(context, listen: false).userDetailsNavigationIndex = index;
|
||||
}
|
||||
5
lib/modules/tautulli/modules/user_details/widgets.dart
Normal file
5
lib/modules/tautulli/modules/user_details/widgets.dart
Normal file
@@ -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';
|
||||
@@ -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<StatefulWidget> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<TautulliUserDetailsHistory> with AutomaticKeepAliveClientMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh());
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
TautulliState _state = Provider.of<TautulliState>(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<TautulliState>(context).userHistory[widget.user.userId],
|
||||
builder: (context, AsyncSnapshot<TautulliHistory> 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(),
|
||||
);
|
||||
}
|
||||
@@ -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<StatefulWidget> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<TautulliUserDetailsIPAddresses> with AutomaticKeepAliveClientMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh());
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
TautulliState _state = Provider.of<TautulliState>(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<TautulliState>(context).userIPs[widget.user.userId],
|
||||
builder: (context, AsyncSnapshot<TautulliUserIPs> 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<TautulliState>(context, listen: false).getImageURLFromPath(record.thumb ?? ''),
|
||||
headers: Provider.of<TautulliState>(context, listen: false).headers,
|
||||
darken: true,
|
||||
)
|
||||
: null,
|
||||
padContent: true,
|
||||
);
|
||||
}
|
||||
@@ -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<IconData> icons = [
|
||||
CustomIcons.user,
|
||||
CustomIcons.history,
|
||||
Icons.sync,
|
||||
Icons.computer,
|
||||
];
|
||||
|
||||
static const List<String> titles = [
|
||||
'Profile',
|
||||
'History',
|
||||
'Synced',
|
||||
'IPs',
|
||||
];
|
||||
|
||||
final PageController pageController;
|
||||
|
||||
TautulliUserDetailsNavigationBar({
|
||||
Key key,
|
||||
@required this.pageController,
|
||||
}): super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<TautulliUserDetailsNavigationBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Selector<TautulliState, int>(
|
||||
selector: (_, state) => state.userDetailsNavigationIndex,
|
||||
builder: (context, index, _) => LSBottomNavigationBar(
|
||||
index: index,
|
||||
icons: TautulliUserDetailsNavigationBar.icons,
|
||||
titles: TautulliUserDetailsNavigationBar.titles,
|
||||
onTap: _navOnTap,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _navOnTap(int index) async {
|
||||
await widget.pageController.animateToPage(
|
||||
index,
|
||||
duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED),
|
||||
curve: Curves.easeOutSine,
|
||||
);
|
||||
Provider.of<TautulliState>(context, listen: false).userDetailsNavigationIndex = index;
|
||||
}
|
||||
}
|
||||
216
lib/modules/tautulli/modules/user_details/widgets/profile.dart
Normal file
216
lib/modules/tautulli/modules/user_details/widgets/profile.dart
Normal file
@@ -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<StatefulWidget> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<TautulliUserDetailsProfile> with AutomaticKeepAliveClientMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@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<void> _refresh() async {
|
||||
TautulliState _state = Provider.of<TautulliState>(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<TautulliState>(context).userProfile[widget.user.userId],
|
||||
Provider.of<TautulliState>(context).userWatchStats[widget.user.userId],
|
||||
Provider.of<TautulliState>(context).userPlayerStats[widget.user.userId],
|
||||
]),
|
||||
builder: (context, AsyncSnapshot<List<Object>> 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<TautulliUserWatchTimeStats> watchtime,
|
||||
@required List<TautulliUserPlayerStats> player,
|
||||
}) => LSListView(
|
||||
children: [
|
||||
..._profile(user),
|
||||
..._globalStats(watchtime),
|
||||
if(player.length > 0) ..._playerStats(player),
|
||||
],
|
||||
);
|
||||
|
||||
List<Widget> _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<Widget> _globalStats(List<TautulliUserWatchTimeStats> 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<Widget> _playerStats(List<TautulliUserPlayerStats> 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<Widget> _block({
|
||||
@required String title,
|
||||
@required List<Widget> 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),
|
||||
);
|
||||
}
|
||||
@@ -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<StatefulWidget> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<TautulliUserDetailsSyncedItems> with AutomaticKeepAliveClientMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh());
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
TautulliState _state = Provider.of<TautulliState>(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<TautulliState>(context).userSyncedItems[widget.user.userId],
|
||||
builder: (context, AsyncSnapshot<List<TautulliSyncedItem>> 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<TautulliSyncedItem> 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(),
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class _State extends State<TautulliUsersRoute> with AutomaticKeepAliveClientMixi
|
||||
selector: (_, state) => state.users,
|
||||
builder: (context, users, _) => FutureBuilder(
|
||||
future: users,
|
||||
builder: (context, snapshot) {
|
||||
builder: (context, AsyncSnapshot<TautulliUsersTable> snapshot) {
|
||||
if(snapshot.hasError) {
|
||||
if(snapshot.connectionState != ConnectionState.waiting) {
|
||||
Logger.error(
|
||||
@@ -55,21 +55,21 @@ class _State extends State<TautulliUsersRoute> 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',
|
||||
|
||||
@@ -28,11 +28,13 @@ class TautulliUserTile extends StatelessWidget {
|
||||
),
|
||||
onTap: () async => _enterDetails(context),
|
||||
),
|
||||
decoration: LSCardBackground(
|
||||
darken: true,
|
||||
uri: Provider.of<TautulliState>(context, listen: false).getImageURLFromPath(user.thumb ?? ''),
|
||||
headers: Provider.of<TautulliState>(context, listen: false).headers.cast<String, String>(),
|
||||
),
|
||||
decoration: user.thumb != null && user.thumb.isNotEmpty
|
||||
? LSCardBackground(
|
||||
darken: true,
|
||||
uri: Provider.of<TautulliState>(context, listen: false).getImageURLFromPath(user.thumb),
|
||||
headers: Provider.of<TautulliState>(context, listen: false).headers.cast<String, String>(),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
Widget _userThumb(BuildContext context) => CachedNetworkImage(
|
||||
@@ -124,12 +126,8 @@ class TautulliUserTile extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
|
||||
Future<void> _enterDetails(BuildContext context) async {
|
||||
LSSnackBar(
|
||||
context: context,
|
||||
title: 'Coming Soon!',
|
||||
message: 'This feature has not yet been implemented',
|
||||
type: SNACKBAR_TYPE.info,
|
||||
);
|
||||
}
|
||||
}
|
||||
Future<void> _enterDetails(BuildContext context) async => Navigator.of(context).pushNamed(
|
||||
TautulliUserDetailsRoute.ROUTE_NAME,
|
||||
arguments: TautulliUserDetailsRouteArguments(user: user),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
27
pubspec.lock
27
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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user