[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:
Jagandeep Brar
2020-08-27 09:26:05 -05:00
committed by GitHub
parent 2e0406d37e
commit b4df26abb6
69 changed files with 1550 additions and 309 deletions

View File

@@ -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

View File

@@ -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",
);

View File

@@ -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)
}
}

View File

@@ -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';

View File

@@ -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 = {

View File

@@ -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');
}

View File

@@ -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();
}
}

View 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
View 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,
});
}

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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';

View 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,
);
}

View File

@@ -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,

View File

@@ -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();
},

View File

@@ -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),

View File

@@ -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,
);
}

View File

@@ -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,
);
}

View File

@@ -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',

View File

@@ -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,
);
}

View File

@@ -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,
);
}

View File

@@ -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),
);
}

View File

@@ -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,
);
}

View File

@@ -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';

View File

@@ -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(),
];
}

View File

@@ -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();
}
),
),
);
}

View File

@@ -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'),

View File

@@ -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';

View File

@@ -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);
}

View File

@@ -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',

View File

@@ -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(),
};
}

View File

@@ -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 *
*********/

View File

@@ -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';

View File

@@ -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(),
);
}

View File

@@ -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(

View File

@@ -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(),
);

View File

@@ -0,0 +1,2 @@
export 'graphs/route.dart';
export 'graphs/widgets.dart';

View File

@@ -0,0 +1 @@
export 'widgets/history_tile.dart';

View 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,
);
}

View File

@@ -0,0 +1,2 @@
export 'libraries/route.dart';
export 'libraries/widgets.dart';

View File

@@ -0,0 +1,2 @@
export 'recently_added/route.dart';
export 'recently_added/widgets.dart';

View File

@@ -0,0 +1,2 @@
export 'statistics/route.dart';
export 'statistics/widgets.dart';

View File

@@ -0,0 +1,2 @@
export 'synced_items/route.dart';
export 'synced_items/widgets.dart';

View 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');
}

View File

@@ -0,0 +1 @@
export 'widgets/synced_item_tile.dart';

View File

@@ -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,
);
}

View File

@@ -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,

View File

@@ -0,0 +1,2 @@
export 'user_details/route.dart';
export 'user_details/widgets.dart';

View 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;
}

View 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';

View File

@@ -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(),
);
}

View File

@@ -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,
);
}

View File

@@ -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;
}
}

View 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),
);
}

View File

@@ -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(),
);
}

View File

@@ -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',

View File

@@ -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),
);
}

View File

@@ -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),
);
}

View File

@@ -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"

View File

@@ -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: