[Release] v5.0.0 (50000008) (#322)

NEW
- [Webhooks] Show series, movie, or artist cover photos in incoming system notifications
- [Webhooks/Radarr] Jump directly into the content by tapping the notification
- [Webhooks/Sonarr] Jump directly into the content by tapping the notification
- [-arr/Details] Added a button to the AppBar to jump directly into the edit page

TWEAKS
- [Radarr/Files] Moved the video block above the audio block

FIXES
- [AppBar/Dropdown] Vertical padding around title when the profile dropdown is visible was incorrect
- [Radarr/Details] If the movie was not found, it would show an infinite loader, now shows a "No Movies Found" message
- [Radarr/Releases] Changing the sorting or filter method would not scroll the list back to the top
This commit is contained in:
Jagandeep Brar
2021-03-03 18:20:01 -06:00
committed by GitHub
parent f57f5837dd
commit 157b44045c
114 changed files with 1243 additions and 561 deletions

View File

@@ -1,5 +1,23 @@
# LunaSea Changelog
## v5.0.0 (50000008)
#### NEW
- `[Webhooks]` Show series, movie, or artist cover photos in incoming system notifications
- `[Webhooks/Radarr]` Jump directly into the content by tapping the notification
- `[Webhooks/Sonarr]` Jump directly into the content by tapping the notification
- `[-arr/Details]` Added a button to the AppBar to jump directly into the edit page
#### TWEAKS
- `[Radarr/Files]` Moved the video block above the audio block
#### FIXES
- `[AppBar/Dropdown]` Vertical padding around title when the profile dropdown is visible was incorrect
- `[Radarr/Details]` If the movie was not found, it would show an infinite loader, now shows a "No Movies Found" message
- `[Radarr/Releases]` Changing the sorting or filter method would not scroll the list back to the top
---
## v5.0.0 (50000007)
#### NEW

View File

@@ -1,71 +1,58 @@
{
"motd": "This build brings some nice quality-of-life updates to usenet indexer search (with many more updates coming in the future).\n\nYou can now view the comments and download links in the expanded tile! Tapping the URL will open the URL, and long pressing the URL will copy it to your clipboard.\n\nThis build also improves the inkwell splashes across the UI, improves the UI when viewing rejection reasons for releases, merges Radarr's health check into the system status section, and fixes a few smaller bugs.",
"motd": "This build introduces deeper support for notifications/webhooks (alpha) by supporting deep linking to content and displaying images within the system notifications!\n\nThis build also adds a new button the AppBar when you are within a series/movie/artist page. The pencil/edit icon now takes you directly to the edit page, and the three vertical dots will open the menu to refresh, delete, etc.",
"version": "5.0.0",
"build": "50000007",
"build": "50000008",
"new": [
{
"module": "Radarr/\nDisk Usage",
"module": "Webhooks",
"changes": [
"Added a progress bar to better visualize disk usage"
"Show series, movie, or artist cover photos in incoming system notifications"
]
},
{
"module": "Search",
"module": "Webhooks/\nRadarr",
"changes": [
"Show comment and download links in the expanded table"
"Jump directly into the content by tapping the notification"
]
},
{
"module": "Settings/\nSearch",
"module": "Webhooks/\nSonarr",
"changes": [
"Ability to toggle off showing comment and download links"
"Jump directly into the content by tapping the notification"
]
},
{
"module": "UI/UX",
"module": "-arr/\nDetails",
"changes": [
"If a URL is in a table, tap it to open the URL or long press to copy the URL"
"Added a button to the AppBar to jump directly into the edit page"
]
}
],
"tweaks": [
{
"module": "-arr/\nReleases",
"module": "Radarr/\nFiles",
"changes": [
"Improve the layout of the rejection reasons dialog"
]
},
{
"module": "Radarr/\nHealth",
"changes": [
"Merged health check into system status"
]
},
{
"module": "UI/UX",
"changes": [
"Added more descriptive tooltips to all popup menu buttons",
"Adjusted the style of splash/highlight inking"
"Moved the video block above the audio block"
]
}
],
"fixes": [
{
"module": "Images",
"module": "AppBar/\nDropdown",
"changes": [
"Images could fail to load with specific headers"
"Vertical padding around title when the profile dropdown is visible was incorrect"
]
},
{
"module": "Radarr/\nDisk Usage",
"module": "Radarr/\nDetails",
"changes": [
"An invalid total or free space value would cause a grey screen"
"If the movie was not found it would show an infinite loader, now shows a \"No Movies Found\" message"
]
},
{
"module": "Radarr/\nReleases",
"changes": [
"Clear button for the search bar would not show up"
"Changing the sorting or filter method would not scroll the list back to the top"
]
}
]

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>ImageNotification</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>NotificationService</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
//
// NotificationService.h
// ImageNotification
//
// Created by Jagandeep Brar on 2021-02-25.
// Copyright © 2021 The Chromium Authors. All rights reserved.
//
#import <UserNotifications/UserNotifications.h>
@interface NotificationService : UNNotificationServiceExtension
@end

View File

@@ -0,0 +1,34 @@
//
// NotificationService.m
// ImageNotification
//
// Created by Jagandeep Brar on 2021-02-25.
// Copyright © 2021 The Chromium Authors. All rights reserved.
//
#import "NotificationService.h"
#import "FirebaseMessaging.h"
@interface NotificationService ()
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
[[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler];
}
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
self.contentHandler(self.bestAttemptContent);
}
@end

View File

@@ -51,3 +51,8 @@ post_install do |installer|
end
end
end
target 'ImageNotification' do
use_frameworks!
pod 'Firebase/Messaging'
end

View File

@@ -1,5 +1,5 @@
PODS:
- cloud_firestore (0.16.0):
- cloud_firestore (0.16.0-1):
- Firebase/Firestore (= 7.3.0)
- firebase_core
- Flutter
@@ -54,14 +54,14 @@ PODS:
- Firebase/Storage (7.3.0):
- Firebase/CoreOnly
- FirebaseStorage (~> 7.3.0)
- firebase_auth (0.20.0-1):
- firebase_auth (0.20.1):
- Firebase/Auth (= 7.3.0)
- firebase_core
- Flutter
- firebase_core (0.7.0):
- Firebase/CoreOnly (= 7.3.0)
- Flutter
- firebase_messaging (8.0.0-dev.14):
- firebase_messaging (8.0.0-dev.15):
- Firebase/Messaging (= 7.3.0)
- firebase_core
- Flutter
@@ -163,6 +163,7 @@ DEPENDENCIES:
- cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Firebase/Messaging
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
@@ -246,15 +247,15 @@ CHECKOUT OPTIONS:
:tag: 7.3.0
SPEC CHECKSUMS:
cloud_firestore: 433cf1aece50c727965355ae00e8dbeeceae66b1
cloud_firestore: 357b92d4778d7715d76a393db5f89e7dcc9b7401
connectivity_plus: f5e5e0b894ec1187bfab9816d6c2f034da8d46da
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
Firebase: 26223c695fe322633274198cb19dca8cb7e54416
firebase_auth: b55e5136528dced982022ee073ed4b48f15c055c
firebase_auth: 7220e26fc14a501a63093edc10443de6bca0d7e8
firebase_core: 91b27774a52f41f8b58484a75edf71197ac01c59
firebase_messaging: d039d8279704fc1e3d05e0a2bb2ce7dc29ee6be8
firebase_messaging: caf0273c76e54f0a10dfe5048ac47b89e75bb78a
firebase_storage: 554b8ce9192bfe0bd35f63da42d0d1f6ae819ccc
FirebaseAuth: c224a0cf1afa0949bd5c7bfcf154b4f5ce8ddef2
FirebaseCore: 4d3c72622ce0e2106aaa07bb4b2935ba2c370972
@@ -284,6 +285,6 @@ SPEC CHECKSUMS:
SwiftyGif: 5d4af95df24caf1c570dbbcb32a3b8a0763bc6d7
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
PODFILE CHECKSUM: 9f0afed62614a65db8eaad79ca26aa6cd9cb4be2
PODFILE CHECKSUM: d7f0a8efbe8010ddad9d00678781441d461a1402
COCOAPODS: 1.10.1

View File

@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2A9501F70E4ADE3B795C4BD7 /* Pods_ImageNotification.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8B58106185CA1FE7B7C6832 /* Pods_ImageNotification.framework */; };
3355779EEB7C12DC262232AF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5A8F6AE14D184A58D8A7331 /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -16,8 +17,20 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9C2991242453FE3100B93AB2 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C2991232453FE3100B93AB2 /* StoreKit.framework */; };
9C5DBBF2257EB50400836D8D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9C5DBBF1257EB50400836D8D /* GoogleService-Info.plist */; };
9C7092B325E77E470056AE0B /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 9C7092B225E77E470056AE0B /* NotificationService.m */; };
9C7092B725E77E470056AE0B /* ImageNotification.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 9C7092AF25E77E470056AE0B /* ImageNotification.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
9C7092B525E77E470056AE0B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9C7092AE25E77E470056AE0B;
remoteInfo = ImageNotification;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@@ -29,6 +42,17 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
9C7092B825E77E470056AE0B /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
9C7092B725E77E470056AE0B /* ImageNotification.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@@ -49,9 +73,17 @@
9C0A396F23E0F00900EC8127 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
9C2991232453FE3100B93AB2 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
9C5DBBF1257EB50400836D8D /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
9C7092AF25E77E470056AE0B /* ImageNotification.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ImageNotification.appex; sourceTree = BUILT_PRODUCTS_DIR; };
9C7092B125E77E470056AE0B /* NotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationService.h; sourceTree = "<group>"; };
9C7092B225E77E470056AE0B /* NotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationService.m; sourceTree = "<group>"; };
9C7092B425E77E470056AE0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9C9E2734C6D4DC2FF1815F04 /* Pods-ImageNotification.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.debug.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.debug.xcconfig"; sourceTree = "<group>"; };
A046209115F73D5EC348E242 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
D5A8F6AE14D184A58D8A7331 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DBA43D9DEB26304D56B42917 /* Pods-ImageNotification.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.profile.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.profile.xcconfig"; sourceTree = "<group>"; };
E2F8912DE8B8A6F72277E96C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
E8B58106185CA1FE7B7C6832 /* Pods_ImageNotification.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImageNotification.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FAED45593175690B5B62C40E /* Pods-ImageNotification.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.release.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -64,6 +96,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
9C7092AC25E77E470056AE0B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
2A9501F70E4ADE3B795C4BD7 /* Pods_ImageNotification.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -84,6 +124,7 @@
9C5DBBF1257EB50400836D8D /* GoogleService-Info.plist */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
9C7092B025E77E470056AE0B /* ImageNotification */,
97C146EF1CF9000F007C117D /* Products */,
F5D05D221249C73674C5DB0D /* Pods */,
B52D05C72433A9F059B6393D /* Frameworks */,
@@ -94,6 +135,7 @@
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
9C7092AF25E77E470056AE0B /* ImageNotification.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -122,11 +164,22 @@
name = "Supporting Files";
sourceTree = "<group>";
};
9C7092B025E77E470056AE0B /* ImageNotification */ = {
isa = PBXGroup;
children = (
9C7092B125E77E470056AE0B /* NotificationService.h */,
9C7092B225E77E470056AE0B /* NotificationService.m */,
9C7092B425E77E470056AE0B /* Info.plist */,
);
path = ImageNotification;
sourceTree = "<group>";
};
B52D05C72433A9F059B6393D /* Frameworks */ = {
isa = PBXGroup;
children = (
9C2991232453FE3100B93AB2 /* StoreKit.framework */,
D5A8F6AE14D184A58D8A7331 /* Pods_Runner.framework */,
E8B58106185CA1FE7B7C6832 /* Pods_ImageNotification.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -137,6 +190,9 @@
E2F8912DE8B8A6F72277E96C /* Pods-Runner.debug.xcconfig */,
7EF2984A57FD1E0C98A7277A /* Pods-Runner.release.xcconfig */,
A046209115F73D5EC348E242 /* Pods-Runner.profile.xcconfig */,
9C9E2734C6D4DC2FF1815F04 /* Pods-ImageNotification.debug.xcconfig */,
FAED45593175690B5B62C40E /* Pods-ImageNotification.release.xcconfig */,
DBA43D9DEB26304D56B42917 /* Pods-ImageNotification.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -157,16 +213,36 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
FB187DB152E6AC8A61142056 /* [CP] Embed Pods Frameworks */,
3BA7760032A55E21F5FE79FC /* [CP] Copy Pods Resources */,
9C7092B825E77E470056AE0B /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
9C7092B625E77E470056AE0B /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
9C7092AE25E77E470056AE0B /* ImageNotification */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9C7092BC25E77E470056AE0B /* Build configuration list for PBXNativeTarget "ImageNotification" */;
buildPhases = (
BBE8B4F04EBFF199FEB18CC2 /* [CP] Check Pods Manifest.lock */,
9C7092AB25E77E470056AE0B /* Sources */,
9C7092AC25E77E470056AE0B /* Frameworks */,
9C7092AD25E77E470056AE0B /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ImageNotification;
productName = ImageNotification;
productReference = 9C7092AF25E77E470056AE0B /* ImageNotification.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -183,6 +259,11 @@
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
9C7092AE25E77E470056AE0B = {
CreatedOnToolsVersion = 12.4;
DevelopmentTeam = VPH33JQH4R;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@@ -199,6 +280,7 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
9C7092AE25E77E470056AE0B /* ImageNotification */,
);
};
/* End PBXProject section */
@@ -216,6 +298,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
9C7092AD25E77E470056AE0B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -287,6 +376,28 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
BBE8B4F04EBFF199FEB18CC2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ImageNotification-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
FB187DB152E6AC8A61142056 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -371,8 +482,24 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
9C7092AB25E77E470056AE0B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9C7092B325E77E470056AE0B /* NotificationService.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
9C7092B625E77E470056AE0B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 9C7092AE25E77E470056AE0B /* ImageNotification */;
targetProxy = 9C7092B525E77E470056AE0B /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -646,6 +773,88 @@
};
name = Release;
};
9C7092B925E77E470056AE0B /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9C9E2734C6D4DC2FF1815F04 /* Pods-ImageNotification.debug.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = VPH33JQH4R;
ENABLE_BITCODE = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ImageNotification/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.lunasea.lunasea.ImageNotification;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
9C7092BA25E77E470056AE0B /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FAED45593175690B5B62C40E /* Pods-ImageNotification.release.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = VPH33JQH4R;
ENABLE_BITCODE = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ImageNotification/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.lunasea.lunasea.ImageNotification;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
9C7092BB25E77E470056AE0B /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = DBA43D9DEB26304D56B42917 /* Pods-ImageNotification.profile.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = VPH33JQH4R;
ENABLE_BITCODE = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ImageNotification/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.lunasea.lunasea.ImageNotification;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -669,6 +878,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
9C7092BC25E77E470056AE0B /* Build configuration list for PBXNativeTarget "ImageNotification" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9C7092B925E77E470056AE0B /* Debug */,
9C7092BA25E77E470056AE0B /* Release */,
9C7092BB25E77E470056AE0B /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;

View File

@@ -177,14 +177,14 @@ class _State extends State<LunaAppBar> {
Widget _sharedLeading(BuildContext context) {
if(widget.useDrawer) return IconButton(
icon: Icon(Icons.menu),
icon: Icon(Icons.menu_rounded),
onPressed: () async {
HapticFeedback.lightImpact();
if(Scaffold.of(context).hasDrawer) Scaffold.of(context).openDrawer();
},
);
return InkWell(
child: Icon(Icons.arrow_back_ios),
child: Icon(Icons.arrow_back_ios_rounded),
onTap: () async {
HapticFeedback.lightImpact();
Navigator.of(context).lunaSafetyPop();
@@ -229,12 +229,13 @@ class _State extends State<LunaAppBar> {
tooltip: 'Change Profiles',
child: Wrap(
direction: Axis.horizontal,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
widget.title,
style: TextStyle(fontSize: Constants.UI_FONT_SIZE_HEADER),
),
LSIcon(icon: Icons.arrow_drop_down),
Icon(Icons.arrow_drop_down),
],
),
onSelected: (result) {

View File

@@ -2,3 +2,4 @@ export 'models/hive_objects.dart';
export 'models/router.dart';
export 'models/state.dart';
export 'models/types.dart';
export 'models/webhooks.dart';

View File

@@ -1,19 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
abstract class LunaModuleState extends ChangeNotifier {
/// [ScrollController] to be used with all scrolling [ListView] (and similar scrolling widgets) within a module.
final ScrollController scrollController = ScrollController();
/// Using [scrollController], scrolls the first scroll position back to the beginning of the list.
Future<void> scrollBackList() => scrollController.lunaAnimateToStart();
@override
void dispose() {
scrollController?.dispose();
super.dispose();
}
/// Reset the state back to the default
void reset();
}

View File

@@ -0,0 +1,4 @@
abstract class LunaWebhooks {
/// Handle the incoming webhook for the module.
Future<void> handle(Map<dynamic, dynamic> data);
}

View File

@@ -14,6 +14,9 @@ class LunaFirebaseMessaging {
/// Returns a [Stream] to handle any notifications that are tapped while the application is in the background (not terminated).
Stream<RemoteMessage> get onMessageOpenedApp => FirebaseMessaging.onMessageOpenedApp;
/// Returns the Firebase Cloud Messaging device token for this device.
Future<String> get token async => instance.getToken();
/// Request for permission to send a user notifications.
///
/// Returns true if permissions are allowed at either a full or provisional level.
@@ -29,22 +32,65 @@ class LunaFirebaseMessaging {
}
}
/// Returns the Firebase Cloud Messaging device token for this device.
Future<String> get token async => instance.getToken();
/// Return a [StreamSubscription] that will show a notification banner on a newly received notification.
///
/// This listens on [FirebaseMessaging.onMessage], where the application must be open and in the foreground.
StreamSubscription<RemoteMessage> showNotificationOnMessageListener() {
StreamSubscription<RemoteMessage> onMessageListener() {
return onMessage.listen((message) {
if(message == null) return;
LunaModule module = (message.data ?? {}).isNotEmpty ? LunaModule.DASHBOARD.fromKey(message.data['module']) : null;
showLunaSnackBar(
title: message.notification?.title ?? 'Unknown Content',
message: message.notification?.body ?? LunaUI.TEXT_EMDASH,
type: LunaSnackbarType.INFO,
position: FlashPosition.top,
duration: Duration(seconds: 6, milliseconds: 750),
showButton: module != null,
buttonOnPressed: () async => _handleWebhook(message),
);
});
}
/// Returns a [StreamSubscription] that will handle messages/notifications that are opened while LunaSea is running in the background.
///
/// This listens on [FirebaseMessaging.onMessageOpenedApp], where the application must be open but in the background.
StreamSubscription<RemoteMessage> onMessageOpenedAppListener() => onMessageOpenedApp.listen(_handleWebhook);
/// Check to see if there was an initial [RemoteMessage] available to be accessed.
///
/// If so, handles the notification webhook.
Future<void> checkAndHandleInitialMessage() async {
RemoteMessage message = await FirebaseMessaging.instance.getInitialMessage();
_handleWebhook(message);
}
/// Shared webhook handler.
Future<void> _handleWebhook(RemoteMessage message) async {
if(message == null || (message.data ?? {}).isEmpty) return;
// Extract module
LunaModule module = LunaModule.DASHBOARD.fromKey(message.data['module']);
if(module == null) {
LunaLogger().warning(
'LunaFirebaseMessaging',
'_handleWebhook',
'Unknown module found inside of RemoteMessage: ${message.data['module'] ?? 'null'}',
);
return;
}
String profile = message.data['profile'];
if(profile?.isEmpty ?? true) {
LunaLogger().warning(
'LunaFirebaseMessaging',
'_handleWebhook',
'Invalid profile received in webhook: ${message.data['profile'] ?? 'null'}',
);
return;
}
bool result = await LunaProfile().safelyChangeProfiles(profile);
if(result) {
module?.handleWebhook(message.data);
} else {
showLunaErrorSnackBar(title: 'Unknown Profile', message: '"$profile" does not exist in ${Constants.APPLICATION_NAME}');
}
}
}

View File

@@ -90,7 +90,7 @@ extension LunaModuleExtension on LunaModule {
case LunaModule.SABNZBD: return SABnzbd.ROUTE_NAME;
case LunaModule.SEARCH: return Search.ROUTE_NAME;
case LunaModule.SETTINGS: return SettingsHomeRouter().route();
case LunaModule.SONARR: return SonarrHomeRouter.ROUTE_NAME;
case LunaModule.SONARR: return SonarrHomeRouter().route();
case LunaModule.TAUTULLI: return TautulliHomeRouter.ROUTE_NAME;
case LunaModule.WAKE_ON_LAN: return null;
}
@@ -104,7 +104,7 @@ extension LunaModuleExtension on LunaModule {
case LunaModule.NZBGET: return CustomIcons.nzbget;
case LunaModule.RADARR: return CustomIcons.radarr;
case LunaModule.SABNZBD: return CustomIcons.sabnzbd;
case LunaModule.SEARCH: return Icons.search;
case LunaModule.SEARCH: return Icons.search_rounded;
case LunaModule.SETTINGS: return CustomIcons.settings;
case LunaModule.SONARR: return CustomIcons.television;
case LunaModule.TAUTULLI: return CustomIcons.tautulli;
@@ -209,6 +209,22 @@ extension LunaModuleExtension on LunaModule {
throw Exception('Invalid LunaModule');
}
Future<void> handleWebhook(Map<String, dynamic> data) async {
switch(this) {
case LunaModule.DASHBOARD: return;
case LunaModule.LIDARR: return LidarrWebhooks().handle(data);
case LunaModule.NZBGET: return;
case LunaModule.RADARR: return RadarrWebhooks().handle(data);
case LunaModule.SABNZBD: return;
case LunaModule.SEARCH: return;
case LunaModule.SETTINGS: return;
case LunaModule.SONARR: return SonarrWebhooks().handle(data);
case LunaModule.TAUTULLI: return;
case LunaModule.WAKE_ON_LAN: return;
}
throw Exception('Invalid LunaModule');
}
Future<void> launch() async {
if(route != null) LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route<dynamic> route) => false);
}

View File

@@ -4,23 +4,23 @@ import 'package:lunasea/modules/settings.dart';
class LunaProfile {
/// Returns a list of the profiles, sorted by their lowercase key/display name.
List<String> profilesList() => Database.profilesBox.keys.map<String>((profile) => profile as String).toList()..sort((a,b) => a.toLowerCase().compareTo(b.toLowerCase()));
/// Safely change profiles.
///
/// Does this safely by:
/// - Ensures that the passed in profile isn't already enabled
/// - Ensures that the profile exists
Future<bool> safelyChangeProfiles(String profile, { bool showSnackbar = true }) async {
if(LunaDatabaseValue.ENABLED_PROFILE.data != profile) {
if(Database.profilesBox.containsKey(profile)) {
LunaDatabaseValue.ENABLED_PROFILE.put(profile);
LunaState.reset(LunaState.navigatorKey.currentContext);
if(showSnackbar) showLunaSuccessSnackBar(title: 'Changed Profile', message: profile);
return true;
} else {
LunaLogger().warning('LunaProfile', 'changeProfile', 'Attempted to change profile to unknown profile: $profile');
}
if(LunaDatabaseValue.ENABLED_PROFILE.data == profile) return true;
if(Database.profilesBox.containsKey(profile)) {
LunaDatabaseValue.ENABLED_PROFILE.put(profile);
LunaState.reset(LunaState.navigatorKey.currentContext);
if(showSnackbar) showLunaSuccessSnackBar(title: 'Changed Profile', message: profile);
return true;
} else {
LunaLogger().warning('LunaProfile', 'changeProfile', 'Attempted to change profile to unknown profile: $profile');
return false;
}
return false;
}
/// Rename a profile.

View File

@@ -54,7 +54,8 @@ class LunaBIOS extends StatefulWidget {
}
class _State extends State<LunaBIOS> {
StreamSubscription _notificationListener;
StreamSubscription _firebaseOnMessageListener;
StreamSubscription _firebaseOnMessageOpenedAppListener;
@override
void initState() {
@@ -65,7 +66,8 @@ class _State extends State<LunaBIOS> {
@override
void dispose() {
Future.wait([
if(_notificationListener != null) _notificationListener.cancel(),
if(_firebaseOnMessageListener != null) _firebaseOnMessageListener.cancel(),
if(_firebaseOnMessageOpenedAppListener != null) _firebaseOnMessageOpenedAppListener.cancel(),
Database().deinitialize(),
LunaInAppPurchases().deinitialize(),
]).then((_) => super.dispose());
@@ -73,10 +75,16 @@ class _State extends State<LunaBIOS> {
/// Runs the first-step boot sequence that is required for widgets
Future<void> _boot() async {
// Initialize notifications: Request notification permission, add device token to Firebase (if logged in), add a notification listener for internal headsup
// Initialize notifications
// - Request notification permission
// - Add device token to Firebase (if logged in)
// - Check and handle initial message (if found)
// - Add notification listeners for onMessage and onMessageOpenedApp
await LunaFirebaseMessaging().requestNotificationPermissions();
LunaFirebaseFirestore().addDeviceToken();
_notificationListener = LunaFirebaseMessaging().showNotificationOnMessageListener();
LunaFirebaseMessaging().checkAndHandleInitialMessage();
_firebaseOnMessageListener = LunaFirebaseMessaging().onMessageListener();
_firebaseOnMessageOpenedAppListener = LunaFirebaseMessaging().onMessageOpenedAppListener();
// Initialize quick actions
LunaQuickActions().initialize();
// Check and show changelog

View File

@@ -71,7 +71,7 @@ class CalendarSonarrData extends CalendarData {
: '';
}
Future<void> enterContent(BuildContext context) async => SonarrSeriesDetailsRouter.navigateTo(
Future<void> enterContent(BuildContext context) async => SonarrSeriesDetailsRouter().navigateTo(
context,
seriesId: seriesID,
);

View File

@@ -118,8 +118,8 @@ class _State extends State<DashboardCalendarWidget> with TickerProviderStateMixi
),
centerHeaderTitle: true,
formatButtonVisible: false,
leftChevronIcon: Icon(Icons.arrow_back_ios),
rightChevronIcon: Icon(Icons.arrow_forward_ios),
leftChevronIcon: Icon(Icons.arrow_back_ios_rounded),
rightChevronIcon: Icon(Icons.arrow_forward_ios_rounded),
),
initialCalendarFormat: (DashboardDatabaseValue.CALENDAR_STARTING_SIZE.data as CalendarStartingSize).data,
availableCalendarFormats: const {

View File

@@ -4,3 +4,4 @@ export 'core/database.dart';
export 'core/dialogs.dart';
export 'core/state.dart';
export 'core/sorting.dart';
export 'core/webhooks.dart';

View File

@@ -0,0 +1,66 @@
import 'package:lunasea/core.dart';
//import 'package:lunasea/modules/Lidarr.dart';
class LidarrWebhooks extends LunaWebhooks {
@override
Future<void> handle(Map<dynamic, dynamic> data) async {
_EventType event = _EventType.GRAB.fromKey(data['event']);
if(event == null) LunaLogger().warning(
'LidarrWebhooks',
'handle',
'Unknown event type: ${data['event'] ?? 'null'}',
);
event?.execute(data);
}
}
enum _EventType {
DOWNLOAD,
GRAB,
RENAME,
RETAG,
TEST,
}
extension _EventTypeExtension on _EventType {
_EventType fromKey(String key) {
switch(key) {
case 'Download': return _EventType.DOWNLOAD;
case 'Grab': return _EventType.GRAB;
case 'Rename': return _EventType.RENAME;
case 'Retag': return _EventType.RETAG;
case 'Test': return _EventType.TEST;
}
return null;
}
Future<void> execute(Map<dynamic, dynamic> data) async {
switch(this) {
case _EventType.DOWNLOAD: return _downloadEvent(data);
case _EventType.GRAB: return _grabEvent(data);
case _EventType.RENAME: return _renameEvent(data);
case _EventType.RETAG: return _retagEvent(data);
case _EventType.TEST: return _testEvent(data);
}
}
Future<void> _downloadEvent(Map<dynamic, dynamic> data) async {
return LunaModule.LIDARR.launch();
}
Future<void> _grabEvent(Map<dynamic, dynamic> data) async {
return LunaModule.LIDARR.launch();
}
Future<void> _renameEvent(Map<dynamic, dynamic> data) async {
return LunaModule.LIDARR.launch();
}
Future<void> _retagEvent(Map<dynamic, dynamic> data) async {
return LunaModule.LIDARR.launch();
}
Future<void> _testEvent(Map<dynamic, dynamic> data) async {
return LunaModule.LIDARR.launch();
}
}

View File

@@ -180,7 +180,7 @@ class _State extends State<LidarrAddDetails> {
return LSCardTile(
title: LSTitle(text: 'Root Folder'),
subtitle: LSSubtitle(text: _rootfolder?.path ?? 'Unknown Root Folder'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await LidarrDialogs.editRootFolder(context, _rootFolders);
if(_values[0]) LidarrDatabaseValue.ADD_ROOT_FOLDER.put(_values[1]);
@@ -195,7 +195,7 @@ class _State extends State<LidarrAddDetails> {
return LSCardTile(
title: LSTitle(text: 'Quality Profile'),
subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await LidarrDialogs.editQualityProfile(context, _qualityProfiles);
if(_values[0]) LidarrDatabaseValue.ADD_QUALITY_PROFILE.put(_values[1]);
@@ -210,7 +210,7 @@ class _State extends State<LidarrAddDetails> {
return LSCardTile(
title: LSTitle(text: 'Metadata Profile'),
subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await LidarrDialogs.editMetadataProfile(context, _metadataProfiles);
if(_values[0]) LidarrDatabaseValue.ADD_METADATA_PROFILE.put(_values[1]);

View File

@@ -73,7 +73,8 @@ class _State extends State<LidarrDetailsArtist> {
? null
: <Widget>[
LidarrDetailsHideButton(),
LidarrDetailsEditButton(
LidarrDetailsEditButton(data: _arguments.data),
LidarrDetailsSettingsButton(
data: _arguments.data,
remove: (bool withData) => _removeCallback(withData),
),

View File

@@ -129,19 +129,19 @@ class _State extends State<LidarrEditArtist> {
LSCardTile(
title: LSTitle(text: 'Artist Path'),
subtitle: LSSubtitle(text: _path),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () => _changePath(),
),
LSCardTile(
title: LSTitle(text: 'Quality Profile'),
subtitle: LSSubtitle(text: _qualityProfile.name),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () => _changeProfile(),
),
LSCardTile(
title: LSTitle(text: 'Metadata Profile'),
subtitle: LSSubtitle(text: _metadataProfile.name),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () => _changeMetadata(),
),
LSButton(

View File

@@ -6,6 +6,7 @@ export 'widgets/catalogue_sorting_button.dart';
export 'widgets/catalogue_hide_button.dart';
export 'widgets/details_edit_button.dart';
export 'widgets/details_hide_button.dart';
export 'widgets/details_settings_button.dart';
export 'widgets/details_album_list.dart';
export 'widgets/details_album_tile.dart';
export 'widgets/details_album_track_tile.dart';

View File

@@ -22,7 +22,7 @@ class LidarrAddSearchResultTile extends StatelessWidget {
padContent: true,
trailing: alreadyAdded
? null
: LSIconButton(icon: Icons.arrow_forward_ios),
: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: alreadyAdded
? () => _showAlreadyAddedMessage(context)
: () async => _enterDetails(context),

View File

@@ -4,11 +4,9 @@ import 'package:lunasea/modules/lidarr.dart';
class LidarrDetailsEditButton extends StatefulWidget {
final LidarrCatalogueData data;
final Function(bool) remove;
LidarrDetailsEditButton({
@required this.data,
@required this.remove,
});
@override
@@ -20,20 +18,10 @@ class _State extends State<LidarrDetailsEditButton> {
Widget build(BuildContext context) => Consumer<LidarrState>(
builder: (context, model, widget) => LSIconButton(
icon: Icons.edit,
onPressed: () async => _handlePopup(context),
onPressed: () async => _enterEditArtist(context),
),
);
Future<void> _handlePopup(BuildContext context) async {
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;
case 'remove_artist': _removeArtist(context); break;
default: LunaLogger().warning('LidarrDetailsEditButton', '_handlePopup', 'Invalid method passed through popup. (${values[1]})');
}
}
Future<void> _enterEditArtist(BuildContext context) async {
final dynamic result = await Navigator.of(context).pushNamed(
LidarrEditArtist.ROUTE_NAME,
@@ -46,30 +34,4 @@ class _State extends State<LidarrDetailsEditButton> {
type: SNACKBAR_TYPE.success,
);
}
Future<void> _refreshArtist(BuildContext context) async {
final _api = LidarrAPI.from(Database.currentProfileObject);
await _api.refreshArtist(widget.data.artistID)
.then((_) => LSSnackBar(context: context, title: 'Refreshing...', message: widget.data.title))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Refresh', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
}
Future<void> _removeArtist(BuildContext context) async {
final _api = LidarrAPI.from(Database.currentProfileObject);
List values = await LidarrDialogs.deleteArtist(context);
if(values[0]) {
if(values[1]) {
values = await LunaDialogs().deleteCatalogueWithFiles(context, widget.data.title);
if(values[0]) {
await _api.removeArtist(widget.data.artistID, deleteFiles: true)
.then((_) => widget.remove(true))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove (With Data)', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
}
} else {
await _api.removeArtist(widget.data.artistID)
.then((_) => widget.remove(false))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
}
}
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/lidarr.dart';
class LidarrDetailsSettingsButton extends StatefulWidget {
final LidarrCatalogueData data;
final Function(bool) remove;
LidarrDetailsSettingsButton({
@required this.data,
@required this.remove,
});
@override
State<LidarrDetailsSettingsButton> createState() => _State();
}
class _State extends State<LidarrDetailsSettingsButton> {
@override
Widget build(BuildContext context) => Consumer<LidarrState>(
builder: (context, model, widget) => LSIconButton(
icon: Icons.more_vert,
onPressed: () async => _handlePopup(context),
),
);
Future<void> _handlePopup(BuildContext context) async {
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;
case 'remove_artist': _removeArtist(context); break;
default: LunaLogger().warning('LidarrDetailsSettingsButton', '_handlePopup', 'Invalid method passed through popup. (${values[1]})');
}
}
Future<void> _enterEditArtist(BuildContext context) async {
final dynamic result = await Navigator.of(context).pushNamed(
LidarrEditArtist.ROUTE_NAME,
arguments: LidarrEditArtistArguments(entry: widget.data),
);
if(result != null && result[0]) LSSnackBar(
context: context,
title: 'Updated',
message: widget.data.title,
type: SNACKBAR_TYPE.success,
);
}
Future<void> _refreshArtist(BuildContext context) async {
final _api = LidarrAPI.from(Database.currentProfileObject);
await _api.refreshArtist(widget.data.artistID)
.then((_) => LSSnackBar(context: context, title: 'Refreshing...', message: widget.data.title))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Refresh', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
}
Future<void> _removeArtist(BuildContext context) async {
final _api = LidarrAPI.from(Database.currentProfileObject);
List values = await LidarrDialogs.deleteArtist(context);
if(values[0]) {
if(values[1]) {
values = await LunaDialogs().deleteCatalogueWithFiles(context, widget.data.title);
if(values[0]) {
await _api.removeArtist(widget.data.artistID, deleteFiles: true)
.then((_) => widget.remove(true))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove (With Data)', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
}
} else {
await _api.removeArtist(widget.data.artistID)
.then((_) => widget.remove(false))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure));
}
}
}
}

View File

@@ -31,7 +31,7 @@ class _State extends State<LidarrHistoryTile> {
),
),
trailing: LSIconButton(
icon: Icons.arrow_forward_ios,
icon: Icons.arrow_forward_ios_rounded,
),
padContent: true,
onTap: () async => _enterArtist(),

View File

@@ -13,7 +13,7 @@ class NZBGetLogTile extends StatelessWidget {
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: data.text),
subtitle: LSSubtitle(text: data.timestamp),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => LunaDialogs().textPreview(context, 'Log Entry', data.text),
);
}

View File

@@ -5,3 +5,4 @@ export 'core/extensions.dart';
export 'core/router.dart';
export 'core/state.dart';
export 'core/types.dart';
export 'core/webhooks.dart';

View File

@@ -0,0 +1,78 @@
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/radarr.dart';
class RadarrWebhooks extends LunaWebhooks {
@override
Future<void> handle(Map<dynamic, dynamic> data) async {
_EventType event = _EventType.GRAB.fromKey(data['event']);
if(event == null) LunaLogger().warning(
'RadarrWebhooks',
'handle',
'Unknown event type: ${data['event'] ?? 'null'}',
);
event?.execute(data);
}
}
enum _EventType {
DOWNLOAD,
GRAB,
HEALTH,
RENAME,
TEST,
}
extension _EventTypeExtension on _EventType {
_EventType fromKey(String key) {
switch(key) {
case 'Download': return _EventType.DOWNLOAD;
case 'Grab': return _EventType.GRAB;
case 'Health': return _EventType.HEALTH;
case 'Rename': return _EventType.RENAME;
case 'Test': return _EventType.TEST;
}
return null;
}
Future<void> execute(Map<dynamic, dynamic> data) async {
switch(this) {
case _EventType.GRAB: return _grabEvent(data);
case _EventType.DOWNLOAD: return _downloadEvent(data);
case _EventType.HEALTH: return _healthEvent(data);
case _EventType.RENAME: return _renameEvent(data);
case _EventType.TEST: return _testEvent(data);
}
}
Future<void> _downloadEvent(Map<dynamic, dynamic> data) async {
int movieId = int.tryParse(data['id']);
if(movieId != null) return RadarrMoviesDetailsRouter().navigateTo(
LunaState.navigatorKey.currentContext,
movieId: movieId,
);
}
Future<void> _grabEvent(Map<dynamic, dynamic> data) async {
int movieId = int.tryParse(data['id']);
if(movieId != null) return RadarrMoviesDetailsRouter().navigateTo(
LunaState.navigatorKey.currentContext,
movieId: movieId,
);
}
Future<void> _healthEvent(Map<dynamic, dynamic> data) async {
return RadarrSystemStatusRouter().navigateTo(LunaState.navigatorKey.currentContext);
}
Future<void> _renameEvent(Map<dynamic, dynamic> data) async {
int movieId = int.tryParse(data['id']);
if(movieId != null) return RadarrMoviesDetailsRouter().navigateTo(
LunaState.navigatorKey.currentContext,
movieId: movieId,
);
}
Future<void> _testEvent(Map<dynamic, dynamic> data) async {
return LunaModule.RADARR.launch();
}
}

View File

@@ -37,7 +37,6 @@ class _State extends State<RadarrAddMovieSearchPage> with AutomaticKeepAliveClie
return LunaAppBar.empty(
child: RadarrAddMovieSearchSearchBar(
query: context.read<RadarrAddMovieState>().searchQuery,
scrollController: context.watch<RadarrState>().scrollController,
),
height: 62.0,
);

View File

@@ -3,12 +3,10 @@ import 'package:lunasea/core.dart';
import 'package:lunasea/modules/radarr.dart';
class RadarrAddMovieSearchSearchBar extends StatefulWidget implements PreferredSizeWidget {
final ScrollController scrollController;
final String query;
RadarrAddMovieSearchSearchBar({
Key key,
@required this.scrollController,
@required this.query,
}) : super(key: key);

View File

@@ -12,7 +12,7 @@ class RadarrAddMovieDetailsMinimumAvailabilityTile extends StatelessWidget {
selector: (_, state) => state.availability,
builder: (context, availability, _) => LunaText.subtitle(text: availability?.readable ?? Constants.TEXT_EMDASH),
),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, RadarrAvailability> values = await RadarrDialogs().editMinimumAvailability(context);
if(values.item1) context.read<RadarrAddMovieDetailsState>().availability = values.item2;

View File

@@ -12,7 +12,7 @@ class RadarrAddMovieDetailsQualityProfileTile extends StatelessWidget {
selector: (_, state) => state.qualityProfile,
builder: (context, profile, _) => LunaText.subtitle(text: profile?.name ?? Constants.TEXT_EMDASH),
),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<RadarrQualityProfile> qualityProfiles = await context.read<RadarrState>().qualityProfiles;
Tuple2<bool, RadarrQualityProfile> values = await RadarrDialogs().editQualityProfile(context, qualityProfiles);

View File

@@ -12,7 +12,7 @@ class RadarrAddMovieDetailsRootFolderTile extends StatelessWidget {
selector: (_, state) => state.rootFolder,
builder: (context, folder, _) => LunaText.subtitle(text: folder?.path ?? Constants.TEXT_EMDASH),
),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<RadarrRootFolder> folders = await context.read<RadarrState>().rootFolders;
Tuple2<bool, RadarrRootFolder> values = await RadarrDialogs().editRootFolder(context, folders);

View File

@@ -11,7 +11,7 @@ class RadarrAddMovieDetailsTagsTile extends StatelessWidget {
subtitle: LunaText.subtitle(text: context.watch<RadarrAddMovieDetailsState>().tags.length == 0
? LunaUI.TEXT_EMDASH
: context.watch<RadarrAddMovieDetailsState>().tags.map((e) => e.label).join(', ')),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => await RadarrDialogs().setAddTags(context),
);
}

View File

@@ -9,7 +9,7 @@ class RadarrMoviesEditPathTile extends StatelessWidget {
context: context,
title: LunaText.title(text: 'Movie Path'),
subtitle: LunaText.subtitle(text: context.watch<RadarrMoviesEditState>().path ?? Constants.TEXT_EMDASH),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(context, 'Movie Path', prefill: context.read<RadarrMoviesEditState>().path);
if(_values.item1) context.read<RadarrMoviesEditState>().path = _values.item2;

View File

@@ -16,7 +16,7 @@ class RadarrMoviesEditQualityProfileTile extends StatelessWidget {
context: context,
title: LunaText.title(text: 'Quality Profile'),
subtitle: LunaText.subtitle(text: context.watch<RadarrMoviesEditState>().qualityProfile?.name ?? Constants.TEXT_EMDASH),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, RadarrQualityProfile> values = await RadarrDialogs().editQualityProfile(context, profiles);
if(values.item1) context.read<RadarrMoviesEditState>().qualityProfile = values.item2;

View File

@@ -9,7 +9,7 @@ class RadarrMoviesEditMinimumAvailabilityTile extends StatelessWidget {
context: context,
title: LunaText.title(text: 'Minimum Availability'),
subtitle: LunaText.subtitle(text: context.watch<RadarrMoviesEditState>().availability.readable),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, RadarrAvailability> _values = await RadarrDialogs().editMinimumAvailability(context);
if(_values.item1) context.read<RadarrMoviesEditState>().availability = _values.item2;

View File

@@ -11,7 +11,7 @@ class RadarrMoviesEditTagsTile extends StatelessWidget {
subtitle: LunaText.subtitle(text: context.watch<RadarrMoviesEditState>().tags.length == 0
? 'Not Set'
: context.watch<RadarrMoviesEditState>().tags.map((e) => e.label).join(', ')),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => await RadarrDialogs().setEditTags(context),
);
}

View File

@@ -39,6 +39,7 @@ class _State extends State<_RadarrMoviesDetailsRoute> with LunaLoadCallbackMixin
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
RadarrMovie movie;
PageController _pageController;
bool _loaded = false;
@override
Future<void> loadCallback() async {
@@ -62,7 +63,10 @@ class _State extends State<_RadarrMoviesDetailsRoute> with LunaLoadCallbackMixin
(movie) => movie.id == widget.movieId,
orElse: () => null,
);
if(mounted) setState(() => movie = _movie);
if(mounted) setState(() {
movie = _movie;
_loaded = true;
});
}
List<RadarrTag> _findTags(List<int> tagIds, List<RadarrTag> tags) {
@@ -82,13 +86,19 @@ class _State extends State<_RadarrMoviesDetailsRoute> with LunaLoadCallbackMixin
return Scaffold(
key: _scaffoldKey,
appBar: _appBar(),
bottomNavigationBar: _bottomNavigationBar(),
body: _body(),
bottomNavigationBar: context.watch<RadarrState>().enabled ? _bottomNavigationBar() : null,
body: context.watch<RadarrState>().enabled ? _body() : LunaMessage.moduleNotEnabled(context: context, module: 'Radarr'),
);
}
Widget _appBar() {
List<Widget> _actions = movie == null ? null : [RadarrAppBarMovieSettingsAction(movieId: widget.movieId)];
List<Widget> _actions = movie == null ? null : [
LunaIconButton(
icon: Icons.edit,
onPressed: () async => RadarrMoviesEditRouter().navigateTo(context, movieId: widget.movieId),
),
RadarrAppBarMovieSettingsAction(movieId: widget.movieId),
];
return LunaAppBar(
pageController: _pageController,
scrollControllers: RadarrMovieDetailsNavigationBar.scrollControllers,
@@ -107,6 +117,10 @@ class _State extends State<_RadarrMoviesDetailsRoute> with LunaLoadCallbackMixin
builder: (context, state, _) => FutureBuilder(
future: Future.wait([state.qualityProfiles, state.tags]),
builder: (context, AsyncSnapshot<List<Object>> snapshot) {
if(_loaded && movie == null) return LunaMessage.goBack(
text: 'Movie Not Found',
context: context,
);
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error(
'Unable to pull Radarr movie details',

View File

@@ -20,7 +20,7 @@ class RadarrAppBarMovieSettingsAction extends StatelessWidget {
if(snapshot.hasData) {
RadarrMovie movie = snapshot.data.firstWhere((element) => element.id == movieId, orElse: () => null);
if(movie != null) return LunaIconButton(
icon: Icons.edit,
icon: Icons.more_vert,
onPressed: () async {
Tuple2<bool, RadarrMovieSettingsType> values = await RadarrDialogs().movieSettings(context, movie);
if(values.item1) values.item2.execute(context, movie);

View File

@@ -57,6 +57,19 @@ class _State extends State<RadarrMovieDetailsFilesFileBlock> {
context: context,
builder: (context) => LunaListViewModal(
children: [
LunaHeader(text: 'Video'),
LunaTableCard(
content: [
LunaTableContent(title: 'bit depth', body: widget.movieFile.mediaInfo.videoBitDepth?.toString() ?? Constants.TEXT_EMDASH),
LunaTableContent(title: 'bitrate', body: [
widget.movieFile.mediaInfo.videoBitrate?.lunaBytesToString(bytes: false) ?? Constants.TEXT_EMDASH,
if(widget.movieFile.mediaInfo.videoBitrate != null) '/s',
].join()),
LunaTableContent(title: 'codec', body: widget.movieFile.mediaInfo.videoCodec ?? Constants.TEXT_EMDASH),
LunaTableContent(title: 'fps', body: widget.movieFile.mediaInfo.videoFps?.toString() ?? Constants.TEXT_EMDASH),
LunaTableContent(title: 'resolution', body: widget.movieFile.mediaInfo.resolution ?? Constants.TEXT_EMDASH),
],
),
LunaHeader(text: 'Audio'),
LunaTableCard(
content: [
@@ -71,19 +84,6 @@ class _State extends State<RadarrMovieDetailsFilesFileBlock> {
LunaTableContent(title: 'streams', body: widget.movieFile.mediaInfo.audioStreamCount?.toString() ?? Constants.TEXT_EMDASH),
],
),
LunaHeader(text: 'Video'),
LunaTableCard(
content: [
LunaTableContent(title: 'bit depth', body: widget.movieFile.mediaInfo.videoBitDepth?.toString() ?? Constants.TEXT_EMDASH),
LunaTableContent(title: 'bitrate', body: [
widget.movieFile.mediaInfo.videoBitrate?.lunaBytesToString(bytes: false) ?? Constants.TEXT_EMDASH,
if(widget.movieFile.mediaInfo.videoBitrate != null) '/s',
].join()),
LunaTableContent(title: 'codec', body: widget.movieFile.mediaInfo.videoCodec ?? Constants.TEXT_EMDASH),
LunaTableContent(title: 'fps', body: widget.movieFile.mediaInfo.videoFps?.toString() ?? Constants.TEXT_EMDASH),
LunaTableContent(title: 'resolution', body: widget.movieFile.mediaInfo.resolution ?? Constants.TEXT_EMDASH),
],
),
LunaHeader(text: 'Other'),
LunaTableCard(
content: [

View File

@@ -56,7 +56,7 @@ class _State extends State<_RadarrReleasesRoute> with LunaScrollControllerMixin
return LunaAppBar(
title: 'Releases',
scrollControllers: [scrollController],
bottom: RadarrReleasesSearchBar(scrollController: context.read<RadarrState>().scrollController),
bottom: RadarrReleasesSearchBar(scrollController: scrollController),
);
}

View File

@@ -30,8 +30,8 @@ class _State extends State<_RadarrSystemStatusRoute> {
return Scaffold(
key: _scaffoldKey,
appBar: _appBar(),
bottomNavigationBar: _bottomNavigationBar(),
body: _body(),
bottomNavigationBar: context.watch<RadarrState>().enabled ? _bottomNavigationBar() : null,
body: context.watch<RadarrState>().enabled ? _body() : LunaMessage.moduleNotEnabled(context: context, module: 'Radarr'),
);
}

View File

@@ -47,7 +47,7 @@ class _State extends State<SABnzbdHistoryStages> {
(index) => LSCardTile(
title: LSTitle(text: _arguments.data.stageLog[index]['name']),
subtitle: LSSubtitle(text: _arguments.data.stageLog[index]['actions'][0].replaceAll('<br/>', '.\n')),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
String _data = _arguments.data.stageLog[index]['actions'].join(',\n').replaceAll('<br/>', '.\n');
LunaDialogs().textPreview(context, _arguments.data.stageLog[index]['name'], _data);

View File

@@ -16,7 +16,7 @@ class SearchCategoryTile extends StatelessWidget {
title: LSTitle(text: category.name),
subtitle: LSSubtitle(text: category.subcategories.length == 0 ? 'No Subcategories Available': category.subcategoriesList),
leading: LSIconButton(icon: category.icon, color: LunaColours.list(index)),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _enterSubcategories(context),
);

View File

@@ -15,7 +15,7 @@ class SearchIndexerTile extends StatelessWidget {
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: indexer.displayName),
subtitle: LSSubtitle(text: indexer.host),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
leading: LSIconButton(icon: Icons.rss_feed, color: LunaColours.list(index)),
onTap: () async => _enterIndexer(context),
);

View File

@@ -21,7 +21,7 @@ class SearchSubcategoryTile extends StatelessWidget {
Widget _card(BuildContext context) => LSCardTile(
title: LSTitle(text: category?.subcategories[index ?? 0]?.name ?? 'Unknown'),
subtitle: LSSubtitle(text: '${category?.name ?? 'Unknown'} > ${category?.subcategories[index ?? 0]?.name ?? 'Unknown'}'),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
leading: LSIconButton(icon: category?.icon, color: LunaColours.list(index+1)),
onTap: () => _enterResults(
context,
@@ -34,7 +34,7 @@ class SearchSubcategoryTile extends StatelessWidget {
title: LSTitle(text: 'All Subcategories'),
subtitle: LSSubtitle(text: '${category?.name} > All'),
leading: LSIconButton(icon: category?.icon, color: LunaColours.list(0)),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _enterResults(
context,
category?.id ?? 0,

View File

@@ -84,7 +84,7 @@ class _State extends State<_SettingsConfigurationAppearanceRoute> with LunaScrol
? 'Disabled'
: '${LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY.data}%'
),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, int> result = await SettingsDialogs().changeBackgroundImageOpacity(context);
if(result.item1) LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY.put(result.item2);

View File

@@ -62,7 +62,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
? '1 Day'
: '${DashboardDatabaseValue.CALENDAR_DAYS_PAST.data} Days',
),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await DashboardDialogs.setPastDays(context);
if(_values[0]) DashboardDatabaseValue.CALENDAR_DAYS_PAST.put(_values[1]);
@@ -80,7 +80,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
? '1 Day'
: '${DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.data} Days',
),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await DashboardDialogs.setFutureDays(context);
if(_values[0]) DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.put(_values[1]);
@@ -137,7 +137,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Starting Type'),
subtitle: LunaText.subtitle(text: (DashboardDatabaseValue.CALENDAR_STARTING_TYPE.data as CalendarStartingType).name),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await SettingsDialogs.editCalendarStartingType(context);
if(_values[0]) DashboardDatabaseValue.CALENDAR_STARTING_TYPE.put(_values[1]);
@@ -152,7 +152,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Starting Day'),
subtitle: LunaText.subtitle(text: (DashboardDatabaseValue.CALENDAR_STARTING_DAY.data as CalendarStartingDay).name),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await SettingsDialogs.editCalendarStartingDay(context);
if(_values[0]) DashboardDatabaseValue.CALENDAR_STARTING_DAY.put(_values[1]);
@@ -167,7 +167,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Starting Size'),
subtitle: LunaText.subtitle(text: (DashboardDatabaseValue.CALENDAR_STARTING_SIZE.data as CalendarStartingSize).name),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List _values = await SettingsDialogs.editCalendarStartingSize(context);
if(_values[0]) DashboardDatabaseValue.CALENDAR_STARTING_SIZE.put(_values[1]);

View File

@@ -66,7 +66,7 @@ class _State extends State<_SettingsConfigurationHomeRoute> with LunaScrollContr
context: context,
title: LunaText.title(text: 'Calendar Settings'),
subtitle: LunaText.subtitle(text: 'Customize the Unified Calendar'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationDashboardCalendarSettingsRouter().navigateTo(context),
);
}

View File

@@ -56,7 +56,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Host'),
subtitle: LunaText.subtitle(text: (host ?? '').isEmpty ? 'Not Set' : host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editHost(context, 'Lidarr Host', prefill: Database.currentProfileObject.lidarrHost ?? '');
if(_values[0]) {
@@ -74,7 +74,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'API Key'),
subtitle: LunaText.subtitle(text: (apiKey ?? '').isEmpty ? 'Not Set' : '••••••••••••'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(context, 'Lidarr API Key', prefill: Database.currentProfileObject.lidarrKey ?? '');
if(_values.item1) {
@@ -133,7 +133,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationLidarrHeadersRouter().navigateTo(context),
);
}

View File

@@ -77,7 +77,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Connection Details'),
subtitle: LunaText.subtitle(text: 'Connection Details for Lidarr'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationLidarrConnectionDetailsRouter().navigateTo(context),
);
}

View File

@@ -57,7 +57,7 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Host'),
subtitle: LunaText.subtitle(text: (host ?? '').isEmpty ? 'Not Set' : host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editHost(context, 'NZBGet Host', prefill: Database.currentProfileObject.nzbgetHost ?? '');
if(_values[0]) {
@@ -75,7 +75,7 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Username'),
subtitle: LunaText.subtitle(text: (username ?? '').isEmpty ? 'Not Set' : username),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(context, 'NZBGet Username', prefill: Database.currentProfileObject.nzbgetUser ?? '');
if(_values.item1) {
@@ -93,7 +93,7 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Password'),
subtitle: LunaText.subtitle(text: (password ?? '').isEmpty ? 'Not Set' : '••••••••••••'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(
context,
@@ -153,7 +153,7 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationNZBGetHeadersRouter().navigateTo(context),
);
}

View File

@@ -77,7 +77,7 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Connection Details'),
subtitle: LunaText.subtitle(text: 'Connection Details for NZBGet'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationNZBGetConnectionDetailsRouter().navigateTo(context),
);
}

View File

@@ -56,7 +56,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Host'),
subtitle: LunaText.subtitle(text: (host ?? '').isEmpty ? 'Not Set' : host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editHost(context, 'Radarr Host', prefill: Database.currentProfileObject.radarrHost ?? '');
if(_values[0]) {
@@ -74,7 +74,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'API Key'),
subtitle: LunaText.subtitle(text: (apiKey ?? '').isEmpty ? 'Not Set' : '••••••••••••'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(context, 'Radarr API Key', prefill: Database.currentProfileObject.radarrKey ?? '');
if(_values.item1) {
@@ -133,7 +133,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationRadarrHeadersRouter().navigateTo(context),
);
}

View File

@@ -55,7 +55,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Movies Sort Category'),
subtitle: LunaText.subtitle(text: (RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.data as RadarrMoviesSorting).readable),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<String> titles = RadarrMoviesSorting.values.map<String>((e) => e.readable).toList();
Tuple2<bool, int> values = await RadarrDialogs().setDefaultSortingOrFiltering(context, titles: titles);
@@ -93,7 +93,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Movies Filter Category'),
subtitle: LunaText.subtitle(text: (RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES.data as RadarrMoviesFilter).readable),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<String> titles = RadarrMoviesFilter.values.map<String>((e) => e.readable).toList();
Tuple2<bool, int> values = await RadarrDialogs().setDefaultSortingOrFiltering(context, titles: titles);
@@ -112,7 +112,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Releases Sort Category'),
subtitle: LunaText.subtitle(text: (RadarrDatabaseValue.DEFAULT_SORTING_RELEASES.data as RadarrReleasesSorting).readable),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<String> titles = RadarrReleasesSorting.values.map<String>((e) => e.readable).toList();
Tuple2<bool, int> values = await RadarrDialogs().setDefaultSortingOrFiltering(context, titles: titles);
@@ -146,7 +146,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Releases Filter Category'),
subtitle: LunaText.subtitle(text: (RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES.data as RadarrReleasesFilter).readable),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<String> titles = RadarrReleasesFilter.values.map<String>((e) => e.readable).toList();
Tuple2<bool, int> values = await RadarrDialogs().setDefaultSortingOrFiltering(context, titles: titles);

View File

@@ -78,7 +78,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Connection Details'),
subtitle: LunaText.subtitle(text: 'Connection Details for Radarr'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationRadarrConnectionDetailsRouter().navigateTo(context),
);
}
@@ -88,7 +88,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Default Pages'),
subtitle: LunaText.subtitle(text: 'Set Default Landing Pages'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationRadarrDefaultPagesRouter().navigateTo(context),
);
}
@@ -98,7 +98,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Default Sorting & Filtering'),
subtitle: LunaText.subtitle(text: 'Set Default Sorting & Filtering Methods'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationRadarrDefaultSortingRouter().navigateTo(context),
);
}

View File

@@ -56,7 +56,7 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> with LunaScrollCo
context: context,
title: LunaText.title(text: 'Host'),
subtitle: LunaText.subtitle(text: (host ?? '').isEmpty ? 'Not Set' : host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editHost(context, 'SABnzbd Host', prefill: Database.currentProfileObject.sabnzbdHost ?? '');
if(_values[0]) {
@@ -74,7 +74,7 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> with LunaScrollCo
context: context,
title: LunaText.title(text: 'API Key'),
subtitle: LunaText.subtitle(text: (apiKey ?? '').isEmpty ? 'Not Set' : '••••••••••••'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(context, 'SABnzbd API Key', prefill: Database.currentProfileObject.sabnzbdKey ?? '');
if(_values.item1) {
@@ -133,7 +133,7 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> with LunaScrollCo
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSABnzbdHeadersRouter().navigateTo(context),
);
}

View File

@@ -77,7 +77,7 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> with LunaScrollCo
context: context,
title: LunaText.title(text: 'Connection Details'),
subtitle: LunaText.subtitle(text: 'Connection Details for SABnzbd'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSABnzbdConnectionDetailsRouter().navigateTo(context),
);
}

View File

@@ -52,7 +52,7 @@ class _State extends State<_SettingsConfigurationSearchAddRoute> with LunaScroll
context: context,
title: LunaText.title(text: 'Display Name'),
subtitle: LunaText.subtitle(text: _indexer.displayName == null || _indexer.displayName.isEmpty ? 'Not Set' : _indexer.displayName),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> values = await LunaDialogs().editText(context, 'Display Name', prefill: _indexer.displayName);
if(values.item1 && mounted) setState(() => _indexer.displayName = values.item2);
@@ -65,7 +65,7 @@ class _State extends State<_SettingsConfigurationSearchAddRoute> with LunaScroll
context: context,
title: LunaText.title(text: 'Indexer API Host'),
subtitle: LunaText.subtitle(text: _indexer.host == null || _indexer.host.isEmpty ? 'Not Set' : _indexer.host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> values = await LunaDialogs().editText(context, 'Indexer API Host', prefill: _indexer.host);
if(values.item1 && mounted) setState(() => _indexer.host = values.item2);
@@ -78,7 +78,7 @@ class _State extends State<_SettingsConfigurationSearchAddRoute> with LunaScroll
context: context,
title: LunaText.title(text: 'Indexer API Key'),
subtitle: LunaText.subtitle(text: _indexer.apiKey == null || _indexer.apiKey.isEmpty ? 'Not Set' : _indexer.apiKey),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> values = await LunaDialogs().editText(context, 'Indexer API Key', prefill: _indexer.apiKey);
if(values.item1 && mounted) setState(() => _indexer.apiKey = values.item2);
@@ -91,7 +91,7 @@ class _State extends State<_SettingsConfigurationSearchAddRoute> with LunaScroll
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSearchAddHeadersRouter().navigateTo(context, indexer: _indexer),
);
}

View File

@@ -82,7 +82,7 @@ class _State extends State<_SettingsConfigurationSearchEditRoute> with LunaScrol
context: context,
title: LunaText.title(text: 'Display Name'),
subtitle: LunaText.subtitle(text: _indexer.displayName == null || _indexer.displayName.isEmpty ? 'Not Set' : _indexer.displayName),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> values = await LunaDialogs().editText(context, 'Display Name', prefill: _indexer.displayName);
if(values.item1) _indexer.displayName = values.item2;
@@ -96,7 +96,7 @@ class _State extends State<_SettingsConfigurationSearchEditRoute> with LunaScrol
context: context,
title: LunaText.title(text: 'Indexer API Host'),
subtitle: LunaText.subtitle(text: _indexer.host == null || _indexer.host.isEmpty ? 'Not Set' : _indexer.host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> values = await LunaDialogs().editText(context, 'Indexer API Host', prefill: _indexer.host);
if(values.item1) _indexer.host = values.item2;
@@ -110,7 +110,7 @@ class _State extends State<_SettingsConfigurationSearchEditRoute> with LunaScrol
context: context,
title: LunaText.title(text: 'Indexer API Key'),
subtitle: LunaText.subtitle(text: _indexer.apiKey == null || _indexer.apiKey.isEmpty ? 'Not Set' : _indexer.apiKey),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> values = await LunaDialogs().editText(context, 'Indexer API Key', prefill: _indexer.apiKey);
if(values.item1) _indexer.apiKey = values.item2;
@@ -124,7 +124,7 @@ class _State extends State<_SettingsConfigurationSearchEditRoute> with LunaScrol
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSearchEditHeadersRouter().navigateTo(context, indexerId: widget.indexerId),
);
}

View File

@@ -78,7 +78,7 @@ class _State extends State<_SettingsConfigurationSearchRoute> with LunaScrollCon
Widget _indexerTile(IndexerHiveObject indexer, int index) => LSCardTile(
title: LSTitle(text: indexer.displayName),
subtitle: LSSubtitle(text: indexer.host),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSearchEditRouter().navigateTo(
context,
indexerId: index,

View File

@@ -73,7 +73,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Host'),
subtitle: LunaText.subtitle(text: (host ?? '').isEmpty ? 'Not Set' : host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editHost(context, 'Sonarr Host', prefill: Database.currentProfileObject.sonarrHost ?? '');
if(_values[0]) {
@@ -91,7 +91,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'API Key'),
subtitle: LunaText.subtitle(text: (apiKey ?? '').isEmpty ? 'Not Set' : '••••••••••••'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(context, 'Sonarr API Key', prefill: Database.currentProfileObject.sonarrKey ?? '');
if(_values.item1) {
@@ -150,7 +150,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSonarrHeadersRouter().navigateTo(context),
);
}

View File

@@ -53,7 +53,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Series Sort Category'),
subtitle: LunaText.subtitle(text: (SonarrDatabaseValue.DEFAULT_SORTING_SERIES.data as SonarrSeriesSorting).readable),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<String> titles = SonarrSeriesSorting.values.map<String>((e) => e.readable).toList();
List values = await SonarrDialogs.setDefaultSortingOrFiltering(context, titles: titles);
@@ -91,7 +91,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Releases Sort Category'),
subtitle: LunaText.subtitle(text: (SonarrDatabaseValue.DEFAULT_SORTING_RELEASES.data as SonarrReleasesSorting).readable),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<String> titles = SonarrReleasesSorting.values.map<String>((e) => e.readable).toList();
List values = await SonarrDialogs.setDefaultSortingOrFiltering(context, titles: titles);

View File

@@ -77,7 +77,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Connection Details'),
subtitle: LunaText.subtitle(text: 'Connection Details for Sonarr'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSonarrConnectionDetailsRouter().navigateTo(context),
);
}
@@ -87,7 +87,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Default Pages'),
subtitle: LunaText.subtitle(text: 'Set Default Landing Pages'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSonarrDefaultPagesRouter().navigateTo(context),
);
}
@@ -97,7 +97,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> with LunaScrollCon
context: context,
title: LunaText.title(text: 'Default Sorting & Filtering'),
subtitle: LunaText.subtitle(text: 'Set Default Sorting & Filtering Methods'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationSonarrDefaultSortingRouter().navigateTo(context),
);
}

View File

@@ -56,7 +56,7 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC
context: context,
title: LunaText.title(text: 'Host'),
subtitle: LunaText.subtitle(text: (host ?? '').isEmpty ? 'Not Set' : host),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editHost(context, 'Tautulli Host', prefill: Database.currentProfileObject.tautulliHost ?? '');
if(_values[0]) {
@@ -74,7 +74,7 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC
context: context,
title: LunaText.title(text: 'API Key'),
subtitle: LunaText.subtitle(text: (apiKey ?? '').isEmpty ? 'Not Set' : '••••••••••••'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
Tuple2<bool, String> _values = await LunaDialogs().editText(context, 'Tautulli API Key', prefill: Database.currentProfileObject.tautulliKey ?? '');
if(_values.item1) {
@@ -133,7 +133,7 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC
context: context,
title: LunaText.title(text: 'Custom Headers'),
subtitle: LunaText.subtitle(text: 'Add Custom Headers to Requests'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationTautulliHeadersRouter().navigateTo(context),
);
}

View File

@@ -79,7 +79,7 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC
context: context,
title: LunaText.title(text: 'Connection Details'),
subtitle: LunaText.subtitle(text: 'Connection Details for Tautulli'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationTautulliConnectionDetailsRouter().navigateTo(context),
);
}
@@ -89,7 +89,7 @@ class _State extends State<_SettingsConfigurationTautulliRoute> with LunaScrollC
context: context,
title: LunaText.title(text: 'Default Pages'),
subtitle: LunaText.subtitle(text: 'Set Default Landing Pages'),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => SettingsConfigurationTautulliDefaultPagesRouter().navigateTo(context),
);
}

View File

@@ -73,7 +73,7 @@ class _State extends State<_SettingsConfigurationWakeOnLANRoute> with LunaScroll
context: context,
title: LunaText.title(text: 'Broadcast Address'),
subtitle: LunaText.subtitle(text: broadcastAddress == null || broadcastAddress == '' ? 'Not Set' : broadcastAddress),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editBroadcastAddress(context, broadcastAddress ?? '');
if(_values[0]) {
@@ -90,7 +90,7 @@ class _State extends State<_SettingsConfigurationWakeOnLANRoute> with LunaScroll
context: context,
title: LunaText.title(text: 'Device MAC Address'),
subtitle: LunaText.subtitle(text: macAddress == null || macAddress == '' ? 'Not Set' : macAddress),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios),
trailing: LunaIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async {
List<dynamic> _values = await SettingsDialogs.editMACAddress(context, macAddress ?? '');
if(_values[0]) {

View File

@@ -5,3 +5,4 @@ export 'core/extensions.dart';
export 'core/router.dart';
export 'core/state.dart';
export 'core/types.dart';
export 'core/webhooks.dart';

View File

@@ -5,16 +5,16 @@ import 'package:lunasea/modules/sonarr.dart';
class SonarrRouter extends LunaModuleRouter {
@override
void defineAllRoutes(FluroRouter router) {
SonarrHomeRouter.defineRoutes(router);
SonarrHomeRouter().defineRoute(router);
// Series
SonarrSeriesAddRouter.defineRoutes(router);
SonarrSeriesAddDetailsRouter.defineRoutes(router);
SonarrSeriesEditRouter.defineRoutes(router);
SonarrSeriesDetailsRouter.defineRoutes(router);
SonarrSeriesSeasonDetailsRouter.defineRoutes(router);
SonarrSeriesDetailsRouter().defineRoute(router);
SonarrSeasonDetailsRouter().defineRoute(router);
// Other
SonarrQueueRouter.defineRoutes(router);
SonarrQueueRouter().defineRoute(router);
SonarrReleasesRouter.defineRoutes(router);
SonarrTagsRouter.defineRoutes(router);
SonarrTagsRouter().defineRoute(router);
}
}

View File

@@ -280,6 +280,17 @@ class SonarrState extends LunaModuleState {
notifyListeners();
}
Future<void> resetSingleSeries(int seriesId) async {
assert(seriesId != null);
if(_api != null) {
SonarrSeries movie = await _api.series.getSeries(seriesId: seriesId);
List<SonarrSeries> allSeries = await _series;
int index = allSeries?.indexWhere((s) => s.id == seriesId) ?? -1;
if(index >= 0) allSeries[index] = movie;
}
notifyListeners();
}
///////////////
/// MISSING ///
///////////////

View File

@@ -0,0 +1,102 @@
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrWebhooks extends LunaWebhooks {
@override
Future<void> handle(Map<dynamic, dynamic> data) async {
_EventType event = _EventType.GRAB.fromKey(data['event']);
if(event == null) LunaLogger().warning(
'SonarrWebhooks',
'handle',
'Unknown event type: ${data['event'] ?? 'null'}',
);
event?.execute(data);
}
}
enum _EventType {
DOWNLOAD,
EPISODE_FILE_DELETE,
GRAB,
HEALTH,
RENAME,
SERIES_DELETE,
TEST,
}
extension _EventTypeExtension on _EventType {
_EventType fromKey(String key) {
switch(key) {
case 'Download': return _EventType.DOWNLOAD;
case 'EpisodeFileDelete': return _EventType.EPISODE_FILE_DELETE;
case 'Grab': return _EventType.GRAB;
case 'Health': return _EventType.HEALTH;
case 'Rename': return _EventType.RENAME;
case 'SeriesDelete': return _EventType.SERIES_DELETE;
case 'Test': return _EventType.TEST;
}
return null;
}
Future<void> execute(Map<dynamic, dynamic> data) async {
switch(this) {
case _EventType.DOWNLOAD: return _downloadEvent(data);
case _EventType.EPISODE_FILE_DELETE: return _episodeFileDeleteEvent(data);
case _EventType.GRAB: return _grabEvent(data);
case _EventType.HEALTH: return _healthEvent(data);
case _EventType.RENAME: return _renameEvent(data);
case _EventType.SERIES_DELETE: return _seriesDeleteEvent(data);
case _EventType.TEST: return _testEvent(data);
}
}
Future<void> _downloadEvent(Map<dynamic, dynamic> data) async {
int seriesId = int.tryParse(data['seriesId']);
int seasonNumber = int.tryParse(data['seasonNumber']);
if(seriesId != null && seasonNumber != null) SonarrSeasonDetailsRouter().navigateTo(
LunaState.navigatorKey.currentContext,
seriesId: seriesId,
seasonNumber: seasonNumber,
);
}
Future<void> _episodeFileDeleteEvent(Map<dynamic, dynamic> data) async {
int seriesId = int.tryParse(data['seriesId']);
int seasonNumber = int.tryParse(data['seasonNumber']);
if(seriesId != null && seasonNumber != null) SonarrSeasonDetailsRouter().navigateTo(
LunaState.navigatorKey.currentContext,
seriesId: seriesId,
seasonNumber: seasonNumber,
);
}
Future<void> _grabEvent(Map<dynamic, dynamic> data) async {
int seriesId = int.tryParse(data['seriesId']);
int seasonNumber = int.tryParse(data['seasonNumber']);
if(seriesId != null && seasonNumber != null) SonarrSeasonDetailsRouter().navigateTo(
LunaState.navigatorKey.currentContext,
seriesId: seriesId,
seasonNumber: seasonNumber,
);
}
Future<void> _healthEvent(Map<dynamic, dynamic> data) async {
return LunaModule.SONARR.launch();
}
Future<void> _renameEvent(Map<dynamic, dynamic> data) async {
int seriesId = int.tryParse(data['seriesId']);
if(seriesId != null) SonarrSeriesDetailsRouter().navigateTo(
LunaState.navigatorKey.currentContext,
seriesId: seriesId,
);
}
Future<void> _seriesDeleteEvent(Map<dynamic, dynamic> data) async {
return LunaModule.SONARR.launch();
}
Future<void> _testEvent(Map<dynamic, dynamic> data) async {
return LunaModule.SONARR.launch();
}
}

View File

@@ -2,12 +2,12 @@ export 'modules/history.dart';
export 'modules/missing.dart';
export 'modules/queue.dart';
export 'modules/releases.dart';
export 'modules/season_details.dart';
export 'modules/series.dart';
export 'modules/series_add.dart';
export 'modules/series_add_details.dart';
export 'modules/series_details.dart';
export 'modules/series_edit.dart';
export 'modules/series_season_details.dart';
export 'modules/sonarr.dart';
export 'modules/tags.dart';
export 'modules/upcoming.dart';

View File

@@ -102,7 +102,7 @@ class _State extends State<SonarrHistoryTile> {
);
Future<void> _onTap() async => SonarrSeriesDetailsRouter.navigateTo(
Future<void> _onTap() async => SonarrSeriesDetailsRouter().navigateTo(
context,
seriesId: widget.record.seriesId,
);

View File

@@ -137,13 +137,13 @@ class _State extends State<SonarrMissingTile> {
maxLines: 1,
);
Future<void> _tileOnTap() async => SonarrSeriesSeasonDetailsRouter.navigateTo(
Future<void> _tileOnTap() async => SonarrSeasonDetailsRouter().navigateTo(
context,
seriesId: widget.record.seriesId,
seasonNumber: widget.record.seasonNumber,
);
Future<void> _tileOnLongPress() async => SonarrSeriesDetailsRouter.navigateTo(
Future<void> _tileOnLongPress() async => SonarrSeriesDetailsRouter().navigateTo(
context,
seriesId: widget.record.seriesId,
);

View File

@@ -4,23 +4,11 @@ import 'package:flutter/scheduler.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrQueueRouter {
static const String ROUTE_NAME = '/sonarr/queue/list';
class SonarrQueueRouter extends LunaPageRouter {
SonarrQueueRouter() : super('/sonarr/queue');
static Future<void> navigateTo(BuildContext context) async => LunaRouter.router.navigateTo(
context,
route(),
);
static String route() => ROUTE_NAME;
static void defineRoutes(FluroRouter router) {
router.define(
ROUTE_NAME,
handler: Handler(handlerFunc: (context, params) => _SonarrQueueRoute()),
transitionType: LunaRouter.transitionType,
);
}
@override
void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _SonarrQueueRoute());
}
class _SonarrQueueRoute extends StatefulWidget {

View File

@@ -23,7 +23,7 @@ class SonarrReleasesRouter {
}) {
if(episodeId != null) return ROUTE_NAME+'/episode/$episodeId';
if(seriesId != null && seasonNumber != null) return ROUTE_NAME+'/series/$seriesId/season/$seasonNumber';
return SonarrHomeRouter.route();
return SonarrHomeRouter().route();
}
static void defineRoutes(FluroRouter router) {

View File

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

View File

@@ -4,41 +4,38 @@ import 'package:flutter/scheduler.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrSeriesSeasonDetailsRouter {
static const String ROUTE_NAME = '/sonarr/series/details/:seriesid/season/:seasonnumber';
class SonarrSeasonDetailsRouter extends LunaPageRouter {
SonarrSeasonDetailsRouter() : super('/sonarr/series/details/:seriesid/season/:seasonnumber');
static Future<void> navigateTo(BuildContext context, {
@override
Future<void> navigateTo(BuildContext context, {
@required int seriesId,
@required int seasonNumber,
}) async => LunaRouter.router.navigateTo(
context,
route(seriesId: seriesId, seasonNumber: seasonNumber),
}) async => LunaRouter.router.navigateTo(context, route(seriesId: seriesId, seasonNumber: seasonNumber));
@override
String route({
@required int seriesId,
@required int seasonNumber,
}) => fullRoute.replaceFirst(':seriesid', seriesId.toString()).replaceFirst(':seasonnumber', seasonNumber.toString());
@override
void defineRoute(FluroRouter router) => router.define(
fullRoute,
handler: Handler(handlerFunc: (context, params) {
int seriesId = params['seriesid'] == null || params['seriesid'].length == 0 ? -1 : (int.tryParse(params['seriesid'][0]) ?? -1);
int seasonNumber = params['seasonnumber'] == null || params['seasonnumber'].length == 0 ? -1 : (int.tryParse(params['seasonnumber'][0]) ?? -1);
return _SonarrSeasonDetailsRoute(seriesId: seriesId, seasonNumber: seasonNumber);
}),
transitionType: LunaRouter.transitionType,
);
static String route({
@required int seriesId,
@required int seasonNumber,
}) => ROUTE_NAME
.replaceFirst(':seriesid', seriesId.toString())
.replaceFirst(':seasonnumber', seasonNumber.toString());
static void defineRoutes(FluroRouter router) {
router.define(
ROUTE_NAME,
handler: Handler(handlerFunc: (context, params) => _SonarrSeriesSeasonDetailsRoute(
seriesId: int.tryParse(params['seriesid'][0]) ?? -1,
seasonNumber: int.tryParse(params['seasonnumber'][0]) ?? -1,
)),
transitionType: LunaRouter.transitionType,
);
}
}
class _SonarrSeriesSeasonDetailsRoute extends StatefulWidget {
class _SonarrSeasonDetailsRoute extends StatefulWidget {
final int seriesId;
final int seasonNumber;
_SonarrSeriesSeasonDetailsRoute({
_SonarrSeasonDetailsRoute({
Key key,
@required this.seriesId,
@required this.seasonNumber,
@@ -48,7 +45,7 @@ class _SonarrSeriesSeasonDetailsRoute extends StatefulWidget {
State<StatefulWidget> createState() => _State();
}
class _State extends State<_SonarrSeriesSeasonDetailsRoute> {
class _State extends State<_SonarrSeasonDetailsRoute> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
@@ -62,19 +59,24 @@ class _State extends State<_SonarrSeriesSeasonDetailsRoute> {
}
Future<void> _refresh() async {
if(context.read<SonarrState>().api != null)
context.read<SonarrState>().fetchEpisodes(widget.seriesId);
if(context.read<SonarrState>().episodes[widget.seriesId] != null)
await context.read<SonarrState>().episodes[widget.seriesId];
if(widget.seriesId > 0) {
if(context.read<SonarrState>().api != null)
context.read<SonarrState>().fetchEpisodes(widget.seriesId);
if(context.read<SonarrState>().episodes[widget.seriesId] != null)
await context.read<SonarrState>().episodes[widget.seriesId];
}
}
@override
Widget build(BuildContext context) => Scaffold(
key: _scaffoldKey,
appBar: _appBar,
body: _body,
floatingActionButton: _floatingActionButton,
);
Widget build(BuildContext context) {
if(widget.seriesId <= 0) return LunaInvalidRoute(title: 'Season Details', message: 'Series Not Found');
return Scaffold(
key: _scaffoldKey,
appBar: _appBar,
body: context.watch<SonarrState>().enabled ? _body : LunaMessage.moduleNotEnabled(context: context, module: 'Sonarr'),
floatingActionButton: _floatingActionButton,
);
}
Widget get _appBar => LunaAppBar(title: 'Season Details');
@@ -96,7 +98,7 @@ class _State extends State<_SonarrSeriesSeasonDetailsRoute> {
builder: (context, AsyncSnapshot<List<SonarrEpisode>> snapshot) {
if(snapshot.hasError) return LSErrorMessage(onTapHandler: () => _refresh());
if(snapshot.hasData) {
if(widget.seasonNumber == -1) return SonarrSeriesSeasonDetailsAllSeasons(
if(widget.seasonNumber == -1) return SonarrSeasonDetailsAllSeasons(
episodes: snapshot.data,
seriesId: widget.seriesId,
);
@@ -105,7 +107,7 @@ class _State extends State<_SonarrSeriesSeasonDetailsRoute> {
).toList();
if(_episodes != null && _episodes.length > 0) {
_episodes.sort((a,b) => (b.episodeNumber ?? 0).compareTo(a.episodeNumber ?? 0));
return SonarrSeriesSeasonDetailsSeason(
return SonarrSeasonDetailsSeason(
episodes: _episodes,
seriesId: widget.seriesId,
seasonNumber: widget.seasonNumber,

View File

@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrSeriesSeasonDetailsAllSeasons extends StatelessWidget {
class SonarrSeasonDetailsAllSeasons extends StatelessWidget {
final int seriesId;
final List<SonarrEpisode> episodes;
SonarrSeriesSeasonDetailsAllSeasons({
SonarrSeasonDetailsAllSeasons({
Key key,
@required this.seriesId,
@required this.episodes,
@@ -38,14 +38,14 @@ class SonarrSeriesSeasonDetailsAllSeasons extends StatelessWidget {
}
List<Widget> _season(List<SonarrEpisode> episodes, int seasonNumber) => [
SonarrSeriesSeasonDetailsSeasonHeader(
SonarrSeasonDetailsSeasonHeader(
seriesId: seriesId,
seasonNumber: seasonNumber,
episodes: episodes,
),
...List.generate(
episodes.length,
(index) => SonarrSeriesSeasonDetailsEpisodeTile(episode: episodes[index]),
(index) => SonarrSeasonDetailsEpisodeTile(episode: episodes[index]),
),
];
}

View File

@@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrSeriesSeasonDetailsSeason extends StatelessWidget {
class SonarrSeasonDetailsSeason extends StatelessWidget {
final int seasonNumber;
final int seriesId;
final List<SonarrEpisode> episodes;
SonarrSeriesSeasonDetailsSeason({
SonarrSeasonDetailsSeason({
Key key,
@required this.seasonNumber,
@required this.seriesId,
@@ -17,14 +17,14 @@ class SonarrSeriesSeasonDetailsSeason extends StatelessWidget {
@override
Widget build(BuildContext context) => LSListView(
children: [
SonarrSeriesSeasonDetailsSeasonHeader(
SonarrSeasonDetailsSeasonHeader(
seriesId: seriesId,
seasonNumber: seasonNumber,
episodes: episodes,
),
...List.generate(
episodes.length,
(index) => SonarrSeriesSeasonDetailsEpisodeTile(episode: episodes[index]),
(index) => SonarrSeasonDetailsEpisodeTile(episode: episodes[index]),
),
],
);

View File

@@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrSeriesSeasonDetailsSeasonHeader extends StatelessWidget {
class SonarrSeasonDetailsSeasonHeader extends StatelessWidget {
final int seasonNumber;
final int seriesId;
final List<SonarrEpisode> episodes;
SonarrSeriesSeasonDetailsSeasonHeader({
SonarrSeasonDetailsSeasonHeader({
Key key,
@required this.seriesId,
@required this.seasonNumber,

View File

@@ -4,19 +4,19 @@ import 'package:intl/intl.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrSeriesSeasonDetailsEpisodeTile extends StatefulWidget {
class SonarrSeasonDetailsEpisodeTile extends StatefulWidget {
final SonarrEpisode episode;
SonarrSeriesSeasonDetailsEpisodeTile({
SonarrSeasonDetailsEpisodeTile({
Key key,
@required this.episode,
}) : super(key: key);
@override
State<SonarrSeriesSeasonDetailsEpisodeTile> createState() => _State();
State<SonarrSeasonDetailsEpisodeTile> createState() => _State();
}
class _State extends State<SonarrSeriesSeasonDetailsEpisodeTile> {
class _State extends State<SonarrSeasonDetailsEpisodeTile> {
final ExpandableController _expandableController = ExpandableController();
@override

View File

@@ -218,5 +218,5 @@ class _State extends State<SonarrSeriesTile> {
maxLines: 1,
);
Future<void> _tileOnTap() async => SonarrSeriesDetailsRouter.navigateTo(context, seriesId: widget.series.id);
Future<void> _tileOnTap() async => SonarrSeriesDetailsRouter().navigateTo(context, seriesId: widget.series.id);
}

View File

@@ -114,7 +114,7 @@ class _State extends State<SonarrSeriesAddSearchResultTile> {
LunaDialogs().textPreview(context, widget.series.title, widget.series.overview ?? 'No summary is available.');
} else if(widget.exists) {
Provider.of<SonarrState>(context, listen: false).enableVersion3
? SonarrSeriesDetailsRouter.navigateTo(context, seriesId: widget.series.id ?? -1)
? SonarrSeriesDetailsRouter().navigateTo(context, seriesId: widget.series.id ?? -1)
: LSSnackBar(
context: context,
title: 'Series Already Exists',

View File

@@ -113,7 +113,7 @@ class _State extends State<SonarrSeriesAddDetailsAddSeriesButton> {
message: context.read<SonarrSeriesAddDetailsState>().series.title,
type: SNACKBAR_TYPE.success,
);
Navigator.of(context).popAndPushNamed(SonarrSeriesDetailsRouter.route(seriesId: addedSeries.id));
Navigator.of(context).popAndPushNamed(SonarrSeriesDetailsRouter().route(seriesId: addedSeries.id));
})
.catchError((error, stack) {
LunaLogger().error('Failed to add series: ${context.read<SonarrSeriesAddDetailsState>().series.tvdbId}', error, stack);

View File

@@ -7,7 +7,7 @@ class SonarrSeriesAddDetailsLanguageProfileTile extends StatelessWidget {
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: 'Language Profile'),
subtitle: LSSubtitle(text: context.watch<SonarrSeriesAddDetailsState>().languageProfile?.name ?? Constants.TEXT_EMDASH),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _onTap(context),
);

View File

@@ -12,7 +12,7 @@ class SonarrSeriesAddDetailsMonitorStatusTile extends StatelessWidget {
text: (SonarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_STATUS.data as SonarrMonitorStatus).name,
),
),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _onTap(context),
);

View File

@@ -7,7 +7,7 @@ class SonarrSeriesAddDetailsQualityProfileTile extends StatelessWidget {
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: 'Quality Profile'),
subtitle: LSSubtitle(text: context.watch<SonarrSeriesAddDetailsState>().qualityProfile?.name ?? Constants.TEXT_EMDASH),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _onTap(context),
);

View File

@@ -7,7 +7,7 @@ class SonarrSeriesAddDetailsRootFolderTile extends StatelessWidget {
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: 'Root Folder'),
subtitle: LSSubtitle(text: context.watch<SonarrSeriesAddDetailsState>().rootFolder?.path ?? Constants.TEXT_EMDASH),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _onTap(context),
);

View File

@@ -7,7 +7,7 @@ class SonarrSeriesAddDetailsSeriesTypeTile extends StatelessWidget {
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: 'Series Type'),
subtitle: LSSubtitle(text: context.watch<SonarrSeriesAddDetailsState>().seriesType?.value?.lunaCapitalizeFirstLetters() ?? Constants.TEXT_EMDASH),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _onTap(context),
);

View File

@@ -9,7 +9,7 @@ class SonarrSeriesAddDetailsTagsTile extends StatelessWidget {
subtitle: LSSubtitle(text: context.watch<SonarrSeriesAddDetailsState>().tags.length == 0
? 'Not Set'
: context.watch<SonarrSeriesAddDetailsState>().tags.map((e) => e.label).join(', ')),
trailing: LSIconButton(icon: Icons.arrow_forward_ios),
trailing: LSIconButton(icon: Icons.arrow_forward_ios_rounded),
onTap: () async => _onTap(context),
);

View File

@@ -1,32 +1,27 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
import 'package:tuple/tuple.dart';
class SonarrSeriesDetailsRouter {
static const String ROUTE_NAME = '/sonarr/series/details/:seriesid';
class SonarrSeriesDetailsRouter extends LunaPageRouter {
SonarrSeriesDetailsRouter() : super('/sonarr/series/details/:seriesid');
static Future<void> navigateTo(BuildContext context, {
@required int seriesId,
}) async => LunaRouter.router.navigateTo(
context,
route(seriesId: seriesId),
@override
Future<void> navigateTo(BuildContext context, { @required int seriesId }) async => LunaRouter.router.navigateTo(context, route(seriesId: seriesId));
@override
String route({ @required int seriesId }) => fullRoute.replaceFirst(':seriesid', seriesId.toString());
@override
void defineRoute(FluroRouter router) => router.define(
fullRoute,
handler: Handler(handlerFunc: (context, params) {
int seriesId = params['seriesid'] == null || params['seriesid'].length == 0 ? -1 : (int.tryParse(params['seriesid'][0]) ?? -1);
return _SonarrSeriesDetailsRoute(seriesId: seriesId);
}),
transitionType: LunaRouter.transitionType,
);
static String route({ @required int seriesId }) => ROUTE_NAME
.replaceFirst(':seriesid', seriesId?.toString() ?? '-1');
static void defineRoutes(FluroRouter router) {
router.define(
ROUTE_NAME,
handler: Handler(handlerFunc: (context, params) => _SonarrSeriesDetailsRoute(
seriesId: int.tryParse(params['seriesid'][0]) ?? -1,
)),
transitionType: LunaRouter.transitionType,
);
}
}
class _SonarrSeriesDetailsRoute extends StatefulWidget {
@@ -41,32 +36,36 @@ class _SonarrSeriesDetailsRoute extends StatefulWidget {
State<StatefulWidget> createState() => _State();
}
class _State extends State<_SonarrSeriesDetailsRoute> {
class _State extends State<_SonarrSeriesDetailsRoute> with LunaLoadCallbackMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
PageController _pageController;
SonarrSeries series;
bool _loaded = false;
@override
void initState() {
super.initState();
_pageController = PageController(initialPage: SonarrDatabaseValue.NAVIGATION_INDEX_SERIES_DETAILS.data);
SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh());
}
Future<void> _refresh() async {
SonarrState _state = Provider.of<SonarrState>(context, listen: false);
SonarrSeries _series = await _state.api.series.getSeries(seriesId: widget.seriesId);
List<SonarrSeries> allSeries = await _state.series;
int _index = allSeries?.indexWhere((element) => element.id == widget.seriesId) ?? -1;
if(_index >= 0) allSeries[_index] = _series;
_state.notify();
if(mounted) setState(() {});
@override
Future<void> loadCallback() async {
if(widget.seriesId > 0) {
_findSeries(await context.read<SonarrState>().series);
await context.read<SonarrState>().resetSingleSeries(widget.seriesId);
_findSeries(await context.read<SonarrState>().series);
}
}
SonarrSeries _findSeries(List<SonarrSeries> series) {
return series.firstWhere(
void _findSeries(List<SonarrSeries> seriesList) {
SonarrSeries _series = seriesList.firstWhere(
(series) => series.id == widget.seriesId,
orElse: () => null,
);
if(mounted) setState(() {
series = _series;
_loaded = true;
});
}
List<SonarrTag> _findTags(List<int> tagIds, List<SonarrTag> tags) {
@@ -89,16 +88,20 @@ class _State extends State<_SonarrSeriesDetailsRoute> {
}
@override
Widget build(BuildContext context) => Scaffold(
key: _scaffoldKey,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
body: _body,
);
Widget build(BuildContext context) {
if(widget.seriesId <= 0) return LunaInvalidRoute(title: 'Series Details', message: 'Series Not Found');
return Scaffold(
key: _scaffoldKey,
appBar: _appBar,
bottomNavigationBar: context.watch<SonarrState>().enabled ? _bottomNavigationBar : null,
body: context.watch<SonarrState>().enabled ? _body : LunaMessage.moduleNotEnabled(context: context, module: 'Sonarr'),
);
}
Widget get _appBar => LunaAppBar(
title: 'Series Details',
actions: [
SonarrAppBarSeriesEditAction(seriesId: widget.seriesId),
SonarrAppBarSeriesSettingsAction(seriesId: widget.seriesId),
],
);
@@ -127,32 +130,34 @@ class _State extends State<_SonarrSeriesDetailsRoute> {
if(tuple.item5) tuple.item4,
]),
builder: (context, AsyncSnapshot<List<Object>> snapshot) {
if(_loaded && series == null) return LunaMessage.goBack(
text: 'Series Not Found',
context: context,
);
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
LunaLogger().error('Unable to pull Sonarr series details', snapshot.error, StackTrace.current);
}
return LSErrorMessage(onTapHandler: () => _refresh());
return LunaMessage.error(onTap: loadCallback);
}
if(snapshot.hasData) {
SonarrSeries series = _findSeries(snapshot.data[0]);
if(series != null) {
SonarrQualityProfile quality = _findQualityProfile(series.profileId, snapshot.data[2]);
SonarrLanguageProfile language = Provider.of<SonarrState>(context, listen: false).enableVersion3
? _findLanguageProfile(series.languageProfileId, snapshot.data[3])
: null;
List<SonarrTag> tags = _findTags(series.tags, snapshot.data[1]);
return series == null
? _unknown
: PageView(
controller: _pageController,
children: _tabs(
series: series,
quality: quality,
language: language,
tags: tags,
),
);
}
if(series == null) return LunaLoader();
SonarrQualityProfile quality = _findQualityProfile(series.profileId, snapshot.data[2]);
SonarrLanguageProfile language = Provider.of<SonarrState>(context, listen: false).enableVersion3
? _findLanguageProfile(series.languageProfileId, snapshot.data[3])
: null;
List<SonarrTag> tags = _findTags(series.tags, snapshot.data[1]);
return series == null
? _unknown
: PageView(
controller: _pageController,
children: _tabs(
series: series,
quality: quality,
language: language,
tags: tags,
),
);
}
return LSLoader();
},

View File

@@ -1,4 +1,5 @@
export 'widgets/appbar_edit_action.dart';
export 'widgets/appbar_settings_action.dart';
export 'widgets/navigation_bar.dart';
export 'widgets/overview.dart';
export 'widgets/season_all_tile.dart';

View File

@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrAppBarSeriesSettingsAction extends StatelessWidget {
class SonarrAppBarSeriesEditAction extends StatelessWidget {
final int seriesId;
SonarrAppBarSeriesSettingsAction({
SonarrAppBarSeriesEditAction({
Key key,
@required this.seriesId,
}) : super(key: key);
@@ -21,119 +21,11 @@ class SonarrAppBarSeriesSettingsAction extends StatelessWidget {
SonarrSeries series = snapshot.data.firstWhere((element) => element.id == seriesId, orElse: () => null);
if(series != null) return LSIconButton(
icon: Icons.edit,
onPressed: () async => handler(context, series),
onPressed: () async => SonarrSeriesEditRouter.navigateTo(context, seriesId: series.id),
);
}
return Container();
},
),
);
static Future<void> handler(
BuildContext context,
SonarrSeries series,
) async {
List values = await SonarrDialogs.seriesSettings(context, series);
if(values[0]) switch(values[1] as SonarrSeriesSettingsType) {
case SonarrSeriesSettingsType.EDIT: _edit(context, series); break;
case SonarrSeriesSettingsType.DELETE: _delete(context, series); break;
case SonarrSeriesSettingsType.REFRESH: _refresh(context, series); break;
case SonarrSeriesSettingsType.MONITORED: _monitored(context, series); break;
default: LunaLogger().warning('SonarrAppBarSeriesSettingsAction', '_handler', 'Unknown case: ${(values[1] as SonarrSeriesSettingsType)}');
}
}
static Future<void> _edit(
BuildContext context,
SonarrSeries series,
) async => SonarrSeriesEditRouter.navigateTo(context, seriesId: series.id);
static Future<void> _monitored(
BuildContext context,
SonarrSeries series,
) async {
SonarrState _state = context.read<SonarrState>();
if(_state.api != null) {
SonarrSeries _series = series.clone();
_series.monitored = !_series.monitored;
_state.api.series.updateSeries(series: _series)
.then((_) {
series.monitored = !series.monitored;
_state.notify();
LSSnackBar(
context: context,
title: series.monitored
? 'Monitoring'
: 'No Longer Monitoring',
message: series.title,
type: SNACKBAR_TYPE.success,
);
})
.catchError((error, stack) {
LunaLogger().error('Failed to toggle monitored state for series: ${series.id} / ${series.monitored}', error, stack);
LSSnackBar(
context: context,
title: series.monitored
? 'Failed to Unmonitor Series'
: 'Failed to Monitor Series',
type: SNACKBAR_TYPE.failure,
);
});
}
}
static Future<void> _refresh(
BuildContext context,
SonarrSeries series,
) async {
Sonarr _sonarr = Provider.of<SonarrState>(context, listen: false).api;
if(_sonarr != null) _sonarr.command.refreshSeries(seriesId: series.id)
.then((_) {
LSSnackBar(
context: context,
title: 'Refreshing...',
message: series.title,
);
})
.catchError((error, stack) {
LunaLogger().error('Unable to refresh series: ${series.id}', error, stack);
LSSnackBar(
context: context,
title: 'Failed to Refresh',
type: SNACKBAR_TYPE.failure,
);
});
}
static Future<void> _delete(
BuildContext context,
SonarrSeries series,
) async {
SonarrState _state = context.read<SonarrState>();
List _values = await SonarrDialogs.confirmDeleteSeries(context);
if(_state.api != null && _values[0]) _state.api.series.deleteSeries(
seriesId: series.id,
deleteFiles: _state.removeSeriesDeleteFiles,
)
.then((_) {
LSSnackBar(
context: context,
title: _state.removeSeriesDeleteFiles
? 'Series Removed (With Data)'
: 'Series Removed',
message: series.title,
type: SNACKBAR_TYPE.success,
);
_state.reset();
if(Navigator.of(context).canPop()) Navigator.of(context).pop();
})
.catchError((error, stack) {
LunaLogger().error('Failed to remove series: ${series.id}', error, stack);
LSSnackBar(
context: context,
title: 'Failed to Remove Series',
type: SNACKBAR_TYPE.failure,
);
});
}
}

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sonarr.dart';
class SonarrAppBarSeriesSettingsAction extends StatelessWidget {
final int seriesId;
SonarrAppBarSeriesSettingsAction({
Key key,
@required this.seriesId,
}) : super(key: key);
@override
Widget build(BuildContext context) => Selector<SonarrState, Future<List<SonarrSeries>>>(
selector: (_, state) => state.series,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<List<SonarrSeries>> snapshot) {
if(snapshot.hasError) return Container();
if(snapshot.hasData) {
SonarrSeries series = snapshot.data.firstWhere((element) => element.id == seriesId, orElse: () => null);
if(series != null) return LSIconButton(
icon: Icons.more_vert,
onPressed: () async => handler(context, series),
);
}
return Container();
},
),
);
static Future<void> handler(
BuildContext context,
SonarrSeries series,
) async {
List values = await SonarrDialogs.seriesSettings(context, series);
if(values[0]) switch(values[1] as SonarrSeriesSettingsType) {
case SonarrSeriesSettingsType.EDIT: _edit(context, series); break;
case SonarrSeriesSettingsType.DELETE: _delete(context, series); break;
case SonarrSeriesSettingsType.REFRESH: _refresh(context, series); break;
case SonarrSeriesSettingsType.MONITORED: _monitored(context, series); break;
default: LunaLogger().warning('SonarrAppBarSeriesSettingsAction', '_handler', 'Unknown case: ${(values[1] as SonarrSeriesSettingsType)}');
}
}
static Future<void> _edit(
BuildContext context,
SonarrSeries series,
) async => SonarrSeriesEditRouter.navigateTo(context, seriesId: series.id);
static Future<void> _monitored(
BuildContext context,
SonarrSeries series,
) async {
SonarrState _state = context.read<SonarrState>();
if(_state.api != null) {
SonarrSeries _series = series.clone();
_series.monitored = !_series.monitored;
_state.api.series.updateSeries(series: _series)
.then((_) {
series.monitored = !series.monitored;
_state.notify();
LSSnackBar(
context: context,
title: series.monitored
? 'Monitoring'
: 'No Longer Monitoring',
message: series.title,
type: SNACKBAR_TYPE.success,
);
})
.catchError((error, stack) {
LunaLogger().error('Failed to toggle monitored state for series: ${series.id} / ${series.monitored}', error, stack);
LSSnackBar(
context: context,
title: series.monitored
? 'Failed to Unmonitor Series'
: 'Failed to Monitor Series',
type: SNACKBAR_TYPE.failure,
);
});
}
}
static Future<void> _refresh(
BuildContext context,
SonarrSeries series,
) async {
Sonarr _sonarr = Provider.of<SonarrState>(context, listen: false).api;
if(_sonarr != null) _sonarr.command.refreshSeries(seriesId: series.id)
.then((_) {
LSSnackBar(
context: context,
title: 'Refreshing...',
message: series.title,
);
})
.catchError((error, stack) {
LunaLogger().error('Unable to refresh series: ${series.id}', error, stack);
LSSnackBar(
context: context,
title: 'Failed to Refresh',
type: SNACKBAR_TYPE.failure,
);
});
}
static Future<void> _delete(
BuildContext context,
SonarrSeries series,
) async {
SonarrState _state = context.read<SonarrState>();
List _values = await SonarrDialogs.confirmDeleteSeries(context);
if(_state.api != null && _values[0]) _state.api.series.deleteSeries(
seriesId: series.id,
deleteFiles: _state.removeSeriesDeleteFiles,
)
.then((_) {
LSSnackBar(
context: context,
title: _state.removeSeriesDeleteFiles
? 'Series Removed (With Data)'
: 'Series Removed',
message: series.title,
type: SNACKBAR_TYPE.success,
);
_state.reset();
if(Navigator.of(context).canPop()) Navigator.of(context).pop();
})
.catchError((error, stack) {
LunaLogger().error('Failed to remove series: ${series.id}', error, stack);
LSSnackBar(
context: context,
title: 'Failed to Remove Series',
type: SNACKBAR_TYPE.failure,
);
});
}
}

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