[Release] v2.3.0 (230004) (#220)

* [Packages] Updated packages
* [Sonarr/Series] Switched to bottom navigation, removed sliver app bar
* [Sonarr/Series] Removed bottom padding from overview
* [README] Added AltStore, Removed Feedback, Updated Android APK Build (#213)
* [Snackbar] Fixed cases where iOS swipe-back would lock UI
* [Android/Back Button] Back button now shows drawer on primary routes instead of closing app
* [Wake on LAN] Added configuration to hive
* [Settings/Modules] Added Wake on LAN section
* [Wake on LAN] Added entry to drawer
* [Lidarr/Details] Removed sliver app bar, removed fanart, switched to bottom navigation bar
* [Radarr/Details] Removed sliver app bar, removed fanart, switched to bottom navigation bar
* [Wake on LAN] Show toast on action
* [Wake on LAN] Created RawDatagramSocket binding
* [WoL] Full support for Wake on LAN functionality
* [Semantics] Cleanup
* [TestFlight] v2.3.0 (230002)
* [Settings/Modules] Implemented settings UI for custom headers
* [Custom Headers] Finished UI implementation
* [Custom Headers] Integrated into API calls
* [TestFlight] v2.3.0 (230003)
* [Images] Attach custom headers to all network image requests
* [Everything] Many Changes (Read Notes) (#218)
    - Unified the structure of each module (core, routes, widgets at the root of every module)
    - Cleaned Most Remaining Dialogs (SABnzbd and NZBGet remain)
    - Reduced font-size across dialogs
    - Unified padding across all dialogs (that have been cleaned so far)
    - Changelog is now a routed page, not a dialog prompt
- SABnzbd: Fixed bug where setting priority to "stop" would cause a fatal crash
* [Networking] Added RegExp Validation for IPv4 & MAC Addresses
* [Networking] Improved network address classes
* Finished Dialog Rework
* [TestFlight] v2.3.0 (230004)
* [README] Updated README for v2.3.0
This commit is contained in:
Jagandeep Brar
2020-06-14 23:58:49 -05:00
committed by GitHub
parent 184d8c7199
commit 5d36aede4a
210 changed files with 6010 additions and 8160 deletions

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ lib/generated_plugin_registrant.dart
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
ios/Flutter/.last_build_id
# LunaSea
LunaSea.ipa

View File

@@ -1,7 +1,6 @@
[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/LunaSeaApp?label=Subreddit&logo=reddit&logoColor=white&style=for-the-badge)](https://www.reddit.com/r/LunaSeaApp)
[![AltStore](https://img.shields.io/badge/AltStore-v2.2.0-red?style=for-the-badge&logo=palo-alto-software&color=%233B878E&logoColor=white)](https://altstore.lunasea.app)
[![App Store](https://img.shields.io/badge/App%20Store-v2.2.0-red?style=for-the-badge&logo=app-store&color=%230D96F6&logoColor=white)](https://apps.apple.com/us/app/lunasea/id1496797802?ls=1)
[![Play Store](https://img.shields.io/badge/Google%20Play-v2.2.0-red?style=for-the-badge&logo=google-play&color=%230F9D58&logoColor=white)](https://play.google.com/store/apps/details?id=app.lunasea.lunasea)
[![App Store](https://img.shields.io/badge/App%20Store-v2.3.0-red?style=for-the-badge&logo=app-store&color=%230D96F6&logoColor=white)](https://apps.apple.com/us/app/lunasea/id1496797802?ls=1)
[![Play Store](https://img.shields.io/badge/Google%20Play-v2.3.0-red?style=for-the-badge&logo=google-play&color=%230F9D58&logoColor=white)](https://play.google.com/store/apps/details?id=app.lunasea.lunasea)
![LunaSea](https://www.lunasea.app/images/banner.png)
@@ -16,6 +15,7 @@ LunaSea is a fully featured, open source self-hosted media manager! Focused on g
- NZBGet
- NZBHydra2
- Newznab Indexer Searching
- Wake on LAN
LunaSea even comes with support for multiple instances of applications using profiles, backing up and restoring your configuration to your filesystem, an AMOLED black theme, and more!

View File

@@ -1,6 +1,39 @@
PODS:
- DKImagePickerController/Core (4.3.1):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.1)
- DKImagePickerController/PhotoGallery (4.3.1):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.1)
- DKPhotoGallery (0.0.15):
- DKPhotoGallery/Core (= 0.0.15)
- DKPhotoGallery/Model (= 0.0.15)
- DKPhotoGallery/Preview (= 0.0.15)
- DKPhotoGallery/Resource (= 0.0.15)
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Core (0.0.15):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Model (0.0.15):
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Preview (0.0.15):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SDWebImageFLPlugin
- DKPhotoGallery/Resource (0.0.15):
- SDWebImage
- SDWebImageFLPlugin
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- FLAnimatedImage (1.0.12)
- Flutter (1.0.0)
- flutter_plugin_android_lifecycle (0.0.1):
- Flutter
@@ -14,6 +47,12 @@ PODS:
- Flutter
- "permission_handler (4.4.0+hotfix.2)":
- Flutter
- SDWebImage (5.8.0):
- SDWebImage/Core (= 5.8.0)
- SDWebImage/Core (5.8.0)
- SDWebImageFLPlugin (0.4.0):
- FLAnimatedImage (>= 1.0.11)
- SDWebImage/Core (~> 5.6)
- url_launcher (0.0.1):
- Flutter
- url_launcher_macos (0.0.1):
@@ -34,6 +73,14 @@ DEPENDENCIES:
- url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`)
- url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- FLAnimatedImage
- SDWebImage
- SDWebImageFLPlugin
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
@@ -59,15 +106,20 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_web/ios"
SPEC CHECKSUMS:
file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245
DKImagePickerController: f3d397887f38b98276a506b02ed9c167425f7cf3
DKPhotoGallery: e880aef16c108333240e1e7327896f2ea380f4f0
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
package_info: 48b108e75b8802c2d5e126f208ef540561c98aef
path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
permission_handler: d5d46b2ca93671e8dcf870b949e304c7ddb65ad0
url_launcher: a1c0cc845906122c4784c542523d8cacbded5626
SDWebImage: 84000f962cbfa70c07f19d2234cbfcf5d779b5dc
SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313
url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c

View File

@@ -314,7 +314,6 @@
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -396,7 +395,6 @@
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -452,7 +450,6 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;

View File

@@ -2,10 +2,12 @@ export './core/constants.dart';
export './core/configuration.dart';
export './core/data.dart';
export './core/database.dart';
export './core/dialogs.dart';
export './core/extensions.dart';
export './core/in_app_purchases.dart';
export './core/logger.dart';
export './core/module_flags.dart';
export './core/networking.dart';
export './core/providers.dart';
export './core/routes.dart';
export './core/theme.dart';

View File

@@ -19,21 +19,25 @@ class Import {
sonarrKey: profile['sonarrKey'] ?? '',
sonarrStrictTLS: profile['sonarrStrictTLS'] ?? true,
sonarrVersion3: profile['sonarrVersion3'] ?? false,
sonarrHeaders: profile['sonarrHeaders'] ?? {},
//Radarr
radarrEnabled: profile['radarrEnabled'] ?? false,
radarrHost: profile['radarrHost'] ?? '',
radarrKey: profile['radarrKey'] ?? '',
radarrStrictTLS: profile['radarrStrictTLS'] ?? true,
radarrHeaders: profile['radarrHeaders'] ?? {},
//Lidarr
lidarrEnabled: profile['lidarrEnabled'] ?? false,
lidarrHost: profile['lidarrHost'] ?? '',
lidarrKey: profile['lidarrKey'] ?? '',
lidarrStrictTLS: profile['lidarrStrictTLS'] ?? true,
lidarrHeaders: profile['lidarrHeaders'] ?? {},
//SABnzbd
sabnzbdEnabled: profile['sabnzbdEnabled'] ?? false,
sabnzbdHost: profile['sabnzbdHost'] ?? '',
sabnzbdKey: profile['sabnzbdKey'] ?? '',
sabnzbdStrictTLS: profile['sabnzbdStrictTLS'] ?? true,
sabnzbdHeaders: profile['sabnzbdHeaders'] ?? {},
//NZBGet
nzbgetEnabled: profile['nzbgetEnabled'] ?? false,
nzbgetHost: profile['nzbgetHost'] ?? '',
@@ -41,6 +45,11 @@ class Import {
nzbgetPass: profile['nzbgetPass'] ?? '',
nzbgetStrictTLS: profile['nzbgetStrictTLS'] ?? true,
nzbgetBasicAuth: profile['nzbgetBasicAuth'] ?? false,
nzbgetHeaders: profile['nzbgetHeaders'] ?? {},
//Wake on LAN
wakeOnLANEnabled: profile['wakeOnLANEnabled'] ?? false,
wakeOnLANBroadcastAddress: profile['wakeOnLANBroadcastAddress'] ?? '',
wakeOnLANMACAddress: profile['wakeOnLANMACAddress'] ?? '',
));
}
}

View File

@@ -1,38 +1,18 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules.dart';
class Constants {
Constants._();
//Services
static const Map SERVICE_MAP = {
'lidarr': LidarrConstants.SERVICE_MAP,
'radarr': RadarrConstants.SERVICE_MAP,
'sonarr': SonarrConstants.SERVICE_MAP,
'nzbget': {
'name': 'NZBGet',
'desc': 'Manage Usenet Downloads',
'icon': CustomIcons.nzbget,
'route': '/nzbget',
},
'sabnzbd': {
'name': 'SABnzbd',
'desc': 'Manage Usenet Downloads',
'icon': CustomIcons.sabnzbd,
'route': '/sabnzbd',
},
'search': {
'name': 'Search',
'desc': 'Search Newznab Indexers',
'icon': Icons.search,
'route': '/search',
},
'settings': {
'name': 'Settings',
'desc': 'Update Configuration',
'icon': Icons.settings,
'route': '/settings',
}
static const Map MODULE_MAP = {
LidarrConstants.MODULE_KEY: LidarrConstants.MODULE_MAP,
RadarrConstants.MODULE_KEY: RadarrConstants.MODULE_MAP,
SonarrConstants.MODULE_KEY: SonarrConstants.MODULE_MAP,
NZBGetConstants.MODULE_KEY: NZBGetConstants.MODULE_MAP,
SABnzbdConstants.MODULE_KEY: SABnzbdConstants.MODULE_MAP,
SearchConstants.MODULE_KEY: SearchConstants.MODULE_MAP,
SettingsConstants.MODULE_KEY: SettingsConstants.MODULE_MAP,
WakeOnLANConstants.MODULE_KEY: WakeOnLANConstants.MODULE_MAP,
};
//Colors
static const PRIMARY_COLOR = 0xFF32323E;
@@ -42,15 +22,16 @@ class Constants {
static const LIST_COLOR_ICONS = [
Colors.blue,
Color(ACCENT_COLOR),
Colors.orange,
Colors.red,
Colors.orange,
Colors.deepPurpleAccent,
Colors.blueGrey,
];
//UI
static const UI_ELEVATION = 2.0;
static const UI_ELEVATION = 0.0;
static const UI_CARD_MARGIN = EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0);
static const UI_NAVIGATION_SPEED = 250;
static const UI_BORDER_RADIUS = 12.0;
//General
static const EMPTY_MAP = {};
static const EMPTY_LIST = [];
@@ -73,6 +54,7 @@ class Constants {
//Changelog
static const EMPTY_CHANGELOG = [{
"version": "Changelog Error",
"date": "Unknown Date",
"new": ['Unable to fetch changes'],
"fixes": ['Unable to fetch changes'],
"tweaks": ['Unable to fetch changes'],

View File

@@ -4,6 +4,8 @@ import 'package:lunasea/core.dart';
part 'profile.g.dart';
//Next HiveField ID: 31
@HiveType(typeId: 0, adapterName: 'ProfileHiveObjectAdapter')
class ProfileHiveObject extends HiveObject {
factory ProfileHiveObject.empty() {
@@ -13,22 +15,26 @@ class ProfileHiveObject extends HiveObject {
lidarrHost: '',
lidarrKey: '',
lidarrStrictTLS: true,
lidarrHeaders: {},
//Radarr
radarrEnabled: false,
radarrHost: '',
radarrKey: '',
radarrStrictTLS: true,
radarrHeaders: {},
//Sonarr
sonarrEnabled: false,
sonarrHost: '',
sonarrKey: '',
sonarrStrictTLS: true,
sonarrVersion3: false,
sonarrHeaders: {},
//SABnzbd
sabnzbdEnabled: false,
sabnzbdHost: '',
sabnzbdKey: '',
sabnzbdStrictTLS: true,
sabnzbdHeaders: {},
//NZBGet
nzbgetEnabled: false,
nzbgetHost: '',
@@ -36,6 +42,11 @@ class ProfileHiveObject extends HiveObject {
nzbgetPass: '',
nzbgetStrictTLS: true,
nzbgetBasicAuth: false,
nzbgetHeaders: {},
//Wake on LAN
wakeOnLANEnabled: false,
wakeOnLANBroadcastAddress: '',
wakeOnLANMACAddress: '',
);
}
@@ -46,22 +57,26 @@ class ProfileHiveObject extends HiveObject {
lidarrHost: obj.lidarrHost,
lidarrKey: obj.lidarrKey,
lidarrStrictTLS: obj.lidarrStrictTLS,
lidarrHeaders: obj.lidarrHeaders,
//Radarr
radarrEnabled: obj.radarrEnabled,
radarrHost: obj.radarrHost,
radarrKey: obj.radarrKey,
radarrStrictTLS: obj.radarrStrictTLS,
radarrHeaders: obj.radarrHeaders,
//Sonarr
sonarrEnabled: obj.sonarrEnabled,
sonarrHost: obj.sonarrHost,
sonarrKey: obj.sonarrKey,
sonarrStrictTLS: obj.sonarrStrictTLS,
sonarrVersion3: obj.sonarrVersion3,
sonarrHeaders: obj.sonarrHeaders,
//SABnzbd
sabnzbdEnabled: obj.sabnzbdEnabled,
sabnzbdHost: obj.sabnzbdHost,
sabnzbdKey: obj.sabnzbdKey,
sabnzbdStrictTLS: obj.sabnzbdStrictTLS,
sabnzbdHeaders: obj.sabnzbdHeaders,
//NZBGet
nzbgetEnabled: obj.nzbgetEnabled,
nzbgetHost: obj.nzbgetHost,
@@ -69,6 +84,11 @@ class ProfileHiveObject extends HiveObject {
nzbgetPass: obj.nzbgetPass,
nzbgetStrictTLS: obj.nzbgetStrictTLS,
nzbgetBasicAuth: obj.nzbgetBasicAuth,
nzbgetHeaders: obj.nzbgetHeaders,
//Wake On LAN
wakeOnLANEnabled: obj.wakeOnLANEnabled,
wakeOnLANBroadcastAddress: obj.wakeOnLANBroadcastAddress,
wakeOnLANMACAddress: obj.wakeOnLANMACAddress,
);
}
@@ -78,22 +98,26 @@ class ProfileHiveObject extends HiveObject {
@required this.lidarrHost,
@required this.lidarrKey,
@required this.lidarrStrictTLS,
@required this.lidarrHeaders,
//Radarr
@required this.radarrEnabled,
@required this.radarrHost,
@required this.radarrKey,
@required this.radarrStrictTLS,
@required this.radarrHeaders,
//Sonarr
@required this.sonarrEnabled,
@required this.sonarrHost,
@required this.sonarrKey,
@required this.sonarrStrictTLS,
@required this.sonarrVersion3,
@required this.sonarrHeaders,
//SABnzbd
@required this.sabnzbdEnabled,
@required this.sabnzbdHost,
@required this.sabnzbdKey,
@required this.sabnzbdStrictTLS,
@required this.sabnzbdHeaders,
//NZBGet
@required this.nzbgetEnabled,
@required this.nzbgetHost,
@@ -101,6 +125,11 @@ class ProfileHiveObject extends HiveObject {
@required this.nzbgetPass,
@required this.nzbgetStrictTLS,
@required this.nzbgetBasicAuth,
@required this.nzbgetHeaders,
//Wake On LAN
@required this.wakeOnLANEnabled,
@required this.wakeOnLANBroadcastAddress,
@required this.wakeOnLANMACAddress,
});
@override
@@ -117,21 +146,25 @@ class ProfileHiveObject extends HiveObject {
"sonarrKey": sonarrKey,
"sonarrStrictTLS": sonarrStrictTLS,
"sonarrVersion3": sonarrVersion3,
"sonarrHeaders": sonarrHeaders,
//Radarr
"radarrEnabled": radarrEnabled,
"radarrHost": radarrHost,
"radarrKey": radarrKey,
"radarrStrictTLS": radarrStrictTLS,
"radarrHeaders": radarrHeaders,
//Lidarr
"lidarrEnabled": lidarrEnabled,
"lidarrHost": lidarrHost,
"lidarrKey": lidarrKey,
"lidarrStrictTLS": lidarrStrictTLS,
"lidarrHeaders": lidarrHeaders,
//SABnzbd
"sabnzbdEnabled": sabnzbdEnabled,
"sabnzbdHost": sabnzbdHost,
"sabnzbdKey": sabnzbdKey,
"sabnzbdStrictTLS": sabnzbdStrictTLS,
"sabnzbdHeaders": sabnzbdHeaders,
//NZBGet
"nzbgetEnabled": nzbgetEnabled,
"nzbgetHost": nzbgetHost,
@@ -139,6 +172,11 @@ class ProfileHiveObject extends HiveObject {
"nzbgetPass": nzbgetPass,
"nzbgetStrictTLS": nzbgetStrictTLS,
"nzbgetBasicAuth": nzbgetBasicAuth,
"nzbgetHeaders": nzbgetHeaders,
//Wake On LAN
"wakeOnLANEnabled": wakeOnLANEnabled,
"wakeOnLANBroadcastAddress": wakeOnLANBroadcastAddress,
"wakeOnLANMACAddress": wakeOnLANMACAddress,
};
}
@@ -151,12 +189,15 @@ class ProfileHiveObject extends HiveObject {
String lidarrKey;
@HiveField(18)
bool lidarrStrictTLS;
@HiveField(26)
Map lidarrHeaders;
Map<String, dynamic> getLidarr() => {
'enabled': lidarrEnabled ?? false,
'host': lidarrHost ?? '',
'key': lidarrKey ?? '',
'strict_tls': lidarrStrictTLS ?? true,
'headers': lidarrHeaders ?? {},
};
//Radarr
@@ -168,12 +209,15 @@ class ProfileHiveObject extends HiveObject {
String radarrKey;
@HiveField(17)
bool radarrStrictTLS;
@HiveField(27)
Map radarrHeaders;
Map<String, dynamic> getRadarr() => {
'enabled': radarrEnabled ?? false,
'host': radarrHost ?? '',
'key': radarrKey ?? '',
'strict_tls': radarrStrictTLS ?? true,
'headers': radarrHeaders ?? {},
};
//Sonarr
@@ -187,6 +231,8 @@ class ProfileHiveObject extends HiveObject {
bool sonarrStrictTLS;
@HiveField(21)
bool sonarrVersion3;
@HiveField(28)
Map sonarrHeaders;
Map<String, dynamic> getSonarr() => {
'enabled': sonarrEnabled ?? false,
@@ -194,6 +240,7 @@ class ProfileHiveObject extends HiveObject {
'key': sonarrKey ?? '',
'strict_tls': sonarrStrictTLS ?? true,
'v3': sonarrVersion3 ?? false,
'headers': sonarrHeaders ?? {},
};
//SABnzbd
@@ -205,12 +252,15 @@ class ProfileHiveObject extends HiveObject {
String sabnzbdKey;
@HiveField(19)
bool sabnzbdStrictTLS;
@HiveField(29)
Map sabnzbdHeaders;
Map<String, dynamic> getSABnzbd() => {
'enabled': sabnzbdEnabled ?? false,
'host': sabnzbdHost ?? '',
'key': sabnzbdKey ?? '',
'strict_tls': sabnzbdStrictTLS ?? true,
'headers': sabnzbdHeaders ?? {},
};
//NZBGet
@@ -226,6 +276,8 @@ class ProfileHiveObject extends HiveObject {
bool nzbgetStrictTLS;
@HiveField(22)
bool nzbgetBasicAuth;
@HiveField(30)
Map nzbgetHeaders;
Map<String, dynamic> getNZBGet() => {
'enabled': nzbgetEnabled ?? false,
@@ -234,6 +286,21 @@ class ProfileHiveObject extends HiveObject {
'pass': nzbgetPass ?? '',
'strict_tls': nzbgetStrictTLS ?? true,
'basic_auth': nzbgetBasicAuth ?? false,
'headers': nzbgetHeaders ?? {},
};
//Wake On LAN
@HiveField(23)
bool wakeOnLANEnabled;
@HiveField(24)
String wakeOnLANBroadcastAddress;
@HiveField(25)
String wakeOnLANMACAddress;
Map<String, dynamic> getWakeOnLAN() => {
'enabled': wakeOnLANEnabled ?? false,
'broadcastAddress': wakeOnLANBroadcastAddress ?? '',
'MACAddress': wakeOnLANMACAddress ?? '',
};
List<String> get enabledServices => [

View File

@@ -21,32 +21,40 @@ class ProfileHiveObjectAdapter extends TypeAdapter<ProfileHiveObject> {
lidarrHost: fields[1] as String,
lidarrKey: fields[2] as String,
lidarrStrictTLS: fields[18] as bool,
lidarrHeaders: (fields[26] as Map)?.cast<dynamic, dynamic>(),
radarrEnabled: fields[3] as bool,
radarrHost: fields[4] as String,
radarrKey: fields[5] as String,
radarrStrictTLS: fields[17] as bool,
radarrHeaders: (fields[27] as Map)?.cast<dynamic, dynamic>(),
sonarrEnabled: fields[6] as bool,
sonarrHost: fields[7] as String,
sonarrKey: fields[8] as String,
sonarrStrictTLS: fields[16] as bool,
sonarrVersion3: fields[21] as bool,
sonarrHeaders: (fields[28] as Map)?.cast<dynamic, dynamic>(),
sabnzbdEnabled: fields[9] as bool,
sabnzbdHost: fields[10] as String,
sabnzbdKey: fields[11] as String,
sabnzbdStrictTLS: fields[19] as bool,
sabnzbdHeaders: (fields[29] as Map)?.cast<dynamic, dynamic>(),
nzbgetEnabled: fields[12] as bool,
nzbgetHost: fields[13] as String,
nzbgetUser: fields[14] as String,
nzbgetPass: fields[15] as String,
nzbgetStrictTLS: fields[20] as bool,
nzbgetBasicAuth: fields[22] as bool,
nzbgetHeaders: (fields[30] as Map)?.cast<dynamic, dynamic>(),
wakeOnLANEnabled: fields[23] as bool,
wakeOnLANBroadcastAddress: fields[24] as String,
wakeOnLANMACAddress: fields[25] as String,
);
}
@override
void write(BinaryWriter writer, ProfileHiveObject obj) {
writer
..writeByte(23)
..writeByte(31)
..writeByte(0)
..write(obj.lidarrEnabled)
..writeByte(1)
@@ -55,6 +63,8 @@ class ProfileHiveObjectAdapter extends TypeAdapter<ProfileHiveObject> {
..write(obj.lidarrKey)
..writeByte(18)
..write(obj.lidarrStrictTLS)
..writeByte(26)
..write(obj.lidarrHeaders)
..writeByte(3)
..write(obj.radarrEnabled)
..writeByte(4)
@@ -63,6 +73,8 @@ class ProfileHiveObjectAdapter extends TypeAdapter<ProfileHiveObject> {
..write(obj.radarrKey)
..writeByte(17)
..write(obj.radarrStrictTLS)
..writeByte(27)
..write(obj.radarrHeaders)
..writeByte(6)
..write(obj.sonarrEnabled)
..writeByte(7)
@@ -73,6 +85,8 @@ class ProfileHiveObjectAdapter extends TypeAdapter<ProfileHiveObject> {
..write(obj.sonarrStrictTLS)
..writeByte(21)
..write(obj.sonarrVersion3)
..writeByte(28)
..write(obj.sonarrHeaders)
..writeByte(9)
..write(obj.sabnzbdEnabled)
..writeByte(10)
@@ -81,6 +95,8 @@ class ProfileHiveObjectAdapter extends TypeAdapter<ProfileHiveObject> {
..write(obj.sabnzbdKey)
..writeByte(19)
..write(obj.sabnzbdStrictTLS)
..writeByte(29)
..write(obj.sabnzbdHeaders)
..writeByte(12)
..write(obj.nzbgetEnabled)
..writeByte(13)
@@ -92,6 +108,14 @@ class ProfileHiveObjectAdapter extends TypeAdapter<ProfileHiveObject> {
..writeByte(20)
..write(obj.nzbgetStrictTLS)
..writeByte(22)
..write(obj.nzbgetBasicAuth);
..write(obj.nzbgetBasicAuth)
..writeByte(30)
..write(obj.nzbgetHeaders)
..writeByte(23)
..write(obj.wakeOnLANEnabled)
..writeByte(24)
..write(obj.wakeOnLANBroadcastAddress)
..writeByte(25)
..write(obj.wakeOnLANMACAddress);
}
}

96
lib/core/dialogs.dart Normal file
View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class GlobalDialogs {
GlobalDialogs._();
static Future<List<dynamic>> editText(BuildContext context, String title, { String prefill = '' }) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController()..text = prefill;
void _setValues(bool flag) {
if(_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context).pop();
}
}
await LSDialog.dialog(
context: context,
title: title,
buttons: [
LSDialog.button(
text: 'Save',
onPressed: () => _setValues(true),
),
],
content: [
Form(
key: _formKey,
child: LSDialog.textFormInput(
controller: _textController,
title: title,
onSubmitted: (_) => _setValues(true),
validator: (_) => null,
),
),
],
contentPadding: LSDialog.inputDialogContentPadding(),
);
return [_flag, _textController.text];
}
static Future<void> textPreview(BuildContext context, String title, String text, { bool alignLeft = false }) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
actions: <Widget>[
LSDialog.button(
text: 'Close',
onPressed: () => Navigator.of(context).pop(),
),
],
title: LSDialog.title(text: title),
content: LSDialog.content(
children: [
LSDialog.textContent(
text: text,
textAlign: alignLeft
? TextAlign.left
: TextAlign.center,
),
],
),
contentPadding: LSDialog.textDialogContentPadding(),
shape: LSDialog.shape(),
),
);
}
static Future<List<dynamic>> deleteCatalogueWithFiles(BuildContext context, String title) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Delete All Files',
buttons: [
LSDialog.button(
text: 'Delete',
textColor: LSColors.red,
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to delete all the files and folders for $title?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag];
}
}

View File

@@ -1,12 +1,13 @@
class ModuleFlags {
ModuleFlags._();
//General
static const SEARCH = true;
static const WAKE_ON_LAN = true;
//Automation
static const AUTOMATION = true;
static const LIDARR = true;
static const RADARR = true;
static const SONARR = true;
//Search
static const SEARCH = true;
//Clients
static const CLIENTS = true;
static const SABNZBD = true;

2
lib/core/networking.dart Normal file
View File

@@ -0,0 +1,2 @@
export './networking/ipv4_address.dart';
export './networking/mac_address.dart';

View File

@@ -0,0 +1,18 @@
class IPv4Address {
static const String _REGEX = r"\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}\b";
final String _address;
IPv4Address._internal(this._address);
factory IPv4Address.from(String address) {
if(!IPv4Address.validate(address)) throw FormatException('Not a valid IPv4 address string');
return IPv4Address._internal(address);
}
String get address => _address;
/// Validate that an IPv4 address has been formatted correctly
static bool validate(String address) {
RegExp exp = new RegExp(_REGEX);
return exp.hasMatch(address);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:convert/convert.dart';
class MacAddress {
static const String _REGEX = r"^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$";
final String _address;
MacAddress._internal(this._address);
factory MacAddress.from(String address) {
if(!MacAddress.validate(address)) throw FormatException('Not a valid MAC address string');
return MacAddress._internal(address);
}
String get address => _address;
List<int> get bytes => _getBytes();
//Parse the address string and returns an int array list of 6 values, each containing one block of the
List<int> _getBytes() => _address.split(":").map((octet) => hex.decode(octet)[0]).toList();
/// Validate that a MAC address has been formatted correctly
static bool validate(String address) {
RegExp exp = new RegExp(_REGEX);
return exp.hasMatch(address);
}
}

View File

@@ -42,15 +42,21 @@ class Routes {
// /settings/modules/*
// Automation
SettingsModulesLidarr.ROUTE_NAME: (context) => SettingsModulesLidarr(),
SettingsModulesLidarrHeaders.ROUTE_NAME: (context) => SettingsModulesLidarrHeaders(),
SettingsModulesRadarr.ROUTE_NAME: (context) => SettingsModulesRadarr(),
SettingsModulesRadarrHeaders.ROUTE_NAME: (context) => SettingsModulesRadarrHeaders(),
SettingsModulesSonarr.ROUTE_NAME: (context) => SettingsModulesSonarr(),
SettingsModulesSonarrHeaders.ROUTE_NAME: (context) => SettingsModulesSonarrHeaders(),
// Clients
SettingsModulesNZBGet.ROUTE_NAME: (context) => SettingsModulesNZBGet(),
SettingsModulesNZBGetHeaders.ROUTE_NAME: (context) => SettingsModulesNZBGetHeaders(),
SettingsModulesSABnzbd.ROUTE_NAME: (context) => SettingsModulesSABnzbd(),
SettingsModulesSABnzbdHeaders.ROUTE_NAME: (context) => SettingsModulesSABnzbdHeaders(),
// General
SettingsModulesCalendar.ROUTE_NAME: (context) => SettingsModulesCalendar(),
SettingsModulesLunaSea.ROUTE_NAME: (context) => SettingsModulesLunaSea(),
SettingsModulesSearch.ROUTE_NAME: (context) => SettingsModulesSearch(),
SettingsModulesWakeOnLAN.ROUTE_NAME: (context) => SettingsModulesWakeOnLAN(),
// /settings/modules/indexers/*
SettingsModulesSearchAdd.ROUTE_NAME: (context) => SettingsModulesSearchAdd(),
SettingsModulesSearchEdit.ROUTE_NAME: (context) => SettingsModulesSearchEdit(),
@@ -59,6 +65,8 @@ class Routes {
SettingsGeneralLogsView.ROUTE_NAME: (context) => SettingsGeneralLogsView(),
SettingsGeneralLogsDetails.ROUTE_NAME: (context) => SettingsGeneralLogsDetails(),
// /settings/system/*
SettingsSystemChangelog.ROUTE_NAME: (context) => SettingsSystemChangelog(),
SettingsSystemChangelogDetails.ROUTE_NAME: (context) => SettingsSystemChangelogDetails(),
SettingsSystemDonations.ROUTE_NAME: (context) => SettingsSystemDonations(),
SettingsSystemDonationsThankYou.ROUTE_NAME: (context) => SettingsSystemDonationsThankYou(),
};

View File

@@ -3,11 +3,12 @@ import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:lunasea/core.dart';
// ignore: non_constant_identifier_names
Decoration LSCardBackground({ @required String uri, bool darken = false }) => BoxDecoration(
Decoration LSCardBackground({ @required String uri, @required Map headers, bool darken = false }) => BoxDecoration(
image: DecorationImage(
image: AdvancedNetworkImage(
uri,
useDiskCache: true,
header: Map<String, String>.from(headers),
loadFailedCallback: () {},
fallbackAssetImage: 'assets/images/colors/secondary.png',
retryLimit: 1,

View File

@@ -8,12 +8,14 @@ class LSDescriptionBlock extends StatefulWidget {
final String title;
final String uri;
final String fallbackImage;
final Map headers;
LSDescriptionBlock({
@required this.description,
@required this.title,
@required this.uri,
@required this.fallbackImage,
@required this.headers,
});
@override
@@ -32,6 +34,7 @@ class _State extends State<LSDescriptionBlock> {
child: TransitionToImage(
image: AdvancedNetworkImage(
widget.uri,
header: Map<String, String>.from(widget.headers),
useDiskCache: true,
fallbackAssetImage: widget.fallbackImage,
retryLimit: 1,
@@ -67,7 +70,7 @@ class _State extends State<LSDescriptionBlock> {
],
),
borderRadius: BorderRadius.all(Radius.circular(4.0)),
onTap: () => LSDialogSystem.textPreview(context, widget.title, widget.description.trim() ?? 'No summary is available.'),
onTap: () => GlobalDialogs.textPreview(context, widget.title, widget.description.trim() ?? 'No summary is available.'),
),
);
}

View File

@@ -1,13 +1,3 @@
//Export
export './dialogs/system.dart';
export './dialogs/lidarr.dart';
export './dialogs/radarr.dart';
export './dialogs/sonarr.dart';
export './dialogs/nzbget.dart';
export './dialogs/sabnzbd.dart';
export './dialogs/settings.dart';
export './dialogs/search.dart';
//Import
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
@@ -16,14 +6,38 @@ abstract class LSDialog {
text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
);
static Widget button({ @required String text, @required void Function() onPressed, Color textColor = Colors.white }) => FlatButton(
static TextSpan bolded({ @required String text, double fontSize = 14.0, Color color }) => TextSpan(
text: text,
style: TextStyle(
color: color == null
? LSColors.accent
: color,
fontWeight: FontWeight.bold,
fontSize: fontSize,
),
);
static Widget richText({ @required List<TextSpan> children, TextAlign alignment = TextAlign.start }) => RichText(
text: TextSpan(
children: children,
),
textAlign: alignment,
);
static Widget button({ @required String text, @required void Function() onPressed, Color textColor }) => FlatButton(
child: Text(
text,
style: TextStyle(color: textColor),
style: TextStyle(
color: textColor == null
? LSColors.accent
: textColor,
fontSize: 12.0,
),
),
onPressed: onPressed,
);
@@ -31,7 +45,10 @@ abstract class LSDialog {
static Widget cancel(BuildContext context, { Color textColor = Colors.white }) => FlatButton(
child: Text(
'Cancel',
style: TextStyle(color: textColor),
style: TextStyle(
color: textColor,
fontSize: 12.0,
),
),
onPressed: () => Navigator.of(context).pop(),
);
@@ -40,13 +57,14 @@ abstract class LSDialog {
child: ListBody(
children: children,
),
padding: EdgeInsets.zero,
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 1.0),
);
static Widget textContent({ @required String text, TextAlign textAlign = TextAlign.center }) => Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: 14.0,
),
textAlign: textAlign,
);
@@ -55,7 +73,7 @@ abstract class LSDialog {
text: text,
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
fontSize: 14.0,
),
);
@@ -72,48 +90,123 @@ abstract class LSDialog {
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: 14.0,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LSColors.accent),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LSColors.accent),
borderSide: BorderSide(color: LSColors.accent.withOpacity(0.3)),
),
),
style: TextStyle(color: Colors.white),
style: TextStyle(
color: Colors.white,
fontSize: 14.0,
),
cursorColor: LSColors.accent,
textInputAction: TextInputAction.done,
onSubmitted: onSubmitted,
);
static Widget textFormInput({
@required TextEditingController controller,
@required String title,
@required Function(String) onSubmitted,
@required Function(String) validator,
bool obscureText = false,
TextInputType keyboardType = TextInputType.text,
}) => TextFormField(
autofocus: true,
autocorrect: false,
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: title,
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: 14.0,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LSColors.accent),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LSColors.accent.withOpacity(0.3)),
),
),
style: TextStyle(
color: Colors.white,
fontSize: 14.0,
),
cursorColor: LSColors.accent,
textInputAction: TextInputAction.done,
validator: validator,
onFieldSubmitted: onSubmitted,
);
static Widget tile({
@required bool icon,
IconData iconData,
@required IconData icon,
Color iconColor,
@required String text,
RichText subtitle,
@required Function onTap,
}) => ListTile(
leading: Icon(
iconData ?? Icons.error_outline,
color: iconColor ?? LSColors.accent,
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
LSIcon(
icon: icon ?? Icons.error_outline,
color: iconColor ?? LSColors.accent,
),
],
),
title: Text(
text,
style: TextStyle(
fontSize: 14.0,
color: Colors.white,
),
),
subtitle: text == null
? null
: subtitle,
onTap: onTap,
contentPadding: tileContentPadding(),
);
static TextSpan bolded({ @required String title, double fontSize = 14.0, Color color }) => TextSpan(
text: title,
style: TextStyle(
color: color == null
? LSColors.accent
: color,
fontWeight: FontWeight.bold,
fontSize: fontSize,
),
static EdgeInsets tileContentPadding() => EdgeInsets.fromLTRB(32.0, 0.0, 16.0, 0.0);
static EdgeInsets textDialogContentPadding() => EdgeInsets.fromLTRB(24.0, 36.0, 24.0, 14.0);
static EdgeInsets listDialogContentPadding() => EdgeInsets.fromLTRB(0.0, 26.0, 24.0, 0.0);
static EdgeInsets inputTextDialogContentPadding() => EdgeInsets.fromLTRB(24.0, 36.0, 24.0, 22.0);
static EdgeInsets inputDialogContentPadding() => EdgeInsets.fromLTRB(24.0, 22.0, 24.0, 22.0);
static ShapeBorder shape() => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
);
static Future<void> dialog({
@required BuildContext context,
@required String title,
@required List<Widget> content,
@required EdgeInsets contentPadding,
List<Widget> buttons,
}) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
actions: <Widget>[
LSDialog.cancel(
context,
textColor: buttons != null ? Colors.white : LSColors.accent,
),
if(buttons != null) ...buttons,
],
title: LSDialog.title(text: title),
content: LSDialog.content(children: content),
contentPadding: contentPadding,
shape: LSDialog.shape(),
),
);
}
}

View File

@@ -1,607 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/lidarr.dart';
class LSDialogLidarr {
LSDialogLidarr._();
static Future<List<dynamic>> showEditQualityProfilePrompt(BuildContext context, List<LidarrQualityProfile> qualities) async {
bool flag = false;
LidarrQualityProfile quality;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Quality Profile',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: qualities.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
qualities[index].name,
style: TextStyle(
color: Colors.white,
),
),
leading: Icon(
Icons.portrait,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
onTap: () {
quality = qualities[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, quality];
}
static Future<List<dynamic>> showEditMetadataProfilePrompt(BuildContext context, List<LidarrMetadataProfile> metadatas) async {
bool flag = false;
LidarrMetadataProfile metadata;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Metadata Profile',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: metadatas.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
metadatas[index].name,
style: TextStyle(
color: Colors.white,
),
),
leading: Icon(
Icons.portrait,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
onTap: () {
metadata = metadatas[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, metadata];
}
static Future<List<dynamic>> showDeleteArtistPrompt(BuildContext context) async {
bool flag = false;
bool files = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Remove Artist',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Remove + Files',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
files = true;
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Remove',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
files = false;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to remove the artist from Lidarr?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag, files];
}
static Future<List<dynamic>> showDownloadWarningPrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Download Release',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Download',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'Are you sure you want to download this release? It has been marked as a rejected release by Lidarr.',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showSearchMissingPrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Search All Missing',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Search',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to search for all missing albums?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showEditArtistPrompt(BuildContext context, LidarrCatalogueData entry) async {
bool flag = false;
String value = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
entry.title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
ListTile(
leading: Icon(
Icons.edit,
color: Colors.blue,
),
title: Text(
'Edit Artist',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'edit_artist';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.refresh,
color: Color(Constants.ACCENT_COLOR),
),
title: Text(
'Refresh Artist',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'refresh_artist';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.delete,
color: Colors.red,
),
title: Text(
'Remove Artist',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'remove_artist';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
],
),
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, value];
}
static Future<List<dynamic>> showEditRootFolderPrompt(BuildContext context, List<LidarrRootFolder> folders) async {
bool flag = false;
LidarrRootFolder folder;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Root Folder',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: folders.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
folders[index].path,
style: TextStyle(
color: Colors.white,
),
),
subtitle: Text(
folders[index].freeSpace.lsBytes_BytesToString(),
style: TextStyle(
color: LSColors.accent,
fontWeight: FontWeight.bold,
),
),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
LSIcon(
icon: Icons.folder,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
],
),
onTap: () {
folder = folders[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, folder];
}
static Future<List<dynamic>> showSettingsPrompt(BuildContext context) async {
bool flag = false;
String value = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Lidarr Settings',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
ListTile(
leading: Icon(
Icons.language,
color: Colors.blue,
),
title: Text(
'View Web GUI',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'web_gui';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.autorenew,
color: Color(Constants.ACCENT_COLOR),
),
title: Text(
'Update Library',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'update_library';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.rss_feed,
color: Colors.orange,
),
title: Text(
'Run RSS Sync',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'rss_sync';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.search,
color: Colors.red,
),
title: Text(
'Search All Missing',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'missing_search';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.save,
color: Colors.deepPurpleAccent,
),
title: Text(
'Backup Database',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'backup';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
],
),
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, value];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,661 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/radarr.dart';
class LSDialogRadarr {
LSDialogRadarr._();
static Future<List<dynamic>> showDeleteMovieFilePrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Delete Movie File',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Delete',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to delete this movie file?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showDeleteMoviePrompt(BuildContext context) async {
bool flag = false;
bool files = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Remove Movie',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Remove + Files',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
files = true;
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Remove',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
files = false;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to remove the movie from Radarr?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag, files];
}
static Future<List<dynamic>> showSearchMissingPrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Search For All Movies',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Search',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to search for all missing movies?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showEditMoviePrompt(BuildContext context, RadarrCatalogueData entry) async {
bool flag = false;
String value = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
entry.title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
ListTile(
leading: Icon(
Icons.edit,
color: Colors.blue,
),
title: Text(
'Edit Movie',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'edit_movie';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.refresh,
color: Color(Constants.ACCENT_COLOR),
),
title: Text(
'Refresh Movie',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'refresh_movie';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.delete,
color: Colors.red,
),
title: Text(
'Remove Movie',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'remove_movie';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
],
),
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, value];
}
static Future<List<dynamic>> showMinimumAvailabilityPrompt(BuildContext context, List<RadarrAvailability> availability) async {
bool flag = false;
RadarrAvailability entry;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Minimum Availability',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: availability.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
availability[index].name,
style: TextStyle(
color: Colors.white,
),
),
leading: Icon(
Icons.folder,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
onTap: () {
entry = availability[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, entry];
}
static Future<List<dynamic>> showEditRootFolderPrompt(BuildContext context, List<RadarrRootFolder> folders) async {
bool flag = false;
RadarrRootFolder folder;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Root Folder',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: folders.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
folders[index].path,
style: TextStyle(
color: Colors.white,
),
),
subtitle: Text(
folders[index].freeSpace.lsBytes_BytesToString(),
style: TextStyle(
color: LSColors.accent,
fontWeight: FontWeight.bold,
),
),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
LSIcon(
icon: Icons.folder,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
],
),
onTap: () {
folder = folders[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, folder];
}
static Future<List<dynamic>> showEditQualityProfilePrompt(BuildContext context, List<RadarrQualityProfile> qualities) async {
bool flag = false;
RadarrQualityProfile quality;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Quality Profile',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: qualities.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
qualities[index].name,
style: TextStyle(
color: Colors.white,
),
),
leading: Icon(
Icons.portrait,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
onTap: () {
quality = qualities[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, quality];
}
static Future<List<dynamic>> showDownloadWarningPrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Download Release',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Download',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'Are you sure you want to download this release? It has been marked as a rejected release by Radarr.',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showSettingsPrompt(BuildContext context) async {
bool flag = false;
String value = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Radarr Settings',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
ListTile(
leading: Icon(
Icons.language,
color: Colors.blue,
),
title: Text(
'View Web GUI',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'web_gui';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.autorenew,
color: Color(Constants.ACCENT_COLOR),
),
title: Text(
'Update Library',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'update_library';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.rss_feed,
color: Colors.orange,
),
title: Text(
'Run RSS Sync',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'rss_sync';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.search,
color: Colors.red,
),
title: Text(
'Search All Missing',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'missing_search';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.save,
color: Colors.deepPurpleAccent,
),
title: Text(
'Backup Database',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'backup';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
],
),
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, value];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,561 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules.dart';
class LSDialogSettings {
LSDialogSettings._();
static Future<List> deleteIndexer(BuildContext context) async {
//Returns
bool _flag = false;
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Delete Indexer'),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Delete',
onPressed: () {
_flag = true;
Navigator.of(context).pop();
},
textColor: Colors.red,
),
],
content: LSDialog.content(
children: [
LSDialog.textContent(
text: 'Are you sure you want to delete this indexer?'
),
],
),
),
);
return [_flag];
}
static Future<void> showChangelog(BuildContext context, List changes) async {
const _headerStyle = TextStyle(
color: Color(Constants.ACCENT_COLOR),
fontWeight: FontWeight.bold,
);
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Changelog'),
actions: <Widget>[
LSDialog.button(
text: 'Close',
textColor: LSColors.accent,
onPressed: () => Navigator.of(context).pop(),
),
],
content: LSDialog.content(
children: List.generate(
changes.length,
(index) => ListTile(
title: LSTitle(
text: '${changes[index]['version']}',
maxLines: 2,
),
subtitle: Padding(
child: RichText(
text: TextSpan(
style: TextStyle(color: Colors.white70),
children: <TextSpan>[
TextSpan(text: 'New\n', style: _headerStyle),
LSDialog.textSpanContent(text: changes[index]['new'].length == 0 ? 'No Changes' : '- ${changes[index]['new'].join('\n- ')}'),
TextSpan(text: '\n\nFixes\n', style: _headerStyle),
LSDialog.textSpanContent(text: changes[index]['fixes'].length == 0 ? 'No Changes' : '- ${changes[index]['fixes'].join('\n- ')}'),
TextSpan(text: '\n\nTweaks\n', style: _headerStyle),
LSDialog.textSpanContent(text: changes[index]['tweaks'].length == 0 ? 'No Changes' : '- ${changes[index]['tweaks'].join('\n- ')}'),
]
),
),
padding: EdgeInsets.symmetric(vertical: 10.0),
),
),
).toList(),
),
),
);
}
static Future<List> exportLogs(BuildContext context) async {
//Returns
bool _flag = false;
//Setter
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Export Logs'),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Export',
onPressed: () => _setValues(true),
textColor: LSColors.accent,
),
],
content: LSDialog.content(
children: [
LSDialog.textContent(text: 'Are you sure you want to export all recorded logs to the filesystem?\n'),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
if(Platform.isIOS || Platform.isAndroid) LSDialog.textSpanContent(text: 'The exported logs can be found in '),
if(Platform.isIOS) LSDialog.bolded(title: '<On My Device>/LunaSea/FLogs', fontSize: 16.0),
if(Platform.isAndroid) LSDialog.bolded(title: '<Storage>/Android/data/app.lunasea.lunasea/files/FLogs', fontSize: 16.0),
LSDialog.textSpanContent(text: '.'),
],
),
),
],
),
),
);
return [_flag];
}
static Future<List> clearLogs(BuildContext context) async {
//Returns
bool _flag = false;
//Setter
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Clear Logs'),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Clear',
onPressed: () => _setValues(true),
textColor: LSColors.red,
),
],
content: LSDialog.content(
children: [
LSDialog.textContent(text: 'Are you sure you want to clear all recorded logs?\n\nLogs can be useful for bug reports and debugging.'),
],
),
),
);
return [_flag];
}
static Future<List> toggleStrictTLS(BuildContext context) async {
//Returns
bool _flag = false;
//Setter
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Disable Strict SSL/TLS Validation'),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Disable',
onPressed: () => _setValues(true),
textColor: LSColors.red,
),
],
content: RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
LSDialog.bolded(
title: 'Please do not modify this setting unless you know what you are doing.\n\n',
color: LSColors.red,
fontSize: 12.0,
),
LSDialog.textSpanContent(text: 'Are you sure you want to disable strict SSL/TLS validation?\n\n'),
LSDialog.textSpanContent(text: 'Disabling strict SSL/TLS validation means that LunaSea will not validate the host machine\'s SSL certificate against a certificate authority.\n\n'),
LSDialog.textSpanContent(text: 'LunaSea will still connect to your host machine securely when using SSL/TLS whether strict SSL/TLS validation is enabled or disabled.\n\n'),
LSDialog.bolded(
title: 'Warning: Disabling strict SSL/TLS for an invalid or self-signed certificate will prevent a large amount of images from loading within LunaSea.',
color: LSColors.red,
fontSize: 12.0,
),
],
),
),
),
);
return [_flag];
}
static Future<List> nzbgetBasicAuthentication(BuildContext context) async {
//Returns
bool _flag = false;
//Setter
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Use Basic Authentication'),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Use',
onPressed: () => _setValues(true),
textColor: LSColors.red,
),
],
content: RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
LSDialog.bolded(
title: 'Please do not modify this setting unless you know what you are doing.\n\n',
color: LSColors.red,
fontSize: 12.0,
),
LSDialog.textSpanContent(text: 'Are you sure you want to use basic authentication?\n\n'),
LSDialog.textSpanContent(text: 'Basic authentication will add your username and password as a header in the request instead of in the URL.\n\n'),
LSDialog.bolded(
title: 'Warning: This will allow you to have more complex passwords, but interfere with layered authentication methods.',
color: LSColors.red,
fontSize: 12.0,
),
],
),
),
),
);
return [_flag];
}
static Future<List> addProfile(BuildContext context) async {
//Returns
TextEditingController _controller = TextEditingController();
bool _flag = false;
//Setter
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Add Profile'),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Save',
onPressed: () => _setValues(true),
textColor: LSColors.accent,
),
],
content: LSDialog.content(
children: [
LSDialog.textInput(
controller: _controller,
onSubmitted: (_) => _setValues(true),
title: 'Profile Name',
),
],
),
),
);
return [_flag, _controller.text];
}
static Future<List> renameProfile(BuildContext context, List<String> profiles) async {
//Returns
bool _flag = false;
String _profile = '';
//Setter
void _setValues(bool flag, String profile) {
_flag = flag;
_profile = profile;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Add Profile'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
profiles.length,
(index) => ListTile(
leading: LSIcon(
icon: Icons.settings,
color: LSColors.list(index),
),
title: Text(
profiles[index],
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, profiles[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _profile];
}
static Future<List> renameProfileSelected(BuildContext context) async {
//Returns
final _controller = TextEditingController();
bool _flag = false;
//Setters
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: LSDialog.title(text: 'Rename Profile'),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(text: 'Rename', onPressed: () => _setValues(true), textColor: LSColors.accent),
],
content: LSDialog.content(
children: [
LSDialog.textInput(
controller: _controller,
onSubmitted: (_) => _setValues(true),
title: 'New Profile Name',
),
],
),
),
);
return [_flag, _controller.text];
}
static Future<List> deleteProfile(BuildContext context, List<String> profiles) async {
//Returns
bool _flag = false;
String _profile = '';
//Setter
void _setValues(bool flag, String profile) {
_flag = flag;
_profile = profile;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Delete Profile'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
profiles.length,
(index) => ListTile(
leading: LSIcon(
icon: Icons.settings,
color: LSColors.list(index),
),
title: Text(
profiles[index],
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, profiles[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _profile];
}
static Future<List> changeProfile(BuildContext context, List<String> profiles) async {
//Returns
bool _flag = false;
String _profile = '';
//Setter
void _setValues(bool flag, String profile) {
_flag = flag;
_profile = profile;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Enabled Profile'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
profiles.length,
(index) => ListTile(
leading: LSIcon(
icon: Icons.settings,
color: LSColors.list(index),
),
title: Text(
profiles[index],
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, profiles[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _profile];
}
static Future<List> changeBrowser(BuildContext context) async {
//Returns
bool _flag = false;
LSBrowsers _browser;
//Setter
void _setValues(bool flag, LSBrowsers browser) {
_flag = flag;
_browser = browser;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Open Links In...'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
LSBrowsers.values.length,
(index) => ListTile(
leading: LSIcon(
icon: LSBrowsers.values[index].icon,
color: LSColors.list(index),
),
title: Text(
LSBrowsers.values[index].name,
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, LSBrowsers.values[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _browser];
}
static Future<List> homeChangeStartingDay(BuildContext context) async {
//Returns
bool _flag = false;
CalendarStartingDay _startingDate;
//Setter
void _setValues(bool flag, CalendarStartingDay startingDate) {
_flag = flag;
_startingDate = startingDate;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Starting Day of Week'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
CalendarStartingDay.values.length,
(index) => ListTile(
leading: LSIcon(
icon: CustomIcons.calendar,
color: LSColors.list(index),
),
title: Text(
CalendarStartingDay.values[index].name,
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, CalendarStartingDay.values[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _startingDate];
}
static Future<List> homeChangeStartingSize(BuildContext context) async {
//Returns
bool _flag = false;
CalendarStartingSize _startingSize;
//Setter
void _setValues(bool flag, CalendarStartingSize startingSize) {
_flag = flag;
_startingSize = startingSize;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Starting Size'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
CalendarStartingSize.values.length,
(index) => ListTile(
leading: LSIcon(
icon: CalendarStartingSize.values[index].icon,
color: LSColors.list(index),
),
title: Text(
CalendarStartingSize.values[index].name,
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, CalendarStartingSize.values[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _startingSize];
}
}

View File

@@ -1,831 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:intl/intl.dart';
import 'package:lunasea/modules/sonarr.dart';
class LSDialogSonarr {
LSDialogSonarr._();
static Future<List<dynamic>> showDownloadWarningPrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Download Release',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Download',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'Are you sure you want to download this release? It has been marked as a rejected release by Sonarr.',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showDeleteSeriesPrompt(BuildContext context) async {
bool flag = false;
bool files = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Remove Series',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Remove + Files',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
files = true;
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Remove',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
files = false;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to remove the series from Sonarr?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag, files];
}
static Future<List<dynamic>> showSearchMissingPrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Search All Missing',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Search',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to search for all missing episodes?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showSettingsPrompt(BuildContext context) async {
bool flag = false;
String value = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Sonarr Settings',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
ListTile(
leading: Icon(
Icons.language,
color: Colors.blue,
),
title: Text(
'View Web GUI',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'web_gui';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.autorenew,
color: Color(Constants.ACCENT_COLOR),
),
title: Text(
'Update Library',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'update_library';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.rss_feed,
color: Colors.orange,
),
title: Text(
'Run RSS Sync',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'rss_sync';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.search,
color: Colors.red,
),
title: Text(
'Search All Missing',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'missing_search';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.save,
color: Colors.deepPurpleAccent,
),
title: Text(
'Backup Database',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'backup';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
],
),
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, value];
}
static Future<List<dynamic>> showEpisodeEditingPrompt(BuildContext context, String title, bool monitored, bool canDelete) async {
bool flag = false;
String value = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
title,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
ListTile(
leading: Icon(
monitored ? Icons.turned_in_not : Icons.turned_in,
color: Colors.blue,
),
title: Text(
monitored ? 'Unmonitor Episode' : 'Monitor Episode',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'monitor_status';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.search,
color: Color(Constants.ACCENT_COLOR),
),
title: Text(
'Automatic Search',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'search_automatic';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.youtube_searched_for,
color: Colors.orange,
),
title: Text(
'Manual Search',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'search_manual';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
if(canDelete) ListTile(
leading: Icon(
Icons.delete,
color: Colors.red,
),
title: Text(
'Delete File',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'delete_file';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
],
),
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, value];
}
static Future<List<dynamic>> showEditSeriesPrompt(BuildContext context, SonarrCatalogueData entry) async {
bool flag = false;
String value = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
entry.title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
ListTile(
leading: Icon(
Icons.edit,
color: Colors.blue,
),
title: Text(
'Edit Series',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'edit_series';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.refresh,
color: Color(Constants.ACCENT_COLOR),
),
title: Text(
'Refresh Series',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'refresh_series';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
ListTile(
leading: Icon(
Icons.delete,
color: Colors.red,
),
title: Text(
'Remove Series',
style: TextStyle(
color: Colors.white,
),
),
onTap: () {
value = 'remove_series';
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
],
),
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, value];
}
static Future<List<dynamic>> showEditRootFolderPrompt(BuildContext context, List<SonarrRootFolder> folders) async {
bool flag = false;
SonarrRootFolder folder;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Root Folder',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: folders.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
folders[index].path,
style: TextStyle(
color: Colors.white,
),
),
subtitle: Text(
folders[index].freeSpace.lsBytes_BytesToString(),
style: TextStyle(
color: LSColors.accent,
fontWeight: FontWeight.bold,
),
),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
LSIcon(
icon: Icons.folder,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
],
),
onTap: () {
folder = folders[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, folder];
}
static Future<List<dynamic>> showEditQualityProfilePrompt(BuildContext context, List<SonarrQualityProfile> qualities) async {
bool flag = false;
SonarrQualityProfile quality;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Quality Profile',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: qualities.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
qualities[index].name,
style: TextStyle(
color: Colors.white,
),
),
leading: Icon(
Icons.portrait,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
onTap: () {
quality = qualities[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, quality];
}
static Future<List<dynamic>> showEditSeriesTypePrompt(BuildContext context) async {
bool flag = false;
SonarrSeriesType type;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Series Type',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: SonarrConstants.SERIES_TYPES.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
toBeginningOfSentenceCase(SonarrConstants.SERIES_TYPES[index].type),
style: TextStyle(
color: Colors.white,
),
),
leading: Icon(
Icons.tab,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
onTap: () {
type = SonarrConstants.SERIES_TYPES[index];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 32.0, 0.0),
);
}
);
return [flag, type];
}
static Future<List<dynamic>> showSearchSeasonPrompt(BuildContext context, int seasonNumber) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Episode Search',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Search',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
seasonNumber == 0 ? 'Search for all episodes in specials?' : 'Search for all episodes in season $seasonNumber?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showDeleteFilePrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Delete Episode File',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Delete',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to delete this episode file?',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
}

View File

@@ -1,435 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSDialogSystem {
LSDialogSystem._();
static Future<List> editText(BuildContext context, String title, {String prefill = '', bool showHostHint = false}) async {
//Returns
bool _flag = false;
TextEditingController _controller = TextEditingController()..text = prefill;
//Setter
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
//Dialog
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: title),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Save',
textColor: LSColors.accent,
onPressed: () => _setValues(true)),
],
content: LSDialog.content(
children: [
if(showHostHint) RichText(
text: TextSpan(
style: TextStyle(color: Colors.white70),
children: [
TextSpan(text: '\tThis is the URL in which you access the web GUI for the service\n'),
TextSpan(text: '\tYou must include either '),
LSDialog.bolded(title: 'http://'),
TextSpan(text: ' or '),
LSDialog.bolded(title: 'https://\n'),
TextSpan(text: '\tWhen not using a reverse proxy, please include the port: '),
LSDialog.bolded(title: 'url:port\n'),
TextSpan(text: '\tTo add layered authentication (Nginx, Apache, etc), use the following format: '),
LSDialog.bolded(title: 'http(s)://user:pass@hostname\n'),
],
),
),
LSDialog.textInput(
controller: _controller,
onSubmitted: (_) => _setValues(true),
title: title,
),
],
),
),
);
return [_flag, _controller.text];
}
static Future<void> textPreview(BuildContext context, String title, String text, {bool alignLeft = false}) async {
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: LSDialog.title(text: title),
actions: <Widget>[
LSDialog.button(
text: 'Close',
onPressed: () => Navigator.of(context).pop(),
textColor: LSColors.accent,
),
],
content: LSDialog.content(
children: [
LSDialog.textContent(
text: text,
textAlign: alignLeft
? TextAlign.left
: TextAlign.center,
),
],
),
);
}
);
}
static Future<List<dynamic>> deleteCatalogueWithFiles(BuildContext context, String title) async {
//Returns
bool _flag = false;
//Setter
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: LSDialog.title(text: title),
actions: <Widget>[
LSDialog.cancel(context),
LSDialog.button(
text: 'Delete',
textColor: LSColors.red,
onPressed: () => _setValues(true),
),
],
content: LSDialog.content(
children: [
LSDialog.textContent(text: 'Are you sure you want to delete all the files and folders for $title?'),
],
),
);
}
);
return [_flag];
}
static Future<List<dynamic>> showBackupConfigurationPrompt(BuildContext context) async {
bool flag = false;
final formKey = GlobalKey<FormState>();
final textController = TextEditingController();
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Backup Configuration',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Backup',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
if(formKey.currentState.validate()) {
flag = true;
Navigator.of(context).pop();
}
},
),
],
content: LSDialog.content(
children: [
LSDialog.textContent(text: 'Are you sure you want to backup your current configuration?\n'),
LSDialog.textContent(text: 'All backups are encrypted before being exported to the filesystem, please enter an encryption key for the backup.\n'),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
if(Platform.isIOS || Platform.isAndroid) LSDialog.textSpanContent(text: 'The backups can be found in '),
if(Platform.isIOS) LSDialog.bolded(title: '<On My Device>/LunaSea/configurations', fontSize: 16.0),
if(Platform.isAndroid) LSDialog.bolded(title: '<Storage>/Android/data/app.lunasea.lunasea/files/configurations', fontSize: 16.0),
LSDialog.textSpanContent(text: '.'),
],
),
),
Form(
key: formKey,
child: TextFormField(
autofocus: true,
autocorrect: false,
obscureText: true,
controller: textController,
decoration: InputDecoration(
labelText: 'Encryption Key',
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
),
style: TextStyle(
color: Colors.white,
),
cursorColor: Color(Constants.ACCENT_COLOR),
validator: (value) {
if(value.length < 8) {
return 'Minimum of 8 characters';
}
return null;
},
),
),
],
),
);
}
);
return [flag, textController.text];
}
static Future<List<dynamic>> showRestoreConfigurationPrompt(BuildContext context, List<String> paths) async {
bool flag = false;
String path = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Restore Configuration',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: paths.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(
Icons.cloud_download,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
title: Text(
paths[(paths.length-index-1)].substring(
paths[(paths.length-index-1)].indexOf('/configurations/')+16,
paths[(paths.length-index-1)].indexOf('.json')
).replaceAll('%', '\n'),
style: TextStyle(
color: Colors.white,
),
),
onTap: () async {
path = paths[paths.length-index-1];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, path];
}
static Future<List<dynamic>> showClearConfigurationPrompt(BuildContext context) async {
bool flag = false;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Reset LunaSea',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Reset',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () {
flag = true;
Navigator.of(context).pop();
},
),
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to reset LunaSea and clear your configuration?\n\nYou will be starting from a clean slate, please ensure you backup your current configuration first!',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
);
}
);
return [flag];
}
static Future<List<dynamic>> showEncryptionKeyPrompt(BuildContext context) async {
bool flag = false;
final formKey = GlobalKey<FormState>();
final textController = TextEditingController();
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Decrypt Backup',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
'Restore',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
if(formKey.currentState.validate()) {
flag = true;
Navigator.of(context).pop();
}
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'Please enter the encryption key used for this backup.',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
Form(
key: formKey,
child: TextFormField(
autofocus: true,
autocorrect: false,
obscureText: true,
controller: textController,
decoration: InputDecoration(
labelText: 'Encryption Key',
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
),
style: TextStyle(
color: Colors.white,
),
cursorColor: Color(Constants.ACCENT_COLOR),
validator: (value) {
if(value.length < 8) {
return 'Minimum of 8 characters';
}
return null;
},
),
),
],
),
),
);
}
);
return [flag, textController.text];
}
}

View File

@@ -3,5 +3,3 @@ export './navigation/appbar_dropdown.dart';
export './navigation/appbar_tabs.dart';
export './navigation/drawer.dart';
export './navigation/bottom_navigation_bar.dart';
export './navigation/sliver_appbar.dart';
export './navigation/sliver_appbar_tabs.dart';

View File

@@ -5,7 +5,10 @@ Widget LSAppBar({
@required String title,
List<Widget> actions
}) => AppBar(
title: Text(title),
title: Text(
title,
overflow: TextOverflow.fade,
),
centerTitle: false,
elevation: 0,
actions: actions,

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/wake_on_lan.dart';
class LSDrawer extends StatelessWidget {
final String page;
@@ -83,6 +84,7 @@ class LSDrawer extends StatelessWidget {
title: 'Search',
route: '/search',
),
if(ModuleFlags.WAKE_ON_LAN && profile.getWakeOnLAN()['enabled']) _buildWakeOnLAN(context: context),
if(ModuleFlags.AUTOMATION && profile.anyAutomationEnabled) ExpansionTile(
leading: Icon(CustomIcons.layers),
title: Text('Automation'),
@@ -91,9 +93,9 @@ class LSDrawer extends StatelessWidget {
Database.currentProfileObject.enabledAutomationServices.length,
(index) => _buildEntry(
context: context,
route: Constants.SERVICE_MAP[Database.currentProfileObject.enabledAutomationServices[index]]['route'],
icon: Constants.SERVICE_MAP[Database.currentProfileObject.enabledAutomationServices[index]]['icon'],
title: Constants.SERVICE_MAP[Database.currentProfileObject.enabledAutomationServices[index]]['name'],
route: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationServices[index]]['route'],
icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationServices[index]]['icon'],
title: Constants.MODULE_MAP[Database.currentProfileObject.enabledAutomationServices[index]]['name'],
padLeft: true,
),
),
@@ -106,9 +108,9 @@ class LSDrawer extends StatelessWidget {
Database.currentProfileObject.enabledClientServices.length,
(index) => _buildEntry(
context: context,
route: Constants.SERVICE_MAP[Database.currentProfileObject.enabledClientServices[index]]['route'],
icon: Constants.SERVICE_MAP[Database.currentProfileObject.enabledClientServices[index]]['icon'],
title: Constants.SERVICE_MAP[Database.currentProfileObject.enabledClientServices[index]]['name'],
route: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientServices[index]]['route'],
icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientServices[index]]['icon'],
title: Constants.MODULE_MAP[Database.currentProfileObject.enabledClientServices[index]]['name'],
padLeft: true,
),
),
@@ -149,4 +151,33 @@ class LSDrawer extends StatelessWidget {
: EdgeInsets.fromLTRB(16.0, 0.0, 0.0, 0.0),
);
}
Widget _buildWakeOnLAN({
@required BuildContext context,
}) {
return ListTile(
leading: LSIcon(icon: Constants.MODULE_MAP['wake_on_lan']['icon']),
title: Text(
Constants.MODULE_MAP['wake_on_lan']['name'],
style: TextStyle(color: Colors.white),
),
onTap: () async {
WakeOnLANAPI _api = WakeOnLANAPI.from(Database.currentProfileObject);
await _api.wake()
.then((_) => LSSnackBar(
context: context,
title: 'Machine is Waking Up...',
message: 'Magic packet successfully sent',
type: SNACKBAR_TYPE.success,
))
.catchError((_) => LSSnackBar(
context: context,
title: 'Failed to Wake Machine',
message: 'Magic packet failed to send',
type: SNACKBAR_TYPE.failure,
));
},
contentPadding: EdgeInsets.fromLTRB(16.0, 0.0, 0.0, 0.0),
);
}
}

View File

@@ -1,70 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_advanced_networkimage/transition.dart';
import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:lunasea/core.dart';
class LSSliverAppBar extends StatelessWidget {
final ScrollController _controller = ScrollController(initialScrollOffset: _EXPANDED_HEIGHT - 80.0);
final String title;
final String backgroundURI;
final List<Widget> actions;
final Widget body;
LSSliverAppBar({
@required this.title,
@required this.backgroundURI,
@required this.body,
this.actions,
});
static const _EXPANDED_HEIGHT = 200.0;
@override
Widget build(BuildContext context) => NestedScrollView(
controller: _controller,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
sliver: SliverSafeArea(
top: false,
bottom: false,
sliver: SliverAppBar(
expandedHeight: _EXPANDED_HEIGHT,
pinned: true,
elevation: Constants.UI_ELEVATION,
title: Text(
title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
centerTitle: false,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: TransitionToImage(
image: AdvancedNetworkImage(
backgroundURI,
useDiskCache: true,
fallbackAssetImage: LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
retryLimit: 1,
),
fit: BoxFit.cover,
loadingWidget: Image.asset(
LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
),
color: LSColors.secondary.withAlpha((255/1.5).floor()),
blendMode: BlendMode.darken,
),
),
actions: actions,
),
),
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
],
body: body,
);
}

View File

@@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_advanced_networkimage/transition.dart';
import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:lunasea/core.dart';
class LSSliverAppBarTabs extends StatelessWidget {
final ScrollController _controller = ScrollController(initialScrollOffset: _EXPANDED_HEIGHT - 80.0);
final String title;
final String backgroundURI;
final List<Widget> actions;
final Widget body;
final Widget bottom;
LSSliverAppBarTabs({
@required this.title,
@required this.backgroundURI,
this.actions,
@required this.body,
@required this.bottom,
});
static const _EXPANDED_HEIGHT = 200.0;
@override
Widget build(BuildContext context) => NestedScrollView(
controller: _controller,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
sliver: SliverSafeArea(
top: false,
bottom: false,
sliver: SliverAppBar(
expandedHeight: _EXPANDED_HEIGHT,
pinned: true,
elevation: Constants.UI_ELEVATION,
title: Text(
title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
centerTitle: false,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: TransitionToImage(
image: AdvancedNetworkImage(
backgroundURI,
useDiskCache: true,
fallbackAssetImage: LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
retryLimit: 1,
),
fit: BoxFit.cover,
loadingWidget: Image.asset(
LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
),
color: LSColors.secondary.withAlpha((255/1.5).floor()),
blendMode: BlendMode.darken,
),
),
actions: actions,
bottom: bottom,
),
),
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
],
body: body,
);
}

View File

@@ -4,6 +4,5 @@ class RiveAnimations {
static const CODER = {
'path': 'assets/rive/coding.flr',
'animation': 'coding',
};
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flushbar/flushbar.dart';
import 'package:flash/flash.dart';
import 'package:lunasea/core.dart';
enum SNACKBAR_TYPE {
@@ -14,7 +14,7 @@ const _LEFT_PADDING = 8.0;
Future<void> LSSnackBar({
@required BuildContext context,
@required String message,
String title,
@required String title,
Duration duration,
SNACKBAR_TYPE type = SNACKBAR_TYPE.info,
bool showButton = false,
@@ -28,58 +28,58 @@ Future<void> LSSnackBar({
case SNACKBAR_TYPE.success: color = LSColors.accent; icon = Icons.check_circle_outline; break;
case SNACKBAR_TYPE.info: color = LSColors.blue; icon = Icons.info_outline; break;
}
Flushbar(
titleText: Padding(
child: Text(
title,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
padding: EdgeInsets.only(left: _LEFT_PADDING),
),
boxShadows: [BoxShadow(blurRadius: 6.0, spreadRadius: 4.0, color: Colors.black.withOpacity(0.10))],
messageText: Padding(
child: Text(
message,
style: TextStyle(
color: Colors.white70,
fontSize: 14.0,
),
),
padding: EdgeInsets.only(left: _LEFT_PADDING),
),
//message: message,
showFlash(
context: context,
duration: duration != null
? duration
: showButton
? Duration(seconds: 4)
: Duration(seconds: 2),
flushbarStyle: FlushbarStyle.FLOATING,
margin: EdgeInsets.all(8),
borderRadius: 8,
icon: Padding(
child: LSIconButton(
icon: icon,
color: color,
),
padding: EdgeInsets.only(left: _LEFT_PADDING),
),
mainButton: showButton
? FlatButton(
child: Text(
buttonText.toUpperCase(),
builder: (context, controller) => Flash(
backgroundColor: LSColors.secondary,
controller: controller,
style: FlashStyle.floating,
boxShadows: [BoxShadow(blurRadius: 6.0, spreadRadius: 4.0, color: Colors.black.withOpacity(0.10))],
horizontalDismissDirection: HorizontalDismissDirection.horizontal,
margin: EdgeInsets.all(8.0),
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
child: FlashBar(
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
textColor: LSColors.accent,
onPressed: buttonOnPressed,
)
: null,
animationDuration: Duration(milliseconds: 375),
backgroundColor: Theme.of(context).primaryColor,
)..show(context);
message: Text(
message,
style: TextStyle(
color: Colors.white70,
),
),
icon: Padding(
child: LSIconButton(
icon: icon,
color: color,
),
padding: EdgeInsets.only(left: _LEFT_PADDING),
),
primaryAction: showButton
? FlatButton(
child: Text(
buttonText.toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
textColor: LSColors.accent,
onPressed: () {
controller.dismiss();
buttonOnPressed();
}
)
: null,
),
),
);
}

View File

@@ -6,3 +6,4 @@ export './modules/sabnzbd.dart';
export './modules/search.dart';
export './modules/settings.dart';
export './modules/sonarr.dart';
export './modules/wake_on_lan.dart';

View File

@@ -60,14 +60,26 @@ class CalendarAPI extends API {
try {
String start = DateFormat('y-MM-dd').format(today.subtract(Duration(days: startOffset)));
String end = DateFormat('y-MM-dd').format(today.add(Duration(days: endOffset)));
String uri = '${lidarr['host']}/api/v1/calendar?apikey=${lidarr['key']}&start=$start&end=$end';
Dio _client = Dio();
Map<String, dynamic> _headers = Map<String, dynamic>.from(lidarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${lidarr['host']}/api/v1/',
queryParameters: {
if(lidarr['key'] != '') 'apikey': lidarr['key'],
'start': start,
'end': end,
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
if(!lidarr['strict_tls']) {
(_client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
}
Response response = await _client.get(uri);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['releaseDate'] ?? '')?.toLocal()?.lsDateTime_floor();
@@ -94,14 +106,26 @@ class CalendarAPI extends API {
try {
String start = DateFormat('y-MM-dd').format(today.subtract(Duration(days: startOffset)));
String end = DateFormat('y-MM-dd').format(today.add(Duration(days: endOffset)));
String uri = '${radarr['host']}/api/calendar?apikey=${radarr['key']}&start=$start&end=$end';
Dio _client = Dio();
Map<String, dynamic> _headers = Map<String, dynamic>.from(radarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${radarr['host']}/api/',
queryParameters: {
if(radarr['key'] != '') 'apikey': radarr['key'],
'start': start,
'end': end,
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
if(!radarr['strict_tls']) {
(_client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
}
Response response = await _client.get(uri);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['physicalRelease'] ?? '')?.toLocal()?.lsDateTime_floor();
@@ -129,14 +153,26 @@ class CalendarAPI extends API {
try {
String start = DateFormat('y-MM-dd').format(today.subtract(Duration(days: startOffset)));
String end = DateFormat('y-MM-dd').format(today.add(Duration(days: endOffset)));
String uri = '${sonarr['host']}/api/calendar?apikey=${sonarr['key']}&start=$start&end=$end';
Dio _client = Dio();
Map<String, dynamic> _headers = Map<String, dynamic>.from(sonarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${sonarr['host']}/api/',
queryParameters: {
if(sonarr['key'] != '') 'apikey': sonarr['key'],
'start': start,
'end': end,
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
if(!sonarr['strict_tls']) {
(_client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
}
Response response = await _client.get(uri);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['airDateUtc'] ?? '')?.toLocal()?.lsDateTime_floor();

View File

@@ -22,23 +22,35 @@ class _State extends State<Home> {
}
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, lunaBox, widget) {
return ValueListenableBuilder(
valueListenable: Database.profilesBox.listenable(keys: [Database.currentProfile]),
builder: (context, profileBox, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
}
);
Widget build(BuildContext context) => WillPopScope(
onWillPop: () async {
if(_scaffoldKey.currentState.isDrawerOpen) {
//If the drawer is open, return true to close it
return true;
} else {
//If the drawer isn't open, open the drawer
_scaffoldKey.currentState.openDrawer();
return false;
}
},
child: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, lunaBox, widget) {
return ValueListenableBuilder(
valueListenable: Database.profilesBox.listenable(keys: [Database.currentProfile]),
builder: (context, profileBox, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
}
);
},
),
);
Widget get _drawer => LSDrawer(page: 'home');

View File

@@ -30,21 +30,21 @@ 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.SERVICE_MAP['search']['name'],
subtitle: Constants.SERVICE_MAP['search']['desc'],
icon: Constants.SERVICE_MAP['search']['icon'],
title: Constants.MODULE_MAP['search']['name'],
subtitle: Constants.MODULE_MAP['search']['desc'],
icon: Constants.MODULE_MAP['search']['icon'],
index: index,
route: Constants.SERVICE_MAP['search']['route'],
route: Constants.MODULE_MAP['search']['route'],
);
if(index == _serviceCount-1) return HomeSummaryTile(
title: Constants.SERVICE_MAP['settings']['name'],
subtitle: Constants.SERVICE_MAP['settings']['desc'],
icon: Constants.SERVICE_MAP['settings']['icon'],
title: Constants.MODULE_MAP['settings']['name'],
subtitle: Constants.MODULE_MAP['settings']['desc'],
icon: Constants.MODULE_MAP['settings']['icon'],
index: index,
route: Constants.SERVICE_MAP['settings']['route'],
route: Constants.MODULE_MAP['settings']['route'],
justPush: true,
);
Map data = Constants.SERVICE_MAP[widget.profile.enabledServices[_hasIndexers ? index-1 : index]];
Map data = Constants.MODULE_MAP[widget.profile.enabledServices[_hasIndexers ? index-1 : index]];
if(data != null) return HomeSummaryTile(
title: data['name'],
subtitle: data['desc'],

View File

@@ -128,17 +128,29 @@ class _State extends State<HomeCalendarWidget> with TickerProviderStateMixin {
),
);
Widget _entry(dynamic event) => LSCardTile(
title: LSTitle(text: event.title),
subtitle: RichText(
text: event.subtitle,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 2,
),
trailing: event.trailing,
onTap: () async => event.enterContent(context),
padContent: true,
decoration: LSCardBackground(uri: event.bannerURI),
);
Widget _entry(dynamic event) {
Map headers;
switch(event.runtimeType) {
case CalendarLidarrData: headers = Database.currentProfileObject.getLidarr()['headers']; break;
case CalendarRadarrData: headers = Database.currentProfileObject.getRadarr()['headers']; break;
case CalendarSonarrData: headers = Database.currentProfileObject.getSonarr()['headers']; break;
default: headers = {}; break;
}
return LSCardTile(
title: LSTitle(text: event.title),
subtitle: RichText(
text: event.subtitle,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 2,
),
trailing: event.trailing,
onTap: () async => event.enterContent(context),
padContent: true,
decoration: LSCardBackground(
uri: event.bannerURI,
headers: headers,
),
);
}
}

View File

@@ -1,4 +1,5 @@
export './core/api.dart';
export './core/constants.dart';
export './core/database.dart';
export './core/dialogs.dart';
export './core/state.dart';

View File

@@ -11,12 +11,14 @@ class LidarrAPI extends API {
LidarrAPI._internal(this._values, this._dio);
factory LidarrAPI.from(ProfileHiveObject profile) {
Map<String, dynamic> _headers = Map<String, dynamic>.from(profile.getLidarr()['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${profile.getLidarr()['host']}/api/v1/',
queryParameters: {
'apikey': profile.getLidarr()['key'],
if(profile.getLidarr()['key'] != '') 'apikey': profile.getLidarr()['key'],
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),

View File

@@ -3,7 +3,9 @@ import 'package:lunasea/core.dart';
class LidarrConstants {
LidarrConstants._();
static const Map SERVICE_MAP = {
static const String MODULE_KEY = 'lidarr';
static const Map MODULE_MAP = {
'name': 'Lidarr',
'desc': 'Manage Music',
'icon': CustomIcons.music,

View File

@@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/lidarr.dart';
class LidarrDialogs {
LidarrDialogs._();
static Future<List<dynamic>> editQualityProfile(BuildContext context, List<LidarrQualityProfile> qualities) async {
bool _flag = false;
LidarrQualityProfile _quality;
void _setValues(bool flag, LidarrQualityProfile quality) {
_flag = flag;
_quality = quality;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Quality Profile',
content: List.generate(
qualities.length,
(index) => LSDialog.tile(
icon: Icons.portrait,
iconColor: LSColors.list(index),
text: qualities[index].name,
onTap: () => _setValues(true, qualities[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _quality];
}
static Future<List<dynamic>> editMetadataProfile(BuildContext context, List<LidarrMetadataProfile> metadatas) async {
bool _flag = false;
LidarrMetadataProfile _metadata;
void _setValues(bool flag, LidarrMetadataProfile metadata) {
_flag = flag;
_metadata = metadata;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Metadata Profile',
content: List.generate(
metadatas.length,
(index) => LSDialog.tile(
icon: Icons.portrait,
iconColor: LSColors.list(index),
text: metadatas[index].name,
onTap: () => _setValues(true, metadatas[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _metadata];
}
static Future<List<dynamic>> deleteArtist(BuildContext context) async {
bool _flag = false;
bool _files = false;
void _setValues(bool flag, bool files) {
_flag = flag;
_files = files;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Remove Artist',
buttons: [
LSDialog.button(
text: 'Remove + Files',
textColor: LSColors.red,
onPressed: () => _setValues(true, true),
),
LSDialog.button(
text: 'Remove',
textColor: LSColors.red,
onPressed: () => _setValues(true, false),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to remove the artist from Lidarr?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag, _files];
}
static Future<List<dynamic>> downloadWarning(BuildContext context) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Download Release',
buttons: <Widget>[
LSDialog.button(
text: 'Download',
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to download this release? It has been marked as a rejected release by Lidarr.'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag];
}
static Future<List<dynamic>> searchAllMissing(BuildContext context) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Search All Missing',
buttons: <Widget>[
LSDialog.button(
text: 'Search',
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to search for all missing albums?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag];
}
static Future<List<dynamic>> editArtist(BuildContext context, LidarrCatalogueData entry) async {
List<List<dynamic>> _options = [
['Edit Artist', Icons.edit, 'edit_artist'],
['Refresh Artist', Icons.refresh, 'refresh_artist'],
['Remove Artist', Icons.delete, 'remove_artist'],
];
bool _flag = false;
String _value = '';
void _setValues(bool flag, String value) {
_flag = flag;
_value = value;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: entry.title,
content: List.generate(
_options.length,
(index) => LSDialog.tile(
icon: _options[index][1],
iconColor: LSColors.list(index),
text: _options[index][0],
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _value];
}
static Future<List<dynamic>> editRootFolder(BuildContext context, List<LidarrRootFolder> folders) async {
bool _flag = false;
LidarrRootFolder _folder;
void _setValues(bool flag, LidarrRootFolder folder) {
_flag = flag;
_folder = folder;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Root Folder',
content: List.generate(
folders.length,
(index) => LSDialog.tile(
text: folders[index].path,
subtitle: LSDialog.richText(
children: [
LSDialog.bolded(text: folders[index].freeSpace.lsBytes_BytesToString()),
],
),
icon: Icons.folder,
iconColor: LSColors.list(index),
onTap: () => _setValues(true, folders[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _folder];
}
static Future<List<dynamic>> globalSettings(BuildContext context) async {
List<List<dynamic>> _options = [
['View Web GUI', Icons.language, 'web_gui'],
['Update Library', Icons.autorenew, 'update_library'],
['Run RSS Sync', Icons.rss_feed, 'rss_sync'],
['Search All Missing', Icons.search, 'missing_search'],
['Backup Database', Icons.save, 'backup'],
];
bool _flag = false;
String _value = '';
void _setValues(bool flag, String value) {
_flag = flag;
_value = value;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Lidarr Settings',
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _value];
}
}

View File

@@ -56,4 +56,12 @@ class LidarrModel extends ChangeNotifier {
_navigationIndex = navigationIndex;
notifyListeners();
}
int _artistNavigationIndex = 0;
int get artistNavigationIndex => _artistNavigationIndex;
set artistNavigationIndex(int artistNavigationIndex) {
assert(artistNavigationIndex != null);
_artistNavigationIndex = artistNavigationIndex;
notifyListeners();
}
}

View File

@@ -147,6 +147,7 @@ class _State extends State<LidarrAddDetails> {
: _arguments.data.overview,
uri: _arguments.data.posterURI ?? '',
fallbackImage: 'assets/images/lidarr/noartistposter.png',
headers: Database.currentProfileObject.getLidarr()['headers'],
),
LSDivider(),
ValueListenableBuilder(
@@ -184,7 +185,7 @@ class _State extends State<LidarrAddDetails> {
subtitle: LSSubtitle(text: _rootfolder?.path ?? 'Unknown Root Folder'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async {
List _values = await LSDialogLidarr.showEditRootFolderPrompt(context, _rootFolders);
List _values = await LidarrDialogs.editRootFolder(context, _rootFolders);
if(_values[0]) box.put(LidarrDatabaseValue.ADD_ROOT_FOLDER.key, _values[1]);
},
);
@@ -199,7 +200,7 @@ class _State extends State<LidarrAddDetails> {
subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async {
List _values = await LSDialogLidarr.showEditQualityProfilePrompt(context, _qualityProfiles);
List _values = await LidarrDialogs.editQualityProfile(context, _qualityProfiles);
if(_values[0]) box.put(LidarrDatabaseValue.ADD_QUALITY_PROFILE.key, _values[1]);
},
);
@@ -214,7 +215,7 @@ class _State extends State<LidarrAddDetails> {
subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async {
List _values = await LSDialogLidarr.showEditMetadataProfilePrompt(context, _metadataProfiles);
List _values = await LidarrDialogs.editMetadataProfile(context, _metadataProfiles);
if(_values[0]) box.put(LidarrDatabaseValue.ADD_METADATA_PROFILE.key, _values[1]);
},
);

View File

@@ -22,18 +22,16 @@ class LidarrDetailsArtist extends StatefulWidget {
class _State extends State<LidarrDetailsArtist> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _pageController = PageController(initialPage: 1);
LidarrDetailsArtistArguments _arguments;
bool _error = false;
final List<String> _tabTitles = [
'Overview',
'Albums',
];
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
_arguments = ModalRoute.of(context).settings.arguments;
Provider.of<LidarrModel>(context, listen: false).artistNavigationIndex = 1;
_fetch();
});
}
@@ -51,46 +49,48 @@ class _State extends State<LidarrDetailsArtist> {
@override
Widget build(BuildContext context) => Scaffold(
key: _scaffoldKey,
appBar: _arguments == null || _arguments.data == null ? _appBar : null,
appBar: _appBar,
bottomNavigationBar: _arguments != null && _arguments.data != null
? _bottomNavigationBar
: null,
body: _arguments != null
? _arguments.data != null
? _body
: _error
: _error
? LSErrorMessage(onTapHandler: () => _fetch())
: LSLoading()
: null,
);
Widget get _appBar => LSAppBar(title: 'Artist Details');
Widget get _body => DefaultTabController(
length: _tabTitles.length,
initialIndex: 1,
child: LSSliverAppBarTabs(
title: _arguments.data.title,
backgroundURI: _arguments.data.fanartURI(highRes: true),
bottom: TabBar(
tabs: <Widget>[
for(int i =0; i<_tabTitles.length; i++) Tab(
child: Text(_tabTitles[i]),
),
],
),
body: TabBarView(
children: <Widget>[
LidarrDetailsOverview(data: _arguments.data),
LidarrDetailsAlbumList(artistID: _arguments.data.artistID),
],
),
actions: <Widget>[
Widget get _appBar => LSAppBar(
title: _arguments == null || _arguments.data == null
? 'Artist Details'
: _arguments.data.title,
actions: _arguments == null || _arguments.data == null
? null
: <Widget>[
LidarrDetailsHideButton(),
LidarrDetailsEditButton(
data: _arguments.data,
remove: (bool withData) => _removeCallback(withData),
),
],
),
);
Widget get _bottomNavigationBar => LidarrArtistNavigationBar(pageController: _pageController);
List<Widget> get _tabs => [
LidarrDetailsOverview(data: _arguments.data),
LidarrDetailsAlbumList(artistID: _arguments.data.artistID),
];
Widget get _body => PageView(
controller: _pageController,
children: _tabs,
onPageChanged: _onPageChanged,
);
void _onPageChanged(int index) => Provider.of<LidarrModel>(context, listen: false).artistNavigationIndex = index;
Future<void> _removeCallback(bool withData) async => Navigator.of(context).pop(['remove_artist', withData]);
}

View File

@@ -157,17 +157,17 @@ class _State extends State<LidarrEditArtist> {
);
Future<void> _changePath() async {
List<dynamic> _values = await LSDialogSystem.editText(context, 'Artist Path', prefill: _path);
List<dynamic> _values = await GlobalDialogs.editText(context, 'Artist Path', prefill: _path);
if(_values[0] && mounted) setState(() => _path = _values[1]);
}
Future<void> _changeProfile() async {
List<dynamic> _values = await LSDialogLidarr.showEditQualityProfilePrompt(context, _qualityProfiles);
List<dynamic> _values = await LidarrDialogs.editQualityProfile(context, _qualityProfiles);
if(_values[0] && mounted) setState(() => _qualityProfile = _values[1]);
}
Future<void> _changeMetadata() async {
List<dynamic> _values = await LSDialogLidarr.showEditMetadataProfilePrompt(context, _metadataProfiles);
List<dynamic> _values = await LidarrDialogs.editMetadataProfile(context, _metadataProfiles);
if(_values[0] && mounted) setState(() => _metadataProfile = _values[1]);
}

View File

@@ -28,18 +28,30 @@ class _State extends State<Lidarr> {
}
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, box, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
Widget build(BuildContext context) => WillPopScope(
onWillPop: () async {
if(_scaffoldKey.currentState.isDrawerOpen) {
//If the drawer is open, return true to close it
return true;
} else {
//If the drawer isn't open, open the drawer
_scaffoldKey.currentState.openDrawer();
return false;
}
},
child: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, box, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
},
),
);
Widget get _drawer => LSDrawer(page: 'lidarr');
@@ -103,7 +115,7 @@ class _State extends State<Lidarr> {
}
Future<void> _handlePopup() async {
List<dynamic> values = await LSDialogLidarr.showSettingsPrompt(context);
List<dynamic> values = await LidarrDialogs.globalSettings(context);
if(values[0]) switch(values[1]) {
case 'web_gui': await _api.host?.toString()?.lsLinks_OpenLink(); break;
case 'update_library': await _api.updateLibrary()
@@ -119,7 +131,7 @@ class _State extends State<Lidarr> {
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Backup Database', message: Constants.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
break;
case 'missing_search': {
List<dynamic> values = await LSDialogLidarr.showSearchMissingPrompt(context);
List<dynamic> values = await LidarrDialogs.searchAllMissing(context);
if(values[0]) await _api.searchAllMissing()
.then((_) => LSSnackBar(context: context, title: 'Searching...', message: 'Search for all missing albums'))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: Constants.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));

View File

@@ -63,7 +63,7 @@ class _State extends State<LidarrSearchDetails> {
title: LSTitle(text: 'Release Title'),
subtitle: LSSubtitle(text: _arguments.data.title),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async => LSDialogSystem.textPreview(context, 'Release Title', _arguments.data.title),
onTap: () async => GlobalDialogs.textPreview(context, 'Release Title', _arguments.data.title),
),
LSContainerRow(
children: <Widget>[
@@ -156,42 +156,22 @@ class _State extends State<LidarrSearchDetails> {
Future<void> _warningAction() async => _showWarnings();
Future<void> _downloadAction() async {
if(_arguments.data.approved) {
await _startDownload()
.then((_) => LSSnackBar(
context: context,
title: 'Downloading...',
message: _arguments.data.title,
type: SNACKBAR_TYPE.success,
showButton: true,
buttonText: 'Back',
buttonOnPressed: () => Navigator.of(context).popUntil(ModalRoute.withName(Lidarr.ROUTE_NAME)),
))
.catchError((_) => LSSnackBar(
context: context,
title: 'Failed to Start Downloading',
message: Constants.CHECK_LOGS_MESSAGE,
type: SNACKBAR_TYPE.failure,
));
} else {
List<dynamic> values = await LSDialogLidarr.showDownloadWarningPrompt(context);
if(values[0]) await _startDownload()
.then((_) => LSSnackBar(
context: context,
title: 'Downloading...',
message: _arguments.data.title,
type: SNACKBAR_TYPE.success,
showButton: true,
buttonText: 'Back',
buttonOnPressed: () => Navigator.of(context).popUntil(ModalRoute.withName(Lidarr.ROUTE_NAME)),
))
.catchError((_) => LSSnackBar(
context: context,
title: 'Failed to Start Downloading',
message: Constants.CHECK_LOGS_MESSAGE,
type: SNACKBAR_TYPE.failure,
));
}
await _startDownload()
.then((_) => LSSnackBar(
context: context,
title: 'Downloading...',
message: _arguments.data.title,
type: SNACKBAR_TYPE.success,
showButton: true,
buttonText: 'Back',
buttonOnPressed: () => Navigator.of(context).popUntil(ModalRoute.withName(Lidarr.ROUTE_NAME)),
))
.catchError((_) => LSSnackBar(
context: context,
title: 'Failed to Start Downloading',
message: Constants.CHECK_LOGS_MESSAGE,
type: SNACKBAR_TYPE.failure,
));
}
Future<void> _showWarnings() async {
@@ -199,7 +179,7 @@ class _State extends State<LidarrSearchDetails> {
for(var i=0; i<_arguments.data.rejections.length; i++) {
reject += '${i+1}. ${_arguments.data.rejections[i]}\n';
}
await LSDialogSystem.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
await GlobalDialogs.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
}
Future<bool> _startDownload() async {

View File

@@ -14,3 +14,4 @@ export './widgets/search_result_tile.dart';
export './widgets/add_search_bar.dart';
export './widgets/add_search_result_tile.dart';
export './widgets/navigation_bar.dart';
export './widgets/artist_navigation_bar.dart';

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import '../../lidarr.dart';
class LidarrArtistNavigationBar extends StatefulWidget {
final PageController pageController;
LidarrArtistNavigationBar({
Key key,
@required this.pageController,
}): super(key: key);
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LidarrArtistNavigationBar> {
static const List<String> _navbarTitles = [
'Overview',
'Albums',
];
static const List<IconData> _navbarIcons = [
Icons.subject,
CustomIcons.music,
];
@override
Widget build(BuildContext context) => Selector<LidarrModel, int>(
selector: (_, model) => model.artistNavigationIndex,
builder: (context, index, _) => LSBottomNavigationBar(
index: index,
icons: _navbarIcons,
titles: _navbarTitles,
onTap: (index) async => await _navOnTap(index),
),
);
Future<void> _navOnTap(int index) async {
await widget.pageController.animateToPage(
index,
duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED),
curve: Curves.easeOutSine,
).then((_) => Provider.of<LidarrModel>(context, listen: false).artistNavigationIndex = index);
}
}

View File

@@ -43,6 +43,7 @@ class _State extends State<LidarrCatalogueTile> {
padContent: true,
decoration: LSCardBackground(
uri: widget.data.bannerURI(),
headers: Database.currentProfileObject.getLidarr()['headers'],
darken: !widget.data.monitored,
),
onTap: () async => _enterArtist(),
@@ -96,7 +97,7 @@ class _State extends State<LidarrCatalogueTile> {
}
Future<void> _handlePopup() async {
List<dynamic> values = await LSDialogLidarr.showEditArtistPrompt(context, widget.data);
List<dynamic> values = await LidarrDialogs.editArtist(context, widget.data);
if(values[0]) switch(values[1]) {
case 'refresh_artist': _refreshArtist(); break;
case 'edit_artist': _enterEditArtist(); break;
@@ -127,10 +128,10 @@ class _State extends State<LidarrCatalogueTile> {
Future<void> _removeArtist() async {
final _api = LidarrAPI.from(Database.currentProfileObject);
List values = await LSDialogLidarr.showDeleteArtistPrompt(context);
List values = await LidarrDialogs.deleteArtist(context);
if(values[0]) {
if(values[1]) {
values = await LSDialogSystem.deleteCatalogueWithFiles(context, widget.data.title);
values = await GlobalDialogs.deleteCatalogueWithFiles(context, widget.data.title);
if(values[0]) {
await _api.removeArtist(widget.data.artistID, deleteFiles: true)
.then((_) {

View File

@@ -76,7 +76,7 @@ class _State extends State<LidarrDetailsAlbumList> with AutomaticKeepAliveClient
data: _filtered[index],
refreshState: _refreshState,
),
padBottom: true,
padBottom: false,
);
},
);

View File

@@ -30,6 +30,7 @@ class _State extends State<LidarrDetailsAlbumTile> {
child: TransitionToImage(
image: AdvancedNetworkImage(
widget.data.albumCoverURI(),
header: Map<String, String>.from(Database.currentProfileObject.getLidarr()['headers']),
useDiskCache: true,
fallbackAssetImage: 'assets/images/lidarr/noalbumart.png',
retryLimit: 1,

View File

@@ -25,7 +25,7 @@ class _State extends State<LidarrDetailsEditButton> {
);
Future<void> _handlePopup(BuildContext context) async {
List<dynamic> values = await LSDialogLidarr.showEditArtistPrompt(context, widget.data);
List<dynamic> values = await LidarrDialogs.editArtist(context, widget.data);
if(values[0]) switch(values[1]) {
case 'refresh_artist': _refreshArtist(context); break;
case 'edit_artist': _enterEditArtist(context); break;
@@ -56,10 +56,10 @@ class _State extends State<LidarrDetailsEditButton> {
Future<void> _removeArtist(BuildContext context) async {
final _api = LidarrAPI.from(Database.currentProfileObject);
List values = await LSDialogLidarr.showDeleteArtistPrompt(context);
List values = await LidarrDialogs.deleteArtist(context);
if(values[0]) {
if(values[1]) {
values = await LSDialogSystem.deleteCatalogueWithFiles(context, widget.data.title);
values = await GlobalDialogs.deleteCatalogueWithFiles(context, widget.data.title);
if(values[0]) {
await _api.removeArtist(widget.data.artistID, deleteFiles: true)
.then((_) => widget.remove(true))

View File

@@ -30,11 +30,12 @@ class _State extends State<LidarrDetailsOverview> with AutomaticKeepAliveClientM
: widget?.data?.overview,
uri: widget?.data?.posterURI() ?? '',
fallbackImage: 'assets/images/lidarr/noartistposter.png',
headers: Database.currentProfileObject.getLidarr()['headers'],
),
LSCardTile(
title: LSTitle(text: 'Artist Path', centerText: true),
subtitle: LSSubtitle(text: widget?.data?.path ?? 'Unknown', centerText: true),
onTap: () => LSDialogSystem.textPreview(context, 'Artist Path', widget?.data?.path ?? 'Unknown'),
onTap: () => GlobalDialogs.textPreview(context, 'Artist Path', widget?.data?.path ?? 'Unknown'),
),
LSContainerRow(
children: <Widget>[

View File

@@ -50,7 +50,10 @@ class _State extends State<LidarrMissingTile> {
),
onTap: () async => _enterAlbum(),
onLongPress: () async => _enterArtist(),
decoration: LSCardBackground(uri: widget.entry.bannerURI()),
decoration: LSCardBackground(
uri: widget.entry.bannerURI(),
headers: Database.currentProfileObject.getLidarr()['headers'],
),
padContent: true,
);

View File

@@ -87,7 +87,7 @@ class LidarrSearchResultTile extends StatelessWidget {
Future<void> _trailingLongPressed(BuildContext context) async {
if(!data.approved) {
List<dynamic> values = await LSDialogLidarr.showDownloadWarningPrompt(context);
List<dynamic> values = await LidarrDialogs.downloadWarning(context);
if(values[0]) await _startDownload()
.then((_) => LSSnackBar(
context: context,
@@ -112,7 +112,7 @@ class LidarrSearchResultTile extends StatelessWidget {
for(var i=0; i<data.rejections.length; i++) {
reject += '${i+1}. ${data.rejections[i]}\n';
}
await LSDialogSystem.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
await GlobalDialogs.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
}
Future<bool> _startDownload() async {

View File

@@ -1,4 +1,3 @@
export './nzbget/api.dart';
export './nzbget/core.dart';
export './nzbget/routes.dart';
export './nzbget/state.dart';
export './nzbget/widgets.dart';

View File

@@ -0,0 +1,5 @@
export './core/api.dart';
export './core/constants.dart';
export './core/database.dart';
export './core/dialogs.dart';
export './core/state.dart';

View File

@@ -4,7 +4,7 @@ import 'dart:math';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:lunasea/core.dart';
import '../../nzbget.dart';
import 'package:lunasea/modules/nzbget.dart';
class NZBGetAPI extends API {
final Map<String, dynamic> _values;
@@ -13,7 +13,7 @@ class NZBGetAPI extends API {
NZBGetAPI._internal(this._values, this._dio);
factory NZBGetAPI.from(ProfileHiveObject profile) {
String _baseURL = Uri.encodeFull(profile.getNZBGet()['host']);
Map<String, dynamic> _headers = {};
Map<String, dynamic> _headers = Map<String, dynamic>.from(profile.getNZBGet()['headers']);
if(profile.getNZBGet()['basic_auth']) {
String _auth = base64.encode(utf8.encode('${profile.getNZBGet()['user']}:${profile.getNZBGet()['pass']}'));
_headers['Authorization'] = 'Basic $_auth';

View File

@@ -0,0 +1,14 @@
import 'package:lunasea/core.dart';
class NZBGetConstants {
NZBGetConstants._();
static const String MODULE_KEY = 'nzbget';
static const Map MODULE_MAP = {
'name': 'NZBGet',
'desc': 'Manage Usenet Downloads',
'icon': CustomIcons.nzbget,
'route': '/nzbget',
};
}

View File

@@ -0,0 +1,3 @@
class NZBGetDatabase {
NZBGetDatabase._();
}

View File

@@ -0,0 +1,568 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/nzbget.dart';
class NZBGetDialogs {
NZBGetDialogs._();
static Future<List<dynamic>> globalSettings(BuildContext context) async {
List<List<dynamic>> _options = [
['View Web GUI', Icons.language, 'web_gui'],
['Add NZB', Icons.add, 'add_nzb'],
['Sort Queue', Icons.sort, 'sort'],
['Status & Statistics', Icons.info_outline, 'server_details'],
];
bool _flag = false;
String _value = '';
void _setValues(bool flag, String value) {
_flag = flag;
_value = value;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'NZBGet Settings',
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _value];
}
static Future<List<dynamic>> queueSettings(BuildContext context, String title, bool isPaused) async {
List<List<dynamic>> _options = [
isPaused
? ['Resume Job', Icons.play_arrow, 'status']
: ['Pause Job', Icons.pause, 'status'],
['Change Category', Icons.category, 'category'],
['Change Priority', Icons.low_priority, 'priority'],
['Set Password', Icons.vpn_key, 'password'],
['Rename Job', Icons.text_format, 'rename'],
['Delete Job', Icons.delete, 'delete'],
];
bool _flag = false;
String _value = '';
void _setValues(bool flag, String value) {
_flag = flag;
_value = value;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: title,
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _value];
}
static Future<List<dynamic>> deleteJob(BuildContext context) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Delete Job',
buttons: [
LSDialog.button(
text: 'Delete',
textColor: LSColors.red,
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to delete this job?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag];
}
static Future<List<dynamic>> renameJob(BuildContext context, String originalName) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController()..text = originalName;
void _setValues(bool flag) {
if(_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context).pop();
}
}
await LSDialog.dialog(
context: context,
title: 'Rename Job',
buttons: [
LSDialog.button(
text: 'Rename',
onPressed: () => _setValues(true),
),
],
content: [
Form(
key: _formKey,
child: LSDialog.textFormInput(
controller: _textController,
title: 'Job Name',
onSubmitted: (_) => _setValues(true),
validator: (value) => value.length < 1
? 'Please enter a valid name'
: null,
),
),
],
contentPadding: LSDialog.inputDialogContentPadding(),
);
return [_flag, _textController.text];
}
static Future<List<dynamic>> changePriority(BuildContext context) async {
bool _flag = false;
NZBGetPriority _priority;
void _setValues(bool flag, NZBGetPriority priority) {
_flag = flag;
_priority = priority;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Change Priority',
content: List.generate(
NZBGetPriority.values.length,
(index) => LSDialog.tile(
text: NZBGetPriority.values[index].name,
icon: Icons.low_priority,
iconColor: LSColors.list(index),
onTap: () => _setValues(true, NZBGetPriority.values[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _priority];
}
static Future<List<dynamic>> changeCategory(BuildContext context, List<NZBGetCategoryData> categories) async {
bool _flag = false;
NZBGetCategoryData _category;
void _setValues(bool flag, NZBGetCategoryData category) {
_flag = flag;
_category = category;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Change Category',
content: List.generate(
categories.length,
(index) => LSDialog.tile(
text: categories[index].name,
icon: Icons.category,
iconColor: LSColors.list(index),
onTap: () => _setValues(true, categories[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _category];
}
static Future<List<dynamic>> setPassword(BuildContext context) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController();
void _setValues(bool flag) {
if(_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context).pop();
}
}
await LSDialog.dialog(
context: context,
title: 'Set Password',
buttons: [
LSDialog.button(
text: 'Set',
onPressed: () => _setValues(true),
),
],
content: [
Form(
key: _formKey,
child: LSDialog.textFormInput(
controller: _textController,
title: 'Job Password',
onSubmitted: (_) => _setValues(true),
validator: (value) => value.length < 1
? 'Please enter a valid password'
: null,
obscureText: true,
),
),
],
contentPadding: LSDialog.inputDialogContentPadding(),
);
return [_flag, _textController.text];
}
static Future<List<dynamic>> deleteHistory(BuildContext context) async {
bool _flag = false;
bool _hide = false;
void _setValues(bool flag, bool hide) {
_flag = flag;
_hide = hide;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Remove History',
buttons: [
LSDialog.button(
text: 'Hide',
onPressed: () => _setValues(true, true),
),
LSDialog.button(
text: 'Delete',
textColor: LSColors.red,
onPressed: () => _setValues(true, false),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to remove the history for this job?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag, _hide];
}
static Future<List<dynamic>> sortQueue(BuildContext context) async {
bool _flag = false;
NZBGetSort _sort;
void _setValues(bool flag, NZBGetSort sort) {
_flag = flag;
_sort = sort;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Sort Queue',
content: List.generate(
NZBGetSort.values.length,
(index) => LSDialog.tile(
text: NZBGetSort.values[index].name,
icon: NZBGetSort.values[index].icon,
iconColor: LSColors.list(index),
onTap: () => _setValues(true, NZBGetSort.values[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _sort];
}
static Future<List<dynamic>> addNZB(BuildContext context) async {
List<List<dynamic>> _options = [
['Add by URL', Icons.link, 'link'],
['Add by File', Icons.sd_card, 'file'],
];
bool _flag = false;
String _type = '';
void _setValues(bool flag, String type) {
_flag = flag;
_type = type;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Add NZB',
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _type];
}
static Future<List<dynamic>> addNZBUrl(BuildContext context) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController();
void _setValues(bool flag) {
if(_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context).pop();
}
}
await LSDialog.dialog(
context: context,
title: 'Add NZB by URL',
buttons: [
LSDialog.button(
text: 'Add',
onPressed: () => _setValues(true),
),
],
content: [
Form(
key: _formKey,
child: LSDialog.textFormInput(
controller: _textController,
title: 'NZB URL',
keyboardType: TextInputType.url,
onSubmitted: (_) => _setValues(true),
validator: (value) => !value.startsWith('http://') && !value.startsWith('https://')
? 'Please enter a valid URL'
: null,
),
),
],
contentPadding: LSDialog.inputDialogContentPadding(),
);
return [_flag, _textController.text];
}
static Future<List<dynamic>> speedLimit(BuildContext context, String currentSpeed) async {
List<List<dynamic>> _options = [
['Unlimited', Icons.timeline, 0],
['125 MB/s', Icons.timeline, 128000],
['100 MB/s', Icons.timeline, 102400],
['75 MB/s', Icons.timeline, 76800],
['50 MB/s', Icons.timeline, 51200],
['25 MB/s', Icons.timeline, 25600],
['10 MB/s', Icons.timeline, 10240],
['Custom...', Icons.timeline, -1],
];
bool _flag = false;
int _limit = 0;
void _setValues(bool flag, int limit) {
_flag = flag;
_limit = limit;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: currentSpeed == 'Unlimited' ? 'Speed Limit (Unlimited)' : 'Speed Limit ($currentSpeed/s)',
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _limit];
}
static Future<List<dynamic>> customSpeedLimit(BuildContext context) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController();
void _setValues(bool flag) {
if(_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context).pop();
}
}
await LSDialog.dialog(
context: context,
title: 'Custom Speed Limit',
buttons: [
LSDialog.button(
text: 'Set',
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Please enter a speed limit in KB/s.'),
Form(
key: _formKey,
child: LSDialog.textFormInput(
controller: _textController,
title: 'Speed Limit',
onSubmitted: (_) => _setValues(true),
keyboardType: TextInputType.number,
validator: (value) {
int _value = int.tryParse(value);
if(_value == null || _value < 1) {
return 'Must be a number greater than 1';
}
return null;
},
),
),
],
contentPadding: LSDialog.inputTextDialogContentPadding(),
);
return [_flag, int.tryParse(_textController.text)];
}
static Future<List<dynamic>> historySettings(BuildContext context, String title) async {
List<List<dynamic>> _options = [
['Retry Job', Icons.autorenew, 'retry'],
['Hide History', Icons.visibility_off, 'hide'],
['Delete History', Icons.delete, 'delete'],
];
bool _flag = false;
String _value = '';
void _setValues(bool flag, String value) {
_flag = flag;
_value = value;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: title,
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _value];
}
static Future<List<dynamic>> pauseFor(BuildContext context) async {
List<List<dynamic>> _options = [
['5 Minutes', Icons.access_time, 5],
['15 Minutes', Icons.access_time, 15],
['30 Minutes', Icons.access_time, 30],
['1 Hour', Icons.access_time, 60],
['3 Hours', Icons.access_time, 180],
['6 Hours', Icons.access_time, 360],
['Custom...', Icons.access_time, -1],
];
bool _flag = false;
int _duration = 0;
void _setValues(bool flag, int duration) {
_flag = flag;
_duration = duration;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Pause Queue For...',
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _duration];
}
static Future<List<dynamic>> customPauseFor(BuildContext context) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController();
void _setValues(bool flag) {
if(_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context).pop();
}
}
await LSDialog.dialog(
context: context,
title: 'Custom Pause Duration',
buttons: [
LSDialog.button(
text: 'Pause',
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.richText(
children: [
LSDialog.textSpanContent(text: 'Please enter how long in '),
LSDialog.bolded(text: 'minutes'),
LSDialog.textSpanContent(text: ' you want to pause the queue for.'),
],
alignment: TextAlign.center,
),
Form(
key: _formKey,
child: LSDialog.textFormInput(
controller: _textController,
title: 'Pause Duration in Minutes',
keyboardType: TextInputType.number,
onSubmitted: (_) => _setValues(true),
validator: (value) {
int _value = int.tryParse(value);
if(_value == null || _value < 1) {
return 'Must be at least 1';
}
return null;
}
),
),
],
contentPadding: LSDialog.inputTextDialogContentPadding(),
);
return [_flag, int.tryParse(_textController.text)];
}
}

View File

@@ -113,7 +113,7 @@ class _State extends State<NZBGetHistoryDetails> {
title: LSTitle(text: 'Storage Location'),
subtitle: LSSubtitle(text: _arguments.data.storageLocation),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async => LSDialogSystem.textPreview(context, 'Storage Location', _arguments.data.storageLocation),
onTap: () async => GlobalDialogs.textPreview(context, 'Storage Location', _arguments.data.storageLocation),
),
LSDivider(),
LSButton(
@@ -126,7 +126,7 @@ class _State extends State<NZBGetHistoryDetails> {
);
Future<void> _delete() async {
List<dynamic> values = await LSDialogNZBGet.showDeleteHistoryPrompt(context);
List<dynamic> values = await NZBGetDialogs.deleteHistory(context);
if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).deleteHistoryEntry(
_arguments.data.id,
hide: values[1],

View File

@@ -29,18 +29,30 @@ class _State extends State<NZBGet> {
}
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, box, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
Widget build(BuildContext context) => WillPopScope(
onWillPop: () async {
if(_scaffoldKey.currentState.isDrawerOpen) {
//If the drawer is open, return true to close it
return true;
} else {
//If the drawer isn't open, open the drawer
_scaffoldKey.currentState.openDrawer();
return false;
}
},
child: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, box, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
},
),
);
Widget get _drawer => LSDrawer(page: 'nzbget');
@@ -82,7 +94,7 @@ class _State extends State<NZBGet> {
);
Future<void> _handlePopup() async {
List<dynamic> values = await LSDialogNZBGet.showSettingsPrompt(context);
List<dynamic> values = await NZBGetDialogs.globalSettings(context);
if(values[0]) switch(values[1]) {
case 'web_gui': _api.host.lsLinks_OpenLink(); break;
case 'add_nzb': _addNZB(); break;
@@ -93,7 +105,7 @@ class _State extends State<NZBGet> {
}
Future<void> _addNZB() async {
List values = await LSDialogNZBGet.showAddNZBPrompt(context);
List values = await NZBGetDialogs.addNZB(context);
if(values[0]) switch(values[1]) {
case 'link': _addByURL(); break;
case 'file': _addByFile(); break;
@@ -102,7 +114,7 @@ class _State extends State<NZBGet> {
}
Future<void> _addByURL() async {
List values = await LSDialogNZBGet.showaddURLPrompt(context);
List values = await NZBGetDialogs.addNZBUrl(context);
if(values[0]) await _api.uploadURL(values[1])
.then((_) => LSSnackBar(
context: context,
@@ -159,7 +171,7 @@ class _State extends State<NZBGet> {
}
Future<void> _sort() async {
List values = await LSDialogNZBGet.showSortPrompt(context);
List values = await NZBGetDialogs.sortQueue(context);
if(values[0]) await _api.sortQueue(values[1])
.then((_) {
_refreshKeys[0]?.currentState?.show();

View File

@@ -50,10 +50,10 @@ class NZBGetAppBarStats extends StatelessWidget {
: speed;
Future<void> _onTap(BuildContext context, String speed) async {
List values = await LSDialogNZBGet.showSpeedPrompt(context, speed);
List values = await NZBGetDialogs.speedLimit(context, speed);
if(values[0]) switch(values[1]) {
case -1: {
values = await LSDialogNZBGet.showCustomSpeedPrompt(context);
values = await NZBGetDialogs.customSpeedLimit(context);
if(values[0]) NZBGetAPI.from(Database.currentProfileObject).setSpeedLimit(values[1])
.then((_) => LSSnackBar(
context: context,

View File

@@ -56,7 +56,7 @@ class NZBGetHistoryTile extends StatelessWidget {
}
Future<void> _handlePopup(BuildContext context) async {
List values = await LSDialogNZBGet.showHistorySettingsPrompt(context, data.name);
List values = await NZBGetDialogs.historySettings(context, data.name);
if(values[0]) switch(values[1]) {
case 'retry': {
await NZBGetAPI.from(Database.currentProfileObject).retryHistoryEntry(data.id)

View File

@@ -14,6 +14,6 @@ class NZBGetLogTile extends StatelessWidget {
title: LSTitle(text: data.text),
subtitle: LSSubtitle(text: data.timestamp),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async => LSDialogSystem.textPreview(context, 'Log Entry', data.text),
onTap: () async => GlobalDialogs.textPreview(context, 'Log Entry', data.text),
);
}

View File

@@ -27,10 +27,10 @@ class NZBGetQueueFAB extends StatelessWidget {
}
Future<void> _toggleFor(BuildContext context) async {
List values = await LSDialogNZBGet.showPauseForPrompt(context);
List values = await NZBGetDialogs.pauseFor(context);
if(values[0]) {
if(values[1] == -1) {
List values = await LSDialogNZBGet.showCustomPauseForPrompt(context);
List values = await NZBGetDialogs.customPauseFor(context);
if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).pauseQueueFor(values[1])
.then((_) => LSSnackBar(
context: context,

View File

@@ -58,7 +58,7 @@ class _State extends State<NZBGetQueueTile> {
Future<void> _handlePopup() async {
_Helper _helper = _Helper(widget.queueContext, widget.data, widget.snackbar, widget.refresh);
List values = await LSDialogNZBGet.showQueueSettingsPrompt(widget.queueContext, widget.data.name, widget.data.paused);
List values = await NZBGetDialogs.queueSettings(widget.queueContext, widget.data.name, widget.data.paused);
if(values[0]) switch(values[1]) {
case 'status': widget.data.paused
? _helper._resumeJob()
@@ -123,7 +123,7 @@ class _Helper {
Future<void> _category() async {
List<NZBGetCategoryData> categories = await NZBGetAPI.from(Database.currentProfileObject).getCategories();
List values = await LSDialogNZBGet.showCategoryPrompt(context, categories);
List values = await NZBGetDialogs.changeCategory(context, categories);
if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).setJobCategory(data.id, values[1])
.then((_) {
snackbar(
@@ -141,7 +141,7 @@ class _Helper {
}
Future<void> _priority() async {
List values = await LSDialogNZBGet.showChangePriorityPrompt(context);
List values = await NZBGetDialogs.changePriority(context);
if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).setJobPriority(data.id, values[1])
.then((_) {
snackbar(
@@ -159,7 +159,7 @@ class _Helper {
}
Future<void> _rename() async {
List values = await LSDialogNZBGet.showRenameJobPrompt(context, data.name);
List values = await NZBGetDialogs.renameJob(context, data.name);
if(values[0]) NZBGetAPI.from(Database.currentProfileObject).renameJob(data.id, values[1])
.then((_) {
snackbar(
@@ -177,7 +177,7 @@ class _Helper {
}
Future<void> _delete() async {
List values = await LSDialogNZBGet.showDeleteJobPrompt(context);
List values = await NZBGetDialogs.deleteJob(context);
if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).deleteJob(data.id)
.then((_) {
snackbar(
@@ -195,7 +195,7 @@ class _Helper {
}
Future<void> _password() async {
List values = await LSDialogNZBGet.showSetPasswordPrompt(context);
List values = await NZBGetDialogs.setPassword(context);
if(values[0]) await NZBGetAPI.from(Database.currentProfileObject).setJobPassword(data.id, values[1])
.then((_) {
snackbar(

View File

@@ -1,4 +1,5 @@
export './core/api.dart';
export './core/constants.dart';
export './core/database.dart';
export './core/dialogs.dart';
export './core/state.dart';

View File

@@ -11,12 +11,14 @@ class RadarrAPI extends API {
RadarrAPI._internal(this._values, this._dio);
factory RadarrAPI.from(ProfileHiveObject profile) {
Map<String, dynamic> _headers = Map<String, dynamic>.from(profile.getRadarr()['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${profile.getRadarr()['host']}/api/',
queryParameters: {
'apikey': profile.getRadarr()['key'],
if(profile.getRadarr()['key'] != '') 'apikey': profile.getRadarr()['key'],
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),

View File

@@ -4,7 +4,9 @@ import 'package:lunasea/modules/radarr.dart';
class RadarrConstants {
RadarrConstants._();
static const Map SERVICE_MAP = {
static const String MODULE_KEY = 'radarr';
static const Map MODULE_MAP = {
'name': 'Radarr',
'desc': 'Manage Movies',
'icon': CustomIcons.movies,

View File

@@ -0,0 +1,269 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/radarr.dart';
class RadarrDialogs {
RadarrDialogs._();
static Future<List<dynamic>> deleteMovieFile(BuildContext context) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Delete Movie File',
buttons: [
LSDialog.button(
text: 'Delete',
textColor: LSColors.red,
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to delete this movie file?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag];
}
static Future<List<dynamic>> deleteMovie(BuildContext context) async {
bool _flag = false;
bool _files = false;
void _setValues(bool flag, bool files) {
_flag = flag;
_files = files;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Remove Movie',
buttons: [
LSDialog.button(
text: 'Remove + Files',
textColor: LSColors.red,
onPressed: () => _setValues(true, true),
),
LSDialog.button(
text: 'Remove',
textColor: LSColors.red,
onPressed: () => _setValues(true, false),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to remove the movie from Radarr?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag, _files];
}
static Future<List<dynamic>> searchAllMissing(BuildContext context) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Search All Missing',
buttons: [
LSDialog.button(
text: 'Search',
textColor: LSColors.accent,
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to search for all missing movies?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag];
}
static Future<List<dynamic>> editMovie(BuildContext context, RadarrCatalogueData entry) async {
List<List<dynamic>> _options = [
['Edit Movie', Icons.edit, 'edit_movie'],
['Refresh Movie', Icons.refresh, 'refresh_movie'],
['Remove Movie', Icons.delete, 'remove_movie'],
];
bool _flag = false;
String _value = '';
void _setValues(bool flag, String value) {
_flag = flag;
_value = value;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: entry.title,
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _value];
}
static Future<List<dynamic>> editMinimumAvailability(BuildContext context, List<RadarrAvailability> availability) async {
bool _flag = false;
RadarrAvailability _entry;
void _setValues(bool flag, RadarrAvailability entry) {
_flag = flag;
_entry = entry;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Minimum Availability',
content: List.generate(
availability.length,
(index) => LSDialog.tile(
text: availability[index].name,
icon: Icons.folder,
iconColor: LSColors.list(index),
onTap: () => _setValues(true, availability[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _entry];
}
static Future<List<dynamic>> editRootFolder(BuildContext context, List<RadarrRootFolder> folders) async {
bool _flag = false;
RadarrRootFolder _folder;
void _setValues(bool flag, RadarrRootFolder folder) {
_flag = flag;
_folder = folder;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Root Folder',
content: List.generate(
folders.length,
(index) => LSDialog.tile(
text: folders[index].path,
subtitle: LSDialog.richText(
children: [
LSDialog.bolded(text: folders[index].freeSpace.lsBytes_BytesToString()),
],
),
icon: Icons.folder,
iconColor: LSColors.list(index),
onTap: () => _setValues(true, folders[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _folder];
}
static Future<List<dynamic>> editQualityProfile(BuildContext context, List<RadarrQualityProfile> qualities) async {
bool _flag = false;
RadarrQualityProfile _quality;
void _setValues(bool flag, RadarrQualityProfile quality) {
_flag = flag;
_quality = quality;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Quality Profile',
content: List.generate(
qualities.length,
(index) => LSDialog.tile(
text: qualities[index].name,
icon: Icons.portrait,
iconColor: LSColors.list(index),
onTap: () => _setValues(true, qualities[index]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _quality];
}
static Future<List<dynamic>> downloadWarning(BuildContext context) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Download Release',
buttons: [
LSDialog.button(
text: 'Download',
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to download this release? It has been marked as a rejected release by Radarr.')
],
contentPadding: LSDialog.textDialogContentPadding(),
);
return [_flag];
}
static Future<List<dynamic>> globalSettings(BuildContext context) async {
List<List<dynamic>> _options = [
['View Web GUI', Icons.language, 'web_gui'],
['Update Library', Icons.autorenew, 'update_library'],
['Run RSS Sync', Icons.rss_feed, 'rss_sync'],
['Search All Missing', Icons.search, 'missing_search'],
['Backup Database', Icons.save, 'backup'],
];
bool _flag = false;
String _value = '';
void _setValues(bool flag, String value) {
_flag = flag;
_value = value;
Navigator.of(context).pop();
}
await LSDialog.dialog(
context: context,
title: 'Radarr Settings',
content: List.generate(
_options.length,
(index) => LSDialog.tile(
text: _options[index][0],
icon: _options[index][1],
iconColor: LSColors.list(index),
onTap: () => _setValues(true, _options[index][2]),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
);
return [_flag, _value];
}
}

View File

@@ -48,4 +48,12 @@ class RadarrModel extends ChangeNotifier {
_navigationIndex = navigationIndex;
notifyListeners();
}
int _movieNavigationIndex = 0;
int get movieNavigationIndex => _movieNavigationIndex;
set movieNavigationIndex(int movieNavigationIndex) {
assert(movieNavigationIndex != null);
_movieNavigationIndex = movieNavigationIndex;
notifyListeners();
}
}

View File

@@ -138,6 +138,7 @@ class _State extends State<RadarrAddDetails> {
: _arguments.data.overview,
uri: _arguments.data.posterURI ?? '',
fallbackImage: 'assets/images/radarr/nomovieposter.png',
headers: Database.currentProfileObject.getRadarr()['headers'],
),
LSDivider(),
ValueListenableBuilder(
@@ -162,7 +163,7 @@ class _State extends State<RadarrAddDetails> {
subtitle: LSSubtitle(text: _rootfolder?.path ?? 'Unknown Root Folder'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async {
List _values = await LSDialogRadarr.showEditRootFolderPrompt(context, _rootFolders);
List _values = await RadarrDialogs.editRootFolder(context, _rootFolders);
if(_values[0]) box.put(RadarrDatabaseValue.ADD_ROOT_FOLDER.key, _values[1]);
},
);
@@ -177,7 +178,7 @@ class _State extends State<RadarrAddDetails> {
subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async {
List _values = await LSDialogRadarr.showEditQualityProfilePrompt(context, _qualityProfiles);
List _values = await RadarrDialogs.editQualityProfile(context, _qualityProfiles);
if(_values[0]) box.put(RadarrDatabaseValue.ADD_QUALITY_PROFILE.key, _values[1]);
},
);
@@ -192,7 +193,7 @@ class _State extends State<RadarrAddDetails> {
subtitle: LSSubtitle(text: _availability?.name ?? 'Unknown Availability'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async {
List _values = await LSDialogRadarr.showMinimumAvailabilityPrompt(context, RadarrConstants.MINIMUM_AVAILBILITIES);
List _values = await RadarrDialogs.editMinimumAvailability(context, RadarrConstants.MINIMUM_AVAILBILITIES);
if(_values[0]) box.put(RadarrDatabaseValue.ADD_AVAILABILITY.key, _values[1]);
},
);

View File

@@ -22,20 +22,16 @@ class RadarrDetailsMovie extends StatefulWidget {
class _State extends State<RadarrDetailsMovie> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _pageController = PageController(initialPage: 1);
RadarrDetailsMovieArguments _arguments;
bool _error = false;
final List<String> _tabTitles = [
'Overview',
'Search',
'Files',
];
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
_arguments = ModalRoute.of(context).settings.arguments;
Provider.of<RadarrModel>(context, listen: false).movieNavigationIndex = 1;
_fetch();
});
}
@@ -53,7 +49,10 @@ class _State extends State<RadarrDetailsMovie> {
@override
Widget build(BuildContext context) => Scaffold(
key: _scaffoldKey,
appBar: _arguments == null || _arguments.data == null ? _appBar : null,
appBar: _appBar,
bottomNavigationBar: _arguments != null && _arguments.data != null
? _bottomNavigationBar
: null,
body: _arguments != null
? _arguments.data != null
? _body
@@ -63,36 +62,35 @@ class _State extends State<RadarrDetailsMovie> {
: null,
);
Widget get _appBar => LSAppBar(title: 'Movie Details');
Widget get _body => DefaultTabController(
length: _tabTitles.length,
initialIndex: 1,
child: LSSliverAppBarTabs(
title: _arguments.data.title,
backgroundURI: _arguments.data.fanartURI(highRes: true),
body: TabBarView(
children: <Widget>[
RadarrDetailsOverview(data: _arguments.data),
RadarrDetailsSearch(data: _arguments.data),
RadarrDetailsFiles(data: _arguments.data),
],
),
bottom: TabBar(
tabs: <Widget>[
for(int i =0; i<_tabTitles.length; i++) Tab(
child: Text(_tabTitles[i]),
),
],
),
actions: <Widget>[
Widget get _appBar => LSAppBar(
title: _arguments == null || _arguments.data == null
? 'Movie Details'
: _arguments.data.title,
actions: _arguments == null || _arguments.data == null
? null
: <Widget>[
RadarrDetailsEditButton(
data: _arguments.data,
remove: (bool withData) => _removeCallback(withData),
),
],
),
);
Widget get _bottomNavigationBar => RadarrMovieNavigationBar(pageController: _pageController);
List<Widget> get _tabs => [
RadarrDetailsOverview(data: _arguments.data),
RadarrDetailsSearch(data: _arguments.data),
RadarrDetailsFiles(data: _arguments.data),
];
Widget get _body => PageView(
controller: _pageController,
children: _tabs,
onPageChanged: _onPageChanged,
);
void _onPageChanged(int index) => Provider.of<RadarrModel>(context, listen: false).movieNavigationIndex = index;
Future<void> _removeCallback(bool withData) async => Navigator.of(context).pop(['remove_movie', withData]);
}

View File

@@ -65,7 +65,7 @@ class _State extends State<RadarrDetailsSearchResults> {
title: LSTitle(text: 'Release Title'),
subtitle: LSSubtitle(text: _arguments.data.title),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
onTap: () async => LSDialogSystem.textPreview(context, 'Release Title', _arguments.data.title),
onTap: () async => GlobalDialogs.textPreview(context, 'Release Title', _arguments.data.title),
),
LSContainerRow(
children: <Widget>[
@@ -156,42 +156,22 @@ class _State extends State<RadarrDetailsSearchResults> {
);
Future<void> _download() async {
if(_arguments.data.approved) {
await _startDownload()
.then((_) => LSSnackBar(
context: context,
title: 'Downloading...',
message: _arguments.data.title,
type: SNACKBAR_TYPE.success,
showButton: true,
buttonText: 'Back',
buttonOnPressed: () => Navigator.of(context).popUntil(ModalRoute.withName(Radarr.ROUTE_NAME)),
))
.catchError((_) => LSSnackBar(
context: context,
title: 'Failed to Start Downloading',
message: Constants.CHECK_LOGS_MESSAGE,
type: SNACKBAR_TYPE.failure,
));
} else {
List<dynamic> values = await LSDialogRadarr.showDownloadWarningPrompt(context);
if(values[0]) await _startDownload()
.then((_) => LSSnackBar(
context: context,
title: 'Downloading...',
message: _arguments.data.title,
type: SNACKBAR_TYPE.success,
showButton: true,
buttonText: 'Back',
buttonOnPressed: () => Navigator.of(context).popUntil(ModalRoute.withName(Radarr.ROUTE_NAME)),
))
.catchError((_) => LSSnackBar(
context: context,
title: 'Failed to Start Downloading',
message: Constants.CHECK_LOGS_MESSAGE,
type: SNACKBAR_TYPE.failure,
));
}
await _startDownload()
.then((_) => LSSnackBar(
context: context,
title: 'Downloading...',
message: _arguments.data.title,
type: SNACKBAR_TYPE.success,
showButton: true,
buttonText: 'Back',
buttonOnPressed: () => Navigator.of(context).popUntil(ModalRoute.withName(Radarr.ROUTE_NAME)),
))
.catchError((_) => LSSnackBar(
context: context,
title: 'Failed to Start Downloading',
message: Constants.CHECK_LOGS_MESSAGE,
type: SNACKBAR_TYPE.failure,
));
}
Future<bool> _startDownload() async {
@@ -204,6 +184,6 @@ class _State extends State<RadarrDetailsSearchResults> {
for(var i=0; i<_arguments.data.rejections.length; i++) {
reject += '${i+1}. ${_arguments.data.rejections[i]}\n';
}
await LSDialogSystem.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
await GlobalDialogs.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
}
}

View File

@@ -140,17 +140,17 @@ class _State extends State<RadarrEditMovie> {
);
Future<void> _changePath() async {
List<dynamic> _values = await LSDialogSystem.editText(context, 'Movie Path', prefill: _path);
List<dynamic> _values = await GlobalDialogs.editText(context, 'Movie Path', prefill: _path);
if(_values[0] && mounted) setState(() => _path = _values[1]);
}
Future<void> _changeProfile() async {
List<dynamic> _values = await LSDialogRadarr.showEditQualityProfilePrompt(context, _qualityProfiles);
List<dynamic> _values = await RadarrDialogs.editQualityProfile(context, _qualityProfiles);
if(_values[0] && mounted) setState(() => _qualityProfile = _values[1]);
}
Future<void> _changeMinimumAvailability() async {
List<dynamic> _values = await LSDialogRadarr.showMinimumAvailabilityPrompt(context, RadarrConstants.MINIMUM_AVAILBILITIES);
List<dynamic> _values = await RadarrDialogs.editMinimumAvailability(context, RadarrConstants.MINIMUM_AVAILBILITIES);
if(_values[0] && mounted) setState(() => _minimumAvailability = _values[1]);
}

View File

@@ -29,18 +29,30 @@ class _State extends State<Radarr> {
}
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, box, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
Widget build(BuildContext context) => WillPopScope(
onWillPop: () async {
if(_scaffoldKey.currentState.isDrawerOpen) {
//If the drawer is open, return true to close it
return true;
} else {
//If the drawer isn't open, open the drawer
_scaffoldKey.currentState.openDrawer();
return false;
}
},
child: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, box, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
},
),
);
Widget get _drawer => LSDrawer(page: 'radarr');
@@ -108,7 +120,7 @@ class _State extends State<Radarr> {
}
Future<void> _handlePopup() async {
List<dynamic> values = await LSDialogRadarr.showSettingsPrompt(context);
List<dynamic> values = await RadarrDialogs.globalSettings(context);
if(values[0]) switch(values[1]) {
case 'web_gui': await _api.host?.toString()?.lsLinks_OpenLink(); break;
case 'update_library': await _api.updateLibrary()
@@ -124,7 +136,7 @@ class _State extends State<Radarr> {
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Backup Database', message: Constants.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
break;
case 'missing_search': {
List<dynamic> values = await LSDialogRadarr.showSearchMissingPrompt(context);
List<dynamic> values = await RadarrDialogs.searchAllMissing(context);
if(values[0]) await _api.searchAllMissing()
.then((_) => LSSnackBar(context: context, title: 'Searching...', message: 'Search for all missing movies'))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: Constants.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));

View File

@@ -14,3 +14,4 @@ export './widgets/details_overview.dart';
export './widgets/add_search_bar.dart';
export './widgets/add_search_result_tile.dart';
export './widgets/navigation_bar.dart';
export './widgets/movie_navigation_bar.dart';

View File

@@ -87,6 +87,7 @@ class _State extends State<RadarrCatalogueTile> {
decoration: LSCardBackground(
uri: widget.data.posterURI(),
darken: !widget.data.monitored,
headers: Database.currentProfileObject.getRadarr()['headers'],
),
);
@@ -137,7 +138,7 @@ class _State extends State<RadarrCatalogueTile> {
}
Future<void> _handlePopup() async {
List<dynamic> values = await LSDialogRadarr.showEditMoviePrompt(context, widget.data);
List<dynamic> values = await RadarrDialogs.editMovie(context, widget.data);
if(values[0]) switch(values[1]) {
case 'refresh_movie': _refreshMovie(); break;
case 'edit_movie': _editMovie(); break;
@@ -170,10 +171,10 @@ class _State extends State<RadarrCatalogueTile> {
Future<void> _removeMovie() async {
final _api = RadarrAPI.from(Database.currentProfileObject);
List values = await LSDialogRadarr.showDeleteMoviePrompt(context);
List values = await RadarrDialogs.deleteMovie(context);
if(values[0]) {
if(values[1]) {
values = await LSDialogSystem.deleteCatalogueWithFiles(context, widget.data.title);
values = await GlobalDialogs.deleteCatalogueWithFiles(context, widget.data.title);
if(values[0]) {
await _api.removeMovie(widget.data.movieID, deleteFiles: true)
.then((_) {

View File

@@ -25,7 +25,7 @@ class _State extends State<RadarrDetailsEditButton> {
);
Future<void> _handlePopup(BuildContext context) async {
List<dynamic> values = await LSDialogRadarr.showEditMoviePrompt(context, widget.data);
List<dynamic> values = await RadarrDialogs.editMovie(context, widget.data);
if(values[0]) switch(values[1]) {
case 'refresh_movie': _refreshMovie(context); break;
case 'edit_movie': _editMovie(context); break;
@@ -56,10 +56,10 @@ class _State extends State<RadarrDetailsEditButton> {
Future<void> _removeMovie(BuildContext context) async {
final _api = RadarrAPI.from(Database.currentProfileObject);
List values = await LSDialogRadarr.showDeleteMoviePrompt(context);
List values = await RadarrDialogs.deleteMovie(context);
if(values[0]) {
if(values[1]) {
values = await LSDialogSystem.deleteCatalogueWithFiles(context, widget.data.title);
values = await GlobalDialogs.deleteCatalogueWithFiles(context, widget.data.title);
if(values[0]) {
await _api.removeMovie(widget.data.movieID, deleteFiles: true)
.then((_) => widget.remove(true))

View File

@@ -46,6 +46,9 @@ class _State extends State<RadarrDetailsFiles> with AutomaticKeepAliveClientMixi
TextSpan(text: '${widget.data.movieFile['quality']['quality']['name'] ?? 'Unknown'}'),
],
),
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
trailing: LSIconButton(
icon: Icons.delete,
@@ -57,7 +60,7 @@ class _State extends State<RadarrDetailsFiles> with AutomaticKeepAliveClientMixi
Widget get _empty => LSGenericMessage(text: 'No Files Found');
Future<void> _delete() async {
List<dynamic> values = await LSDialogRadarr.showDeleteMovieFilePrompt(context);
List<dynamic> values = await RadarrDialogs.deleteMovieFile(context);
RadarrAPI _api = RadarrAPI.from(Database.currentProfileObject);
if(values[0]) _api.removeMovieFile(widget.data.movieFile['id'])
.then((_) {

View File

@@ -30,11 +30,12 @@ class _State extends State<RadarrDetailsOverview> with AutomaticKeepAliveClientM
: widget?.data?.overview,
uri: widget?.data?.posterURI() ?? '',
fallbackImage: 'assets/images/radarr/nomovieposter.png',
headers: Database.currentProfileObject.getRadarr()['headers'],
),
LSCardTile(
title: LSTitle(text: 'Movie Path', centerText: true),
subtitle: LSSubtitle(text: widget?.data?.path ?? 'Unknown', centerText: true),
onTap: () => LSDialogSystem.textPreview(context, widget?.data?.title, widget?.data?.path ?? 'Unknown'),
onTap: () => GlobalDialogs.textPreview(context, widget?.data?.title, widget?.data?.path ?? 'Unknown'),
),
LSContainerRow(
children: <Widget>[

View File

@@ -84,7 +84,7 @@ class RadarrDetailsSearchTile extends StatelessWidget {
Future<void> _trailingLongPressed(BuildContext context) async {
if(!data.approved) {
List<dynamic> values = await LSDialogRadarr.showDownloadWarningPrompt(context);
List<dynamic> values = await RadarrDialogs.downloadWarning(context);
if(values[0]) await _startDownload()
.then((_) => LSSnackBar(
context: context,
@@ -114,6 +114,6 @@ class RadarrDetailsSearchTile extends StatelessWidget {
for(var i=0; i<data.rejections.length; i++) {
reject += '${i+1}. ${data.rejections[i]}\n';
}
await LSDialogSystem.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
await GlobalDialogs.textPreview(context, 'Rejection Reasons', reject.substring(0, reject.length-1));
}
}

View File

@@ -34,7 +34,10 @@ class _State extends State<RadarrMissingTile> {
onPressed: () async => _search(),
),
onTap: () async => _enterMovie(),
decoration: LSCardBackground(uri: widget.data.posterURI()),
decoration: LSCardBackground(
uri: widget.data.posterURI(),
headers: Database.currentProfileObject.getRadarr()['headers'],
),
padContent: true,
);

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import '../../radarr.dart';
class RadarrMovieNavigationBar extends StatefulWidget {
final PageController pageController;
RadarrMovieNavigationBar({
Key key,
@required this.pageController,
}): super(key: key);
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<RadarrMovieNavigationBar> {
static const List<String> _navbarTitles = [
'Overview',
'Search',
'Files',
];
static const List<IconData> _navbarIcons = [
Icons.subject,
CustomIcons.movies,
Icons.insert_drive_file,
];
@override
Widget build(BuildContext context) => Selector<RadarrModel, int>(
selector: (_, model) => model.movieNavigationIndex,
builder: (context, index, _) => LSBottomNavigationBar(
index: index,
icons: _navbarIcons,
titles: _navbarTitles,
onTap: (index) async => await _navOnTap(index),
),
);
Future<void> _navOnTap(int index) async {
await widget.pageController.animateToPage(
index,
duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED),
curve: Curves.easeOutSine,
).then((_) => Provider.of<RadarrModel>(context, listen: false).movieNavigationIndex = index);
}
}

View File

@@ -35,7 +35,10 @@ class _State extends State<RadarrUpcomingTile> {
),
onTap: () async => _enterMovie(),
padContent: true,
decoration: LSCardBackground(uri: widget.data.posterURI()),
decoration: LSCardBackground(
uri: widget.data.posterURI(),
headers: Database.currentProfileObject.getRadarr()['headers'],
),
);
Future<void> _search() async {

View File

@@ -1,4 +1,3 @@
export './sabnzbd/api.dart';
export './sabnzbd/core.dart';
export './sabnzbd/routes.dart';
export './sabnzbd/state.dart';
export './sabnzbd/widgets.dart';

Some files were not shown because too many files have changed in this diff Show More