diff --git a/CHANGELOG.md b/CHANGELOG.md index cc771535..aa36e2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,55 @@ # LunaSea Changelog -## 4.2.1 (40201001) +## v5.0.0 (50000001) + +#### NEW +- `[Radarr]` Completely rewritten from the ground-up +- `[Radarr]` View system status, disk usage, and manage tags +- `[Radarr]` Catalogue, upcoming, missing, tags, and quality profiles are now held in-state between module switches +- `[Radarr/Add]` Long-pressing a search result tile will open the movie's TMDB entry +- `[Radarr/History]` Now an infinite scrolling list +- `[Radarr/Movie]` Cast information, movie-specific history, more detailed file information, more detailed overview page +- `[Radarr/Filtering]` Improved sorting and filtering speed, performance, and consistency +- `[Radarr/Filtering]` Ability to set sorting and filtering defaults in settings +- `[Radarr/Filtering]` Many additional filtering options +- `[Haptics]` Added haptic feedback to all buttons, toggles, and dropdowns +- `[Quick Actions]` Default "Settings" quick action added to end of the list (if there is room) +- `[UI/UX]` (Radarr Only For Now) Ability to tap the AppBar to scroll to top +- `[UI/UX]` (Radarr Only For Now) If the list is in a tabbed PageView, tapping the active tab will scroll the list to the top +- `[UI/UX]` (Radarr Only For Now) If the keyboard is open, scrolling the list, swiping between pages, or opening the drawer will dismiss the keyboard +- `[Webhooks]` Notification webhooks will now show as an in-app heads up if LunaSea is open at the time of receiving the notification + +#### TWEAKS +- `[Radarr]` Now only supports v3.0.0 and higher of Radarr +- `[Radarr]` Many visual changes, too many to list +- `[Radarr]` The tab "History" has been replaced with "More" +- `[Radarr/Edit]` Monitor toggle has been moved to the edit prompt +- `[Sonarr/Filtering]` Updated button to use standard filtering icon +- `[Tautulli/Activity]` Show session type and bandwidth type breakdown on activity page +- `[Tautulli/Activity]` Cleanup player information to more closely resemble web GUI +- `[Tautulli/Activity]` Show subtitle stream decision +- `[UI/UX]` Reduced the weight of bold text across the application +- `[UI/UX]` Added padding to bottom of lists for devices that require a safe area +- `[UI/UX]` Active tab in navigation bar now has a border with the AMOLED theme + +#### FIXES +- `[Changelog]` Properly align/pad "Full Changelog" button at the end of the changelog +- `[Flutter]` Update packages +- `[Flutter]` Now using the beta channel +- `[Quick Actions]` Improve internal implementation of quick actions to be more stable and reliable +- `[Settings/Modules]` Newly set headers would sometimes not be passed to the connection test +- `[Sonarr/Edit]` Series path prompt would show an incorrect title +- `[Sonarr/Description]` Show "No summary is available" when there is no summary instead of an empty string +- `[Sonarr/Releases]` Default sorting direction was using catalogue default value +- `[Tautulli/Graphs]` Description of some graphs were not correct (Thanks @ZuluWhiskey!) +- `[Tautulli/IPs]` Fix location showing as null for local addresses +- `[UI/UX]` Border radius for the splash highlight wouldn't clip correctly on slim buttons +- `[UI/UX]` Margin/size of sort/filter buttons were off by a few pixels +- `[UI/UX]` InkWell splash now clips correctly to the rounded borders of popup menu buttons + +--- + +## v4.2.1 (40201001) #### NEW - `[Accounts]` Ability to send a password reset email diff --git a/README.md b/README.md index 231148a3..1dafe923 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,53 @@ -[![App Store](https://img.shields.io/badge/%20-App%20Store-red?style=for-the-badge&logo=app-store&color=%232196F3&logoColor=white)](https://www.lunasea.app/appstore) -[![Play Store](https://img.shields.io/badge/%20-Play%20Store-red?style=for-the-badge&logo=google-play&color=%234CAF50&logoColor=white)](https://www.lunasea.app/playstore) -[![Codemagic Build Status](https://img.shields.io/badge/%20-Codemagic-red?style=for-the-badge&logo=circleci&color=%2332323E&logoColor=white)](https://codemagic.io/apps/5ef3424e03ad6b478a268d4c/5ef3424e03ad6b478a268d4b/latest_build) -[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/LunaSeaApp?label=%20&logo=reddit&color=%23F44336&logoColor=white&style=for-the-badge)](https://www.lunasea.app/reddit) -[![Discord](https://img.shields.io/discord/673591529834807358?color=7289da&label=%20&logo=discord&logoColor=white&style=for-the-badge)](https://www.lunasea.app/discord) -![LunaSea](https://uploads-ssl.webflow.com/5fcfe91dceef944b8c203a63/5fd4d46b86a0830f10cad146_lunasea_splash_2.png) +# LunaSea  LunaSea + +[![App Store](https://img.shields.io/badge/%20Download-ios-red?style=for-the-badge&logo=app-store&color=%232196F3&logoColor=white)](https://www.lunasea.app/appstore) +[![Play Store](https://img.shields.io/badge/%20Download-android-red?style=for-the-badge&logo=google-play&color=%234CAF50&logoColor=white)](https://www.lunasea.app/playstore) +[![Hellonext](https://img.shields.io/badge/%20Feedback-Hellonext-red?style=for-the-badge&logo=redux&color=%234ECCA3&logoColor=white)](https://www.lunasea.app/feedback) +[![Discord](https://img.shields.io/discord/673591529834807358?color=7289da&label=%20Discord&logo=discord&logoColor=white&style=for-the-badge)](https://www.lunasea.app/discord) +[![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/LunaSeaApp?label=%20Reddit&logo=reddit&color=%23F44336&logoColor=white&style=for-the-badge)](https://www.lunasea.app/reddit) LunaSea is a fully featured, open source self-hosted controller! Focused on giving you a seamless experience between all of your self-hosted media software, LunaSea supports: -- Lidarr -- Radarr -- Sonarr -- NZBGet -- SABnzbd -- Newznab Indexer Searching -- NZBHydra2 -- Tautulli -- Wake on LAN +- [Lidarr](https://github.com/lidarr/lidarr) +- [Radarr](https://github.com/radarr/radarr) +- [Sonarr](https://github.com/sonarr/sonarr) +- [NZBGet](https://github.com/nzbget/nzbget) +- [SABnzbd](https://github.com/sabnzbd/sabnzbd) +- [Newznab Indexer Searching](https://newznab.readthedocs.io/en/latest/misc/api/) +- [NZBHydra2](https://github.com/theotherp/nzbhydra2) +- [Tautulli](https://github.com/Tautulli/Tautulli) +- [Wake on LAN](https://en.wikipedia.org/wiki/Wake-on-LAN) LunaSea even comes with support for multiple instances of applications using profiles, backing up and restoring your configuration to your filesystem, an AMOLED black theme, and more! > Please note that LunaSea is purely a remote control application, it does not offer any functionality without software installed on a server/computer. ---- +## Getting Started -## Developing, Installing, & Building (iOS) +- [Releases](https://github.com/CometTools/LunaSea/releases) + - [(iOS) App Store](https://www.lunasea.app/appstore) + - [(Android) Play Store](https://www.lunasea.app/playstore) + - [Codemagic](https://codemagic.io/apps/5ef3424e03ad6b478a268d4c/5ef3424e03ad6b478a268d4b/latest_build) +- [Build Guide](https://github.com/CometTools/LunaSea/wiki/Build-Guide) + - [(iOS) Development Certificate Setup](https://github.com/CometTools/LunaSea/wiki/Setup-of-Development-Certificate) + - [(Android) Keystore Setup](https://github.com/CometTools/LunaSea/wiki/Configure-Keystore) +- [Documentation](https://www.lunasea.app/docs) +- [Feedback & Feature Requests Board](https://www.lunasea.app/feedback) +- [Frequently Asked Questions](https://docs.lunasea.app/getting-started/frequently-asked-questions) -#### Requirements +## Support -1. A MacOS Machine -2. An AppleID account (does not require developer account) -3. [Flutter Framework (Stable Channel)](https://flutter.dev/) -4. [XCode](https://apps.apple.com/ca/app/xcode/id497799835?mt=12) -5. [Developer Certificate Configured](https://github.com/LunaSeaApp/LunaSea/wiki/Setup-of-Development-Certificate) -6. [FlutterFire for IOS Configured](https://firebase.flutter.dev/docs/installation/ios/) +> Please only post bugs and issues to GitHub. Use the [feedback & feature requests board](https://www.lunasea.app/feedback) for requests and general feedback. -#### Developing +- [Discord](https://www.lunasea.app/discord) +- [Email](mailto:hello@comet.tools) +- [GitHub (Issues & Bug Reports Only)](https://github.com/CometTools/LunaSea/issues) +- [Reddit](https://www.lunasea.app/reddit) +- [Website](https://www.lunasea.app) -1. Clone the repository - - `git clone git@github.com:LunaSeaApp/LunaSea.git` -2. Install the Flutter packages - - `flutter pub get` -3. Start your simulator or plug in your device and ensure you have accepted it is a trusted device -4. Install LunaSea in development mode on your device or simulator - - `flutter run` +## Tools & Services -#### Installing +> Thank you to all tools and services for offering free subscriptions for open-source projects! -> Release builds can only be installed on physical devices - -1. Clone the repository - - `git clone git@github.com:LunaSeaApp/LunaSea.git` -2. Install the Flutter packages - - `flutter pub get` -3. Plug in your device and ensure you have accepted it is a trusted device -4. Install a production version of the application on your device - - `flutter run --release` - -#### Building (.ipa) - -1. Clone the repository - - `git clone git@github.com:LunaSeaApp/LunaSea.git` -2. Install the Flutter packages - - `flutter pub get` -3. Run `build_ipa` inside of the `scripts` folder -4. The IPA will be placed in the root of the project directory - ---- - -## Developing, Installing, & Building (Android) - -#### Requirements - -1. [Android SDK/Android Studio Installed & Configured](https://developer.android.com/studio) -2. [Flutter Framework (Stable Channel)](https://flutter.dev/) -3. [Keystore Configured](https://github.com/LunaSeaApp/LunaSea/wiki/Configure-Keystore) -4. [FlutterFire for Android Configured](https://firebase.flutter.dev/docs/installation/android/) - -#### Developing - -1. Clone the repository - - `git clone git@github.com:LunaSeaApp/LunaSea.git` -2. Install the Flutter packages - - `flutter pub get` -3. Start your simulator or plug in your device and ensure you have enabled USB Debugging -4. Install LunaSea in development mode on your device or simulator - - `flutter run` - -#### Installing - -> Release builds can only be installed on physical devices - -1. Clone the repository - - `git clone git@github.com:LunaSeaApp/LunaSea.git` -2. Install the Flutter packages - - `flutter pub get` -3. Plug in your device and ensure you have enabled USB Debugging -4. Install a production version of the application on your device - - `flutter run --release` - -#### Building (.apk) - -1. Clone the repository - - `git clone git@github.com:LunaSeaApp/LunaSea.git` -2. Install the Flutter packages - - `flutter pub get` -3. Run `build_apk` inside of the `scripts` folder -4. The APKs will be placed in the root of the project directory - - ARM 32-bit: `LunaSea-armeabi-v7a-release.apk` - - ARM 64-bit: `LunaSea-arm64-v8a-release.apk` - - x86 64-bit: `LunaSea-x86_64-release.apk` +- [Gitbook](https://www.gitbook.com): Gitbook is used to host LunaSea's documentation +- [Hellonext](https://www.hellonext.co): Hellonext is used to host LunaSea's feedback, changelog, and roadmap diff --git a/assets/changelog.json b/assets/changelog.json index 39a55e41..dfb4e884 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,35 +1,169 @@ { - "motd": "A minor update to improve some things before the next big update!\n\nNote: The future 5.0.0 release will include a full rework of Radarr with many additional features, but will drop support for Radarr v0.20.x or older. Only v3.0.0 and higher will be supported to ensure the best possible support within LunaSea for Radarr, and not be held back by the limitations of v0.20.x and older.", - "version": "4.2.1", - "build": "40201001", + "motd": "This is the first release of LunaSea v5.0.0! There is a lot that has changed, but the biggest change of all is that Radarr has been rewritten from the ground up, completely from scratch! What this means is Radarr is faster and more performant, has a ton of new features and a much better visual style. Some new features include the ability to manage tags, view disk usage and system status, view the cast and crew in a movie, view movie-specific history, much more detailed file and overview information, and more!\n\nSome features are still being developed, including the queue, health check, and the movie discovery features. They will be released in subsequent builds of v5.0.0 before the final release.\n\nSome other new quality of life features that are definitely worth noting include haptic feedback across the UI, support for infinite scrolling lists, the ability to tap the AppBar to scroll back to the top, and now the keyboard will automatically be dismissed if you scroll a list, change pages, or open the drawer.\n\nCurrently most of these new quality of life features are only present within Radarr. Many of them required fundamental changes to how things are handled internally, and I just want to ensure stability before adding support across the entirety of LunaSea, but they will be making their way across all modules before the final v5.0.0 release!", + "version": "5.0.0", + "build": "50000001", "new": [ { - "module": "Accounts", + "module": "Radarr", "changes": [ - "Ability to send a password reset email", - "Associate the LunaSea website domain for better autofill support" + "Completely rewritten from the ground-up", + "View system status, disk usage, and manage tags", + "Catalogue, upcoming, missing, tags, and quality profiles are now held in-state between module switches" + ] + }, + { + "module": "Radarr/Add", + "changes": [ + "Long-pressing a search result tile will open the movie's TMDB entry" + ] + }, + { + "module": "Radarr/History", + "changes": [ + "Now an infinite scrolling list" + ] + }, + { + "module": "Radarr/Movie", + "changes": [ + "Cast information, movie-specific history, more detailed file information, more detailed overview page" + ] + }, + { + "module": "Radarr/Filtering", + "changes": [ + "Improved sorting and filtering speed, performance, and consistency", + "Ability to set sorting and filtering defaults in settings", + "Many additional filtering options" + ] + }, + { + "module": "Haptics", + "changes": [ + "Added haptic feedback to all buttons, toggles, and dropdowns" + ] + }, + { + "module": "Quick Actions", + "changes": [ + "Default \"Settings\" quick action added to end of the list (if there is room)" + ] + }, + { + "module": "UI/UX", + "changes": [ + "(Radarr Only For Now) Ability to tap the AppBar to scroll to top", + "(Radarr Only For Now) If the list is in a tabbed PageView, tapping the active tab will scroll the list to the top", + "(Radarr Only For Now) If the keyboard is open, scrolling the list, swiping between pages, or opening the drawer will dismiss the keyboard" + ] + }, + { + "module": "Webhooks", + "changes": [ + "Notification webhooks will now show as an in-app heads up if LunaSea is open at the time of receiving the notification" ] } ], "tweaks": [ { - "module": "Sonarr/Upcoming", + "module": "Radarr", "changes": [ - "Change \"Not Downloaded\" to \"Missing\"" + "Now only supports v3.0.0 and higher of Radarr", + "Lots and lots of visual changes", + "The tab \"History\" has been replaced with \"More\"" + ] + }, + { + "module": "Radarr/Edit", + "changes": [ + "Monitor toggle has been moved to the edit prompt" + ] + }, + { + "module": "Sonarr/Filtering", + "changes": [ + "Updated button to use standard filtering icon" + ] + }, + { + "module": "Tautulli/Activity", + "changes": [ + "Show session type and bandwidth type breakdown on activity page", + "Cleanup player information to more closely resemble web GUI", + "Show subtitle stream decision" + ] + }, + { + "module": "UI/UX", + "changes": [ + "Reduced the weight of bold text across the application", + "Added padding to bottom of lists for devices that require a safe area", + "Active tab in navigation bar now has a border with the AMOLED theme" ] } ], "fixes": [ { - "module": "Alerts/Changelog", + "module": "Changelog", "changes": [ - "Changelog would be shown again when restoring a backup from a previous version" + "Properly align/pad \"Full Changelog\" button at the end of the changelog" ] }, { - "module": "Sonarr/Add", + "module": "Flutter", "changes": [ - "Fixed error log showing series ID as null" + "Update packages", + "Now using the beta channel" + ] + }, + { + "module": "Quick Actions", + "changes": [ + "Improve internal implementation of quick actions to be more stable and reliable" + ] + }, + { + "module": "Settings/Modules", + "changes": [ + "Newly set headers would sometimes not be passed to the connection test" + ] + }, + { + "module": "Sonarr/Edit", + "changes": [ + "Series path prompt would show an incorrect title" + ] + }, + { + "module": "Sonarr/Description", + "changes": [ + "Show \"No summary is available\" when there is no summary instead of an empty string" + ] + }, + { + "module": "Sonarr/Releases", + "changes": [ + "Default sorting direction was using catalogue default value" + ] + }, + { + "module": "Tautulli/Graphs", + "changes": [ + "Description of some graphs were not correct (Thanks @ZuluWhiskey!)" + ] + }, + { + "module": "Tautulli/IPs", + "changes": [ + "Fix location showing as null for local addresses" + ] + }, + { + "module": "UI/UX", + "changes": [ + "Border radius for the splash highlight wouldn't clip correctly on slim buttons", + "Margin/size of sort/filter buttons were off by a few pixels", + "InkWell splash now clips correctly to the rounded borders of popup menu buttons" ] } ] diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index e8efba11..b2f5fae9 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 399e9340..88c29144 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d489bcd5..a31c74c5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,6 +3,9 @@ PODS: - Firebase/Firestore (= 7.3.0) - firebase_core - Flutter + - connectivity_plus (0.0.1): + - Flutter + - Reachability - DKImagePickerController/Core (4.3.2): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource @@ -75,18 +78,18 @@ PODS: - FirebaseCoreDiagnostics (~> 7.0) - GoogleUtilities/Environment (~> 7.0) - GoogleUtilities/Logger (~> 7.0) - - FirebaseCoreDiagnostics (7.4.0): + - FirebaseCoreDiagnostics (7.6.0): - GoogleDataTransport (~> 8.0) - GoogleUtilities/Environment (~> 7.0) - GoogleUtilities/Logger (~> 7.0) - nanopb (~> 2.30907.0) - FirebaseFirestore (7.3.0) - - FirebaseInstallations (7.4.0): + - FirebaseInstallations (7.6.0): - FirebaseCore (~> 7.0) - GoogleUtilities/Environment (~> 7.0) - GoogleUtilities/UserDefaults (~> 7.0) - PromisesObjC (~> 1.2) - - FirebaseInstanceID (7.4.0): + - FirebaseInstanceID (7.6.0): - FirebaseCore (~> 7.0) - FirebaseInstallations (~> 7.0) - GoogleUtilities/Environment (~> 7.0) @@ -104,22 +107,22 @@ PODS: - Flutter (1.0.0) - GoogleDataTransport (8.2.0): - nanopb (~> 2.30907.0) - - GoogleUtilities/AppDelegateSwizzler (7.2.0): + - GoogleUtilities/AppDelegateSwizzler (7.2.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.2.0): + - GoogleUtilities/Environment (7.2.2): - PromisesObjC (~> 1.2) - - GoogleUtilities/Logger (7.2.0): + - GoogleUtilities/Logger (7.2.2): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.2.0): + - GoogleUtilities/Network (7.2.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.2.0)" - - GoogleUtilities/Reachability (7.2.0): + - "GoogleUtilities/NSData+zlib (7.2.2)" + - GoogleUtilities/Reachability (7.2.2): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.2.0): + - GoogleUtilities/UserDefaults (7.2.2): - GoogleUtilities/Logger - GTMSessionFetcher/Core (1.5.0) - in_app_purchase (0.0.1): @@ -129,32 +132,36 @@ PODS: - nanopb/encode (= 2.30907.0) - nanopb/decode (2.30907.0) - nanopb/encode (2.30907.0) + - network_info_plus (0.0.1): + - Flutter - package_info (0.0.1): - Flutter - path_provider (0.0.1): - Flutter - - "permission_handler (5.0.1+1)": + - "permission_handler (5.1.0+2)": - Flutter - PromisesObjC (1.2.12) - quick_actions (0.0.1): - Flutter - - SDWebImage (5.10.2): - - SDWebImage/Core (= 5.10.2) - - SDWebImage/Core (5.10.2) - - Sentry (6.0.12): - - Sentry/Core (= 6.0.12) - - Sentry/Core (6.0.12) + - Reachability (3.2) + - SDWebImage (5.10.4): + - SDWebImage/Core (= 5.10.4) + - SDWebImage/Core (5.10.4) + - Sentry (6.1.4): + - Sentry/Core (= 6.1.4) + - Sentry/Core (6.1.4) - sentry_flutter (0.0.1): - Flutter - - Sentry (~> 6.0.10) + - Sentry (~> 6.1.4) - share_plus (0.0.1): - Flutter - - SwiftyGif (5.3.0) + - SwiftyGif (5.4.0) - url_launcher (0.0.1): - Flutter 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_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -163,6 +170,7 @@ DEPENDENCIES: - FirebaseFirestore (from `https://github.com/invertase/firestore-ios-sdk-frameworks.git`, tag `7.3.0`) - Flutter (from `Flutter`) - in_app_purchase (from `.symlinks/plugins/in_app_purchase/ios`) + - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - permission_handler (from `.symlinks/plugins/permission_handler/ios`) @@ -188,6 +196,7 @@ SPEC REPOS: - GTMSessionFetcher - nanopb - PromisesObjC + - Reachability - SDWebImage - Sentry - SwiftyGif @@ -195,6 +204,8 @@ SPEC REPOS: EXTERNAL SOURCES: cloud_firestore: :path: ".symlinks/plugins/cloud_firestore/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" firebase_auth: @@ -212,6 +223,8 @@ EXTERNAL SOURCES: :path: Flutter in_app_purchase: :path: ".symlinks/plugins/in_app_purchase/ios" + network_info_plus: + :path: ".symlinks/plugins/network_info_plus/ios" package_info: :path: ".symlinks/plugins/package_info/ios" path_provider: @@ -234,6 +247,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: cloud_firestore: 433cf1aece50c727965355ae00e8dbeeceae66b1 + connectivity_plus: f5e5e0b894ec1187bfab9816d6c2f034da8d46da DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 @@ -244,30 +258,32 @@ SPEC CHECKSUMS: firebase_storage: 554b8ce9192bfe0bd35f63da42d0d1f6ae819ccc FirebaseAuth: c224a0cf1afa0949bd5c7bfcf154b4f5ce8ddef2 FirebaseCore: 4d3c72622ce0e2106aaa07bb4b2935ba2c370972 - FirebaseCoreDiagnostics: 3770093ac4f2be4590fa03cfa1d3a6e5602d4557 + FirebaseCoreDiagnostics: ee1184d51da3293335b83355aad20f537acc24cf FirebaseFirestore: 46e32f479aac127e742b1cf873930f7bb43e7b45 - FirebaseInstallations: 30646fc9a61c6f4ee3cd7a8b7231721842b40c95 - FirebaseInstanceID: 46d93d287108246fabbd85371ca27141d2c21ff9 + FirebaseInstallations: 6e4e77396559bc2ae0504823837ed737b1c7e47f + FirebaseInstanceID: cf243611700eddc916b093353adbd4da822cfa63 FirebaseMessaging: 68d1bcb14880189558a8ae57167abe0b7e417232 FirebaseStorage: 5002b1895bfe74a5ce92ad54f966e6162d0da2e5 - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c GoogleDataTransport: 1024b1a4dfbd7a0e92cb20d7e0a6f1fb66b449a4 - GoogleUtilities: d866834472f1324d080496bc67ab3ce5d0d46027 + GoogleUtilities: 31c5b01f978a70c6cff2afc6272b3f1921614b43 GTMSessionFetcher: b3503b20a988c4e20cc189aa798fd18220133f52 in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6 nanopb: 59221d7f958fb711001e6a449489542d92ae113e + network_info_plus: b78876159360f5580608c2cea620d6ceffabd0ad package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c - permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6 + permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97 quick_actions: 6cb2390c4dab0e737c94573c27e18d9666710720 - SDWebImage: b969dcfc02c40a5da71eac0b03b8f1a0c794a86f - Sentry: 8ffcfe3416e0a58b90bb1ae319e8e8a13022a03e - sentry_flutter: e57e33f56d5791e4effa48e85e44505367686b27 + Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 + SDWebImage: c666b97e1fa9c64b4909816a903322018f0a9c84 + Sentry: 9d055e2de30a77685e86b219acf02e59b82091fc + sentry_flutter: 8b2b2e1c0101ef562106b5d272d3c0c741512c01 share_plus: 2e6276f752c279a84cd68a9fc27dfbbe2a74b072 - SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 + SwiftyGif: 5d4af95df24caf1c570dbbcb32a3b8a0763bc6d7 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef PODFILE CHECKSUM: 9f0afed62614a65db8eaad79ca26aa6cd9cb4be2 -COCOAPODS: 1.10.0 +COCOAPODS: 1.10.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 714c0478..a0cf8c09 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, FB187DB152E6AC8A61142056 /* [CP] Embed Pods Frameworks */, - 5BA92B8D22335C6E222F251E /* [CP] Copy Pods Resources */, + 3BA7760032A55E21F5FE79FC /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -233,7 +233,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 5BA92B8D22335C6E222F251E /* [CP] Copy Pods Resources */ = { + 3BA7760032A55E21F5FE79FC /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -303,17 +303,19 @@ "${BUILT_PRODUCTS_DIR}/FirebaseInstanceID/FirebaseInstanceID.framework", "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework", "${BUILT_PRODUCTS_DIR}/FirebaseStorage/FirebaseStorage.framework", - "${PODS_ROOT}/../Flutter/Flutter.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", + "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", + "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/in_app_purchase/in_app_purchase.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${BUILT_PRODUCTS_DIR}/network_info_plus/network_info_plus.framework", "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", "${BUILT_PRODUCTS_DIR}/quick_actions/quick_actions.framework", @@ -332,17 +334,19 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstanceID.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseStorage.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/network_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/quick_actions.framework", diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e989fcee..c6ca70e4 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -42,6 +42,8 @@ NSLocationWhenInUseUsageDescription LunaSea would like to access your location for determining local-remote host switching + NSLocationAlwaysAndWhenInUseUsageDescription + LunaSea would like to access your location for determining local-remote host switching NSPhotoLibraryAddUsageDescription LunaSea would like to save photos from the app to your gallery NSPhotoLibraryUsageDescription diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 9a12c1ff..3d6b50ee 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -8,5 +8,7 @@ webcredentials:www.lunasea.app + com.apple.developer.networking.wifi-info + diff --git a/lib/core.dart b/lib/core.dart index ab447915..f5f94593 100644 --- a/lib/core.dart +++ b/lib/core.dart @@ -12,6 +12,7 @@ export 'core/firebase.dart'; export 'core/in_app_purchases.dart'; export 'core/logger.dart'; export 'core/luna_ui.dart'; +export 'core/mixins.dart'; export 'core/module_metadata.dart'; export 'core/networking.dart'; export 'core/profile.dart'; @@ -22,3 +23,9 @@ export 'core/theme.dart'; export 'core/types.dart'; export 'core/ui.dart'; export 'core/uuid.dart'; +export 'core/widgets.dart'; +// Dart/Flutter +export 'dart:async'; +// Third-Party +export 'package:fluro/fluro.dart'; +export 'package:tuple/tuple.dart'; diff --git a/lib/core/adapters/profile.dart b/lib/core/adapters/profile.dart index 541a3b3a..00faec32 100644 --- a/lib/core/adapters/profile.dart +++ b/lib/core/adapters/profile.dart @@ -405,7 +405,6 @@ class ProfileHiveObject extends HiveObject { List get enabledMonitoringModules => [ if(tautulliEnabled ?? false) TautulliConstants.MODULE_KEY, - if(ombiEnabled ?? false) OmbiConstants.MODULE_KEY, ]; bool get anyAutomationEnabled => enabledAutomationModules.isNotEmpty; diff --git a/lib/core/bottom_modal_sheet.dart b/lib/core/bottom_modal_sheet.dart index a22902a4..8f124d86 100644 --- a/lib/core/bottom_modal_sheet.dart +++ b/lib/core/bottom_modal_sheet.dart @@ -24,43 +24,47 @@ class LunaBottomModalSheet { await DefaultAssetBundle.of(context).loadString("assets/changelog.json") .then((data) { Map changelog = json.decode(data); - Database.alertsBox.put('ALERTS_CHANGELOG', buildNumber); + AlertsDatabaseValue.CHANGELOG.put(buildNumber); showModal( context: context, - builder: (context) => LSListView( + builder: (context) => LunaListViewModal( children: [ - LSHeader(text: '${changelog['version']} (${changelog['build']})', subtitle: changelog['motd']), - if((changelog['new'] as List).length != 0) LSTableBlock( - title: 'New', - children: List.generate( + LunaHeader(text: '${changelog['version']} (${changelog['build']})', subtitle: changelog['motd']), + LunaHeader(text: 'New'), + if((changelog['new'] as List).length != 0) LunaTableCard( + content: List.generate( (changelog['new'] as List).length, - (index) => LSTableContent( + (index) => LunaTableContent( title: changelog['new'][index]['module'], body: (changelog['new'][index]['changes'] as List).fold('', (data, object) => data += '${Constants.TEXT_BULLET}\t$object\n').trim(), ), ), ), - if((changelog['tweaks'] as List).length != 0) LSTableBlock( - title: 'Tweaks', - children: List.generate( + LunaHeader(text: 'Tweaks'), + if((changelog['tweaks'] as List).length != 0) LunaTableCard( + content: List.generate( (changelog['tweaks'] as List).length, - (index) => LSTableContent( + (index) => LunaTableContent( title: changelog['tweaks'][index]['module'], body: (changelog['tweaks'][index]['changes'] as List).fold('', (data, object) => data += '${Constants.TEXT_BULLET}\t$object\n').trim(), ), ), ), - if((changelog['fixes'] as List).length != 0) LSTableBlock( - title: 'Fixes', - children: List.generate( + LunaHeader(text: 'Fixes'), + if((changelog['fixes'] as List).length != 0) LunaTableCard( + content: List.generate( (changelog['fixes'] as List).length, - (index) => LSTableContent( + (index) => LunaTableContent( title: changelog['fixes'][index]['module'], body: (changelog['fixes'][index]['changes'] as List).fold('', (data, object) => data += '${Constants.TEXT_BULLET}\t$object\n').trim(), ), ), ), - LSButton(text: 'Full Changelog', onTap: () async => Constants.URL_CHANGELOG.lunaOpenGenericLink()), + LunaButtonContainer( + children: [ + LunaButton(text: 'Full Changelog', onTap: () async => Constants.URL_CHANGELOG.lunaOpenGenericLink()), + ], + ), ], ), ); diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index eda86c3d..1f32d761 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -9,7 +9,6 @@ import 'package:lunasea/modules/radarr/core.dart' show RadarrDatabase, RadarrCon import 'package:lunasea/modules/sonarr/core.dart' show SonarrDatabase, SonarrConstants; import 'package:lunasea/modules/nzbget/core.dart' show NZBGetDatabase, NZBGetConstants; import 'package:lunasea/modules/sabnzbd/core.dart' show SABnzbdDatabase, SABnzbdConstants; -import 'package:lunasea/modules/ombi/core.dart' show OmbiDatabase, OmbiConstants; import 'package:lunasea/modules/tautulli/core.dart' show TautulliDatabase, TautulliConstants; class LunaConfiguration { @@ -49,7 +48,7 @@ class LunaConfiguration { /// On a failed import, resets LunaSea back to the default/base state Future import(BuildContext context, String data) async { Map config = json.decode(data); - Database.clearAllBoxes(); + Database().clearAllBoxes(); try { // Set the profilers, indexer, and global LunaSea boxes if(config['profiles'] != null) _setProfiles(config['profiles']); @@ -67,11 +66,10 @@ class LunaConfiguration { if(config[NZBGetConstants.MODULE_KEY] != null) NZBGetDatabase().import(config[NZBGetConstants.MODULE_KEY]); if(config[SABnzbdConstants.MODULE_KEY] != null) SABnzbdDatabase().import(config[SABnzbdConstants.MODULE_KEY]); //Monitoring - if(config[OmbiConstants.MODULE_KEY] != null) OmbiDatabase().import(config[OmbiConstants.MODULE_KEY]); if(config[TautulliConstants.MODULE_KEY] != null) TautulliDatabase().import(config[TautulliConstants.MODULE_KEY]); } catch (error, stack) { LunaLogger().error('Failed to import configuration, resetting to default', error, stack); - Database.setDefaults(); + Database().setDefaults(); } // Reset the entire app's state LunaState.reset(context); @@ -97,7 +95,6 @@ class LunaConfiguration { NZBGetConstants.MODULE_KEY: NZBGetDatabase().export(), SABnzbdConstants.MODULE_KEY: SABnzbdDatabase().export(), // Monitoring - OmbiConstants.MODULE_KEY: OmbiDatabase().export(), TautulliConstants.MODULE_KEY: TautulliDatabase().export(), }); } diff --git a/lib/core/constants.dart b/lib/core/constants.dart index ac730547..fea86759 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/lidarr/core.dart' show LidarrConstants; import 'package:lunasea/modules/radarr/core.dart' show RadarrConstants; @@ -14,6 +13,7 @@ class Constants { Constants._(); static const APPLICATION_NAME = 'LunaSea'; + //ignore: non_constant_identifier_names static Map MODULE_METADATA = { LidarrConstants.MODULE_KEY: LidarrConstants.MODULE_METADATA, @@ -27,20 +27,12 @@ class Constants { TautulliConstants.MODULE_KEY: TautulliConstants.MODULE_METADATA, }; - static const EMPTY_MAP = {}; - static const EMPTY_LIST = []; - static const EMPTY_STRING = ''; - static const TEXT_EMDASH = '—'; static const TEXT_BULLET = '•'; static const TEXT_RARROW = '→'; static const TEXT_LARROW = '←'; static const TEXT_ELLIPSIS = '…'; - static const UI_ELEVATION = 0.0; - static const UI_CARD_MARGIN = EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0); - static const UI_NAVIGATION_SPEED = 250; - static const UI_IMAGE_FADEIN_SPEED = 125; static const UI_BORDER_RADIUS = 10.0; static const UI_FONT_SIZE_HEADER = 18.0; static const UI_FONT_SIZE_STICKYHEADER = 14.0; @@ -51,15 +43,15 @@ class Constants { static const NO_SERVICES_ENABLED = '<>'; + static const URL_CHANGELOG = 'https://feedback.lunasea.app/changelog'; + static const URL_DISCORD = 'https://www.lunasea.app/discord'; static const URL_DOCUMENTATION = 'https://www.lunasea.app/docs'; + static const URL_FEEDBACK = 'https://www.lunasea.app/feedback'; static const URL_GITHUB = 'https://www.lunasea.app/github'; static const URL_REDDIT = 'https://www.lunasea.app/reddit'; - static const URL_WEBSITE = 'https://www.lunasea.app'; - static const URL_FEEDBACK = 'https://www.lunasea.app/feedback'; - static const URL_CHANGELOG = 'https://github.com/CometTools/LunaSea/blob/master/CHANGELOG.md'; - static const URL_DISCORD = 'https://www.lunasea.app/discord'; static const URL_SENTRY = 'https://sentry.io'; static const URL_TESTFLIGHT = 'https://www.lunasea.app/testflight'; + static const URL_WEBSITE = 'https://www.lunasea.app'; static const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15'; static const Map historyReasonMessages = { diff --git a/lib/core/database.dart b/lib/core/database.dart index 53c45880..868c8b2e 100644 --- a/lib/core/database.dart +++ b/lib/core/database.dart @@ -1,3 +1,4 @@ export 'database/database.dart'; +export 'database/alerts_database.dart'; export 'database/luna_database.dart'; export 'database/module_database.dart'; diff --git a/lib/core/database/alerts_database.dart b/lib/core/database/alerts_database.dart new file mode 100644 index 00000000..dfefdc9e --- /dev/null +++ b/lib/core/database/alerts_database.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +enum AlertsDatabaseValue { + CHANGELOG, +} + +extension AlertsDatabaseValueExtension on AlertsDatabaseValue { + String get key { + switch(this) { + case AlertsDatabaseValue.CHANGELOG: return 'ALERTS_CHANGELOG'; + } + throw Exception('key not found'); + } + + dynamic get data { + final box = Database.alertsBox; + switch(this) { + case AlertsDatabaseValue.CHANGELOG: return box.get(this.key, defaultValue: ''); + } + throw Exception('data not found'); + } + + void put(dynamic value) { + final box = Database.alertsBox; + switch(this) { + case AlertsDatabaseValue.CHANGELOG: if(value.runtimeType == String) box.put(this.key, value); return; + } + LunaLogger().warning('AlertsDatabaseValueExtension', 'put', 'Attempted to enter data for invalid AlertsDatabaseValue: ${this?.toString() ?? 'null'}'); + } + + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( + valueListenable: Database.alertsBox.listenable(keys: [this.key]), + builder: builder, + ); +} \ No newline at end of file diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index a725ff13..23c8570c 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -10,7 +10,6 @@ import 'package:lunasea/modules/radarr/core.dart' show RadarrDatabase; import 'package:lunasea/modules/sonarr/core.dart' show SonarrDatabase; import 'package:lunasea/modules/nzbget/core.dart' show NZBGetDatabase; import 'package:lunasea/modules/sabnzbd/core.dart' show SABnzbdDatabase; -import 'package:lunasea/modules/ombi/core.dart' show OmbiDatabase; import 'package:lunasea/modules/tautulli/core.dart' show TautulliDatabase; // Exports export 'package:hive/hive.dart'; @@ -25,7 +24,7 @@ class Database { /// - Register all adapters /// - Open all boxes /// - Set default database state of necessary - static Future initialize() async { + Future initialize() async { await Hive.initFlutter(_DATABASE_PATH); _registerAdapters(); await _openBoxes(); @@ -33,10 +32,10 @@ class Database { } /// Deinitialize the database by closing all open hive boxes. - static Future deinitialize() async => await Hive.close(); + Future deinitialize() async => await Hive.close(); /// Registers all necessary object adapters for Hive. - static void _registerAdapters() { + void _registerAdapters() { //Core Hive.registerAdapter(IndexerHiveObjectAdapter()); Hive.registerAdapter(ProfileHiveObjectAdapter()); @@ -53,12 +52,11 @@ class Database { NZBGetDatabase().registerAdapters(); SABnzbdDatabase().registerAdapters(); //Monitoring - OmbiDatabase().registerAdapters(); TautulliDatabase().registerAdapters(); } /// Open all Hive boxes for reading. - static Future _openBoxes() async { + Future _openBoxes() async { await Hive.openBox('lunasea'); await Hive.openBox('alerts'); await Hive.openBox('indexers'); @@ -66,7 +64,7 @@ class Database { } /// Set the default state for all hive boxes. - static void setDefaults({ bool clearAlerts = false }) { + void setDefaults({ bool clearAlerts = false }) { //Clear all the boxes clearAllBoxes(clearAlerts: clearAlerts); //Set default profile & enabled profile @@ -81,11 +79,11 @@ class Database { static Box get indexersBox => Hive.box('indexers'); //Clear boxes - static void clearLunaSeaBox() => lunaSeaBox.deleteAll(lunaSeaBox.keys); - static void clearProfilesBox() => profilesBox.deleteAll(profilesBox.keys); - static void clearIndexersBox() => indexersBox.deleteAll(indexersBox.keys); - static void clearAlertsBox() => alertsBox.deleteAll(alertsBox.keys); - static void clearAllBoxes({ bool clearAlerts = false }) { + void clearLunaSeaBox() => lunaSeaBox.deleteAll(lunaSeaBox.keys); + void clearProfilesBox() => profilesBox.deleteAll(profilesBox.keys); + void clearIndexersBox() => indexersBox.deleteAll(indexersBox.keys); + void clearAlertsBox() => alertsBox.deleteAll(alertsBox.keys); + void clearAllBoxes({ bool clearAlerts = false }) { clearLunaSeaBox(); clearProfilesBox(); clearIndexersBox(); diff --git a/lib/core/database/luna_database.dart b/lib/core/database/luna_database.dart index f05ddad5..66640d35 100644 --- a/lib/core/database/luna_database.dart +++ b/lib/core/database/luna_database.dart @@ -29,6 +29,7 @@ class LunaDatabase extends LunaModuleDatabase { case LunaDatabaseValue.QUICK_ACTIONS_SONARR: case LunaDatabaseValue.QUICK_ACTIONS_NZBGET: case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD: + case LunaDatabaseValue.QUICK_ACTIONS_OMBI: case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI: case LunaDatabaseValue.QUICK_ACTIONS_SEARCH: case LunaDatabaseValue.USE_24_HOUR_TIME: data[value.key] = value.data; break; @@ -60,6 +61,7 @@ class LunaDatabase extends LunaModuleDatabase { case LunaDatabaseValue.QUICK_ACTIONS_NZBGET: case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD: case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI: + case LunaDatabaseValue.QUICK_ACTIONS_OMBI: case LunaDatabaseValue.QUICK_ACTIONS_SEARCH: case LunaDatabaseValue.USE_24_HOUR_TIME: value.put(config[key]); break; } @@ -83,6 +85,7 @@ class LunaDatabase extends LunaModuleDatabase { case 'LUNASEA_QUICK_ACTIONS_SONARR': return LunaDatabaseValue.QUICK_ACTIONS_SONARR; case 'LUNASEA_QUICK_ACTIONS_NZBGET': return LunaDatabaseValue.QUICK_ACTIONS_NZBGET; case 'LUNASEA_QUICK_ACTIONS_SABNZBD': return LunaDatabaseValue.QUICK_ACTIONS_SABNZBD; + case 'LUNASEA_QUICK_ACTIONS_OMBI': return LunaDatabaseValue.QUICK_ACTIONS_OMBI; case 'LUNASEA_QUICK_ACTIONS_TAUTULLI': return LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI; case 'LUNASEA_QUICK_ACTIONS_SEARCH': return LunaDatabaseValue.QUICK_ACTIONS_SEARCH; case 'LUNASEA_USE_24_HOUR_TIME': return LunaDatabaseValue.USE_24_HOUR_TIME; @@ -107,6 +110,7 @@ enum LunaDatabaseValue { QUICK_ACTIONS_SONARR, QUICK_ACTIONS_NZBGET, QUICK_ACTIONS_SABNZBD, + QUICK_ACTIONS_OMBI, QUICK_ACTIONS_TAUTULLI, QUICK_ACTIONS_SEARCH, USE_24_HOUR_TIME, @@ -131,6 +135,7 @@ extension LunaDatabaseValueExtension on LunaDatabaseValue { case LunaDatabaseValue.QUICK_ACTIONS_NZBGET: return 'LUNASEA_QUICK_ACTIONS_NZBGET'; case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD: return 'LUNASEA_QUICK_ACTIONS_SABNZBD'; case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI: return 'LUNASEA_QUICK_ACTIONS_TAUTULLI'; + case LunaDatabaseValue.QUICK_ACTIONS_OMBI: return 'LUNASEA_QUICK_ACTIONS_OMBI'; case LunaDatabaseValue.QUICK_ACTIONS_SEARCH: return 'LUNASEA_QUICK_ACTIONS_SEARCH'; case LunaDatabaseValue.USE_24_HOUR_TIME: return 'LUNASEA_USE_24_HOUR_TIME'; } @@ -156,6 +161,7 @@ extension LunaDatabaseValueExtension on LunaDatabaseValue { case LunaDatabaseValue.QUICK_ACTIONS_NZBGET: return box.get(this.key, defaultValue: false); case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD: return box.get(this.key, defaultValue: false); case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI: return box.get(this.key, defaultValue: false); + case LunaDatabaseValue.QUICK_ACTIONS_OMBI: return box.get(this.key, defaultValue: false); case LunaDatabaseValue.QUICK_ACTIONS_SEARCH: return box.get(this.key, defaultValue: false); case LunaDatabaseValue.USE_24_HOUR_TIME: return box.get(this.key, defaultValue: false); } @@ -180,6 +186,7 @@ extension LunaDatabaseValueExtension on LunaDatabaseValue { case LunaDatabaseValue.QUICK_ACTIONS_SONARR: if(value.runtimeType == bool) box.put(this.key, value); return; case LunaDatabaseValue.QUICK_ACTIONS_NZBGET: if(value.runtimeType == bool) box.put(this.key, value); return; case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD: if(value.runtimeType == bool) box.put(this.key, value); return; + case LunaDatabaseValue.QUICK_ACTIONS_OMBI: if(value.runtimeType == bool) box.put(this.key, value); return; case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI: if(value.runtimeType == bool) box.put(this.key, value); return; case LunaDatabaseValue.QUICK_ACTIONS_SEARCH: if(value.runtimeType == bool) box.put(this.key, value); return; case LunaDatabaseValue.USE_24_HOUR_TIME: if(value.runtimeType == bool) box.put(this.key, value); return; @@ -187,7 +194,7 @@ extension LunaDatabaseValueExtension on LunaDatabaseValue { LunaLogger().warning('LunaDatabaseValueExtension', 'put', 'Attempted to enter data for invalid LunaDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/core/extensions.dart b/lib/core/extensions.dart index 5cb2ad29..25a5d62b 100644 --- a/lib/core/extensions.dart +++ b/lib/core/extensions.dart @@ -4,4 +4,7 @@ export 'extensions/duration.dart'; export 'extensions/flog_log_level.dart'; export 'extensions/iap_product_details.dart'; export 'extensions/int.dart'; +export 'extensions/navigator_state.dart'; +export 'extensions/page_controller.dart'; +export 'extensions/scroll_controller.dart'; export 'extensions/string.dart'; diff --git a/lib/core/extensions/datetime.dart b/lib/core/extensions/datetime.dart index 393a2f85..962661f8 100644 --- a/lib/core/extensions/datetime.dart +++ b/lib/core/extensions/datetime.dart @@ -19,14 +19,19 @@ extension DateTimeExtension on DateTime { return '${diff.inSeconds} ${diff.inSeconds == 1 ? 'Second' : 'Seconds'} Ago'; } - /// Returns a string representation of how far in the future the day is. + /// Returns a string representation of the difference in days/months/years. /// - /// Compares to [DateTime.now().toLocal()] to calculate the upcoming time. - String get lunaUpcomingDays { - if(this == null) return 'Unknown'; - Duration diff = this.difference(DateTime.now().toLocal()); - if(diff.inDays == 0) return 'Today'; - return 'In ${diff.inDays} ${diff.inDays == 1 ? "Day" : "Days"}'; + /// Compares to [DateTime.now()] to calculate the difference. + String get lunaDaysDifference { + if(this == null) return null; + Duration diff = this.difference(DateTime.now()); + int absoluteDays = diff.inDays.abs(); + if(absoluteDays == 0) return 'Today'; + // If greater than 2 years, show in years + if(absoluteDays >= 365*2) return '${(absoluteDays/365).round()} Years'; + // If greater than 3 months, show in months + if(absoluteDays >= 30*3) return '${(absoluteDays/30).round()} Months'; + return '$absoluteDays ${absoluteDays == 1 ? "Day" : "Days"}'; } /// Returns just the time as a string. @@ -39,5 +44,19 @@ extension DateTimeExtension on DateTime { /// Formatted as YYYY-MM-DD String get lunaDate => '${this.year.toString().padLeft(4, '0')}-${this.month.toString().padLeft(2, '0')}-${this.day.toString().padLeft(2, '0')}'; + /// Returns the date as a string. + /// + /// Formatted as " , ". + String get lunaDateReadable => DateFormat('MMMM dd, y').format(this.toLocal()); + /// Returns the date and time as a string. + /// + /// Formatted as ` , ::`. + /// + /// Set `timeOnNewLine` to true to have the time returned on a new line instead of separated by a bullet. + String lunaDateTimeReadable({ bool timeOnNewLine = false }) => DateFormat( + timeOnNewLine + ? LunaDatabaseValue.USE_24_HOUR_TIME.data ? 'MMMM dd, y\nHH:mm:ss' : 'MMMM dd, y\nhh:mm:ss a' + : LunaDatabaseValue.USE_24_HOUR_TIME.data ? 'MMMM dd, y ${LunaUI.TEXT_BULLET} HH:mm:ss' : 'MMMM dd, y ${LunaUI.TEXT_BULLET} hh:mm:ss a' + ).format(this.toLocal()); } diff --git a/lib/core/extensions/iap_product_details.dart b/lib/core/extensions/iap_product_details.dart index 80f1aca0..223fa088 100644 --- a/lib/core/extensions/iap_product_details.dart +++ b/lib/core/extensions/iap_product_details.dart @@ -6,10 +6,10 @@ extension ProductDetailsExtension on ProductDetails { /// Returns [IconData] icon corresponding to the IAP IconData get lunaIcon { switch(this.id) { - case LunaInAppPurchases.IAP_ID_DONATION_01: return Icons.local_drink; - case LunaInAppPurchases.IAP_ID_DONATION_03: return Icons.local_cafe; - case LunaInAppPurchases.IAP_ID_DONATION_05: return Icons.local_bar; - case LunaInAppPurchases.IAP_ID_DONATION_10: return Icons.fastfood; + case LunaInAppPurchases.DONATION_01: return Icons.local_drink; + case LunaInAppPurchases.DONATION_03: return Icons.local_cafe; + case LunaInAppPurchases.DONATION_05: return Icons.local_bar; + case LunaInAppPurchases.DONATION_10: return Icons.fastfood; default: return Icons.attach_money; } } @@ -17,10 +17,10 @@ extension ProductDetailsExtension on ProductDetails { /// Returns a readable name for the corresponding IAP String get lunaName { switch(this.id) { - case LunaInAppPurchases.IAP_ID_DONATION_01: return 'Buy Me A Soda'; - case LunaInAppPurchases.IAP_ID_DONATION_03: return 'Buy Me A Coffee'; - case LunaInAppPurchases.IAP_ID_DONATION_05: return 'Buy Me A Beer'; - case LunaInAppPurchases.IAP_ID_DONATION_10: return 'Buy Me A Burger'; + case LunaInAppPurchases.DONATION_01: return 'Buy Me A Soda'; + case LunaInAppPurchases.DONATION_03: return 'Buy Me A Coffee'; + case LunaInAppPurchases.DONATION_05: return 'Buy Me A Beer'; + case LunaInAppPurchases.DONATION_10: return 'Buy Me A Burger'; default: return 'Unknown In-App Purchase'; } } diff --git a/lib/core/extensions/navigator_state.dart b/lib/core/extensions/navigator_state.dart new file mode 100644 index 00000000..e8c90ba1 --- /dev/null +++ b/lib/core/extensions/navigator_state.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +extension NavigatorStateExtension on NavigatorState { + /// Safely pops the route by first checking that the route *can* be popped. + /// + /// If the route can not be popped (it is the first/lowest route in the navigator), nothing occurs. + void lunaSafetyPop() { + if(this.canPop()) this.pop(); + } + + /// Pops every route until the route is the first/lowest route in the navigator. + void lunaPopToFirst() { + this.popUntil((route) => route.isFirst); + } +} diff --git a/lib/core/extensions/page_controller.dart b/lib/core/extensions/page_controller.dart new file mode 100644 index 00000000..60b3e10d --- /dev/null +++ b/lib/core/extensions/page_controller.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +extension PageControllerExtension on PageController { + /// Animate a [PageView] back to 1.00, with a predefined duration and curve. + Future lunaAnimateToPage(int index) async { + if(this.hasClients) this.animateToPage( + index, + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), + curve: Curves.decelerate, + ); + } +} diff --git a/lib/core/extensions/scroll_controller.dart b/lib/core/extensions/scroll_controller.dart new file mode 100644 index 00000000..486fed04 --- /dev/null +++ b/lib/core/extensions/scroll_controller.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +extension ScrollControllerExtension on ScrollController { + /// Animate a [Scrollable] back to 1.00, with a predefined duration and curve. + Future lunaAnimateToStart() async { + if(this.hasClients) this.animateTo( + 1.00, + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED*2), + curve: Curves.decelerate, + ); + } +} diff --git a/lib/core/extensions/string.dart b/lib/core/extensions/string.dart index b19c69c1..eb28d4fa 100644 --- a/lib/core/extensions/string.dart +++ b/lib/core/extensions/string.dart @@ -17,6 +17,15 @@ extension StringExtension on String { /// /// Example "LunaSea Flutter" => 'lunasea-flutter'; String lunaConvertToSlug() => this.toLowerCase().replaceAll(RegExp(r'[\ \.]'), '-').replaceAll(RegExp(r'[^a-zA-Z0-9\-]'), '').trim(); + + /// Pad a string on both sides with the spacer value [count] amount of times. + /// Count is the amount of times to add the [padding] on each time, and not the maximum total width of the string unlike [padLeft] and [padRight]. + /// + /// Example "LunaSea" with count 2 and padding "1" would return "11LunaSea11". + String lunaPad([int count = 1, String padding = ' ']) { + String _value = this.padLeft((this.length+count), padding).padRight((this.length+(count*2)), padding); + return _value; + } } extension StringLinksExtension on String { @@ -48,7 +57,10 @@ extension StringLinksExtension on String { Future lunaOpenYouTube() async => await _openLink('https://www.youtube.com/watch?v=$this'); /// Attach this string as a movie ID to TheMovieDB and attempt to launch it as a URL. - Future lunaOpenTheMovieDB() async => await _openLink('https://www.themoviedb.org/movie/$this'); + Future lunaOpenTheMovieDBMovie() async => await _openLink('https://www.themoviedb.org/movie/$this'); + + /// Attach this string as a person ID to TheMovieDB and attempt to launch it as a URL. + Future lunaOpenTheMovieDBCredits() async => await _openLink('https://www.themoviedb.org/person/$this'); /// Attach this string as a series ID to TheTVDB and attempt to launch it as a URL. Future lunaOpenTheTVDB() async => await _openLink('https://www.thetvdb.com/?id=$this&tab=series'); diff --git a/lib/core/firebase/core.dart b/lib/core/firebase/core.dart index 7f6ddc99..c36396cb 100644 --- a/lib/core/firebase/core.dart +++ b/lib/core/firebase/core.dart @@ -9,5 +9,5 @@ class LunaFirebase { /// Initialize Firebase and configuration. /// /// This must be called before anything accesses Firebase services, or an exception will be thrown. - static Future initialize({ String name }) async => await Firebase.initializeApp(); + Future initialize({ String name }) async => await Firebase.initializeApp(); } diff --git a/lib/core/firebase/messaging.dart b/lib/core/firebase/messaging.dart index e2d20038..7963a0a3 100644 --- a/lib/core/firebase/messaging.dart +++ b/lib/core/firebase/messaging.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flash/flash.dart'; import 'package:lunasea/core.dart'; class LunaFirebaseMessaging { @@ -7,6 +9,12 @@ class LunaFirebaseMessaging { /// Throws an error if [LunaFirebase.initialize] has not been called. static FirebaseMessaging get instance => FirebaseMessaging.instance; + /// Returns a [Stream] to handle any new messages that are received while the application is in the open and in foreground. + Stream get onMessage => FirebaseMessaging.onMessage; + + /// Returns a [Stream] to handle any notifications that are tapped while the application is in the background (not terminated). + Stream get onMessageOpenedApp => FirebaseMessaging.onMessageOpenedApp; + /// Request for permission to send a user notifications. /// /// Returns true if permissions are allowed at either a full or provisional level. @@ -24,4 +32,20 @@ class LunaFirebaseMessaging { /// Returns the Firebase Cloud Messaging device token for this device. Future 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 showNotificationOnMessageListener() { + return onMessage.listen((message) { + if(message == null) return; + showLunaSnackBar( + title: message.notification?.title ?? 'Unknown Content', + message: message.notification?.body ?? Constants.TEXT_EMDASH, + type: LunaSnackbarType.INFO, + position: FlashPosition.top, + duration: Duration(seconds: 6, milliseconds: 750), + ); + }); + } } diff --git a/lib/core/image_cache.dart b/lib/core/image_cache.dart index 1fb34eee..b9da440d 100644 --- a/lib/core/image_cache.dart +++ b/lib/core/image_cache.dart @@ -2,7 +2,7 @@ import 'package:flutter/painting.dart'; class LunaImageCache { /// Initialize the image cache by setting [ImageCache]'s maximumSize and maximumSizeBytes. - static void initialize() { + void initialize() { ImageCache().maximumSize = 1000; ImageCache().maximumSizeBytes = 128 << 20; } diff --git a/lib/core/in_app_purchases.dart b/lib/core/in_app_purchases.dart index 99bfaa74..174648ac 100644 --- a/lib/core/in_app_purchases.dart +++ b/lib/core/in_app_purchases.dart @@ -3,35 +3,40 @@ import 'package:in_app_purchase/in_app_purchase.dart'; class LunaInAppPurchases { // Donation IAP IDs - static const IAP_ID_DONATION_01 = 'donation_01'; - static const IAP_ID_DONATION_03 = 'donation_03'; - static const IAP_ID_DONATION_05 = 'donation_05'; - static const IAP_ID_DONATION_10 = 'donation_10'; - static const IAP_DONATION_IDS = [ - IAP_ID_DONATION_01, - IAP_ID_DONATION_03, - IAP_ID_DONATION_05, - IAP_ID_DONATION_10, - ]; + static const DONATION_01 = 'donation_01'; + static const DONATION_03 = 'donation_03'; + static const DONATION_05 = 'donation_05'; + static const DONATION_10 = 'donation_10'; + static const DONATION_IDS = [DONATION_01, DONATION_03, DONATION_05, DONATION_10]; static InAppPurchaseConnection get connection => InAppPurchaseConnection.instance; static StreamSubscription> _purchaseStream; static List donationIAPs = []; static bool isAvailable = false; + + /// Callback function for [purchaseUpdatedStream]. + static Future _purchasedCallback(List purchases) async { + for(var purchase in purchases) { + /// Handle donations by always completing the purchase + if(DONATION_IDS.contains(purchase.productID)) { + if(purchase.pendingCompletePurchase) await connection.completePurchase(purchase); + } + } + } /// Initialize the in-app purchases connection. /// /// - Enables pending purchase /// - Attach [InAppPurchaseConnection.instance.purchaseUpdateStream] listener for automatically completing IAP purchases /// - Fetch and store donation IAPs - static Future initialize() async { + Future initialize() async { InAppPurchaseConnection.enablePendingPurchases(); isAvailable = await connection.isAvailable(); if(isAvailable) { // Open listener _purchaseStream = connection.purchaseUpdatedStream.listen((data) => LunaInAppPurchases._purchasedCallback(data)); // Load donation IAPs - ProductDetailsResponse donation = await connection.queryProductDetails(Set.from(IAP_DONATION_IDS)); + ProductDetailsResponse donation = await connection.queryProductDetails(Set.from(DONATION_IDS)); donationIAPs = donation?.productDetails ?? []; donationIAPs.sort((a, b) => a?.id?.compareTo(b?.id) ?? 0); } @@ -39,19 +44,6 @@ class LunaInAppPurchases { /// Deinitialize the in-app purchases connection. /// - /// - Cleanly cancel/close the [InAppPurchaseConnection.instance.purchaseUpdateStream] listener - static Future deinitialize() async => _purchaseStream?.cancel(); - - /// Callback function for [purchaseUpdatedStream]. - /// - /// **Donations**: - /// - Automatically completes the purchase, as there is no actual product to deliver - static Future _purchasedCallback(List purchases) async { - for(var purchase in purchases) { - /// Handle donations by always completing the purchase - if(IAP_DONATION_IDS.contains(purchase.productID)) { - if(purchase.pendingCompletePurchase) await connection.completePurchase(purchase); - } - } - } + /// - Cleanly cancel/close the [InAppPurchaseConnection.instance.purchaseUpdateStream] listener. + Future deinitialize() async => _purchaseStream?.cancel(); } diff --git a/lib/core/logger.dart b/lib/core/logger.dart index 2d7b2dad..c10b6931 100644 --- a/lib/core/logger.dart +++ b/lib/core/logger.dart @@ -13,7 +13,7 @@ class LunaLogger { static const CHECK_LOGS_MESSAGE = 'Check the logs for more details'; /// Initialize the logger by setting the timestamp format and capturing errors on [FlutterError.onError]. - static void initialize() { + void initialize() { LogsConfig config = FLog.getDefaultConfigurations() ..formatType = FormatType.FORMAT_SQUARE ..timestampFormat = 'MMMM dd, y - hh:mm:ss a'; diff --git a/lib/core/luna_ui.dart b/lib/core/luna_ui.dart index a3ec5acd..931ca3d3 100644 --- a/lib/core/luna_ui.dart +++ b/lib/core/luna_ui.dart @@ -1,11 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; export 'luna_ui/appbar.dart'; +export 'luna_ui/bottom_navigation_bar.dart'; +export 'luna_ui/button.dart'; +export 'luna_ui/card.dart'; +export 'luna_ui/colors.dart'; +export 'luna_ui/containers.dart'; +export 'luna_ui/controllers.dart'; export 'luna_ui/decoration.dart'; +export 'luna_ui/divider.dart'; +export 'luna_ui/header.dart'; +export 'luna_ui/highlighted_node.dart'; +export 'luna_ui/icons.dart'; +export 'luna_ui/input_bar.dart'; +export 'luna_ui/invalid_route.dart'; +export 'luna_ui/list_tile.dart'; +export 'luna_ui/list_view.dart'; +export 'luna_ui/loader.dart'; +export 'luna_ui/message.dart'; +export 'luna_ui/network_image.dart'; +export 'luna_ui/popup_menu_button.dart'; +export 'luna_ui/refresh_indicator.dart'; +export 'luna_ui/shape.dart'; export 'luna_ui/sliver_sticky_header.dart'; export 'luna_ui/snackbar.dart'; +export 'luna_ui/switch.dart'; +export 'luna_ui/table.dart'; export 'luna_ui/text.dart'; class LunaUI { - static const double FONT_SIZE_APPBAR = 18.0; - - LunaUI._(); + // Text Constants + static const String TEXT_ARROW_LEFT = '←'; + static const String TEXT_ARROW_RIGHT = '→'; + static const String TEXT_BULLET = '•'; + static const String TEXT_ELLIPSIS = '…'; + static const String TEXT_EMDASH = '—'; + // Font-Related + static const double FONT_SIZE_APP_BAR = 18.0; + static const double FONT_SIZE_BUTTON = 14.0; + static const double FONT_SIZE_GRAPH_LEGEND = 10.0; + static const double FONT_SIZE_HEADER = 18.0; + static const double FONT_SIZE_HIGHLIGHTED_NODE = 12.0; + static const double FONT_SIZE_MESSAGES = 16.0; + static const double FONT_SIZE_NAVIGATION_BAR = 13.0; + static const double FONT_SIZE_SUBHEADER = 12.0; + static const double FONT_SIZE_SUBTITLE = 13.0; + static const double FONT_SIZE_TITLE = 16.0; + static const FontWeight FONT_WEIGHT_BOLD = FontWeight.w600; + // UI-Related + static const int ANIMATION_SPEED = 250; + static const int ANIMATION_IMAGE_FADE_IN_SPEED = 125; + static const double BORDER_RADIUS = 10.0; + static const double ELEVATION = 0.0; + static const EdgeInsets MARGIN_CARD = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0); + // Border-Related + static bool get shouldUseBorder => LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data; + static ShapeBorder get shapeBorder => shouldUseBorder ? LunaShapeBorder.roundedWithBorder() : LunaShapeBorder.rounded(); } diff --git a/lib/core/luna_ui/appbar.dart b/lib/core/luna_ui/appbar.dart index 98f3c3ea..ddba0d71 100644 --- a/lib/core/luna_ui/appbar.dart +++ b/lib/core/luna_ui/appbar.dart @@ -1 +1,263 @@ -export 'appbar/appbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lunasea/core.dart'; + +enum _APPBAR_TYPE { + DEFAULT, + EMPTY, + DROPDOWN, +} + +class LunaAppBar extends StatefulWidget implements PreferredSizeWidget { + final _APPBAR_TYPE type; + final String title; + final List actions; + final PreferredSizeWidget bottom; + final bool useDrawer; + final LunaModuleState state; + final Widget child; + final double height; + final List profiles; + final PageController pageController; + final List scrollControllers; + + @override + Size get preferredSize { + double _size = kToolbarHeight+6.0; + if(bottom != null) _size += bottom.preferredSize.height; + return new Size.fromHeight(_size); + } + + LunaAppBar._internal({ + @required this.type, + @required this.useDrawer, + this.title, + this.actions, + this.bottom, + this.state, + this.child, + this.height, + this.profiles, + this.pageController, + this.scrollControllers, + }); + + /// Create a new [AppBar] widget pre-styled for LunaSea. + /// + /// Will register an onTap gesture for the AppBar if: + /// - [state] is supplied, and will call [scrollBackList] on the state controller. + /// - [pageController] and [scrollControllers] are supplied, will register a listener on the page controller and scroll back the respective scroll controller. + /// + /// Passing in all 3 will result in state.scrollBackList taking precedence. + factory LunaAppBar({ + @required String title, + List actions, + PreferredSizeWidget bottom, + bool useDrawer = false, + LunaModuleState state, + PageController pageController, + List scrollControllers, + }) { + assert(title != null); + if(pageController != null) assert(scrollControllers != null, 'pageController is defined, scrollControllers should as well.'); + return LunaAppBar._internal( + title: title, + actions: actions, + bottom: bottom, + useDrawer: useDrawer, + state: state, + pageController: pageController, + scrollControllers: scrollControllers, + type: _APPBAR_TYPE.DEFAULT, + ); + } + + /// Create a new, empty [LunaAppBar] which can be used to attach to a [Scaffold] in a [PageView] that is already wrapped in an [AppBar]. + /// + /// Example usages would be a [PageView] but a single page needs an [AppBar] bottom widget. + factory LunaAppBar.empty({ + @required Widget child, + @required double height, + }) { + assert(child != null); + assert(height != null); + return LunaAppBar._internal( + child: child, + height: height, + useDrawer: false, + type: _APPBAR_TYPE.EMPTY, + ); + } + + /// Create a [lunaAppBar] with the title widget having a dropdown to switch to the supplied list of profiles. + /// + /// Will register an onTap gesture for the AppBar if: + /// - [state] is supplied, and will call [scrollBackList] on the state controller. + /// - [pageController] and [scrollControllers] are supplied, will register a listener on the page controller and scroll back the respective scroll controller. + /// + /// Passing in all 3 will result in state.scrollBackList taking precedence. + factory LunaAppBar.dropdown({ + @required String title, + @required List profiles, + bool useDrawer = true, + List actions, + LunaModuleState state, + PageController pageController, + List scrollControllers, + }) { + assert(title != null); + assert(profiles != null); + if(pageController != null) assert(scrollControllers != null, 'if pageController is defined, scrollControllers should as well.'); + if(profiles == null || profiles.length < 2) return LunaAppBar._internal( + title: title, + actions: actions, + useDrawer: useDrawer, + state: state, + pageController: pageController, + scrollControllers: scrollControllers, + type: _APPBAR_TYPE.DEFAULT, + ); + return LunaAppBar._internal( + title: title, + profiles: profiles, + actions: actions, + useDrawer: useDrawer, + state: state, + pageController: pageController, + scrollControllers: scrollControllers, + type: _APPBAR_TYPE.DROPDOWN, + ); + } + + @override + State createState() => _State(); +} + +class _State extends State { + int _index; + + @override + void initState() { + _index = widget.pageController?.initialPage ?? 0; + widget.pageController?.addListener(_pageControllerListener); + super.initState(); + } + + @override + void dispose() { + widget.pageController?.removeListener(_pageControllerListener); + super.dispose(); + } + + void _pageControllerListener() { + if((widget.pageController?.page?.round() ?? _index) == _index) return; + _index = widget.pageController.page.round(); + } + + void _onTap() { + try { + if(widget.scrollControllers != null && widget.scrollControllers[_index] != null) widget.scrollControllers[_index].lunaAnimateToStart(); + } catch (error, stack) { + LunaLogger().error('Failed to scroll back: Index: $_index, ScrollControllers: ${widget.scrollControllers?.length ?? 0}', error, stack); + } + } + + @override + Widget build(BuildContext context) { + Widget child; + switch(widget.type) { + case _APPBAR_TYPE.DEFAULT: child = _default(context); break; + case _APPBAR_TYPE.EMPTY: child = _empty(context); break; + case _APPBAR_TYPE.DROPDOWN: child = _dropdown(context); break; + default: throw Exception('Unknown AppBar type.'); + } + return GestureDetector( + child: child, + onTap: widget.state?.scrollBackList ?? _onTap, + ); + } + + Widget _sharedLeading(BuildContext context) { + if(widget.useDrawer) return IconButton( + icon: Icon(Icons.menu), + onPressed: () async { + HapticFeedback.lightImpact(); + if(Scaffold.of(context).hasDrawer) Scaffold.of(context).openDrawer(); + }, + ); + return InkWell( + child: Icon(Icons.arrow_back_ios), + onTap: () async { + HapticFeedback.lightImpact(); + Navigator.of(context).lunaSafetyPop(); + }, + onLongPress: () async { + HapticFeedback.heavyImpact(); + Navigator.of(context).lunaPopToFirst(); + }, + borderRadius: BorderRadius.circular(28.0), + ); + } + + Widget _default(BuildContext context) { + return AppBar( + title: Text( + widget.title ?? '', + overflow: TextOverflow.fade, + style: TextStyle(fontSize: LunaUI.FONT_SIZE_APP_BAR), + ), + leading: _sharedLeading(context), + centerTitle: false, + elevation: 0, + actions: widget.actions, + bottom: widget.bottom, + ); + } + + Widget _empty(BuildContext context) { + return AppBar( + automaticallyImplyLeading: false, + toolbarHeight: widget.height, + leadingWidth: 0.0, + elevation: 0.0, + titleSpacing: 0.0, + title: widget.child, + ); + } + + Widget _dropdown(BuildContext context) { + return AppBar( + title: LunaPopupMenuButton( + child: Wrap( + direction: Axis.horizontal, + children: [ + Text( + widget.title, + style: TextStyle(fontSize: Constants.UI_FONT_SIZE_HEADER), + ), + LSIcon(icon: Icons.arrow_drop_down), + ], + ), + onSelected: (result) { + HapticFeedback.selectionClick(); + LunaProfile().safelyChangeProfiles(context, result); + }, + itemBuilder: (context) { + return >[for(String profile in widget.profiles) PopupMenuItem( + value: profile, + child: Text( + profile, + style: TextStyle( + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + ), + )]; + }, + ), + leading: _sharedLeading(context), + centerTitle: false, + elevation: 0, + actions: widget.actions, + ); + } +} diff --git a/lib/core/luna_ui/appbar/appbar.dart b/lib/core/luna_ui/appbar/appbar.dart deleted file mode 100644 index a5c75ef1..00000000 --- a/lib/core/luna_ui/appbar/appbar.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class LunaAppBar extends AppBar { - /// Create a new [AppBar] widget pre-styled for LunaSea - LunaAppBar({ - @required BuildContext context, - @required String title, - List actions, - PreferredSizeWidget bottom, - bool hideLeading = false, - }) : super( - title: Text( - title ?? '', - overflow: TextOverflow.fade, - style: TextStyle( - fontSize: LunaUI.FONT_SIZE_APPBAR, - ), - ), - leading: hideLeading ? null : InkWell( - child: Icon(Icons.arrow_back_ios), - onTap: () async => Navigator.of(context).pop(), - onLongPress: () async => Navigator.of(context).popUntil((route) => route.isFirst), - borderRadius: BorderRadius.circular(28.0), - ), - centerTitle: false, - elevation: 0, - actions: actions, - bottom: bottom, - ); - - LunaAppBar.empty({ - @required Widget child, - @required double height, - }) : super( - automaticallyImplyLeading: false, - toolbarHeight: height, - leadingWidth: 0.0, - elevation: 0.0, - titleSpacing: 0.0, - title: child, - ); -} - diff --git a/lib/core/luna_ui/bottom_navigation_bar.dart b/lib/core/luna_ui/bottom_navigation_bar.dart new file mode 100644 index 00000000..aca69743 --- /dev/null +++ b/lib/core/luna_ui/bottom_navigation_bar.dart @@ -0,0 +1,2 @@ +export 'bottom_navigation_bar/badge.dart'; +export 'bottom_navigation_bar/bottom_navigation_bar.dart'; diff --git a/lib/core/luna_ui/bottom_navigation_bar/badge.dart b/lib/core/luna_ui/bottom_navigation_bar/badge.dart new file mode 100644 index 00000000..17cdaee3 --- /dev/null +++ b/lib/core/luna_ui/bottom_navigation_bar/badge.dart @@ -0,0 +1,33 @@ +import 'package:badges/badges.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaNavigationBarBadge extends Badge { + LunaNavigationBarBadge({ + Key key, + @required String text, + @required IconData icon, + @required bool showBadge, + @required bool isActive, + }) : super( + key: key, + badgeColor: LunaColours.accent, + elevation: 0, + animationDuration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), + animationType: BadgeAnimationType.fade, + shape: BadgeShape.circle, + position: BadgePosition.topEnd( + top: -15, + end: -15, + ), + badgeContent: Text( + text, + style: TextStyle(color: Colors.white), + ), + child: Icon( + icon, + color: isActive ? LunaColours.accent : Colors.white, + ), + showBadge: showBadge, + ); +} \ No newline at end of file diff --git a/lib/core/luna_ui/bottom_navigation_bar/bottom_navigation_bar.dart b/lib/core/luna_ui/bottom_navigation_bar/bottom_navigation_bar.dart new file mode 100644 index 00000000..ab74611c --- /dev/null +++ b/lib/core/luna_ui/bottom_navigation_bar/bottom_navigation_bar.dart @@ -0,0 +1,99 @@ +import 'package:google_nav_bar/google_nav_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaBottomNavigationBar extends StatefulWidget { + final PageController pageController; + final List icons; + final List titles; + final List scrollControllers; + final Function(int) onTabChange; + final List leadingOnTab; + + LunaBottomNavigationBar({ + Key key, + @required this.pageController, + @required this.icons, + @required this.titles, + this.onTabChange, + this.leadingOnTab, + this.scrollControllers, + }) : super(key: key) { + assert(icons.length == titles.length, 'An unequal amount of titles and icons were passed to LunaNavigationBar.'); + if(leadingOnTab != null) assert(icons.length == leadingOnTab.length, 'An unequal amount of icons and leadingOnTab were passed to LunaNavigationBar.'); + if(scrollControllers != null) assert(icons.length == scrollControllers.length, 'An unequal amount of icons and scrollControllers were passed to LunaNavigationBar.'); + } + + @override + State createState() => _State(); +} + +class _State extends State { + int _index; + + @override + void initState() { + _index = widget.pageController.initialPage; + widget.pageController?.addListener(_pageControllerListener); + super.initState(); + } + + @override + void dispose() { + widget.pageController?.removeListener(_pageControllerListener); + super.dispose(); + } + + void _pageControllerListener() { + if((widget.pageController?.page?.round() ?? _index) == _index) return; + setState(() => _index = widget.pageController.page.round()); + } + + @override + Widget build(BuildContext context) { + return Container( + child: SafeArea( + child: Padding( + child: GNav( + gap: 8.0, + iconSize: 24.0, + padding: EdgeInsets.fromLTRB(18.0, 10.0, 12.0, 10.0), + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), + tabBackgroundColor: Theme.of(context).canvasColor, + activeColor: LunaColours.accent, + tabs: List.generate(widget.icons.length, (index) => GButton( + icon: widget.icons[index], + text: widget.titles[index], + iconSize: 22.0, + haptic: true, + iconColor: Colors.white, + textStyle: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_NAVIGATION_BAR, + color: LunaColours.accent, + ), + leading: widget.leadingOnTab == null ? null : widget.leadingOnTab[index], + )).toList(), + tabActiveBorder: LunaUI.shouldUseBorder ? Border.all(color: Colors.white12) : null, + selectedIndex: _index, + onTabChange: (index) { + _onTabChange(index); + if(widget.onTabChange != null) widget.onTabChange(index); + }, + ), + padding: EdgeInsets.all(12.0), + ), + top: false, + ), + decoration: BoxDecoration(color: Theme.of(context).primaryColor), + ); + } + + void _onTabChange(int index) { + if(index == _index) { + if(widget.scrollControllers != null && widget.scrollControllers[index] != null) widget.scrollControllers[index].lunaAnimateToStart(); + } else { + widget.pageController.lunaAnimateToPage(index); + } + } +} diff --git a/lib/core/luna_ui/button.dart b/lib/core/luna_ui/button.dart new file mode 100644 index 00000000..47d72a74 --- /dev/null +++ b/lib/core/luna_ui/button.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lunasea/core.dart'; + +enum _BUTTON_SIZE { + REGULAR, + SLIM, +} + +class LunaButton extends StatelessWidget { + final Color textColor; + final Color backgroundColor; + final EdgeInsets margin; + final LunaLoadingState loadingState; + final Function onTap; + final Function onLongPress; + final String text; + final Widget child; + final _BUTTON_SIZE size; + + LunaButton._({ + Key key, + @required this.onTap, + @required this.onLongPress, + @required this.size, + @required this.text, + @required this.child, + @required this.textColor, + @required this.backgroundColor, + @required this.margin, + @required this.loadingState, + }) : super(key: key); + + /// Create a standard-sized button. + factory LunaButton({ + Key key, + @required Function onTap, + Function onLongPress, + String text = '', + Widget child, + Color textColor = Colors.white, + Color backgroundColor = LunaColours.accent, + EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + LunaLoadingState loadingState = LunaLoadingState.INACTIVE, + }) { + return LunaButton._( + key: key, + onTap: onTap, + onLongPress: onLongPress, + text: text, + child: child, + textColor: textColor, + backgroundColor: backgroundColor, + margin: margin, + loadingState: loadingState, + size: _BUTTON_SIZE.REGULAR, + ); + } + + /// Create a slim-sized button, typically used within table blocks. + factory LunaButton.slim({ + Key key, + @required Function onTap, + Function onLongPress, + String text = '', + Widget child, + Color textColor = Colors.white, + Color backgroundColor = LunaColours.accent, + EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + LunaLoadingState loadingState = LunaLoadingState.INACTIVE, + }) { + return LunaButton._( + key: key, + onTap: onTap, + onLongPress: onLongPress, + text: text, + child: child, + textColor: textColor, + backgroundColor: backgroundColor, + margin: margin, + loadingState: loadingState, + size: _BUTTON_SIZE.SLIM, + ); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Card( + child: InkWell( + child: size == _BUTTON_SIZE.REGULAR ? _regular() : _slim(), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async { + HapticFeedback.lightImpact(); + if(onTap != null && loadingState != LunaLoadingState.ACTIVE) onTap(); + }, + onLongPress: () async { + HapticFeedback.heavyImpact(); + if(onLongPress != null && loadingState != LunaLoadingState.ACTIVE) onLongPress(); + }, + ), + margin: margin, + color: backgroundColor, + elevation: LunaUI.ELEVATION, + shape: LunaUI.shapeBorder, + ), + ), + ], + ); + } + + Widget _regular() { + return ListTile(title: _title()); + } + + Widget _slim() { + return Padding( + child: _title(), + padding: EdgeInsets.symmetric(vertical: 14.0), + ); + } + + Widget _title() { + if(loadingState == LunaLoadingState.ACTIVE) return LunaLoader( + color: textColor, + size: size == _BUTTON_SIZE.REGULAR ? 20.0 : 17.0, + ); + if(loadingState == LunaLoadingState.ERROR) return Icon( + Icons.error, + color: textColor, + size: size == _BUTTON_SIZE.REGULAR ? 20.0 : 17.0, + ); + if(child != null) return child; + return Text( + text, + style: TextStyle( + color: textColor, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_BUTTON, + ), + textAlign: TextAlign.center, + ); + } +} \ No newline at end of file diff --git a/lib/core/luna_ui/card.dart b/lib/core/luna_ui/card.dart new file mode 100644 index 00000000..3844552e --- /dev/null +++ b/lib/core/luna_ui/card.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaCard extends Card { + LunaCard({ + Key key, + @required BuildContext context, + @required Widget child, + EdgeInsets margin = LunaUI.MARGIN_CARD, + Color color, + Decoration decoration, + }) : super( + key: key, + child: Container(child: child, decoration: decoration), + margin: margin, + color: color == null ? Theme.of(context).primaryColor : color, + shape: LunaUI.shapeBorder, + elevation: LunaUI.ELEVATION, + clipBehavior: Clip.antiAlias, + ) { + assert(context != null); + assert(child != null); + } +} diff --git a/lib/core/ui/colors.dart b/lib/core/luna_ui/colors.dart similarity index 62% rename from lib/core/ui/colors.dart rename to lib/core/luna_ui/colors.dart index 4083cbe2..c4f5f926 100644 --- a/lib/core/ui/colors.dart +++ b/lib/core/luna_ui/colors.dart @@ -17,16 +17,16 @@ class LunaColours { Colors.blueGrey, ]; - static Color get accent => const Color(ACCENT_COLOR); - static Color get primary => const Color(PRIMARY_COLOR); - static Color get secondary => const Color(SECONDARY_COLOR); - static Color get splash => const Color(SPLASH_COLOR); + static const Color accent = const Color(ACCENT_COLOR); + static const Color primary = const Color(PRIMARY_COLOR); + static const Color secondary = const Color(SECONDARY_COLOR); + static const Color splash = const Color(SPLASH_COLOR); - static Color get blue => Colors.blue; - static Color get orange => Colors.orange; - static Color get red => Colors.red; - static Color get purple => Colors.purpleAccent; - static Color get blueGrey => Colors.blueGrey; + static const Color blue = Colors.blue; + static const Color orange = Colors.orange; + static const Color red = Colors.red; + static const Color purple = Colors.purpleAccent; + static const Color blueGrey = Colors.blueGrey; static Color list(int i) { return LIST_COLOR_ICONS[i%LIST_COLOR_ICONS.length]; diff --git a/lib/core/luna_ui/containers.dart b/lib/core/luna_ui/containers.dart new file mode 100644 index 00000000..d6d516d7 --- /dev/null +++ b/lib/core/luna_ui/containers.dart @@ -0,0 +1 @@ +export 'containers/button_container.dart'; diff --git a/lib/core/luna_ui/containers/button_container.dart b/lib/core/luna_ui/containers/button_container.dart new file mode 100644 index 00000000..096aead1 --- /dev/null +++ b/lib/core/luna_ui/containers/button_container.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class LunaButtonContainer extends StatelessWidget { + final List children; + final EdgeInsets padding; + + LunaButtonContainer({ + Key key, + @required this.children, + this.padding = const EdgeInsets.symmetric(horizontal: 6.0), + }) { + assert(children != null); + assert(children.length > 0); + assert(padding != null); + } + + @override + Widget build(BuildContext context) { + return Padding( + child: Row( + children: children.map((button) => Expanded(child: button)).toList(), + ), + padding: padding, + ); + } +} \ No newline at end of file diff --git a/lib/core/luna_ui/controllers.dart b/lib/core/luna_ui/controllers.dart new file mode 100644 index 00000000..7df70c0a --- /dev/null +++ b/lib/core/luna_ui/controllers.dart @@ -0,0 +1 @@ +export 'controllers/page_controller.dart'; diff --git a/lib/core/luna_ui/controllers/page_controller.dart b/lib/core/luna_ui/controllers/page_controller.dart new file mode 100644 index 00000000..7f951d90 --- /dev/null +++ b/lib/core/luna_ui/controllers/page_controller.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +/// Re-definition of [PageController] that adds a listener that will unfocus primary focus on a page change. +/// +/// Needed for situations that a page has a keyboard prompt to unfocus the keyboard. +class LunaPageController extends PageController { + LunaPageController({ + int initialPage, + }) : super( + initialPage: initialPage, + ) { + this?.addListener(_focusListener); + } + + void _focusListener() { + FocusManager.instance.primaryFocus?.unfocus(); + } + + @override + void dispose() { + this?.removeListener(_focusListener); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/core/luna_ui/divider.dart b/lib/core/luna_ui/divider.dart new file mode 100644 index 00000000..17331404 --- /dev/null +++ b/lib/core/luna_ui/divider.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaDivider extends Divider { + LunaDivider() : super( + thickness: 1.0, + color: LunaColours.splash, + indent: 60.0, + endIndent: 60.0, + ); +} diff --git a/lib/core/luna_ui/header.dart b/lib/core/luna_ui/header.dart new file mode 100644 index 00000000..b42aed2f --- /dev/null +++ b/lib/core/luna_ui/header.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaHeader extends StatelessWidget { + final String text; + final String subtitle; + + LunaHeader({ + @required this.text, + this.subtitle, + }); + + @override + Widget build(BuildContext context) { + return Padding( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text.toUpperCase(), + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_HEADER, + color: Colors.white, + ), + ), + Padding( + child: Container( + height: 2.0, + width: 48.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + color: LunaColours.accent, + ), + ), + padding: EdgeInsets.only( + top: 4.0, + left: 1.0, + bottom: subtitle != null ? 6.0 : 0.0, + ), + ), + if(subtitle != null) Text( + subtitle, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_SUBHEADER, + color: Colors.white70, + fontWeight: FontWeight.w300, + ), + ), + ], + ), + padding: EdgeInsets.fromLTRB(12.0, 6.0, 12.0, 8.0), + ); + } +} diff --git a/lib/core/luna_ui/highlighted_node.dart b/lib/core/luna_ui/highlighted_node.dart new file mode 100644 index 00000000..170d6713 --- /dev/null +++ b/lib/core/luna_ui/highlighted_node.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaHighlightedNode extends StatelessWidget { + final Color backgroundColor; + final Color textColor; + final String text; + + LunaHighlightedNode({ + @required this.text, + this.backgroundColor = LunaColours.accent, + this.textColor = Colors.white, + }) { + assert(text != null); + } + + @override + Widget build(BuildContext context) { + return Container( + child: Padding( + child: Text( + text, + maxLines: 1, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_HIGHLIGHTED_NODE, + color: textColor, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + ), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + ), + ); + } +} diff --git a/lib/core/luna_ui/icons.dart b/lib/core/luna_ui/icons.dart new file mode 100644 index 00000000..34de38ff --- /dev/null +++ b/lib/core/luna_ui/icons.dart @@ -0,0 +1,2 @@ +export 'icons/icon_button.dart'; +export 'icons/icon_custom.dart'; diff --git a/lib/core/luna_ui/icons/icon_button.dart b/lib/core/luna_ui/icons/icon_button.dart new file mode 100644 index 00000000..ef58cf5e --- /dev/null +++ b/lib/core/luna_ui/icons/icon_button.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lunasea/core.dart'; + +class LunaIconButton extends StatelessWidget { + final IconData icon; + final double iconSize; + final String text; + final double textSize; + final Color color; + final Function onPressed; + final Function onLongPress; + final LunaLoadingState loadingState; + + LunaIconButton({ + Key key, + this.text, + this.textSize = 10.0, + this.icon, + this.iconSize = 24.0, + this.color = Colors.white, + this.onPressed, + this.onLongPress, + this.loadingState, + }) : super(key: key) { + assert((text != null || icon != null), 'both text and icon cannot be null'); + } + + @override + Widget build(BuildContext context) { + return InkWell( + child: IconButton( + icon: loadingState == LunaLoadingState.ACTIVE ? _loader() : _icon(), + iconSize: iconSize, + onPressed: _onPressed(), + ), + onLongPress: _onLongPress(), + ); + } + + Function _onPressed() { + if(onPressed == null) return null; + if(loadingState == LunaLoadingState.ACTIVE) return null; + return () async { + HapticFeedback.lightImpact(); + onPressed(); + }; + } + + Function _onLongPress() { + if(onLongPress == null) return null; + if(loadingState == LunaLoadingState.ACTIVE) return null; + return () async { + HapticFeedback.heavyImpact(); + onLongPress(); + }; + } + + Widget _loader() { + return LunaLoader(size: 12.0, color: color); + } + + Widget _icon() { + if(icon != null) return Icon(icon, color: color); + return Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: color, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: textSize, + ), + ); + } +} \ No newline at end of file diff --git a/lib/core/luna_ui/icons/icon_custom.dart b/lib/core/luna_ui/icons/icon_custom.dart new file mode 100644 index 00000000..41d73972 --- /dev/null +++ b/lib/core/luna_ui/icons/icon_custom.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart'; + +class LunaIcons { + LunaIcons._(); + + static const _kFontFam = 'CustomIcons'; + static const _kFontPkg = null; + + static const IconData arrow_right = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar_missing = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData calendar = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData clients = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData documentation = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData github = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData history = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData home = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData layers = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData monitoring = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData movies = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData music = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData nzbget = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData queue = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData reddit = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData rss = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sabnzbd = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData settings = IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData television = IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData upcoming = IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData user = IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData bravebrowser = IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData microsoftedge = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData firefox = IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData safari = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chrome = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData code = IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData modules = IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData discord = IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData tautulli = IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData plex = IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData radarr = IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/core/luna_ui/input_bar.dart b/lib/core/luna_ui/input_bar.dart new file mode 100644 index 00000000..5a42c3e9 --- /dev/null +++ b/lib/core/luna_ui/input_bar.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaTextInputBar extends StatefulWidget { + final TextEditingController controller; + final TextInputAction action; + final TextInputType keyboardType; + final Iterable autofillHints; + final String labelText; + final IconData labelIcon; + final bool autofocus; + final bool obscureText; + final bool isFormField; + final void Function(String) onChanged; + final void Function(String) onSubmitted; + final String Function(String) validator; + final EdgeInsets margin; + + LunaTextInputBar({ + @required this.controller, + this.onChanged, + this.onSubmitted, + this.validator, + this.autofillHints, + this.action = TextInputAction.search, + this.keyboardType = TextInputType.text, + this.labelText = 'Search...', + this.labelIcon = Icons.search, + this.margin = LunaUI.MARGIN_CARD, + this.autofocus = false, + this.obscureText = false, + this.isFormField = false, + }) { + assert(controller != null); + } + + @override + State createState() => _State(); +} + +class _State extends State { + bool _isFocused = false; + + @override + Widget build(BuildContext context) => LSCard( + margin: widget.margin, + child: FocusScope( + child: Focus( + onFocusChange: (value) => setState(() => _isFocused = value), + child: widget.isFormField ? _isForm : _isNotForm, + ), + ), + color: Theme.of(context).canvasColor, + ); + + TextStyle get _sharedTextStyle => TextStyle( + color: Colors.white, + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ); + + InputDecoration get _sharedInputDecoration => InputDecoration( + labelText: widget.labelText, + labelStyle: TextStyle( + color: Colors.white54, + decoration: TextDecoration.none, + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + suffixIcon: AnimatedOpacity( + child: GestureDetector( + child: Icon( + Icons.close, + color: LunaColours.accent, + size: 24.0, + ), + onTap: widget.onChanged == null ? null : () { + widget.controller.text = ''; + widget.onChanged(''); + } + ), + opacity: !_isFocused || widget.controller.text == '' ? 0.0 : 1.0, + duration: Duration(milliseconds: 200), + ), + icon: Padding( + child: Icon( + widget.labelIcon, + color: LunaColours.accent, + ), + padding: EdgeInsets.only(left: 16.0), + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 8.0), + ); + + TextFormField get _isForm => TextFormField( + autofillHints: widget.autofillHints, + autofocus: widget.autofocus, + controller: widget.controller, + decoration: _sharedInputDecoration, + style: _sharedTextStyle, + cursorColor: LunaColours.accent, + textInputAction: widget.action, + obscureText: widget.obscureText, + autocorrect: false, + keyboardType: widget.keyboardType, + validator: widget?.validator, + onChanged: widget.onChanged == null ? null : widget.onChanged, + onFieldSubmitted: widget?.onSubmitted, + ); + + TextField get _isNotForm => TextField( + autofillHints: widget.autofillHints, + autofocus: widget.autofocus, + controller: widget.controller, + decoration: _sharedInputDecoration, + style: _sharedTextStyle, + cursorColor: LunaColours.accent, + textInputAction: widget.action, + obscureText: widget.obscureText, + autocorrect: false, + keyboardType: widget.keyboardType, + onChanged: widget.onChanged == null ? null : widget.onChanged, + onSubmitted: widget?.onSubmitted, + ); +} diff --git a/lib/core/luna_ui/invalid_route.dart b/lib/core/luna_ui/invalid_route.dart new file mode 100644 index 00000000..f14f0269 --- /dev/null +++ b/lib/core/luna_ui/invalid_route.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaInvalidRoute extends StatelessWidget { + final String title; + final String message; + + LunaInvalidRoute({ + Key key, + this.title = 'LunaSea', + this.message = '404: Not Found', + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: LunaAppBar(title: title), + body: LunaMessage.goBack( + context: context, + text: message, + ), + ); + } +} diff --git a/lib/core/luna_ui/list_tile.dart b/lib/core/luna_ui/list_tile.dart new file mode 100644 index 00000000..81a9c9d5 --- /dev/null +++ b/lib/core/luna_ui/list_tile.dart @@ -0,0 +1 @@ +export 'list_tile/list_tile.dart'; diff --git a/lib/core/luna_ui/list_tile/list_tile.dart b/lib/core/luna_ui/list_tile/list_tile.dart new file mode 100644 index 00000000..cac0af82 --- /dev/null +++ b/lib/core/luna_ui/list_tile/list_tile.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaListTile extends Card { + LunaListTile({ + Key key, + @required BuildContext context, + EdgeInsets margin = LunaUI.MARGIN_CARD, + @required Widget title, + Widget subtitle, + Widget trailing, + Widget leading, + Color color, + Decoration decoration, + Function onTap, + Function onLongPress, + bool contentPadding = false, + EdgeInsets customContentPadding, + }) : super( + key: key, + child: Container( + child: InkWell( + child: ListTile( + title: title, + subtitle: subtitle, + trailing: trailing, + leading: leading, + contentPadding: contentPadding + ? customContentPadding != null + ? customContentPadding + : EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0) + : null, + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: onTap, + onLongPress: onLongPress, + ), + decoration: decoration, + ), + margin: margin, + elevation: LunaUI.ELEVATION, + shape: LunaUI.shapeBorder, + color: color ?? Theme.of(context).primaryColor, + ); +} \ No newline at end of file diff --git a/lib/core/luna_ui/list_view.dart b/lib/core/luna_ui/list_view.dart new file mode 100644 index 00000000..58da96dc --- /dev/null +++ b/lib/core/luna_ui/list_view.dart @@ -0,0 +1,4 @@ +export 'list_view/list_view.dart'; +export 'list_view/list_view_builder.dart'; +export 'list_view/list_view_modal.dart'; +export 'list_view/paged_list_view.dart'; diff --git a/lib/core/luna_ui/list_view/list_view.dart b/lib/core/luna_ui/list_view/list_view.dart new file mode 100644 index 00000000..45a9eac3 --- /dev/null +++ b/lib/core/luna_ui/list_view/list_view.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class LunaListView extends StatefulWidget { + final List children; + final EdgeInsetsGeometry padding; + final ScrollController scrollController; + final ScrollPhysics physics; + + LunaListView({ + Key key, + @required this.children, + this.scrollController, + this.padding, + this.physics = const AlwaysScrollableScrollPhysics(), + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = widget.scrollController ?? ScrollController(); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus(); + return null; + }, + child: Scrollbar( + controller: _scrollController, + child: ListView( + controller: _scrollController, + children: widget.children, + padding: widget.padding != null ? widget.padding : EdgeInsets.only( + top: 8.0, + bottom: 8.0+(MediaQuery.of(context).padding.bottom/5), + ), + physics: widget.physics, + ), + ), + ); + } +} diff --git a/lib/core/luna_ui/list_view/list_view_builder.dart b/lib/core/luna_ui/list_view/list_view_builder.dart new file mode 100644 index 00000000..13ddbe93 --- /dev/null +++ b/lib/core/luna_ui/list_view/list_view_builder.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class LunaListViewBuilder extends StatefulWidget { + final int itemCount; + final Widget Function(BuildContext, int) itemBuilder; + final EdgeInsetsGeometry padding; + final ScrollController scrollController; + final ScrollPhysics physics; + + LunaListViewBuilder({ + Key key, + @required this.itemCount, + @required this.itemBuilder, + this.scrollController, + this.padding, + this.physics = const AlwaysScrollableScrollPhysics(), + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = widget.scrollController ?? ScrollController(); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus(); + return null; + }, + child: Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: widget.padding != null ? widget.padding : EdgeInsets.only( + top: 8.0, + bottom: 8.0+(MediaQuery.of(context).padding.bottom/5), + ), + physics: widget.physics, + itemCount: widget.itemCount, + itemBuilder: widget.itemBuilder, + ), + ), + ); + } +} diff --git a/lib/core/luna_ui/list_view/list_view_modal.dart b/lib/core/luna_ui/list_view/list_view_modal.dart new file mode 100644 index 00000000..515ff1e3 --- /dev/null +++ b/lib/core/luna_ui/list_view/list_view_modal.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class LunaListViewModal extends StatelessWidget { + final List children; + + LunaListViewModal({ + Key key, + @required this.children, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scrollbar( + child: ListView( + children: children, + padding: EdgeInsets.only( + top: 8.0, + bottom: 8.0+(MediaQuery.of(context).padding.bottom/5), + ), + physics: AlwaysScrollableScrollPhysics(), + ), + ); + } +} diff --git a/lib/core/luna_ui/list_view/paged_list_view.dart b/lib/core/luna_ui/list_view/paged_list_view.dart new file mode 100644 index 00000000..382a3f42 --- /dev/null +++ b/lib/core/luna_ui/list_view/paged_list_view.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:lunasea/core.dart'; + +class LunaPagedListView extends StatefulWidget { + final GlobalKey refreshKey; + final PagingController pagingController; + final ScrollController scrollController; + final void Function(int) listener; + final Widget Function(BuildContext, T, int) itemBuilder; + final EdgeInsetsGeometry padding; + final String noItemsFoundMessage; + + LunaPagedListView({ + Key key, + @required this.refreshKey, + @required this.pagingController, + this.scrollController, + @required this.listener, + @required this.itemBuilder, + @required this.noItemsFoundMessage, + this.padding, + }) : super(key: key) { + assert(refreshKey != null); + assert(pagingController != null); + assert(listener != null); + assert(itemBuilder != null); + assert(noItemsFoundMessage != null); + } + + @override + State createState() => _State(); +} + +class _State extends State> { + ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = widget.scrollController ?? ScrollController(); + widget.pagingController.addPageRequestListener((pageKey) => widget.listener(pageKey)); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus(); + return null; + }, + child: LunaRefreshIndicator( + key: widget.refreshKey, + context: context, + onRefresh: () => Future.sync(() => widget.pagingController.refresh()), + child: Scrollbar( + controller: _scrollController, + child: PagedListView( + pagingController: widget.pagingController, + scrollController: _scrollController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: widget.itemBuilder, + firstPageErrorIndicatorBuilder: (context) => LunaMessage.error(onTap: () => Future.sync(() => widget.pagingController.refresh())), + firstPageProgressIndicatorBuilder: (context) => LunaLoader(), + newPageProgressIndicatorBuilder: (context) => Container( + alignment: Alignment.center, + height: 40.0, + child: LunaLoader(size: 16.0), + ), + newPageErrorIndicatorBuilder: (context) => LunaIconButton(icon: Icons.error, color: LunaColours.red), + noItemsFoundIndicatorBuilder: (context) => LunaMessage( + text: widget.noItemsFoundMessage, + buttonText: 'Try Again', + onTap: () => Future.sync(() => widget.pagingController.refresh()), + ), + noMoreItemsIndicatorBuilder: (context) => LunaIconButton(icon: Icons.check, color: LunaColours.accent), + ), + padding: widget.padding != null ? widget.padding : EdgeInsets.only( + top: 8.0, + bottom: 8.0+(MediaQuery.of(context).padding.bottom/5), + ), + physics: AlwaysScrollableScrollPhysics(), + ), + ), + ), + ); + } +} diff --git a/lib/core/luna_ui/loader.dart b/lib/core/luna_ui/loader.dart new file mode 100644 index 00000000..fc9cc497 --- /dev/null +++ b/lib/core/luna_ui/loader.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:lunasea/core.dart'; + +class LunaLoader extends StatelessWidget { + final double size; + final Color color; + + LunaLoader({ + Key key, + this.size = 25.0, + this.color, + }): super(key: key); + + @override + Widget build(BuildContext context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SpinKitThreeBounce( + color: color != null ? color : LunaColours.accent, + size: size, + ), + ], + ); +} diff --git a/lib/core/luna_ui/message.dart b/lib/core/luna_ui/message.dart new file mode 100644 index 00000000..38ba8631 --- /dev/null +++ b/lib/core/luna_ui/message.dart @@ -0,0 +1 @@ +export 'message/message.dart'; diff --git a/lib/core/luna_ui/message/message.dart b/lib/core/luna_ui/message/message.dart new file mode 100644 index 00000000..b261e8d3 --- /dev/null +++ b/lib/core/luna_ui/message/message.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/home.dart'; + +class LunaMessage extends StatelessWidget { + final String text; + final Color textColor; + final String buttonText; + final Function onTap; + final bool useSafeArea; + + LunaMessage({ + Key key, + @required this.text, + this.textColor = Colors.white, + this.buttonText, + this.onTap, + this.useSafeArea = true, + }) { + assert(text != null); + if(buttonText != null) assert(onTap != null, 'onTap must be defined if buttonText is defined'); + } + + /// Return a message that is meant to be shown within a [ListView]. + factory LunaMessage.inList({ + Key key, + @required String text, + bool useSafeArea = false, + }) { + assert(text != null); + return LunaMessage( + key: key, + text: text, + useSafeArea: useSafeArea, + ); + } + + /// Returns a centered message with a simple message, with a button to pop out of the route. + factory LunaMessage.goBack({ + Key key, + @required String text, + @required BuildContext context, + bool useSafeArea = true, + }) { + assert(context != null); + return LunaMessage( + key: key, + text: text, + buttonText: 'Go Back', + onTap: () => Navigator.of(context).pop(), + useSafeArea: useSafeArea, + ); + } + + /// Return a pre-structured "An Error Has Occurred" message, with a "Try Again" button shown. + factory LunaMessage.error({ + Key key, + @required Function onTap, + bool useSafeArea = true, + }) { + assert(onTap != null); + return LunaMessage( + key: key, + text: 'An Error Has Occurred', + buttonText: 'Try Again', + onTap: onTap, + useSafeArea: useSafeArea, + ); + } + + /// Return a pre-structured " Is Not Enabled" message, with a "Return Home" button shown. + factory LunaMessage.moduleNotEnabled({ + Key key, + @required BuildContext context, + @required String module, + bool useSafeArea = true, + }) { + assert(module != null); + assert(context != null); + return LunaMessage( + key: key, + text: '$module Is Not Enabled', + buttonText: 'Return Home', + onTap: () async => HomeConstants.MODULE_METADATA.launch(), + useSafeArea: useSafeArea, + ); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: useSafeArea, + left: useSafeArea, + right: useSafeArea, + bottom: useSafeArea, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Card( + margin: LunaUI.MARGIN_CARD, + elevation: LunaUI.ELEVATION, + shape: LunaUI.shapeBorder, + child: Row( + children: [ + Expanded( + child: Container( + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: textColor, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_MESSAGES, + ), + ), + margin: EdgeInsets.symmetric(vertical: 24.0, horizontal: 12.0), + ), + ), + ], + ), + ), + if(buttonText != null) LSButton(text: buttonText, onTap: onTap), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/core/luna_ui/network_image.dart b/lib/core/luna_ui/network_image.dart new file mode 100644 index 00000000..9c83b2ac --- /dev/null +++ b/lib/core/luna_ui/network_image.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaNetworkImage extends ClipRRect { + LunaNetworkImage({ + @required double height, + @required double width, + String url, + @required String placeholderAsset, + Map headers, + bool roundCorners = true, + }) : super( + child: Container( + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + placeholderAsset, + height: height, + width: width, + ), + if(url != null && url.isNotEmpty) FadeInImage( + height: height, + width: width, + fadeInDuration: Duration(milliseconds: LunaUI.ANIMATION_IMAGE_FADE_IN_SPEED), + fadeOutDuration: Duration(milliseconds: 1), + placeholder: AssetImage(placeholderAsset), + fit: BoxFit.cover, + image: NetworkImage(url, headers: headers.cast()), + ), + ], + ), + height: height, + width: width, + ), + borderRadius: roundCorners ? BorderRadius.circular(LunaUI.BORDER_RADIUS) : null, + ) { + assert(height != null); + assert(width != null); + assert(placeholderAsset != null); + } +} diff --git a/lib/core/luna_ui/popup_menu_button.dart b/lib/core/luna_ui/popup_menu_button.dart new file mode 100644 index 00000000..b4b0d540 --- /dev/null +++ b/lib/core/luna_ui/popup_menu_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lunasea/core.dart'; + +class LunaPopupMenuButton extends PopupMenuButton { + LunaPopupMenuButton({ + IconData icon, + Widget child, + @required void Function(T) onSelected, + @required List> Function(BuildContext) itemBuilder, + }) : super( + shape: LunaUI.shapeBorder, + icon: icon == null ? null : Icon(icon), + child: child, + onSelected: onSelected == null ? null : (result) { + HapticFeedback.selectionClick(); + onSelected(result); + }, + itemBuilder: itemBuilder, + ) { + if(icon == null) assert(child != null, 'both icon and child cannot be null'); + if(child == null) assert(icon != null, 'both icon and child cannot be null'); + } +} diff --git a/lib/core/luna_ui/refresh_indicator.dart b/lib/core/luna_ui/refresh_indicator.dart new file mode 100644 index 00000000..af0d67c5 --- /dev/null +++ b/lib/core/luna_ui/refresh_indicator.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class LunaRefreshIndicator extends RefreshIndicator { + LunaRefreshIndicator({ + GlobalKey key, + @required BuildContext context, + @required Future Function() onRefresh, + @required Widget child, + }) : super( + key: key, + backgroundColor: Theme.of(context).primaryColor, + onRefresh: onRefresh, + child: child, + ); +} diff --git a/lib/core/luna_ui/shape.dart b/lib/core/luna_ui/shape.dart new file mode 100644 index 00000000..7d369d31 --- /dev/null +++ b/lib/core/luna_ui/shape.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaShapeBorder extends RoundedRectangleBorder { + LunaShapeBorder._internal({ + bool useBorder = false, + }) : super( + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + side: useBorder ? BorderSide(color: Colors.white12) : BorderSide.none, + ); + + factory LunaShapeBorder.rounded() => LunaShapeBorder._internal(); + factory LunaShapeBorder.roundedWithBorder() => LunaShapeBorder._internal(useBorder: true); +} diff --git a/lib/core/luna_ui/snackbar.dart b/lib/core/luna_ui/snackbar.dart index 04c495b6..b4d4b8f5 100644 --- a/lib/core/luna_ui/snackbar.dart +++ b/lib/core/luna_ui/snackbar.dart @@ -1,97 +1,4 @@ +export 'snackbar/snackbar.dart'; export 'snackbar/snackbar_error.dart'; export 'snackbar/snackbar_info.dart'; export 'snackbar/snackbar_success.dart'; -import 'package:flash/flash.dart'; -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -enum LunaSnackbarType { - SUCCESS, - ERROR, - INFO, -} - -extension LunaSnackbarTypeExtension on LunaSnackbarType { - Color get color { - switch(this) { - case LunaSnackbarType.SUCCESS: return LunaColours.accent; - case LunaSnackbarType.ERROR: return LunaColours.red; - case LunaSnackbarType.INFO: return LunaColours.blue; - default: return LunaColours.purple; - } - } - - IconData get icon { - switch(this) { - case LunaSnackbarType.SUCCESS: return Icons.check_circle_outline; - case LunaSnackbarType.ERROR: return Icons.error_outline; - case LunaSnackbarType.INFO: return Icons.info_outline; - default: return Icons.help_outline; - } - } -} - -const _PADDING = 8.0; - -Future showLunaSnackBar({ - @required BuildContext context, - @required String title, - @required LunaSnackbarType type, - @required String message, - bool showButton = false, - String buttonText = 'view', - Function buttonOnPressed, -}) async => showFlash( - context: context, - duration: Duration(seconds: showButton ? 4 : 2), - builder: (context, controller) => Flash( - backgroundColor: Theme.of(context).primaryColor, - controller: controller, - style: FlashStyle.floating, - boxShadows: [BoxShadow(blurRadius: 6.0, spreadRadius: 4.0, color: Colors.black.withOpacity(0.10))], - horizontalDismissDirection: HorizontalDismissDirection.horizontal, - margin: EdgeInsets.all(_PADDING), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - borderColor: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? Colors.white12 - : Colors.transparent, - child: FlashBar( - title: Text( - title, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: Constants.UI_FONT_SIZE_TITLE, - ), - ), - message: Text( - message, - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - icon: Padding( - child: LSIconButton( - icon: type.icon, - color: type.color, - ), - padding: EdgeInsets.only(left: _PADDING), - ), - primaryAction: showButton - ? FlatButton( - child: Text( - buttonText.toUpperCase(), - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - textColor: LunaColours.accent, - onPressed: () { - controller.dismiss(); - buttonOnPressed(); - } - ) - : null, - ), - ), -); diff --git a/lib/core/luna_ui/snackbar/snackbar.dart b/lib/core/luna_ui/snackbar/snackbar.dart new file mode 100644 index 00000000..63ca3a7d --- /dev/null +++ b/lib/core/luna_ui/snackbar/snackbar.dart @@ -0,0 +1,98 @@ +import 'package:flash/flash.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lunasea/core.dart'; + +enum LunaSnackbarType { + SUCCESS, + ERROR, + INFO, +} + +extension LunaSnackbarTypeExtension on LunaSnackbarType { + Color get color { + switch(this) { + case LunaSnackbarType.SUCCESS: return LunaColours.accent; + case LunaSnackbarType.ERROR: return LunaColours.red; + case LunaSnackbarType.INFO: return LunaColours.blue; + default: return LunaColours.purple; + } + } + + IconData get icon { + switch(this) { + case LunaSnackbarType.SUCCESS: return Icons.check_circle_outline; + case LunaSnackbarType.ERROR: return Icons.error_outline; + case LunaSnackbarType.INFO: return Icons.info_outline; + default: return Icons.help_outline; + } + } +} + +const _PADDING = 8.0; + +Future showLunaSnackBar({ + @required String title, + @required LunaSnackbarType type, + @required String message, + Duration duration, + FlashPosition position = FlashPosition.bottom, + bool showButton = false, + String buttonText = 'view', + Function buttonOnPressed, +}) async => showFlash( + context: LunaState.navigatorKey.currentContext, + duration: duration != null ? duration : Duration(seconds: showButton ? 4 : 2), + builder: (context, controller) => Flash( + backgroundColor: Theme.of(context).primaryColor, + controller: controller, + style: FlashStyle.floating, + boxShadows: [BoxShadow(blurRadius: 6.0, spreadRadius: 4.0, color: Colors.black.withOpacity(0.10))], + horizontalDismissDirection: HorizontalDismissDirection.horizontal, + margin: EdgeInsets.all(_PADDING), + position: position, + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + borderColor: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data + ? Colors.white12 + : Colors.transparent, + child: FlashBar( + title: Text( + title, + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: Constants.UI_FONT_SIZE_TITLE, + ), + ), + message: Text( + message, + style: TextStyle( + color: Colors.white70, + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + ), + icon: Padding( + child: LSIconButton( + icon: type.icon, + color: type.color, + ), + padding: EdgeInsets.only(left: _PADDING), + ), + primaryAction: showButton + ? TextButton( + child: Text( + buttonText.toUpperCase(), + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: LunaColours.accent, + ), + ), + onPressed: () { + HapticFeedback.lightImpact(); + controller.dismiss(); + buttonOnPressed(); + }, + ) + : null, + ), + ), +); diff --git a/lib/core/luna_ui/snackbar/snackbar_error.dart b/lib/core/luna_ui/snackbar/snackbar_error.dart index 35ba0655..2da0c15c 100644 --- a/lib/core/luna_ui/snackbar/snackbar_error.dart +++ b/lib/core/luna_ui/snackbar/snackbar_error.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; Future showLunaErrorSnackBar({ - @required BuildContext context, + // Not needed anymore, but keeping it to prevent breaking all instances for now + BuildContext context, @required String title, dynamic error, String message = LunaLogger.CHECK_LOGS_MESSAGE, }) async => showLunaSnackBar( - context: context, title: title, message: message, type: LunaSnackbarType.ERROR, diff --git a/lib/core/luna_ui/snackbar/snackbar_info.dart b/lib/core/luna_ui/snackbar/snackbar_info.dart index 30da9dcc..0c1fa06f 100644 --- a/lib/core/luna_ui/snackbar/snackbar_info.dart +++ b/lib/core/luna_ui/snackbar/snackbar_info.dart @@ -3,14 +3,14 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; Future showLunaInfoSnackBar({ - @required BuildContext context, + // Not needed anymore, but keeping it to prevent breaking all instances for now + BuildContext context, @required String title, @required String message, bool showButton = false, String buttonText = 'view', Function buttonOnPressed, }) async => showLunaSnackBar( - context: context, title: title, message: message, type: LunaSnackbarType.INFO, diff --git a/lib/core/luna_ui/snackbar/snackbar_success.dart b/lib/core/luna_ui/snackbar/snackbar_success.dart index cd393889..8a955bf5 100644 --- a/lib/core/luna_ui/snackbar/snackbar_success.dart +++ b/lib/core/luna_ui/snackbar/snackbar_success.dart @@ -3,14 +3,14 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; Future showLunaSuccessSnackBar({ - @required BuildContext context, + // Not needed anymore, but keeping it to prevent breaking all instances for now + BuildContext context, @required String title, @required String message, bool showButton = false, String buttonText = 'view', Function buttonOnPressed, }) async => showLunaSnackBar( - context: context, title: title, message: message, type: LunaSnackbarType.SUCCESS, diff --git a/lib/core/luna_ui/switch.dart b/lib/core/luna_ui/switch.dart new file mode 100644 index 00000000..db27ecec --- /dev/null +++ b/lib/core/luna_ui/switch.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class LunaSwitch extends Switch { + LunaSwitch({ + Key key, + @required bool value, + @required void Function(bool) onChanged, + }) : super( + key: key, + value: value, + onChanged: onChanged == null ? null : (value) { + HapticFeedback.lightImpact(); + onChanged(value); + }, + ); +} diff --git a/lib/core/luna_ui/table.dart b/lib/core/luna_ui/table.dart new file mode 100644 index 00000000..5d4c60a9 --- /dev/null +++ b/lib/core/luna_ui/table.dart @@ -0,0 +1,2 @@ +export 'table/table_card.dart'; +export 'table/table_content.dart'; diff --git a/lib/core/luna_ui/table/table_card.dart b/lib/core/luna_ui/table/table_card.dart new file mode 100644 index 00000000..5685a7dc --- /dev/null +++ b/lib/core/luna_ui/table/table_card.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaTableCard extends StatelessWidget { + final List content; + final List buttons; + + LunaTableCard({ + Key key, + @required this.content, + this.buttons, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Padding( + child: _body(), + padding: buttons == null + ? EdgeInsets.symmetric(vertical: 8.0) + : EdgeInsets.only(top: 8.0, bottom: 6.0), + ), + ); + } + + Widget _body() { + return Column( + children: [ + ..._content(), + _buttons(), + ], + ); + } + + List _content() { + return content.map((content) => Padding( + child: content, + padding: EdgeInsets.symmetric(horizontal: 12.0), + )).toList(); + } + + Widget _buttons() { + if(buttons == null) return Container(height: 0.0); + return Padding( + child: Row( + children: buttons.map((button) => Expanded(child: button)).toList(), + ), + padding: EdgeInsets.symmetric(horizontal: 6.0), + ); + } +} diff --git a/lib/core/luna_ui/table/table_content.dart b/lib/core/luna_ui/table/table_content.dart new file mode 100644 index 00000000..0a3ecd03 --- /dev/null +++ b/lib/core/luna_ui/table/table_content.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaTableContent extends StatelessWidget { + final String title; + final String body; + final bool bodyIsUrl; + final int titleFlex; + final int bodyFlex; + final TextAlign titleAlign; + final TextAlign bodyAlign; + final Color titleColor; + final Color bodyColor; + final EdgeInsets padding; + + LunaTableContent({ + Key key, + @required this.title, + @required this.body, + this.bodyIsUrl = false, + this.titleAlign = TextAlign.end, + this.bodyAlign = TextAlign.start, + this.titleFlex = 2, + this.bodyFlex = 5, + this.titleColor = Colors.white70, + this.bodyColor = Colors.white, + this.padding = const EdgeInsets.symmetric(vertical: 4.0), + }) : super(key: key) { + assert(title != null); + assert(body != null); + } + + @override + Widget build(BuildContext context) { + return Padding( + child: Row( + children: [ + _title(), + Container(width: 16.0, height: 0.0), + _subtitle(), + ], + crossAxisAlignment: CrossAxisAlignment.start, + ), + padding: padding, + ); + } + + Widget _title() { + return Expanded( + child: Text( + title.toUpperCase(), + textAlign: titleAlign, + style: TextStyle( + color: titleColor, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + ), + flex: titleFlex, + ); + } + + Widget _subtitle() { + return Expanded( + child: InkWell( + child: Text( + body, + textAlign: bodyAlign, + style: TextStyle( + color: bodyColor, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + ), + ), + onTap: !bodyIsUrl ? null : () async => body.lunaOpenGenericLink(), + ), + flex: bodyFlex, + ); + } +} diff --git a/lib/core/luna_ui/text.dart b/lib/core/luna_ui/text.dart index 121fc8a7..b3bd0cb4 100644 --- a/lib/core/luna_ui/text.dart +++ b/lib/core/luna_ui/text.dart @@ -21,10 +21,10 @@ class LunaText extends Text { textAlign: textAlign, ); - /// Create a [Text] widget with the styling pre-assigned to be a LunaSea title. + /// Create a [LunaText] widget with the styling pre-assigned to be a LunaSea title. factory LunaText.title({ - @required String text, Key key, + @required String text, int maxLines = 1, bool softWrap = false, TextAlign textAlign = TextAlign.start, @@ -38,9 +38,30 @@ class LunaText extends Text { softWrap: softWrap, textAlign: textAlign, style: TextStyle( - color: darken ? color.withOpacity(0.3) : color, - fontWeight: FontWeight.bold, - fontSize: Constants.UI_FONT_SIZE_TITLE, + color: darken ? color.withOpacity(0.30) : color, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_TITLE, + ), + ); + + /// Create a [LunaText] widget with the styling pre-assigned to be a LunaSea subtitle. + factory LunaText.subtitle({ + Key key, + @required String text, + int maxLines = 1, + bool softWrap = false, + TextAlign textAlign = TextAlign.start, + Color color = Colors.white70, + bool darken = false, + }) => LunaText( + key: key, + text: text, + softWrap: softWrap, + maxLines: maxLines, + textAlign: textAlign, + style: TextStyle( + color: darken ? color.withOpacity(0.30) : color, + fontSize: LunaUI.FONT_SIZE_SUBTITLE, ), ); } diff --git a/lib/core/mixins.dart b/lib/core/mixins.dart new file mode 100644 index 00000000..dcba8fa6 --- /dev/null +++ b/lib/core/mixins.dart @@ -0,0 +1 @@ +export 'mixins/load_callback.dart'; diff --git a/lib/core/mixins/load_callback.dart b/lib/core/mixins/load_callback.dart new file mode 100644 index 00000000..11c02500 --- /dev/null +++ b/lib/core/mixins/load_callback.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +/// Simple mixin to force a definition of [loadCallback], a post-frame callback added to the [initState] override. +mixin LunaLoadCallbackMixin on State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => loadCallback()); + } + + Future loadCallback(); +} diff --git a/lib/core/module_metadata.dart b/lib/core/module_metadata.dart index c3d8cde2..9e9303a7 100644 --- a/lib/core/module_metadata.dart +++ b/lib/core/module_metadata.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; import 'package:quick_actions/quick_actions.dart'; /// [LunaModuleMetadata] stores general information about a module, including: @@ -19,10 +20,9 @@ class LunaModuleMetadata { final String github; final IconData icon; final Color color; - final Function pushBaseRoute; final ShortcutItem shortcutItem; - const LunaModuleMetadata({ + LunaModuleMetadata({ @required this.name, @required this.description, @required this.settingsDescription, @@ -32,7 +32,8 @@ class LunaModuleMetadata { @required this.github, @required this.icon, @required this.color, - @required this.pushBaseRoute, @required this.shortcutItem, }); + + Future launch() => LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false); } diff --git a/lib/core/networking.dart b/lib/core/networking.dart index ca2edd20..938c6d02 100644 --- a/lib/core/networking.dart +++ b/lib/core/networking.dart @@ -2,7 +2,7 @@ import 'dart:io'; class LunaNetworking extends HttpOverrides { /// Initialize the default HTTP client by setting [HttpOverrides]'s global HTTP client to [LunaNetworking]. - static void initialize() { + void initialize() { HttpOverrides.global = LunaNetworking(); } @@ -15,4 +15,12 @@ class LunaNetworking extends HttpOverrides { client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; return client; } + + /// Overrides the default [findProxyFromEnvironment] function. + /// + /// Currently just calls the super function to use the default finding function. + @override + String findProxyFromEnvironment(Uri url, Map environment) { + return super.findProxyFromEnvironment(url, environment); + } } diff --git a/lib/core/profile.dart b/lib/core/profile.dart index abebc95b..53c85d7f 100644 --- a/lib/core/profile.dart +++ b/lib/core/profile.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; class LunaProfile { - LunaProfile(); - /// Safely change profiles. /// /// Does this safely by: diff --git a/lib/core/quick_actions.dart b/lib/core/quick_actions.dart index 6d4a5ee2..e0066258 100644 --- a/lib/core/quick_actions.dart +++ b/lib/core/quick_actions.dart @@ -1,22 +1,21 @@ import 'package:lunasea/core.dart'; import 'package:quick_actions/quick_actions.dart'; import 'package:lunasea/modules.dart'; +export 'package:quick_actions/quick_actions.dart' show ShortcutItem; -class LunaQuickActions { - LunaQuickActions._(); - +class LunaQuickActions { static final QuickActions _quickActions = QuickActions(); /// Initialize the quick actions by setting the action handler. /// /// Required before the handler can actually receive any intents. - static void initialize() { + void initialize() { _quickActions.initialize((action) => _handler(action)); setShortcutItems(); } /// Sets the shortcut items by checking the database and enabling the respective action if enabled. - static void setShortcutItems() { + void setShortcutItems() { _quickActions.setShortcutItems([ if(LunaDatabaseValue.QUICK_ACTIONS_SEARCH.data) SearchConstants.MODULE_METADATA.shortcutItem, if(LunaDatabaseValue.QUICK_ACTIONS_LIDARR.data) LidarrConstants.MODULE_METADATA.shortcutItem, @@ -25,20 +24,22 @@ class LunaQuickActions { if(LunaDatabaseValue.QUICK_ACTIONS_NZBGET.data) NZBGetConstants.MODULE_METADATA.shortcutItem, if(LunaDatabaseValue.QUICK_ACTIONS_SABNZBD.data) SABnzbdConstants.MODULE_METADATA.shortcutItem, if(LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI.data) TautulliConstants.MODULE_METADATA.shortcutItem, + SettingsConstants.MODULE_METADATA.shortcutItem, ]); } /// The actual shortcut command handler - static void _handler(String action) { + void _handler(String action) { if(action != null) { switch(action) { - case SearchConstants.MODULE_KEY: SearchConstants.MODULE_METADATA.pushBaseRoute(); break; - case LidarrConstants.MODULE_KEY: LidarrConstants.MODULE_METADATA.pushBaseRoute(); break; - case RadarrConstants.MODULE_KEY: RadarrConstants.MODULE_METADATA.pushBaseRoute(); break; - case SonarrConstants.MODULE_KEY: SonarrConstants.MODULE_METADATA.pushBaseRoute(); break; - case NZBGetConstants.MODULE_KEY: NZBGetConstants.MODULE_METADATA.pushBaseRoute(); break; - case SABnzbdConstants.MODULE_KEY: SABnzbdConstants.MODULE_METADATA.pushBaseRoute(); break; - case TautulliConstants.MODULE_KEY: TautulliConstants.MODULE_METADATA.pushBaseRoute(); break; + case SearchConstants.MODULE_KEY: SearchConstants.MODULE_METADATA.launch(); break; + case LidarrConstants.MODULE_KEY: LidarrConstants.MODULE_METADATA.launch(); break; + case RadarrConstants.MODULE_KEY: RadarrConstants.MODULE_METADATA.launch(); break; + case SonarrConstants.MODULE_KEY: SonarrConstants.MODULE_METADATA.launch(); break; + case NZBGetConstants.MODULE_KEY: NZBGetConstants.MODULE_METADATA.launch(); break; + case SABnzbdConstants.MODULE_KEY: SABnzbdConstants.MODULE_METADATA.launch(); break; + case TautulliConstants.MODULE_KEY: TautulliConstants.MODULE_METADATA.launch(); break; + case SettingsConstants.MODULE_KEY: SettingsConstants.MODULE_METADATA.launch(); break; } } } diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index 007abed1..3c13ac77 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -1,38 +1,38 @@ import 'package:flutter/material.dart'; import 'package:fluro/fluro.dart'; +import 'package:lunasea/core.dart'; import 'package:lunasea/modules/home/routes.dart'; import 'package:lunasea/modules/search/routes.dart'; import 'package:lunasea/modules/lidarr/routes.dart'; -import 'package:lunasea/modules/radarr/routes.dart'; import 'package:lunasea/modules/nzbget/routes.dart'; import 'package:lunasea/modules/sabnzbd/routes.dart'; -import 'package:lunasea/modules.dart' show OmbiRouter, SettingsRouter, SonarrRouter, TautulliRouter; +import 'package:lunasea/modules.dart' show SettingsRouter, RadarrRouter, SonarrRouter, TautulliRouter; class LunaRouter { static FluroRouter router = FluroRouter(); static TransitionType get transitionType => TransitionType.native; /// Calls `defineAllRoutes()` on all module routers that implement [LunaModuleRouter]. - static void intialize() { + void intialize() { + router.notFoundHandler = Handler(handlerFunc: (context, params) => LunaInvalidRoute()); SettingsRouter().defineAllRoutes(router); SonarrRouter().defineAllRoutes(router); - OmbiRouter().defineAllRoutes(router); + RadarrRouter().defineAllRoutes(router); TautulliRouter().defineAllRoutes(router); } /// **Will be removed when all module routers are integrated.** /// /// Returns a map of all module routes. - static Map get routes => { + Map get routes => { Home.ROUTE_NAME: (context) => Home(), ..._search, ..._lidarr, - ..._radarr, ..._sabnzbd, ..._nzbget, }; - static Map get _search => { + Map get _search => { // /search Search.ROUTE_NAME: (context) => Search(), // /search/* @@ -42,7 +42,7 @@ class LunaRouter { SearchResults.ROUTE_NAME: (context) => SearchResults(), }; - static Map get _lidarr => { + Map get _lidarr => { // /lidarr Lidarr.ROUTE_NAME: (context) => Lidarr(), // /lidarr/add/* @@ -57,24 +57,12 @@ class LunaRouter { LidarrSearchResults.ROUTE_NAME: (context) => LidarrSearchResults(), }; - static Map get _radarr => { - // /radarr - Radarr.ROUTE_NAME: (context) => Radarr(), - // /radarr/add/* - RadarrAddSearch.ROUTE_NAME: (context) => RadarrAddSearch(), - RadarrAddDetails.ROUTE_NAME: (context) => RadarrAddDetails(), - // /radarr/details/* - RadarrDetailsMovie.ROUTE_NAME: (context) => RadarrDetailsMovie(), - RadarrEditMovie.ROUTE_NAME: (context) => RadarrEditMovie(), - RadarrSearchResults.ROUTE_NAME: (context) => RadarrSearchResults(), - }; - - static Map get _nzbget => { + Map get _nzbget => { NZBGet.ROUTE_NAME: (context) => NZBGet(), NZBGetStatistics.ROUTE_NAME: (context) => NZBGetStatistics(), }; - static Map get _sabnzbd => { + Map get _sabnzbd => { SABnzbd.ROUTE_NAME: (context) => SABnzbd(), SABnzbdStatistics.ROUTE_NAME: (context) => SABnzbdStatistics(), SABnzbdHistoryStages.ROUTE_NAME: (context) => SABnzbdHistoryStages(), diff --git a/lib/core/state/module_state.dart b/lib/core/state/module_state.dart index ccd2adcf..493b91e4 100644 --- a/lib/core/state/module_state.dart +++ b/lib/core/state/module_state.dart @@ -1,9 +1,19 @@ 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 scrollBackList() => scrollController.lunaAnimateToStart(); + + @override + void dispose() { + scrollController?.dispose(); + super.dispose(); + } + /// Reset the state back to the default void reset(); - - /// Notify listeners of an update - void notify() => notifyListeners(); } diff --git a/lib/core/state/state.dart b/lib/core/state/state.dart index 0b9b950c..cc2b5909 100644 --- a/lib/core/state/state.dart +++ b/lib/core/state/state.dart @@ -8,13 +8,14 @@ import 'package:lunasea/modules/radarr/core.dart' show RadarrState; import 'package:lunasea/modules/sonarr/core.dart' show SonarrState; import 'package:lunasea/modules/nzbget/core.dart' show NZBGetState; import 'package:lunasea/modules/sabnzbd/core.dart' show SABnzbdState; -import 'package:lunasea/modules/ombi/core.dart' show OmbiState; import 'package:lunasea/modules/tautulli/core.dart' show TautulliState; export 'package:provider/provider.dart'; class LunaState { LunaState._(); + static final GlobalKey navigatorKey = GlobalKey(); + /// Calls `.reset()` on all states which extend [LunaModuleState]. static void reset(BuildContext context) { // General @@ -29,7 +30,6 @@ class LunaState { Provider.of(context, listen: false)?.reset(); Provider.of(context, listen: false)?.reset(); // Monitoring - Provider.of(context, listen: false)?.reset(); Provider.of(context, listen: false)?.reset(); } @@ -50,7 +50,6 @@ class LunaState { ChangeNotifierProvider(create: (_) => NZBGetState()), ChangeNotifierProvider(create: (_) => SABnzbdState()), // Monitoring - ChangeNotifierProvider(create: (_) => OmbiState()), ChangeNotifierProvider(create: (_) => TautulliState()), ], child: child, diff --git a/lib/core/theme.dart b/lib/core/theme.dart index 7ef6c878..57602758 100644 --- a/lib/core/theme.dart +++ b/lib/core/theme.dart @@ -4,13 +4,12 @@ import 'package:lunasea/core.dart'; class LunaTheme { /// Returns the active [ThemeData] by checking the theme database value. - static ThemeData activeTheme() { + ThemeData activeTheme() { return LunaDatabaseValue.THEME_AMOLED.data ? _pureBlackTheme() : _midnightTheme(); } /// Midnight theme (Default) - static ThemeData _midnightTheme() { - const _textStyle = TextStyle(color: Colors.white); + ThemeData _midnightTheme() { return ThemeData( brightness: Brightness.dark, canvasColor: LunaColours.primary, @@ -25,28 +24,14 @@ class LunaTheme { color: Colors.white, ), unselectedWidgetColor: Colors.white, - textTheme: TextTheme( - bodyText1: _textStyle, - bodyText2: _textStyle, - headline1: _textStyle, - headline2: _textStyle, - headline3: _textStyle, - headline4: _textStyle, - headline5: _textStyle, - headline6: _textStyle, - button: _textStyle, - caption: _textStyle, - subtitle1: _textStyle, - subtitle2: _textStyle, - overline: _textStyle, - ), + textTheme: _sharedTextTheme, + textButtonTheme: _sharedTextButtonThemeData, visualDensity: VisualDensity.adaptivePlatformDensity, ); } /// AMOLED/Pure black theme - static ThemeData _pureBlackTheme() { - const _textStyle = TextStyle(color: Colors.white); + ThemeData _pureBlackTheme() { return ThemeData( brightness: Brightness.dark, canvasColor: Colors.black, @@ -61,22 +46,36 @@ class LunaTheme { color: Colors.white, ), unselectedWidgetColor: Colors.white, - textTheme: TextTheme( - bodyText1: _textStyle, - bodyText2: _textStyle, - headline1: _textStyle, - headline2: _textStyle, - headline3: _textStyle, - headline4: _textStyle, - headline5: _textStyle, - headline6: _textStyle, - button: _textStyle, - caption: _textStyle, - subtitle1: _textStyle, - subtitle2: _textStyle, - overline: _textStyle, - ), + textTheme: _sharedTextTheme, + textButtonTheme: _sharedTextButtonThemeData, visualDensity: VisualDensity.adaptivePlatformDensity, ); } -} \ No newline at end of file + + TextTheme get _sharedTextTheme { + const textStyle = TextStyle(color: Colors.white); + return TextTheme( + bodyText1: textStyle, + bodyText2: textStyle, + headline1: textStyle, + headline2: textStyle, + headline3: textStyle, + headline4: textStyle, + headline5: textStyle, + headline6: textStyle, + button: textStyle, + caption: textStyle, + subtitle1: textStyle, + subtitle2: textStyle, + overline: textStyle, + ); + } + + TextButtonThemeData get _sharedTextButtonThemeData { + return TextButtonThemeData( + style: ButtonStyle( + overlayColor: MaterialStateProperty.all(LunaColours.splash.withOpacity(0.30)), + ), + ); + } +} diff --git a/lib/core/ui.dart b/lib/core/ui.dart index 33a53df7..72df90fb 100644 --- a/lib/core/ui.dart +++ b/lib/core/ui.dart @@ -1,7 +1,5 @@ -export 'ui/appbar.dart'; export 'ui/button.dart'; export 'ui/card.dart'; -export 'ui/colors.dart'; export 'ui/containers.dart'; export 'ui/description_block.dart'; export 'ui/dialogs.dart'; diff --git a/lib/core/ui/appbar.dart b/lib/core/ui/appbar.dart deleted file mode 100644 index 1a67a3f2..00000000 --- a/lib/core/ui/appbar.dart +++ /dev/null @@ -1 +0,0 @@ -export 'appbar/appbar_dropdown.dart'; diff --git a/lib/core/ui/appbar/appbar_dropdown.dart b/lib/core/ui/appbar/appbar_dropdown.dart deleted file mode 100644 index 08bb77ad..00000000 --- a/lib/core/ui/appbar/appbar_dropdown.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -// ignore: non_constant_identifier_names -Widget LSAppBarDropdown({ - @required BuildContext context, - @required String title, - @required List profiles, - List actions -}) => profiles != null && profiles.length < 2 - ? LunaAppBar(context: context, title: title, actions: actions, hideLeading: true) - : AppBar( - title: PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - child: Wrap( - direction: Axis.horizontal, - children: [ - Text( - title, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_HEADER, - ), - ), - LSIcon( - icon: Icons.arrow_drop_down, - ), - ], - ), - onSelected: (result) => LunaProfile().safelyChangeProfiles(context, result), - itemBuilder: (context) { - return >[for(String profile in profiles) PopupMenuItem( - value: profile, - child: Text( - profile, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - )]; - }, - ), - centerTitle: false, - elevation: 0, - actions: actions, - ); diff --git a/lib/core/ui/button/button.dart b/lib/core/ui/button/button.dart index dfc93e77..b36ffa15 100644 --- a/lib/core/ui/button/button.dart +++ b/lib/core/ui/button/button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; class LSButton extends StatelessWidget { @@ -7,7 +8,7 @@ class LSButton extends StatelessWidget { final Color backgroundColor; final Color textColor; final bool reducedMargin; - final bool isLoading; + final LunaLoadingState loadingState; LSButton({ @required this.text, @@ -15,7 +16,7 @@ class LSButton extends StatelessWidget { this.backgroundColor = const Color(LunaColours.ACCENT_COLOR), this.textColor = Colors.white, this.reducedMargin = false, - this.isLoading = false, + this.loadingState = LunaLoadingState.INACTIVE, }); @override @@ -25,7 +26,7 @@ class LSButton extends StatelessWidget { child: Card( child: InkWell( child: ListTile( - title: isLoading + title: loadingState == LunaLoadingState.ACTIVE ? LSLoader( color: Colors.white, size: 20.0, @@ -34,23 +35,26 @@ class LSButton extends StatelessWidget { text, style: TextStyle( color: textColor, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, ), textAlign: TextAlign.center, ), ), borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: isLoading ? null : onTap, + onTap: loadingState == LunaLoadingState.ACTIVE ? null : onTap == null ? null : () async { + HapticFeedback.lightImpact(); + onTap(); + }, ), color: backgroundColor, margin: reducedMargin ? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0) - : Constants.UI_CARD_MARGIN, - elevation: Constants.UI_ELEVATION, + : LunaUI.MARGIN_CARD, + elevation: LunaUI.ELEVATION, shape: LSRoundedShape(), ), ), ], ); -} \ No newline at end of file +} diff --git a/lib/core/ui/button/button_slim.dart b/lib/core/ui/button/button_slim.dart index 2f695046..922aa0d4 100644 --- a/lib/core/ui/button/button_slim.dart +++ b/lib/core/ui/button/button_slim.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; class LSButtonSlim extends StatelessWidget { @@ -29,18 +30,22 @@ class LSButtonSlim extends StatelessWidget { text, style: TextStyle( color: textColor, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, ), textAlign: TextAlign.center, ), padding: EdgeInsets.symmetric(vertical: 14.0), ), - onTap: onTap, + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + onTap: onTap == null ? null : () async { + HapticFeedback.lightImpact(); + onTap(); + } ), color: backgroundColor, margin: margin, - elevation: Constants.UI_ELEVATION, + elevation: LunaUI.ELEVATION, shape: LSRoundedShape(), ), ), diff --git a/lib/core/ui/card/card.dart b/lib/core/ui/card/card.dart index 1ec82da8..ebbd89bf 100644 --- a/lib/core/ui/card/card.dart +++ b/lib/core/ui/card/card.dart @@ -10,7 +10,7 @@ class LSCard extends StatelessWidget { LSCard({ @required this.child, - this.margin = Constants.UI_CARD_MARGIN, + this.margin = LunaUI.MARGIN_CARD, this.reducedMargin = false, this.color, this.decoration, @@ -27,7 +27,7 @@ class LSCard extends StatelessWidget { child: child, decoration: decoration, ), - elevation: Constants.UI_ELEVATION, + elevation: LunaUI.ELEVATION, margin: reducedMargin ? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0) : margin, diff --git a/lib/core/ui/card/card_tile.dart b/lib/core/ui/card/card_tile.dart index 7379a2c6..ac469baa 100644 --- a/lib/core/ui/card/card_tile.dart +++ b/lib/core/ui/card/card_tile.dart @@ -59,9 +59,9 @@ class LSCardTile extends StatelessWidget { margin: customMargin == null ? reducedMargin ? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0) - : Constants.UI_CARD_MARGIN + : LunaUI.MARGIN_CARD : customMargin, - elevation: Constants.UI_ELEVATION, + elevation: LunaUI.ELEVATION, shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data ? LSRoundedShapeWithBorder() : LSRoundedShape(), diff --git a/lib/core/ui/dialogs.dart b/lib/core/ui/dialogs.dart index fb2134ef..d90eefa4 100644 --- a/lib/core/ui/dialogs.dart +++ b/lib/core/ui/dialogs.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; abstract class LSDialog { @@ -12,7 +13,7 @@ abstract class LSDialog { textAlign: TextAlign.center, style: TextStyle( fontSize: LSDialog.HEADER_SIZE, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ); @@ -22,7 +23,7 @@ abstract class LSDialog { color: color == null ? LunaColours.accent : color, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: fontSize, ), ); @@ -37,7 +38,7 @@ abstract class LSDialog { textAlign: alignment, ); - static Widget button({ @required String text, @required void Function() onPressed, Color textColor }) => FlatButton( + static Widget button({ @required String text, @required void Function() onPressed, Color textColor }) => TextButton( child: Text( text, style: TextStyle( @@ -47,10 +48,13 @@ abstract class LSDialog { fontSize: LSDialog.BUTTON_SIZE, ), ), - onPressed: onPressed, + onPressed: onPressed == null ? null : () async { + HapticFeedback.lightImpact(); + onPressed(); + }, ); - static Widget cancel(BuildContext context, { Color textColor = Colors.white }) => FlatButton( + static Widget cancel(BuildContext context, { Color textColor = Colors.white }) => TextButton( child: Text( 'Cancel', style: TextStyle( @@ -58,7 +62,10 @@ abstract class LSDialog { fontSize: LSDialog.BUTTON_SIZE, ), ), - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + HapticFeedback.lightImpact(); + Navigator.of(context).pop(); + }, ); static Widget content({ @required List children }) => SingleChildScrollView( @@ -179,7 +186,27 @@ abstract class LSDialog { subtitle: text == null ? null : subtitle, - onTap: onTap, + onTap: () async { + HapticFeedback.selectionClick(); + onTap(); + }, + contentPadding: tileContentPadding(), + ); + + static CheckboxListTile checkbox({ + @required String title, + @required bool value, + @required void Function(bool) onChanged, + }) => CheckboxListTile( + title: Text( + title, + style: TextStyle( + fontSize: BODY_SIZE, + color: Colors.white, + ), + ), + value: value, + onChanged: onChanged, contentPadding: tileContentPadding(), ); diff --git a/lib/core/ui/drawer/drawer_categories.dart b/lib/core/ui/drawer/drawer_categories.dart index a2e9a021..881a511b 100644 --- a/lib/core/ui/drawer/drawer_categories.dart +++ b/lib/core/ui/drawer/drawer_categories.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/wake_on_lan.dart'; class LSDrawerCategories extends StatelessWidget { @@ -140,7 +139,7 @@ class LSDrawerCategories extends StatelessWidget { ), onTap: () async { Navigator.of(context).pop(); - if(!currentPage) LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false); + if(!currentPage) LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false); }, contentPadding: padLeft ? EdgeInsets.fromLTRB(42.0, 0.0, 0.0, 0.0) diff --git a/lib/core/ui/drawer/drawer_header.dart b/lib/core/ui/drawer/drawer_header.dart index 4e14b748..fe89d0ea 100644 --- a/lib/core/ui/drawer/drawer_header.dart +++ b/lib/core/ui/drawer/drawer_header.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; // ignore: non_constant_identifier_names @@ -12,10 +13,7 @@ Widget LSDrawerHeader() => UserAccountsDrawerHeader( builder: (context, lunaBox, widget) => ValueListenableBuilder( valueListenable: Database.profilesBox.listenable(), builder: (context, profilesBox, widget) => Padding( - child: PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), + child: LunaPopupMenuButton( child: Row( mainAxisSize: MainAxisSize.max, children: [ @@ -29,7 +27,10 @@ Widget LSDrawerHeader() => UserAccountsDrawerHeader( ), ], ), - onSelected: (result) => LunaProfile().safelyChangeProfiles(context, result), + onSelected: (result) { + HapticFeedback.selectionClick(); + LunaProfile().safelyChangeProfiles(context, result); + }, itemBuilder: (context) { return >[for(String profile in (profilesBox as Box).keys) PopupMenuItem( value: profile, diff --git a/lib/core/ui/drawer/drawer_nocategories.dart b/lib/core/ui/drawer/drawer_nocategories.dart index d8912db5..178aaf42 100644 --- a/lib/core/ui/drawer/drawer_nocategories.dart +++ b/lib/core/ui/drawer/drawer_nocategories.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/wake_on_lan.dart'; class LSDrawerNoCategories extends StatelessWidget { @@ -106,7 +105,7 @@ class LSDrawerNoCategories extends StatelessWidget { ), onTap: () async { Navigator.of(context).pop(); - if(!currentPage) LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false); + if(!currentPage) LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false); }, contentPadding: EdgeInsets.fromLTRB(16.0, 0.0, 0.0, 0.0), ); diff --git a/lib/core/ui/floating_action_button/floating_action_button_extended.dart b/lib/core/ui/floating_action_button/floating_action_button_extended.dart index 9879a55a..a5731ff8 100644 --- a/lib/core/ui/floating_action_button/floating_action_button_extended.dart +++ b/lib/core/ui/floating_action_button/floating_action_button_extended.dart @@ -27,7 +27,7 @@ class LSFloatingActionButtonExtended extends StatelessWidget { label, style: TextStyle( color: labelColor, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, letterSpacing: 0.25, ), diff --git a/lib/core/ui/icon/icon_button.dart b/lib/core/ui/icon/icon_button.dart index 5f01d8f7..ada3707d 100644 --- a/lib/core/ui/icon/icon_button.dart +++ b/lib/core/ui/icon/icon_button.dart @@ -1,31 +1,51 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; class LSIconButton extends StatelessWidget { final IconData icon; + final String text; final Color color; final Function onPressed; final Function onLongPress; final double iconSize; + final double textSize; LSIconButton({ - @required this.icon, + this.icon, + this.text, this.color = Colors.white, this.onPressed, this.onLongPress, this.iconSize = 24.0, + this.textSize = 10.0, }); @override - Widget build(BuildContext context) => InkWell( - child: IconButton( - icon: LSIcon( - icon: icon, - color: color, + Widget build(BuildContext context) { + return InkWell( + child: IconButton( + icon: text != null + ? Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: color, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: textSize, + ), + ) + : LSIcon(icon: icon, color: color), + iconSize: iconSize, + onPressed: onPressed == null ? null : () async { + HapticFeedback.lightImpact(); + onPressed(); + } ), - iconSize: iconSize, - onPressed: onPressed, - ), - onLongPress: onLongPress, - ); + onLongPress: onLongPress == null ? null : () async { + HapticFeedback.heavyImpact(); + onLongPress(); + }, + ); + } } diff --git a/lib/core/ui/input_bar.dart b/lib/core/ui/input_bar.dart index b9877452..741b6448 100644 --- a/lib/core/ui/input_bar.dart +++ b/lib/core/ui/input_bar.dart @@ -26,7 +26,7 @@ class LSTextInputBar extends StatefulWidget { this.keyboardType = TextInputType.text, this.labelText = 'Search...', this.labelIcon = Icons.search, - this.margin = Constants.UI_CARD_MARGIN, + this.margin = LunaUI.MARGIN_CARD, this.autofocus = false, this.obscureText = false, this.isFormField = false, diff --git a/lib/core/ui/listview/listview.dart b/lib/core/ui/listview/listview.dart index ee0e95f4..2ae3175d 100644 --- a/lib/core/ui/listview/listview.dart +++ b/lib/core/ui/listview/listview.dart @@ -8,7 +8,7 @@ class LSListView extends StatelessWidget { LSListView({ @required this.children, this.controller, - this.customPadding = const EdgeInsets.symmetric(vertical: 8.0), + this.customPadding, }); @override @@ -17,7 +17,10 @@ class LSListView extends StatelessWidget { child: ListView( controller: controller, children: children, - padding: customPadding, + padding: customPadding != null ? customPadding : EdgeInsets.only( + top: 8.0, + bottom: 8.0+(MediaQuery.of(context).padding.bottom/5), + ), physics: AlwaysScrollableScrollPhysics(), ), ); diff --git a/lib/core/ui/listview/listview_builder.dart b/lib/core/ui/listview/listview_builder.dart index 75e191fc..7efbb671 100644 --- a/lib/core/ui/listview/listview_builder.dart +++ b/lib/core/ui/listview/listview_builder.dart @@ -11,7 +11,7 @@ class LSListViewBuilder extends StatelessWidget { @required this.itemCount, @required this.itemBuilder, this.reverse = false, - this.customPadding = const EdgeInsets.symmetric(vertical: 8.0), + this.customPadding, }); @override @@ -21,7 +21,10 @@ class LSListViewBuilder extends StatelessWidget { controller: controller, itemCount: itemCount, itemBuilder: itemBuilder, - padding: customPadding, + padding: customPadding != null ? customPadding : EdgeInsets.only( + top: 8.0, + bottom: 8.0+(MediaQuery.of(context).padding.bottom/5), + ), reverse: reverse, physics: AlwaysScrollableScrollPhysics(), ), diff --git a/lib/core/ui/listview/listview_stickyheader.dart b/lib/core/ui/listview/listview_stickyheader.dart index 19d5012c..a5c27bc9 100644 --- a/lib/core/ui/listview/listview_stickyheader.dart +++ b/lib/core/ui/listview/listview_stickyheader.dart @@ -8,7 +8,7 @@ class LSListViewStickyHeader extends StatelessWidget { LSListViewStickyHeader({ @required this.slivers, @required this.controller, - this.padding = EdgeInsets.zero, + this.padding, }); @override @@ -22,7 +22,10 @@ class LSListViewStickyHeader extends StatelessWidget { ], physics: AlwaysScrollableScrollPhysics(), ), - padding: padding, + padding: padding != null ? padding : EdgeInsets.only( + top: 0.0, + bottom: MediaQuery.of(context).padding.bottom/5, + ), ), ); } diff --git a/lib/core/ui/messages/generic.dart b/lib/core/ui/messages/generic.dart index f22ae918..cbcc83cb 100644 --- a/lib/core/ui/messages/generic.dart +++ b/lib/core/ui/messages/generic.dart @@ -27,8 +27,8 @@ class LSGenericMessage extends StatelessWidget { children: [ Card( child: child, - margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - elevation: Constants.UI_ELEVATION, + margin: LunaUI.MARGIN_CARD, + elevation: LunaUI.ELEVATION, shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data ? LSRoundedShapeWithBorder() : LSRoundedShape(), @@ -48,7 +48,7 @@ class LSGenericMessage extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle( color: Colors.white, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_TITLE, ), ), diff --git a/lib/core/ui/messages/typewriter.dart b/lib/core/ui/messages/typewriter.dart index 69ffe2ef..66395185 100644 --- a/lib/core/ui/messages/typewriter.dart +++ b/lib/core/ui/messages/typewriter.dart @@ -54,8 +54,8 @@ class _State extends State with TickerProviderStateMixin { children: [ Card( child: child, - margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - elevation: Constants.UI_ELEVATION, + margin: LunaUI.MARGIN_CARD, + elevation: LunaUI.ELEVATION, shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data ? LSRoundedShapeWithBorder() : LSRoundedShape(), @@ -75,7 +75,7 @@ class _State extends State with TickerProviderStateMixin { textAlign: TextAlign.center, style: TextStyle( color: Colors.white, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_TITLE, ), ), diff --git a/lib/core/ui/navigation_bar/navigation_bar.dart b/lib/core/ui/navigation_bar/navigation_bar.dart index dd29425d..d6bfe921 100644 --- a/lib/core/ui/navigation_bar/navigation_bar.dart +++ b/lib/core/ui/navigation_bar/navigation_bar.dart @@ -7,12 +7,14 @@ class LSBottomNavigationBar extends StatelessWidget { final Function onTap; final List icons; final List titles; + final List leadingOnGButton; LSBottomNavigationBar({ @required this.index, @required this.onTap, @required this.icons, @required this.titles, + this.leadingOnGButton, }); @override @@ -23,8 +25,8 @@ class LSBottomNavigationBar extends StatelessWidget { child: GNav( gap: 8.0, iconSize: 24.0, - padding: EdgeInsets.fromLTRB(18.0, 5.0, 12.0, 5.0), - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + padding: EdgeInsets.fromLTRB(18.0, 10.0, 12.0, 10.0), + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), tabBackgroundColor: Theme.of(context).canvasColor, activeColor: LunaColours.accent, tabs: List.generate( @@ -33,13 +35,17 @@ class LSBottomNavigationBar extends StatelessWidget { icon: icons[index], text: titles[index], iconSize: 22.0, + haptic: true, + iconColor: Colors.white, textStyle: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, color: LunaColours.accent, ), + leading: leadingOnGButton == null ? null : leadingOnGButton[index], ) ).toList(), + tabActiveBorder: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data ? Border.all(color: Colors.white12) : null, selectedIndex: index, onTabChange: onTap, ), @@ -48,7 +54,6 @@ class LSBottomNavigationBar extends StatelessWidget { ), decoration: BoxDecoration( color: Theme.of(context).primaryColor, - //LunaColours.secondary, ), ); } diff --git a/lib/core/ui/network_image.dart b/lib/core/ui/network_image.dart index 988e21f5..43534516 100644 --- a/lib/core/ui/network_image.dart +++ b/lib/core/ui/network_image.dart @@ -15,10 +15,10 @@ Widget LSNetworkImage({ fit: StackFit.expand, children: [ Image.asset(placeholder), - FadeInImage( + if(url != null) FadeInImage( height: height, width: width, - fadeInDuration: Duration(milliseconds: Constants.UI_IMAGE_FADEIN_SPEED), + fadeInDuration: Duration(milliseconds: LunaUI.ANIMATION_IMAGE_FADE_IN_SPEED), fadeOutDuration: Duration(milliseconds: 1), placeholder: AssetImage(placeholder), fit: BoxFit.cover, diff --git a/lib/core/ui/snackbar.dart b/lib/core/ui/snackbar.dart index d54a8aa5..84285668 100644 --- a/lib/core/ui/snackbar.dart +++ b/lib/core/ui/snackbar.dart @@ -51,7 +51,7 @@ Future LSSnackBar({ title: Text( title, style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_TITLE, ), ), @@ -70,18 +70,18 @@ Future LSSnackBar({ padding: EdgeInsets.only(left: _LEFT_PADDING), ), primaryAction: showButton - ? FlatButton( + ? TextButton( child: Text( buttonText.toUpperCase(), style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: LunaColours.accent, ), ), - textColor: LunaColours.accent, onPressed: () { controller.dismiss(); buttonOnPressed(); - } + }, ) : null, ), diff --git a/lib/core/ui/text/header.dart b/lib/core/ui/text/header.dart index ab376e34..07ca0df0 100644 --- a/lib/core/ui/text/header.dart +++ b/lib/core/ui/text/header.dart @@ -18,7 +18,7 @@ class LSHeader extends StatelessWidget { Text( text.toUpperCase(), style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_HEADER, color: Colors.white, ), diff --git a/lib/core/ui/text/text_highlighted.dart b/lib/core/ui/text/text_highlighted.dart index c5861c45..911b31d9 100644 --- a/lib/core/ui/text/text_highlighted.dart +++ b/lib/core/ui/text/text_highlighted.dart @@ -24,7 +24,7 @@ class LSTextHighlighted extends StatelessWidget { style: TextStyle( fontSize: Constants.UI_FONT_SIZE_SUBHEADER, color: fgColor, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), diff --git a/lib/core/ui/text/text_title.dart b/lib/core/ui/text/text_title.dart index 596d4692..1562f2dc 100644 --- a/lib/core/ui/text/text_title.dart +++ b/lib/core/ui/text/text_title.dart @@ -27,7 +27,7 @@ class LSTitle extends StatelessWidget { : TextAlign.start, style: TextStyle( color: darken ? Colors.white30 : Colors.white, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_TITLE, ), ); diff --git a/lib/core/uuid.dart b/lib/core/uuid.dart index cc315930..6cd29af6 100644 --- a/lib/core/uuid.dart +++ b/lib/core/uuid.dart @@ -1,7 +1,7 @@ import 'package:uuid/uuid.dart'; class LunaUUID { - static final Uuid _generator = Uuid(); + final Uuid _generator = Uuid(); /// Generate a new v4 UUID with the base configuration. String get uuid => _generator.v4(); diff --git a/lib/core/widgets.dart b/lib/core/widgets.dart new file mode 100644 index 00000000..94fc4f43 --- /dev/null +++ b/lib/core/widgets.dart @@ -0,0 +1,3 @@ +export 'widgets/expandable_list_tile.dart'; +export 'widgets/four_line_card_with_poster.dart'; +export 'widgets/two_line_card_with_poster.dart'; diff --git a/lib/core/widgets/expandable_list_tile.dart b/lib/core/widgets/expandable_list_tile.dart new file mode 100644 index 00000000..42d6ae14 --- /dev/null +++ b/lib/core/widgets/expandable_list_tile.dart @@ -0,0 +1,153 @@ +import 'package:expandable/expandable.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaExpandableListTile extends StatefulWidget { + final String title; + final TextSpan collapsedSubtitle1; + final TextSpan collapsedSubtitle2; + final LunaIconButton collapsedTrailing; + final Function onLongPress; + final List expandedHighlightedNodes; + final List expandedTableContent; + final List expandedTableButtons; + final Widget expandedCustomWidget; + + /// Create a [LunaExpandableListTile] which is a list tile that expands into a table-style card. + /// + /// If [expandedWidget] is supplied, that widget is used as the body within the expanded card. + /// Any + LunaExpandableListTile({ + Key key, + @required this.title, + @required this.collapsedSubtitle1, + this.collapsedSubtitle2, + this.collapsedTrailing, + this.onLongPress, + this.expandedHighlightedNodes, + this.expandedTableContent, + this.expandedTableButtons, + this.expandedCustomWidget, + }) : super(key: key) { + assert(title != null); + assert(collapsedSubtitle1 != null); + if(expandedCustomWidget == null) { + assert(expandedTableContent != null); + } + } + + @override + State createState() => _State(); +} + +class _State extends State { + final ExpandableController controller = ExpandableController(); + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ExpandableNotifier( + controller: controller, + child: Expandable( + collapsed: collapsed(), + expanded: expanded(), + ), + ); + } + + Widget collapsed() { + return LunaListTile( + context: context, + title: LunaText.title(text: widget.title), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + color: Colors.white70, + ), + children: [ + widget.collapsedSubtitle1, + if(widget.collapsedSubtitle2 != null) TextSpan(text: '\n'), + if(widget.collapsedSubtitle2 != null) widget.collapsedSubtitle2, + ], + ), + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 2, + ), + onTap: controller.toggle, + onLongPress: widget.onLongPress, + contentPadding: widget.collapsedSubtitle2 != null, + trailing: widget.collapsedTrailing, + ); + } + + Widget expanded() { + Widget child = widget.expandedCustomWidget ?? Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + child: LunaText.title( + text: widget.title, + softWrap: true, + maxLines: 8, + ), + padding: EdgeInsets.only(bottom: 8.0, left: 12.0, right: 12.0, top: 10.0), + ), + if(widget.expandedHighlightedNodes != null) Padding( + child: Wrap( + direction: Axis.horizontal, + spacing: 6.0, + runSpacing: 6.0, + children: widget.expandedHighlightedNodes, + ), + padding: EdgeInsets.only(bottom: 8.0, left: 12.0, right: 12.0, top: 0.0), + ), + Padding( + child: Column( + children: [ + ...widget.expandedTableContent.map((content) => Padding( + child: content, + padding: EdgeInsets.symmetric(horizontal: 12.0), + )).toList(), + if(widget.expandedTableButtons != null) Padding( + child: Row( + children: [ + ...widget.expandedTableButtons.map((button) => Expanded( + child: Padding( + child: button, + padding: EdgeInsets.zero, + ), + )).toList(), + ], + ), + padding: EdgeInsets.symmetric(horizontal: 6.0), + ), + ], + ), + padding: EdgeInsets.only(bottom: 6.0), + ), + ], + ), + ), + ], + ); + return LunaCard( + context: context, + child: InkWell( + child: child, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: controller.toggle, + onLongPress: widget.onLongPress, + ), + ); + } +} diff --git a/lib/core/widgets/four_line_card_with_poster.dart b/lib/core/widgets/four_line_card_with_poster.dart new file mode 100644 index 00000000..b4afcc44 --- /dev/null +++ b/lib/core/widgets/four_line_card_with_poster.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaFourLineCardWithPoster extends StatelessWidget { + final Function onTap; + final Function onLongPress; + final String backgroundUrl; + final String posterUrl; + final String posterPlaceholder; + final String title; + final TextSpan subtitle1; + final TextSpan subtitle2; + final TextSpan subtitle3; + final int subtitle1MaxLines; + final int subtitle2MaxLines; + final int subtitle3MaxLines; + final Widget customSubtitle1; + final Widget customSubtitle2; + final Widget customSubtitle3; + final bool darken; + final Color titleColor; + final Map posterHeaders; + final LunaIconButton trailing; + final double _height = 90.0; + final double _width = 60.0; + final double _padding = 8.0; + + LunaFourLineCardWithPoster({ + Key key, + @required this.posterPlaceholder, + @required this.posterUrl, + @required this.posterHeaders, + @required this.title, + this.backgroundUrl, + this.titleColor = Colors.white, + this.subtitle1, + this.subtitle2, + this.subtitle3, + this.subtitle1MaxLines = 1, + this.subtitle2MaxLines = 1, + this.subtitle3MaxLines = 1, + this.customSubtitle1, + this.customSubtitle2, + this.customSubtitle3, + this.darken = false, + this.onTap, + this.onLongPress, + this.trailing, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + child: Row( + children: [ + _poster(context), + _body(context), + if(trailing != null) _trailing(), + ], + ), + onTap: onTap, + onLongPress: onLongPress, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + ), + decoration: backgroundUrl == null ? null : LunaCardDecoration( + uri: backgroundUrl, + headers: posterHeaders, + ), + ); + } + + Widget _poster(BuildContext context) { + return LunaNetworkImage( + url: posterUrl, + placeholderAsset: posterPlaceholder, + height: _height, + width: _width, + headers: posterHeaders, + ); + } + + Widget _body(BuildContext context) { + return Expanded( + child: Padding( + child: Container( + child: Column( + children: [ + _title(), + if(customSubtitle1 != null) customSubtitle1, + if(customSubtitle1 == null && subtitle1 != null) _subtitle(subtitle1, subtitle1MaxLines), + if(customSubtitle2 != null) customSubtitle2, + if(customSubtitle2 == null && subtitle2 != null) _subtitle(subtitle2, subtitle2MaxLines), + if(customSubtitle3 != null) customSubtitle3, + if(customSubtitle3 == null && subtitle3 != null) _subtitle(subtitle3, subtitle3MaxLines), + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + ), + height: (_height-(_padding*2)), + ), + padding: EdgeInsets.all(_padding), + ), + ); + } + + Widget _title() { + return LunaText.title( + text: title, + color: titleColor, + darken: darken, + maxLines: 1, + ); + } + + Widget _subtitle(TextSpan text, int maxLines) { + return RichText( + text: TextSpan( + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + color: darken ? Colors.white30 : Colors.white70, + ), + children: [text], + ), + overflow: TextOverflow.fade, + softWrap: maxLines == 1 ? false : true, + maxLines: maxLines, + ); + } + + Widget _trailing() { + return Container( + child: Padding( + child: trailing, + padding: EdgeInsets.only(right: 12.0), + ), + height: _height, + ); + } +} diff --git a/lib/core/widgets/two_line_card_with_poster.dart b/lib/core/widgets/two_line_card_with_poster.dart new file mode 100644 index 00000000..e36feedf --- /dev/null +++ b/lib/core/widgets/two_line_card_with_poster.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class LunaTwoLineCardWithPoster extends StatelessWidget { + final Function onTap; + final Function onLongPress; + final String backgroundUrl; + final String posterUrl; + final String posterPlaceholder; + final String title; + final TextSpan subtitle1; + final TextSpan subtitle2; + final Widget customSubtitle1; + final Widget customSubtitle2; + final bool darken; + final Color titleColor; + final Map posterHeaders; + final LunaIconButton trailing; + final double _height = 67.0; + final double _width = 67.0; + final double _padding = 8.0; + + LunaTwoLineCardWithPoster({ + Key key, + @required this.posterPlaceholder, + @required this.posterUrl, + @required this.posterHeaders, + @required this.title, + this.backgroundUrl, + this.titleColor = Colors.white, + this.subtitle1, + this.subtitle2, + this.customSubtitle1, + this.customSubtitle2, + this.darken = false, + this.onTap, + this.onLongPress, + this.trailing, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: InkWell( + child: Row( + children: [ + _poster(context), + _body(context), + if(trailing != null) _trailing(), + ], + ), + onTap: onTap, + onLongPress: onLongPress, + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + ), + decoration: backgroundUrl == null ? null : LunaCardDecoration( + uri: backgroundUrl, + headers: posterHeaders, + ), + ); + } + + Widget _poster(BuildContext context) { + return LunaNetworkImage( + url: posterUrl, + placeholderAsset: posterPlaceholder, + height: _height, + width: _width, + headers: posterHeaders, + ); + } + + Widget _body(BuildContext context) { + return Expanded( + child: Padding( + child: Container( + child: Column( + children: [ + _title(), + if(customSubtitle1 != null) customSubtitle1, + if(customSubtitle1 == null && subtitle1 != null) _subtitle(subtitle1), + if(customSubtitle2 != null) customSubtitle2, + if(customSubtitle2 == null && subtitle2 != null) _subtitle(subtitle2), + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + ), + height: (_height-(_padding*2)), + ), + padding: EdgeInsets.all(_padding), + ), + ); + } + + Widget _title() { + return LunaText.title( + text: title, + color: titleColor, + darken: darken, + maxLines: 1, + ); + } + + Widget _subtitle(TextSpan text) { + return RichText( + text: TextSpan( + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + color: darken ? Colors.white30 : Colors.white70, + ), + children: [text], + ), + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1, + ); + } + + Widget _trailing() { + return Container( + child: Padding( + child: trailing, + padding: EdgeInsets.only(right: 12.0), + ), + height: _height, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index d4bc4619..abbb42b3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; +import 'package:package_info/package_info.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; /// LunaSea Entry Point: Initialize & Run Application @@ -39,23 +40,23 @@ Future _init() async { statusBarColor: Colors.transparent, )); //LunaSea initialization - LunaLogger.initialize(); - LunaNetworking.initialize(); - LunaImageCache.initialize(); - LunaRouter.intialize(); - await LunaFirebase.initialize(); - await LunaInAppPurchases.initialize(); - await Database.initialize(); + LunaLogger().initialize(); + LunaNetworking().initialize(); + LunaImageCache().initialize(); + LunaRouter().intialize(); + await LunaFirebase().initialize(); + await LunaInAppPurchases().initialize(); + await Database().initialize(); } class LunaBIOS extends StatefulWidget { - static final GlobalKey navigatorKey = GlobalKey(); - @override State createState() => _State(); } class _State extends State { + StreamSubscription _notificationListener; + @override void initState() { super.initState(); @@ -64,35 +65,50 @@ class _State extends State { @override void dispose() { - Database.deinitialize() - .whenComplete(() => LunaInAppPurchases.deinitialize()) - .whenComplete(() => super.dispose()); + Future.wait([ + if(_notificationListener != null) _notificationListener.cancel(), + Database().deinitialize(), + LunaInAppPurchases().deinitialize(), + ]).then((_) => super.dispose()); } /// Runs the first-step boot sequence that is required for widgets Future _boot() async { - // Request notifications and register the device token. + // Initialize notifications: Request notification permission, add device token to Firebase (if logged in), add a notification listener for internal headsup await LunaFirebaseMessaging().requestNotificationPermissions(); LunaFirebaseFirestore().addDeviceToken(); + _notificationListener = LunaFirebaseMessaging().showNotificationOnMessageListener(); + // Initialize quick actions + LunaQuickActions().initialize(); + // Check if the changelog needs to be shown, and show if true + PackageInfo.fromPlatform(). + then((package) { + if(AlertsDatabaseValue.CHANGELOG.data != package.buildNumber) LunaBottomModalSheet().showChangelog( + LunaState.navigatorKey.currentContext, + package.buildNumber, + ); + }) + .catchError((error, stack) => LunaLogger().error('Failed to fetch package info', error, stack)); } @override Widget build(BuildContext context) => LunaState.providers( child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.THEME_AMOLED.key]), + valueListenable: Database.lunaSeaBox.listenable(keys: [ + LunaDatabaseValue.THEME_AMOLED.key, + LunaDatabaseValue.THEME_AMOLED_BORDER.key, + ]), builder: (context, box, _) { return MaterialApp( - routes: LunaRouter.routes, + routes: LunaRouter().routes, onGenerateRoute: LunaRouter.router.generator, - darkTheme: LunaTheme.activeTheme(), - theme: LunaTheme.activeTheme(), + darkTheme: LunaTheme().activeTheme(), + theme: LunaTheme().activeTheme(), title: Constants.APPLICATION_NAME, - navigatorKey: LunaBIOS.navigatorKey, - navigatorObservers: [ - SentryNavigatorObserver(), - ], + navigatorKey: LunaState.navigatorKey, + navigatorObservers: [ SentryNavigatorObserver() ], ); - } + }, ), ); } diff --git a/lib/modules.dart b/lib/modules.dart index 60a81b7d..201a899f 100644 --- a/lib/modules.dart +++ b/lib/modules.dart @@ -1,7 +1,6 @@ export 'modules/home.dart'; export 'modules/lidarr.dart'; export 'modules/nzbget.dart'; -export 'modules/ombi.dart'; export 'modules/radarr.dart'; export 'modules/sabnzbd.dart'; export 'modules/search.dart'; diff --git a/lib/modules/home/core/api/data/lidarr.dart b/lib/modules/home/core/api/data/lidarr.dart index dfa2796e..f383f8cd 100644 --- a/lib/modules/home/core/api/data/lidarr.dart +++ b/lib/modules/home/core/api/data/lidarr.dart @@ -40,14 +40,14 @@ class CalendarLidarrData extends CalendarData { if(!hasAllFiles) TextSpan( text: '\nNot Downloaded', style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: Colors.red, ), ), if(hasAllFiles) TextSpan( text: '\nDownloaded', style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.accent, ), ) diff --git a/lib/modules/home/core/api/data/radarr.dart b/lib/modules/home/core/api/data/radarr.dart index d40ed69f..a20ea1ae 100644 --- a/lib/modules/home/core/api/data/radarr.dart +++ b/lib/modules/home/core/api/data/radarr.dart @@ -33,14 +33,14 @@ class CalendarRadarrData extends CalendarData { if(!hasFile) TextSpan( text: '\nNot Downloaded', style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: Colors.red, ), ), if(hasFile) TextSpan( text: '\nDownloaded ($fileQualityProfile)', style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.accent, ), ) @@ -55,13 +55,7 @@ class CalendarRadarrData extends CalendarData { : ''; } - Future enterContent(BuildContext context) async => Navigator.of(context).pushNamed( - RadarrDetailsMovie.ROUTE_NAME, - arguments: RadarrDetailsMovieArguments( - data: null, - movieID: id, - ), - ); + Future enterContent(BuildContext context) async => RadarrMoviesDetailsRouter().navigateTo(context, movieId: id); Widget trailing(BuildContext context) => LSIconButton( icon: Icons.search, @@ -70,18 +64,8 @@ class CalendarRadarrData extends CalendarData { ); @override - Future trailingOnPress(BuildContext context) async { - await RadarrAPI.from(Database.currentProfileObject).automaticSearchMovie(id) - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: title)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - } + Future trailingOnPress(BuildContext context) async => RadarrAPIHelper().automaticSearch(context: context, movieId: id, title: title); @override - Future trailingOnLongPress(BuildContext context) async => Navigator.of(context).pushNamed( - RadarrSearchResults.ROUTE_NAME, - arguments: RadarrSearchResultsArguments( - movieID: id, - title: title, - ), - ); + Future trailingOnLongPress(BuildContext context) async => RadarrReleasesRouter().navigateTo(context, movieId: id); } diff --git a/lib/modules/home/core/api/data/sonarr.dart b/lib/modules/home/core/api/data/sonarr.dart index 5bd39072..753cbb87 100644 --- a/lib/modules/home/core/api/data/sonarr.dart +++ b/lib/modules/home/core/api/data/sonarr.dart @@ -44,14 +44,14 @@ class CalendarSonarrData extends CalendarData { if(!hasFile) TextSpan( text: hasAired ? '\nMissing' : '\nUnaired', style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: hasAired ? Colors.red : LunaColours.blue, ), ), if(hasFile) TextSpan( text: '\nDownloaded ($fileQualityProfile)', style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.accent, ), ) @@ -77,19 +77,11 @@ class CalendarSonarrData extends CalendarData { ); Widget trailing(BuildContext context) => InkWell( - child: IconButton( - icon: Text( - airTimeString, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ), + child: LSIconButton( + text: airTimeString, onPressed: () async => trailingOnPress(context), + onLongPress: () => trailingOnLongPress(context), ), - onLongPress: () async => trailingOnLongPress(context), ); DateTime get airTimeObject { diff --git a/lib/modules/home/core/constants.dart b/lib/modules/home/core/constants.dart index 677e8fcf..ba2668ff 100644 --- a/lib/modules/home/core/constants.dart +++ b/lib/modules/home/core/constants.dart @@ -1,8 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/home.dart'; -import 'package:quick_actions/quick_actions.dart'; class HomeConstants { HomeConstants._(); @@ -16,11 +13,10 @@ class HomeConstants { settingsDescription: 'Configure the Home Screen', helpMessage: '', icon: CustomIcons.home, - route: '/', - color: Color(LunaColours.ACCENT_COLOR), + route: Home.ROUTE_NAME, + color: LunaColours.accent, website: '', github: '', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(Home.ROUTE_NAME, (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Home'), ); } diff --git a/lib/modules/home/core/database.dart b/lib/modules/home/core/database.dart index a9082df7..b5650f64 100644 --- a/lib/modules/home/core/database.dart +++ b/lib/modules/home/core/database.dart @@ -136,7 +136,7 @@ extension HomeDatabaseValueExtension on HomeDatabaseValue { LunaLogger().warning('HomeDatabaseValueExtension', 'put', 'Attempted to enter data for invalid HomeDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/home/core/dialogs.dart b/lib/modules/home/core/dialogs.dart index 1326681f..db30c15c 100644 --- a/lib/modules/home/core/dialogs.dart +++ b/lib/modules/home/core/dialogs.dart @@ -17,7 +17,7 @@ class HomeDialogs { await LSDialog.dialog( context: context, - title: 'Default Page', + title: 'Page', content: List.generate( HomeNavigationBar.titles.length, (index) => LSDialog.tile( diff --git a/lib/modules/home/routes/home.dart b/lib/modules/home/routes/home.dart index 7b07bbf2..8b1234f4 100644 --- a/lib/modules/home/routes/home.dart +++ b/lib/modules/home/routes/home.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/home.dart'; -import 'package:package_info/package_info.dart'; import 'package:tuple/tuple.dart'; class Home extends StatefulWidget { @@ -17,18 +16,6 @@ class _State extends State { final List _refreshKeys = [GlobalKey()]; String _profileState = Database.currentProfileObject.toString(); - @override - void initState() { - super.initState(); - LunaQuickActions.initialize(); - // Check and show changelog if needed - PackageInfo.fromPlatform() - .then((package) { - if(Database.alertsBox.get('ALERTS_CHANGELOG') != package.buildNumber) LunaBottomModalSheet().showChangelog(context, package.buildNumber); - }) - .catchError((error, stack) => LunaLogger().error('Failed to fetch package info', error, stack)); - } - @override Widget build(BuildContext context) => WillPopScope( onWillPop: _willPopScope, @@ -64,9 +51,8 @@ class _State extends State { Widget get _drawer => LSDrawer(page: 'home'); Widget get _appBar => LunaAppBar( - context: context, title: Constants.APPLICATION_NAME, - hideLeading: true, + useDrawer: true, actions: Database.currentProfileObject.anyAutomationEnabled ? [ Selector>( diff --git a/lib/modules/home/widgets/calendar.dart b/lib/modules/home/widgets/calendar.dart index 2d621fa5..5d07ea04 100644 --- a/lib/modules/home/widgets/calendar.dart +++ b/lib/modules/home/widgets/calendar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/home.dart'; @@ -21,17 +22,17 @@ class HomeCalendarWidget extends StatefulWidget { class _State extends State with TickerProviderStateMixin { final TextStyle dayTileStyle = TextStyle( color: Colors.white, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, ); final TextStyle outsideDayTileStyle = TextStyle( color: Colors.white30, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, ); final TextStyle weekdayTitleStyle = TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, ); @@ -56,6 +57,7 @@ class _State extends State with TickerProviderStateMixin { } void _onDaySelected(DateTime day, List events, List _) { + HapticFeedback.selectionClick(); setState(() { _selectedEvents = events; }); @@ -112,7 +114,7 @@ class _State extends State with TickerProviderStateMixin { headerStyle: HeaderStyle( titleTextStyle: TextStyle( fontSize: Constants.UI_FONT_SIZE_HEADER, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), centerHeaderTitle: true, formatButtonVisible: false, diff --git a/lib/modules/home/widgets/navigation_bar.dart b/lib/modules/home/widgets/navigation_bar.dart index c037ad12..54d4f33f 100644 --- a/lib/modules/home/widgets/navigation_bar.dart +++ b/lib/modules/home/widgets/navigation_bar.dart @@ -45,10 +45,7 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ).then((_) => Provider.of(context, listen: false).navigationIndex = index); + widget.pageController.lunaAnimateToPage(index) + .then((_) => Provider.of(context, listen: false).navigationIndex = index); } } \ No newline at end of file diff --git a/lib/modules/home/widgets/summary_tile.dart b/lib/modules/home/widgets/summary_tile.dart index 6f146a78..a054ea02 100644 --- a/lib/modules/home/widgets/summary_tile.dart +++ b/lib/modules/home/widgets/summary_tile.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/home.dart'; class HomeSummaryTile extends StatelessWidget { @@ -32,7 +31,7 @@ class HomeSummaryTile extends StatelessWidget { ? color : LunaColours.list(index), ), - onTap: () async => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false), + onTap: () async => LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route route) => false), ), ); } \ No newline at end of file diff --git a/lib/modules/lidarr/core/api/api.dart b/lib/modules/lidarr/core/api/api.dart index aee02d80..d9f493d3 100644 --- a/lib/modules/lidarr/core/api/api.dart +++ b/lib/modules/lidarr/core/api/api.dart @@ -39,8 +39,8 @@ class LidarrAPI { Future> getAllArtists() async { try { - Map _qualities = await getQualityProfiles().catchError((error) { return Future.error(error); }); - Map _metadatas = await getMetadataProfiles().catchError((error) { return Future.error(error); }); + Map _qualities = await getQualityProfiles(); + Map _metadatas = await getMetadataProfiles(); Response response = await _dio.get('artist'); List entries = []; for(var entry in response.data) { @@ -113,8 +113,8 @@ class LidarrAPI { Future getArtist(int artistID) async { try { - Map _qualities = await getQualityProfiles().catchError((error) { return Future.error(error); }); - Map _metadatas = await getMetadataProfiles().catchError((error) { return Future.error(error); }); + Map _qualities = await getQualityProfiles(); + Map _metadatas = await getMetadataProfiles(); Response response = await _dio.get('artist/$artistID'); return LidarrCatalogueData( title: response.data['artistName'] ?? 'Unknown Artist', diff --git a/lib/modules/lidarr/core/api/data/history.dart b/lib/modules/lidarr/core/api/data/history.dart index 15a045c3..28d004a2 100644 --- a/lib/modules/lidarr/core/api/data/history.dart +++ b/lib/modules/lidarr/core/api/data/history.dart @@ -58,7 +58,7 @@ class LidarrHistoryDataGeneric extends LidarrHistoryData { text: '$eventType', style: TextStyle( color: LunaColours.purple, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ]; @@ -85,7 +85,7 @@ class LidarrHistoryDataGrabbed extends LidarrHistoryData { text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]} $indexer', style: TextStyle( color: Colors.orange, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) ]; @@ -112,7 +112,7 @@ class LidarrHistoryDataTrackFileImported extends LidarrHistoryData { text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]} ($quality)', style: TextStyle( color: Color(LunaColours.ACCENT_COLOR), - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) ]; @@ -139,7 +139,7 @@ class LidarrHistoryDataDownloadImported extends LidarrHistoryData { text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]} ($quality)', style: TextStyle( color: Color(LunaColours.ACCENT_COLOR), - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) ]; @@ -163,7 +163,7 @@ class LidarrHistoryDataAlbumImportIncomplete extends LidarrHistoryData { text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]}', style: TextStyle( color: Colors.orange, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) ]; @@ -190,7 +190,7 @@ class LidarrHistoryDataTrackFileDeleted extends LidarrHistoryData { text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]} (${Constants.historyReasonMessages[reason] ?? reason})', style: TextStyle( color: Colors.red, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) ]; @@ -214,7 +214,7 @@ class LidarrHistoryDataTrackFileRenamed extends LidarrHistoryData { text: '${LidarrConstants.EVENT_TYPE_MESSAGES[eventType]}', style: TextStyle( color: Color(LunaColours.ACCENT_COLOR), - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) ]; diff --git a/lib/modules/lidarr/core/api/data/track.dart b/lib/modules/lidarr/core/api/data/track.dart index 808e43fc..82ee870d 100644 --- a/lib/modules/lidarr/core/api/data/track.dart +++ b/lib/modules/lidarr/core/api/data/track.dart @@ -24,7 +24,7 @@ class LidarrTrackData { text: 'Downloaded', style: TextStyle( color: monitored ? Color(LunaColours.ACCENT_COLOR) : Color(LunaColours.ACCENT_COLOR).withOpacity(0.30), - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ); } else { @@ -32,7 +32,7 @@ class LidarrTrackData { text: 'Not Downloaded', style: TextStyle( color: monitored ? Colors.red : Colors.red.withOpacity(0.30), - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ); } diff --git a/lib/modules/lidarr/core/constants.dart b/lib/modules/lidarr/core/constants.dart index c90fb1a4..ae6a1b40 100644 --- a/lib/modules/lidarr/core/constants.dart +++ b/lib/modules/lidarr/core/constants.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/lidarr.dart'; -import 'package:quick_actions/quick_actions.dart'; class LidarrConstants { LidarrConstants._(); @@ -16,11 +14,10 @@ class LidarrConstants { settingsDescription: 'Configure Lidarr', helpMessage: 'Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.', icon: CustomIcons.music, - route: '/lidarr', + route: Lidarr.ROUTE_NAME, color: Color(0xFF159552), website: 'https://lidarr.audio', github: 'https://github.com/Lidarr/Lidarr', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(Lidarr.ROUTE_NAME, (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Lidarr'), ); diff --git a/lib/modules/lidarr/core/database.dart b/lib/modules/lidarr/core/database.dart index 614f8dff..fdefc141 100644 --- a/lib/modules/lidarr/core/database.dart +++ b/lib/modules/lidarr/core/database.dart @@ -108,7 +108,7 @@ extension LidarrDatabaseValueExtension on LidarrDatabaseValue { LunaLogger().warning('LidarrDatabaseValueExtension', 'put', 'Attempted to enter data for invalid LidarrDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/lidarr/core/dialogs.dart b/lib/modules/lidarr/core/dialogs.dart index 5766edf0..7470c0c2 100644 --- a/lib/modules/lidarr/core/dialogs.dart +++ b/lib/modules/lidarr/core/dialogs.dart @@ -252,7 +252,7 @@ class LidarrDialogs { await LSDialog.dialog( context: context, - title: 'Default Page', + title: 'Page', content: List.generate( LidarrNavigationBar.titles.length, (index) => LSDialog.tile( diff --git a/lib/modules/lidarr/routes/add_details.dart b/lib/modules/lidarr/routes/add_details.dart index 761f7fc4..c031ff06 100644 --- a/lib/modules/lidarr/routes/add_details.dart +++ b/lib/modules/lidarr/routes/add_details.dart @@ -102,7 +102,6 @@ class _State extends State { Widget get _appBar => _arguments == null ? null : LunaAppBar( - context: context, title: _arguments.data.title, actions: [ LSIconButton( @@ -154,7 +153,7 @@ class _State extends State { return LSCardTile( title: LSTitle(text: 'Monitored'), subtitle: LSSubtitle(text: 'Monitor artist for new releases'), - trailing: Switch( + trailing: LunaSwitch( value: LidarrDatabaseValue.ADD_MONITORED.data, onChanged: (value) => LidarrDatabaseValue.ADD_MONITORED.put(value), ), @@ -167,7 +166,7 @@ class _State extends State { return LSCardTile( title: LSTitle(text: 'Use Album Folders'), subtitle: LSSubtitle(text: 'Sort tracks into album folders'), - trailing: Switch( + trailing: LunaSwitch( value: LidarrDatabaseValue.ADD_ALBUM_FOLDERS.data, onChanged: (value) => LidarrDatabaseValue.ADD_ALBUM_FOLDERS.put(value), ), diff --git a/lib/modules/lidarr/routes/add_search.dart b/lib/modules/lidarr/routes/add_search.dart index 08b76fac..30353ab2 100644 --- a/lib/modules/lidarr/routes/add_search.dart +++ b/lib/modules/lidarr/routes/add_search.dart @@ -44,10 +44,7 @@ class _State extends State { .catchError((_) => _availableIDs = []); } - Widget get _appBar => LunaAppBar( - context: context, - title: 'Add Artist', - ); + Widget get _appBar => LunaAppBar(title: 'Add Artist'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/lidarr/routes/details_album.dart b/lib/modules/lidarr/routes/details_album.dart index 091e2c72..1736952d 100644 --- a/lib/modules/lidarr/routes/details_album.dart +++ b/lib/modules/lidarr/routes/details_album.dart @@ -54,16 +54,12 @@ class _State extends State { ); Widget get _appBar => LunaAppBar( - context: context, title: _arguments == null ? 'Details Album' : _arguments.title, actions: [ - InkWell( - child: LSIconButton( - icon: Icons.search, - onPressed: () async => _automaticSearch(), - ), + LSIconButton( + icon: Icons.search, + onPressed: () async => _automaticSearch(), onLongPress: () async => _manualSearch(), - borderRadius: BorderRadius.all(Radius.circular(28.0)), ), ], ); diff --git a/lib/modules/lidarr/routes/details_artist.dart b/lib/modules/lidarr/routes/details_artist.dart index 3b30105e..164c596f 100644 --- a/lib/modules/lidarr/routes/details_artist.dart +++ b/lib/modules/lidarr/routes/details_artist.dart @@ -66,7 +66,6 @@ class _State extends State { ); Widget get _appBar => LunaAppBar( - context: context, title: _arguments == null || _arguments.data == null ? 'Artist Details' : _arguments.data.title, diff --git a/lib/modules/lidarr/routes/edit_artist.dart b/lib/modules/lidarr/routes/edit_artist.dart index dda82db9..9d126e0f 100644 --- a/lib/modules/lidarr/routes/edit_artist.dart +++ b/lib/modules/lidarr/routes/edit_artist.dart @@ -59,8 +59,7 @@ class _State extends State { _monitored = _arguments.entry.monitored; _albumFolders = _arguments.entry.albumFolders; return true; - }) - .catchError((error) => Future.error(error)); + }); } Future _fetchProfiles(LidarrAPI api) async { @@ -74,8 +73,7 @@ class _State extends State { } } } - }) - .catchError((error) => Future.error(error)); + }); } Future _fetchMetadata(LidarrAPI api) async { @@ -89,14 +87,10 @@ class _State extends State { } } } - }) - .catchError((error) => Future.error(error)); + }); } - Widget get _appBar => LunaAppBar( - context: context, - title: _arguments?.entry?.title ?? 'Edit Artist', - ); + Widget get _appBar => LunaAppBar(title: _arguments?.entry?.title ?? 'Edit Artist'); Widget get _body => FutureBuilder( future: _future, @@ -119,7 +113,7 @@ class _State extends State { LSCardTile( title: LSTitle(text: 'Monitored'), subtitle: LSSubtitle(text: 'Monitor artist for new releases'), - trailing: Switch( + trailing: LunaSwitch( value: _monitored, onChanged: (value) => setState(() => _monitored = value), ), @@ -127,7 +121,7 @@ class _State extends State { LSCardTile( title: LSTitle(text: 'Use Album Folders'), subtitle: LSSubtitle(text: 'Sort tracks into album folders'), - trailing: Switch( + trailing: LunaSwitch( value: _albumFolders, onChanged: (value) => setState(() => _albumFolders = value), ), diff --git a/lib/modules/lidarr/routes/lidarr.dart b/lib/modules/lidarr/routes/lidarr.dart index e2836bda..fdc2b2e5 100644 --- a/lib/modules/lidarr/routes/lidarr.dart +++ b/lib/modules/lidarr/routes/lidarr.dart @@ -79,8 +79,7 @@ class _State extends State { onPageChanged: _onPageChanged, ); - Widget get _appBar => LSAppBarDropdown( - context: context, + Widget get _appBar => LunaAppBar.dropdown( title: LidarrConstants.MODULE_METADATA.name, profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).lidarrEnabled) diff --git a/lib/modules/lidarr/routes/search_results.dart b/lib/modules/lidarr/routes/search_results.dart index 8fe01aaa..af016e00 100644 --- a/lib/modules/lidarr/routes/search_results.dart +++ b/lib/modules/lidarr/routes/search_results.dart @@ -55,10 +55,7 @@ class _State extends State { Widget get _appBar => _arguments == null ? null - : LunaAppBar( - context: context, - title: _arguments.title, - ); + : LunaAppBar(title: _arguments.title); Widget get _body => _arguments == null ? null diff --git a/lib/modules/lidarr/widgets/artist_navigation_bar.dart b/lib/modules/lidarr/widgets/artist_navigation_bar.dart index 8aaca71c..a8632330 100644 --- a/lib/modules/lidarr/widgets/artist_navigation_bar.dart +++ b/lib/modules/lidarr/widgets/artist_navigation_bar.dart @@ -21,7 +21,7 @@ class _State extends State { ]; static const List _navbarIcons = [ - Icons.subject, + Icons.subject_rounded, CustomIcons.music, ]; @@ -37,10 +37,7 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ).then((_) => Provider.of(context, listen: false).artistNavigationIndex = index); + widget.pageController.lunaAnimateToPage(index) + .then((_) => Provider.of(context, listen: false).artistNavigationIndex = index); } } diff --git a/lib/modules/lidarr/widgets/catalogue_hide_button.dart b/lib/modules/lidarr/widgets/catalogue_hide_button.dart index 204dfe15..18650edf 100644 --- a/lib/modules/lidarr/widgets/catalogue_hide_button.dart +++ b/lib/modules/lidarr/widgets/catalogue_hide_button.dart @@ -16,7 +16,8 @@ class LidarrCatalogueHideButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( builder: (context, model, widget) => InkWell( child: LSIconButton( diff --git a/lib/modules/lidarr/widgets/catalogue_search_bar.dart b/lib/modules/lidarr/widgets/catalogue_search_bar.dart index 4d7ed0ae..dbb463c8 100644 --- a/lib/modules/lidarr/widgets/catalogue_search_bar.dart +++ b/lib/modules/lidarr/widgets/catalogue_search_bar.dart @@ -15,7 +15,6 @@ class _State extends State { child: Consumer( builder: (context, state, widget) => LSTextInputBar( controller: _textController, - labelText: 'Search Artists...', onChanged: (text, update) => _onChanged(state, text, update), margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), ), diff --git a/lib/modules/lidarr/widgets/catalogue_sorting_button.dart b/lib/modules/lidarr/widgets/catalogue_sorting_button.dart index 9ef7e0a6..938cb5db 100644 --- a/lib/modules/lidarr/widgets/catalogue_sorting_button.dart +++ b/lib/modules/lidarr/widgets/catalogue_sorting_button.dart @@ -16,13 +16,11 @@ class LidarrCatalogueSortButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, model, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.sort), + builder: (context, model, _) => LunaPopupMenuButton( + icon: Icons.sort, onSelected: (result) { if(model.sortCatalogueType == result) { model.sortCatalogueAscending = !model.sortCatalogueAscending; @@ -30,7 +28,7 @@ class _State extends State { model.sortCatalogueAscending = true; model.sortCatalogueType = result; } - _scrollBack(); + widget.controller.lunaAnimateToStart(); }, itemBuilder: (context) => List>.generate( LidarrCatalogueSorting.values.length, @@ -61,14 +59,4 @@ class _State extends State { margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), color: Theme.of(context).canvasColor, ); - - void _scrollBack() { - widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } } diff --git a/lib/modules/lidarr/widgets/details_album_tile.dart b/lib/modules/lidarr/widgets/details_album_tile.dart index eb67fb82..a17765cf 100644 --- a/lib/modules/lidarr/widgets/details_album_tile.dart +++ b/lib/modules/lidarr/widgets/details_album_tile.dart @@ -51,7 +51,7 @@ class _State extends State { text: '\n${widget.data.releaseDateString}', style: TextStyle( color: widget.data.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30), - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ], diff --git a/lib/modules/lidarr/widgets/details_album_track_tile.dart b/lib/modules/lidarr/widgets/details_album_track_tile.dart index 5d9cdfda..3125ff9b 100644 --- a/lib/modules/lidarr/widgets/details_album_track_tile.dart +++ b/lib/modules/lidarr/widgets/details_album_track_tile.dart @@ -41,7 +41,7 @@ class _State extends State { '${widget?.data?.trackNumber}', style: TextStyle( color: widget.monitored ? Colors.white : Colors.white30, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_TITLE, ), ), diff --git a/lib/modules/lidarr/widgets/missing_tile.dart b/lib/modules/lidarr/widgets/missing_tile.dart index c4bb3833..9c4eb8f1 100644 --- a/lib/modules/lidarr/widgets/missing_tile.dart +++ b/lib/modules/lidarr/widgets/missing_tile.dart @@ -38,7 +38,7 @@ class _State extends State { text: '\nReleased ${widget.entry.releaseDateString}', style: TextStyle( color: Colors.red, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ], diff --git a/lib/modules/lidarr/widgets/navigation_bar.dart b/lib/modules/lidarr/widgets/navigation_bar.dart index e5546521..0c294787 100644 --- a/lib/modules/lidarr/widgets/navigation_bar.dart +++ b/lib/modules/lidarr/widgets/navigation_bar.dart @@ -47,11 +47,7 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); - Provider.of(context, listen: false).navigationIndex = index; + widget.pageController.lunaAnimateToPage(index) + .then((_) => Provider.of(context, listen: false).navigationIndex = index); } -} \ No newline at end of file +} diff --git a/lib/modules/lidarr/widgets/releases_hide_button.dart b/lib/modules/lidarr/widgets/releases_hide_button.dart index 180359ec..5e2f4931 100644 --- a/lib/modules/lidarr/widgets/releases_hide_button.dart +++ b/lib/modules/lidarr/widgets/releases_hide_button.dart @@ -16,16 +16,14 @@ class LidarrReleasesHideButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, model, widget) => InkWell( - child: LSIconButton( - icon: model.hideRejectedReleases - ? Icons.visibility_off - : Icons.visibility, - ), - onTap: () => model.hideRejectedReleases = !model.hideRejectedReleases, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + builder: (context, model, widget) => LSIconButton( + icon: model.hideRejectedReleases + ? Icons.visibility_off + : Icons.visibility, + onPressed: () => model.hideRejectedReleases = !model.hideRejectedReleases, ), ), margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), diff --git a/lib/modules/lidarr/widgets/releases_sorting_button.dart b/lib/modules/lidarr/widgets/releases_sorting_button.dart index 2052fbbb..92076cb4 100644 --- a/lib/modules/lidarr/widgets/releases_sorting_button.dart +++ b/lib/modules/lidarr/widgets/releases_sorting_button.dart @@ -16,13 +16,11 @@ class LidarrReleasesSortButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, model, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.sort), + builder: (context, model, _) => LunaPopupMenuButton( + icon: Icons.sort, onSelected: (result) { if(model.sortReleasesType == result) { model.sortReleasesAscending = !model.sortReleasesAscending; @@ -30,7 +28,7 @@ class _State extends State { model.sortReleasesAscending = true; model.sortReleasesType = result; } - _scrollBack(); + widget.controller.lunaAnimateToStart(); }, itemBuilder: (context) => List>.generate( LidarrReleasesSorting.values.length, @@ -61,14 +59,4 @@ class _State extends State { margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), color: Theme.of(context).canvasColor, ); - - void _scrollBack() { - widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } } diff --git a/lib/modules/lidarr/widgets/search_result_tile.dart b/lib/modules/lidarr/widgets/search_result_tile.dart index ff6a99d7..ab078737 100644 --- a/lib/modules/lidarr/widgets/search_result_tile.dart +++ b/lib/modules/lidarr/widgets/search_result_tile.dart @@ -57,7 +57,7 @@ class LidarrSearchResultTile extends StatelessWidget { text: '${data.seeders} Seeders\t•\t${data.leechers} Leechers\n', style: TextStyle( color: LunaColours.purple, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), TextSpan(text: '${data.quality ?? 'Unknown'}\t•\t'), @@ -120,7 +120,7 @@ class LidarrSearchResultTile extends StatelessWidget { color: data.isTorrent ? LunaColours.purple : LunaColours.blue, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), text: data.protocol.lunaCapitalizeFirstLetters(), ), @@ -128,7 +128,7 @@ class LidarrSearchResultTile extends StatelessWidget { text: ' (${data.seeders}/${data.leechers})', style: TextStyle( color: LunaColours.purple, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), TextSpan(text: '\t•\t${data.indexer}\t•\t'), diff --git a/lib/modules/nzbget/core/api/api.dart b/lib/modules/nzbget/core/api/api.dart index 6b4591bd..8a661398 100644 --- a/lib/modules/nzbget/core/api/api.dart +++ b/lib/modules/nzbget/core/api/api.dart @@ -42,11 +42,11 @@ class NZBGetAPI { String get user => _values['user']; String get pass => _values['pass']; - String getBody(String method, {List params = Constants.EMPTY_LIST}) { + String getBody(String method, { List params }) { return json.encode({ "jsonrpc": "2.0", "method": method, - "params": params, + "params": params ?? [], "id": 1, }); } diff --git a/lib/modules/nzbget/core/constants.dart b/lib/modules/nzbget/core/constants.dart index 17dce0f4..23320bd3 100644 --- a/lib/modules/nzbget/core/constants.dart +++ b/lib/modules/nzbget/core/constants.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/nzbget.dart'; -import 'package:quick_actions/quick_actions.dart'; class NZBGetConstants { NZBGetConstants._(); @@ -16,11 +14,10 @@ class NZBGetConstants { settingsDescription: 'Configure NZBGet', helpMessage: 'NZBGet is a binary downloader, which downloads files from Usenet based on information given in nzb-files.', icon: CustomIcons.nzbget, - route: '/nzbget', + route: NZBGet.ROUTE_NAME, color: Color(0xFF42D535), website: 'https://nzbget.net', github: 'https://github.com/nzbget/nzbget', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(NZBGet.ROUTE_NAME, (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'NZBGet'), ); } diff --git a/lib/modules/nzbget/core/database.dart b/lib/modules/nzbget/core/database.dart index 30cbafe8..e5167bd5 100644 --- a/lib/modules/nzbget/core/database.dart +++ b/lib/modules/nzbget/core/database.dart @@ -65,7 +65,7 @@ extension NZBGetDatabaseValueExtension on NZBGetDatabaseValue { LunaLogger().warning('NZBGetDatabaseValueExtension', 'put', 'Attempted to enter data for invalid NZBGetDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/nzbget/core/dialogs.dart b/lib/modules/nzbget/core/dialogs.dart index dbc8efea..26f49fb9 100644 --- a/lib/modules/nzbget/core/dialogs.dart +++ b/lib/modules/nzbget/core/dialogs.dart @@ -578,7 +578,7 @@ class NZBGetDialogs { await LSDialog.dialog( context: context, - title: 'Default Page', + title: 'Page', content: List.generate( NZBGetNavigationBar.titles.length, (index) => LSDialog.tile( diff --git a/lib/modules/nzbget/routes/nzbget.dart b/lib/modules/nzbget/routes/nzbget.dart index 2e26380f..ae79a72f 100644 --- a/lib/modules/nzbget/routes/nzbget.dart +++ b/lib/modules/nzbget/routes/nzbget.dart @@ -69,8 +69,7 @@ class _State extends State { onPageChanged: _onPageChanged, ); - Widget get _appBar => LSAppBarDropdown( - context: context, + Widget get _appBar => LunaAppBar.dropdown( title: 'NZBGet', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).nzbgetEnabled) diff --git a/lib/modules/nzbget/routes/queue.dart b/lib/modules/nzbget/routes/queue.dart index 9a71e86b..1282ce16 100644 --- a/lib/modules/nzbget/routes/queue.dart +++ b/lib/modules/nzbget/routes/queue.dart @@ -83,8 +83,7 @@ class _State extends State with TickerProviderStateMixin, Automatic Future _fetchQueue(NZBGetAPI api) async { final _model = Provider.of(context, listen: false); return await api.getQueue(_model.speed, 100) - .then((data) => _queue = data) - .catchError((error) => Future.error(error)); + .then((data) => _queue = data); } Future _fetchStatus(NZBGetAPI api) async { @@ -99,8 +98,7 @@ class _State extends State with TickerProviderStateMixin, Automatic _model.queueTimeLeft = data.timeLeft; _model.speedLimit = data.speedlimitString; } - }) - .catchError((error) => Future.error(error)); + }); } void _setError(bool error) { diff --git a/lib/modules/nzbget/routes/statistics.dart b/lib/modules/nzbget/routes/statistics.dart index 682d0a56..f0868158 100644 --- a/lib/modules/nzbget/routes/statistics.dart +++ b/lib/modules/nzbget/routes/statistics.dart @@ -33,20 +33,15 @@ class _State extends State { final _api = NZBGetAPI.from(Database.currentProfileObject); return _fetchStatistics(_api) .then((_) => _fetchLogs(_api)) - .then((_) => true) - .catchError((error) => Future.error(error)); + .then((_) => true); } Future _fetchStatistics(NZBGetAPI api) async { - return await api.getStatistics() - .then((stats) => { _statistics = stats }) - .catchError((error) => Future.error(error)); + return await api.getStatistics().then((stats) => { _statistics = stats }); } Future _fetchLogs(NZBGetAPI api) async { - return await api.getLogs() - .then((logs) => { _logs = logs }) - .catchError((error) => Future.error(error)); + return await api.getLogs().then((logs) => { _logs = logs }); } @override @@ -56,10 +51,7 @@ class _State extends State { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Server Statistics', - ); + Widget get _appBar => LunaAppBar(title: 'Server Statistics'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/nzbget/widgets/app_bar_stats.dart b/lib/modules/nzbget/widgets/app_bar_stats.dart index f3cd61cf..c2d11fb8 100644 --- a/lib/modules/nzbget/widgets/app_bar_stats.dart +++ b/lib/modules/nzbget/widgets/app_bar_stats.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; import 'package:tuple/tuple.dart'; import 'package:lunasea/modules/nzbget.dart'; @@ -26,7 +27,7 @@ class NZBGetAppBarStats extends StatelessWidget { TextSpan( text: _status(data.item1, data.item2), style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_HEADER, color: LunaColours.accent, ), @@ -53,6 +54,7 @@ class NZBGetAppBarStats extends StatelessWidget { : speed; Future _onTap(BuildContext context, String speed) async { + HapticFeedback.lightImpact(); List values = await NZBGetDialogs.speedLimit(context, speed); if(values[0]) switch(values[1]) { case -1: { diff --git a/lib/modules/nzbget/widgets/history_hide_button.dart b/lib/modules/nzbget/widgets/history_hide_button.dart index 20cadbaa..df7a2b1b 100644 --- a/lib/modules/nzbget/widgets/history_hide_button.dart +++ b/lib/modules/nzbget/widgets/history_hide_button.dart @@ -9,16 +9,14 @@ class NZBGetHistoryHideButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, model, widget) => InkWell( - child: LSIconButton( - icon: model.historyHideFailed - ? Icons.visibility_off - : Icons.visibility, - ), - onTap: () => model.historyHideFailed = !model.historyHideFailed, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + builder: (context, model, widget) => LSIconButton( + icon: model.historyHideFailed + ? Icons.visibility_off + : Icons.visibility, + onPressed: () => model.historyHideFailed = !model.historyHideFailed, ), ), margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), diff --git a/lib/modules/nzbget/widgets/history_tile.dart b/lib/modules/nzbget/widgets/history_tile.dart index 0dcadd97..7232c481 100644 --- a/lib/modules/nzbget/widgets/history_tile.dart +++ b/lib/modules/nzbget/widgets/history_tile.dart @@ -134,7 +134,7 @@ class NZBGetHistoryTile extends StatelessWidget { text: data.statusString, style: TextStyle( color: data.statusColor, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ], diff --git a/lib/modules/nzbget/widgets/navigation_bar.dart b/lib/modules/nzbget/widgets/navigation_bar.dart index e938969d..4852226a 100644 --- a/lib/modules/nzbget/widgets/navigation_bar.dart +++ b/lib/modules/nzbget/widgets/navigation_bar.dart @@ -45,11 +45,7 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); - Provider.of(context, listen: false).navigationIndex = index; + widget.pageController.lunaAnimateToPage(index) + .then((_) => Provider.of(context, listen: false).navigationIndex = index); } } diff --git a/lib/modules/nzbget/widgets/queue_fab.dart b/lib/modules/nzbget/widgets/queue_fab.dart index 54be9b38..d91e5683 100644 --- a/lib/modules/nzbget/widgets/queue_fab.dart +++ b/lib/modules/nzbget/widgets/queue_fab.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; import 'package:tuple/tuple.dart'; import 'package:lunasea/modules/nzbget.dart'; @@ -38,14 +39,14 @@ class _State extends State with TickerProviderStateMixin { void _setupIconController() { _iconController = AnimationController( vsync: this, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), ); } void _setupHideController() { _hideController = AnimationController( vsync: this, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), ); _hideController.forward(); widget.scrollController.addListener(() { @@ -88,6 +89,7 @@ class _State extends State with TickerProviderStateMixin { ); Future _toggle(BuildContext context, bool paused) async { + HapticFeedback.lightImpact(); NZBGetAPI _api = NZBGetAPI.from(Database.currentProfileObject); paused ? _resume(context, _api) @@ -95,6 +97,7 @@ class _State extends State with TickerProviderStateMixin { } Future _toggleFor(BuildContext context) async { + HapticFeedback.heavyImpact(); List values = await NZBGetDialogs.pauseFor(context); if(values[0]) { if(values[1] == -1) { diff --git a/lib/modules/ombi.dart b/lib/modules/ombi.dart deleted file mode 100644 index 3d14222f..00000000 --- a/lib/modules/ombi.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'ombi/core.dart'; -export 'ombi/modules.dart'; diff --git a/lib/modules/ombi/core.dart b/lib/modules/ombi/core.dart deleted file mode 100644 index c2a19ad4..00000000 --- a/lib/modules/ombi/core.dart +++ /dev/null @@ -1,7 +0,0 @@ -export 'core/constants.dart'; -export 'core/database.dart'; -export 'core/dialogs.dart'; -export 'core/extensions.dart'; -export 'core/router.dart'; -export 'core/state.dart'; -export 'core/types.dart'; diff --git a/lib/modules/ombi/core/constants.dart b/lib/modules/ombi/core/constants.dart deleted file mode 100644 index 165b9210..00000000 --- a/lib/modules/ombi/core/constants.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; -import 'package:lunasea/modules/ombi.dart'; -import 'package:quick_actions/quick_actions.dart'; - -class OmbiConstants { - OmbiConstants._(); - - static const MODULE_KEY = 'ombi'; - - //ignore: non_constant_identifier_names - static LunaModuleMetadata MODULE_METADATA = LunaModuleMetadata( - name: 'Ombi', - description: 'Manage Requests for Media', - settingsDescription: 'Configure Ombi', - helpMessage: 'Ombi is a self-hosted web application that automatically gives your shared Plex or Emby users the ability to request content by themselves! Ombi can be linked to multiple TV Show and Movie DVR tools to create a seamless end-to-end experience for your users.', - icon: CustomIcons.tautulli, - route: '/ombi', - color: Color(0xFFD4782C), - website: 'https://ombi.io', - github: 'https://github.com/tidusjar/Ombi', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(OmbiHomeRouter.route(), (Route route) => false), - shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Ombi'), - ); -} diff --git a/lib/modules/ombi/core/database.dart b/lib/modules/ombi/core/database.dart deleted file mode 100644 index 6912afdc..00000000 --- a/lib/modules/ombi/core/database.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class OmbiDatabase extends LunaModuleDatabase { - @override - void registerAdapters() {} - - @override - Map export() { - Map data = {}; - for(OmbiDatabaseValue value in OmbiDatabaseValue.values) { - switch(value) { - // Primitive values - case OmbiDatabaseValue.NAVIGATION_INDEX: data[value.key] = value.data; break; - } - } - return data; - } - - @override - void import(Map config) { - for(String key in config.keys) { - OmbiDatabaseValue value = valueFromKey(key); - if(value != null) switch(value) { - // Primitive values - case OmbiDatabaseValue.NAVIGATION_INDEX: value.put(config[key]); break; - } - } - } - - @override - OmbiDatabaseValue valueFromKey(String key) { - switch(key) { - case 'OMBI_NAVIGATION_INDEX': return OmbiDatabaseValue.NAVIGATION_INDEX; - default: return null; - } - } -} - -enum OmbiDatabaseValue { - NAVIGATION_INDEX, -} - -extension OmbiDatabaseValueExtension on OmbiDatabaseValue { - String get key { - switch(this) { - case OmbiDatabaseValue.NAVIGATION_INDEX: return 'OMBI_NAVIGATION_INDEX'; - } - throw Exception('key not found'); - } - - dynamic get data { - final _box = Database.lunaSeaBox; - switch(this) { - case OmbiDatabaseValue.NAVIGATION_INDEX: return _box.get(this.key, defaultValue: 0); - } - throw Exception('data not found'); - } - - void put(dynamic value) { - final box = Database.lunaSeaBox; - switch(this) { - case OmbiDatabaseValue.NAVIGATION_INDEX: if(value.runtimeType == int) box.put(this.key, value); return; - } - LunaLogger().warning('OmbiDatabaseValueExtension', 'put', 'Attempted to enter data for invalid OmbiDatabaseValue: ${this?.toString() ?? 'null'}'); - } - - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), - builder: builder, - ); -} diff --git a/lib/modules/ombi/core/dialogs.dart b/lib/modules/ombi/core/dialogs.dart deleted file mode 100644 index 443dfaf5..00000000 --- a/lib/modules/ombi/core/dialogs.dart +++ /dev/null @@ -1,3 +0,0 @@ -class OmbiDialogs { - OmbiDialogs._(); -} diff --git a/lib/modules/ombi/core/extensions.dart b/lib/modules/ombi/core/extensions.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/modules/ombi/core/router.dart b/lib/modules/ombi/core/router.dart deleted file mode 100644 index 5bf32261..00000000 --- a/lib/modules/ombi/core/router.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:fluro/fluro.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/ombi.dart'; - -class OmbiRouter extends LunaModuleRouter { - @override - void defineAllRoutes(FluroRouter router) { - OmbiHomeRouter.defineRoutes(router); - } -} diff --git a/lib/modules/ombi/core/state.dart b/lib/modules/ombi/core/state.dart deleted file mode 100644 index 4bf628b0..00000000 --- a/lib/modules/ombi/core/state.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:lunasea/core.dart'; - -class OmbiState extends LunaModuleState { - @override - void reset() {} -} diff --git a/lib/modules/ombi/core/types.dart b/lib/modules/ombi/core/types.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/modules/ombi/modules.dart b/lib/modules/ombi/modules.dart deleted file mode 100644 index eb2003f6..00000000 --- a/lib/modules/ombi/modules.dart +++ /dev/null @@ -1 +0,0 @@ -export 'modules/ombi.dart'; diff --git a/lib/modules/ombi/modules/ombi.dart b/lib/modules/ombi/modules/ombi.dart deleted file mode 100644 index f953db5b..00000000 --- a/lib/modules/ombi/modules/ombi.dart +++ /dev/null @@ -1 +0,0 @@ -export 'ombi/route.dart'; diff --git a/lib/modules/ombi/modules/ombi/route.dart b/lib/modules/ombi/modules/ombi/route.dart deleted file mode 100644 index 7fdd7d85..00000000 --- a/lib/modules/ombi/modules/ombi/route.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:fluro/fluro.dart'; -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class OmbiHomeRouter { - static const ROUTE_NAME = '/ombi'; - - static Future 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) => _OmbiHomeRoute()), - transitionType: LunaRouter.transitionType, - ); - } - - OmbiHomeRouter._(); -} - -class _OmbiHomeRoute extends StatefulWidget { - @override - State<_OmbiHomeRoute> createState() => _State(); -} - -class _State extends State<_OmbiHomeRoute> { - final GlobalKey _scaffoldKey = GlobalKey(); - - @override - Widget build(BuildContext context) => WillPopScope( - onWillPop: _onWillPop, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [ LunaDatabaseValue.ENABLED_PROFILE.key ]), - builder: (context, box, _) => Scaffold( - key: _scaffoldKey, - drawer: _drawer, - ), - ), - ); - - Future _onWillPop() async { - if(_scaffoldKey.currentState.isDrawerOpen) return true; - _scaffoldKey.currentState.openDrawer(); - return false; - } - - Widget get _drawer => LSDrawer(page: 'ombi'); -} diff --git a/lib/modules/radarr.dart b/lib/modules/radarr.dart index e23bd399..dff7ab06 100644 --- a/lib/modules/radarr.dart +++ b/lib/modules/radarr.dart @@ -1,3 +1,3 @@ +export 'package:radarr/radarr.dart'; export 'radarr/core.dart'; -export 'radarr/routes.dart'; -export 'radarr/widgets.dart'; +export 'radarr/modules.dart'; diff --git a/lib/modules/radarr/core.dart b/lib/modules/radarr/core.dart index 0a55df2c..0199f128 100644 --- a/lib/modules/radarr/core.dart +++ b/lib/modules/radarr/core.dart @@ -1,6 +1,8 @@ -export 'core/api.dart'; +export 'core/api_helper.dart'; export 'core/constants.dart'; export 'core/database.dart'; export 'core/dialogs.dart'; +export 'core/extensions.dart'; +export 'core/router.dart'; export 'core/state.dart'; -export 'core/sorting.dart'; +export 'core/types.dart'; diff --git a/lib/modules/radarr/core/api.dart b/lib/modules/radarr/core/api.dart deleted file mode 100644 index 1d50e48f..00000000 --- a/lib/modules/radarr/core/api.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'api/api.dart'; -export 'api/data.dart'; diff --git a/lib/modules/radarr/core/api/api.dart b/lib/modules/radarr/core/api/api.dart deleted file mode 100644 index 37f18cdc..00000000 --- a/lib/modules/radarr/core/api/api.dart +++ /dev/null @@ -1,676 +0,0 @@ -import 'dart:convert'; -import 'package:dio/dio.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrAPI { - final Map _values; - final Dio _dio; - - RadarrAPI._internal(this._values, this._dio); - factory RadarrAPI.from(ProfileHiveObject profile) { - Map _headers = Map.from(profile.getRadarr()['headers']); - Dio _client = Dio( - BaseOptions( - baseUrl: (profile.getRadarr()['host'] as String).endsWith('/') - ? '${profile.getRadarr()['host']}api/' - : '${profile.getRadarr()['host']}/api/', - queryParameters: { - if(profile.getRadarr()['key'] != '') 'apikey': profile.getRadarr()['key'], - }, - headers: _headers, - followRedirects: true, - maxRedirects: 5, - ), - ); - return RadarrAPI._internal( - profile.getRadarr(), - _client, - ); - } - - void logError(String text, Object error, StackTrace trace) => LunaLogger().error('Radarr: $text', error, trace); - - bool get enabled => _values['enabled']; - String get host => _values['host']; - String get key => _values['key']; - - Future testConnection() async => _dio.get('system/status'); - - /// addMovie: Adds a movie to Radarr, returns Radarr ID (integer) for added movie - Future addMovie(RadarrSearchData entry, RadarrQualityProfile quality, RadarrRootFolder rootFolder, RadarrAvailability minAvailability, bool monitored, {bool search = false}) async { - try { - Response response = await _dio.post( - 'movie', - data: json.encode({ - 'title': entry.title, - 'titleSlug': entry.titleSlug, - 'qualityProfileId': quality.id, - 'rootFolderPath': rootFolder.path, - 'images': entry.images, - 'tmdbId': entry.tmdbId, - 'year': entry.year, - 'minimumAvailability': minAvailability.id, - 'monitored': monitored, - 'addOptions': { - 'searchForMovie': search, - } - }), - ); - return response.data['id']; - } on DioError catch (error, stack) { - logError('Failed to add movie (${entry.title})', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to add movie (${entry.title})', error, stack); - return Future.error(error); - } - } - - Future refreshMovie(int id) async { - try { - await _dio.post( - 'command', - data: json.encode({ - 'name': 'RefreshMovie', - 'movieId': id, - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to refresh movie ($id)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to refresh movie ($id)', error, stack); - return Future.error(error); - } - } - - Future updateLibrary() async { - try { - await _dio.post( - 'command', - data: json.encode({ - 'name': 'RefreshMovie', - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to update library', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to update library', error, stack); - return Future.error(error); - } - } - - Future triggerRssSync() async { - try { - await _dio.post( - 'command', - data: json.encode({ - 'name': 'RssSync', - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to trigger RSS sync', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to trigger RSS sync', error, stack); - return Future.error(error); - } - } - - Future triggerBackup() async { - try { - await _dio.post( - 'command', - data: json.encode({ - 'name': 'Backup', - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to backup database', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to backup database', error, stack); - return Future.error(error); - } - } - - Future editMovie(int movieID, RadarrQualityProfile qualityProfile, RadarrAvailability availability, String path, bool monitored, bool staticPath) async { - try { - Response response = await _dio.get('movie/$movieID'); - Map movie = response.data; - movie['monitored'] = monitored; - movie['minimumAvailability'] = availability.id; - movie['profileId'] = qualityProfile.id; - movie['path'] = path; - movie['pathState'] = staticPath ? 'static' : 'dynamic'; - response = await _dio.put( - 'movie', - data: json.encode(movie), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to edit movie ($movieID)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to edit movie ($movieID)', error, stack); - return Future.error(error); - } - } - - Future removeMovie(int movieID, {bool deleteFiles = false, bool addExclusion = false }) async { - try { - await _dio.delete( - 'movie/$movieID', - queryParameters: { - 'deleteFiles': deleteFiles, - 'addExclusion': addExclusion, - }, - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to remove movie ($movieID)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to remove movie ($movieID)', error, stack); - return Future.error(error); - } - } - - Future> getAllMovies() async { - try { - Map _qualities = await getQualityProfiles().catchError((error) { return Future.error(error); }); - Response response = await _dio.get('movie'); - List entries = []; - for(var entry in response.data) { - var _quality = entry['qualityProfileId'] != null ? _qualities[entry['qualityProfileId']] : null; - entries.add(RadarrCatalogueData( - title: entry['title'] ?? 'Unknown Title', - sortTitle: entry['sortTitle'] ?? 'Unknown Title', - studio: entry['studio'] ?? 'Unknown Studio', - physicalRelease: entry['physicalRelease'] ?? '', - inCinemas: entry['inCinemas'] ?? '', - added: entry['added'] ?? '', - status: entry['status'] ?? 'Unknown Status', - year: entry['year'] ?? 0, - movieID: entry['id'] ?? -1, - monitored: entry['monitored'] ?? false, - downloaded: entry['downloaded'] ?? false, - sizeOnDisk: entry['sizeOnDisk'] ?? 0, - runtime: entry['runtime'] ?? 0, - profile: _quality != null ? _quality.name : '', - movieFile: entry['downloaded'] ? entry['movieFile'] : null, - overview: entry['overview'] ?? 'No summary is available.', - path: entry['path'] ?? 'Unknown Path', - qualityProfile: entry['qualityProfileId'] ?? -1, - minimumAvailability: entry['minimumAvailability'] ?? '', - youtubeId: entry['youTubeTrailerId'] ?? '', - imdbId: entry['imdbId'] ?? '', - tmdbId: entry['tmdbId'] ?? 0, - staticPath: entry['pathState'] == 'static' ? true : false, - )); - } - return entries; - } on DioError catch (error, stack) { - logError('Failed to fetch all movies', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch all movies', error, stack); - return Future.error(error); - } - } - - Future> getAllMovieIDs() async { - try { - Response response = await _dio.get('movie'); - List _entries = []; - for(var entry in response.data) { - _entries.add(entry['tmdbId'] ?? ''); - } - return _entries; - } on DioError catch (error, stack) { - logError('Failed to fetch all movie IDs', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch all movie IDs', error, stack); - return Future.error(error); - } - } - - Future getMovie(int movieID) async { - try { - Map _qualities = await getQualityProfiles().catchError((error) { return Future.error(error); }); - Response response = await _dio.get('movie/$movieID'); - var _quality = response.data['qualityProfileId'] != null ? _qualities[response.data['qualityProfileId']] : null; - return RadarrCatalogueData( - title: response.data['title'] ?? 'Unknown Title', - sortTitle: response.data['sortTitle'] ?? 'Unknown Title', - studio: response.data['studio'] ?? 'Unknown Studio', - physicalRelease: response.data['physicalRelease'] ?? '', - inCinemas: response.data['inCinemas'] ?? '', - added: response.data['added'] ?? '', - status: response.data['status'] ?? 'Unknown Status', - year: response.data['year'] ?? 0, - movieID: response.data['id'] ?? -1, - monitored: response.data['monitored'] ?? false, - downloaded: response.data['downloaded'] ?? false, - sizeOnDisk: response.data['sizeOnDisk'] ?? 0, - runtime: response.data['runtime'] ?? 0, - profile: _quality != null ? _quality.name : '', - movieFile: response.data['downloaded'] ? response.data['movieFile'] : null, - overview: response.data['overview'] ?? 'No summary is available.', - path: response.data['path'] ?? 'Unknown Path', - qualityProfile: response.data['qualityProfileId'] ?? -1, - minimumAvailability: response.data['minimumAvailability'] ?? '', - youtubeId: response.data['youTubeTrailerId'] ?? '', - imdbId: response.data['imdbId'] ?? '', - tmdbId: response.data['tmdbId'] ?? 0, - staticPath: response.data['pathState'] == 'static' ? true : false, - ); - } on DioError catch (error, stack) { - logError('Failed to fetch movie ($movieID)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch movie ($movieID)', error, stack); - return Future.error(error); - } - } - - Future removeMovieFile(int movieID) async { - try { - await _dio.delete('moviefile/$movieID'); - return true; - } on DioError catch (error, stack) { - logError('Failed to remove movie file ($movieID)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to remove movie file ($movieID)', error, stack); - return Future.error(error); - } - } - - Future> getMissing() async { - try { - Map _qualities = await getQualityProfiles().catchError((error) { return Future.error(error); }); - Response response = await _dio.get('movie'); - List _entries = []; - for(var entry in response.data) { - if(!entry['downloaded'] && entry['monitored'] && entry['status'] == 'released') { - var _quality = entry['qualityProfileId'] != null ? _qualities[entry['qualityProfileId']] : null; - _entries.add(RadarrMissingData( - movieID: entry['id'] ?? -1, - title: entry['title'] ?? 'Unknown Title', - sortTitle: entry['sortTitle'] ?? 'Unknown Title', - studio: entry['studio'] ?? 'Unknown Studio', - physicalRelease: entry['physicalRelease'] ?? '', - inCinemas: entry['inCinemas'] ?? '', - profile: _quality != null ? _quality.name : '', - year: entry['year'] ?? 0, - runtime: entry['runtime'] ?? 0, - status: entry['status'] ?? 'Unknown Status', - )); - } - } - //Sort the list by the appropriate dates - _entries.sort((a,b) { - if(a.physicalReleaseObject == null) return 1; - if(b.physicalReleaseObject == null) return -1; - return b.physicalReleaseObject.compareTo(a.physicalReleaseObject); - }); - return _entries; - } on DioError catch (error, stack) { - logError('Failed to fetch missing movies', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch missing movies', error, stack); - return Future.error(error); - } - } - - Future> getUpcoming() async { - try { - Map _qualities = await getQualityProfiles().catchError((error) { return Future.error(error); }); - Response response = await _dio.get('movie'); - List _cinemaEntries = []; - List _announcedEntries = []; - for(var entry in response.data) { - if(!entry['downloaded'] && entry['monitored']) { - var _quality = entry['qualityProfileId'] != null ? _qualities[entry['qualityProfileId']] : null; - if(entry['status'] == 'inCinemas') { - _cinemaEntries.add(RadarrMissingData( - movieID: entry['id'] ?? -1, - title: entry['title'] ?? 'Unknown Title', - sortTitle: entry['sortTitle'] ?? 'Unknown Title', - studio: entry['studio'] ?? 'Unknown Studio', - physicalRelease: entry['physicalRelease'] ?? '', - inCinemas: entry['inCinemas'] ?? '', - profile: _quality != null ? _quality.name : '', - year: entry['year'] ?? 0, - runtime: entry['runtime'] ?? 0, - status: entry['status'] ?? 'Unknown Status', - )); - } - if(entry['status'] == 'announced') { - _announcedEntries.add(RadarrMissingData( - movieID: entry['id'] ?? -1, - title: entry['title'] ?? 'Unknown Title', - sortTitle: entry['sortTitle'] ?? 'Unknown Title', - studio: entry['studio'] ?? 'Unknown Studio', - physicalRelease: entry['physicalRelease'] ?? '', - inCinemas: entry['inCinemas'] ?? '', - profile: _quality != null ? _quality.name : '', - year: entry['year'] ?? 0, - runtime: entry['runtime'] ?? 0, - status: entry['status'] ?? 'Unknown Status', - )); - } - } - } - _cinemaEntries.sort((a,b) { - if(a.physicalReleaseObject == null) return 1; - if(b.physicalReleaseObject == null) return -1; - return a.physicalReleaseObject.compareTo(b.physicalReleaseObject); - }); - _announcedEntries.sort((a,b) { - if(a.inCinemasObject == null) return 1; - if(b.inCinemasObject == null) return -1; - return a.inCinemasObject.compareTo(b.inCinemasObject); - }); - return [_cinemaEntries, _announcedEntries].expand((x) => x).toList(); - } on DioError catch (error, stack) { - logError('Failed to fetch upcoming movies', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch upcoming movies', error, stack); - return Future.error(error); - } - } - - Future> getHistory({ String sortKey = 'date', int pageSize = 250, String sortDir = 'desc' }) async { - try { - Response response = await _dio.get( - 'history', - queryParameters: { - 'sortKey': sortKey, - 'pageSize': pageSize, - 'sortDir': sortDir, - }, - ); - List _entries = []; - for(var entry in response.data['records']) { - switch(entry['eventType']) { - case 'downloadFolderImported': { - _entries.add(RadarrHistoryDataDownloadImported( - movieID: entry['movieId'] ?? -1, - movieTitle: entry['movie']['title'] ?? 'Unknown Title', - timestamp: entry['date'] ?? '', - quality: entry['quality']['quality']['name'] ?? 'Unknown Quality', - )); - break; - } - case 'downloadFailed': { - _entries.add(RadarrHistoryDataDownloadFailed( - movieID: entry['movieId'] ?? -1, - movieTitle: entry['movie']['title'] ?? 'Unknown Title', - timestamp: entry['date'] ?? '', - )); - break; - } - case 'movieFileDeleted': { - _entries.add(RadarrHistoryDataFileDeleted( - movieID: entry['movieId'] ?? -1, - movieTitle: entry['movie']['title'] ?? 'Unknown Title', - timestamp: entry['date'] ?? '', - reason: entry['data']['reason'] ?? 'Unknown Reason', - )); - break; - } - case 'movieFileRenamed': { - _entries.add(RadarrHistoryDataFileRenamed( - movieID: entry['movieId'] ?? -1, - movieTitle: entry['movie']['title'] ?? 'Unknown Title', - timestamp: entry['date'] ?? '', - )); - break; - } - case 'grabbed': { - _entries.add(RadarrHistoryDataGrabbed( - movieID: entry['movieId'] ?? -1, - movieTitle: entry['movie']['title'] ?? 'Unknown Title', - timestamp: entry['date'] ?? '', - indexer: entry['data']['indexer'] ?? 'Unknown Indexer', - )); - break; - } - default: { - _entries.add(RadarrHistoryDataGeneric( - movieID: entry['movieId'] ?? -1, - movieTitle: entry['movie']['title'] ?? 'Unknown Title', - timestamp: entry['date'] ?? '', - eventType: entry['eventType'] ?? 'Unknown Event Type', - )); - break; - } - } - } - return _entries; - } on DioError catch (error, stack) { - logError('Failed to fetch history', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch history', error, stack); - return Future.error(error); - } - } - - Future searchMissingMovies(List movieIDs) async { - try { - await _dio.post( - 'command', - data: json.encode({ - 'name': 'MoviesSearch', - 'movieIds': movieIDs, - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to search for missing movies (${movieIDs.toString()})', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to search for missing movies (${movieIDs.toString()})', error, stack); - return Future.error(error); - } - } - - Future searchAllMissing() async { - try { - await _dio.post( - 'command', - data: json.encode({ - 'name': 'missingMoviesSearch', - 'filterKey': 'monitored', - 'filterValue': true, - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to search for all missing movies', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to search for all missing movies', error, stack); - return Future.error(error); - } - } - - Future toggleMovieMonitored(int movieID, bool status) async { - try { - Response response = await _dio.get('movie/$movieID'); - Map data = response.data; - data['monitored'] = status; - response = await _dio.put( - 'movie', - data: json.encode(data), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to toggle movie monitored status ($movieID)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to toggle movie monitored status ($movieID)', error, stack); - return Future.error(error); - } - } - - Future> getQualityProfiles() async { - try { - Response response = await _dio.get('profile'); - var _entries = new Map(); - for(var entry in response.data) { - _entries[entry['id']] = RadarrQualityProfile( - id: entry['id'] ?? -1, - name: entry['name'] ?? 'Unknown Quality Profile', - ); - } - return _entries; - } on DioError catch (error, stack) { - logError('Failed to fetch quality profiles', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch quality profiles', error, stack); - return Future.error(error); - } - } - - Future> getReleases(int movieID) async { - try { - Response response = await _dio.get( - 'release', - queryParameters: { - 'movieId': movieID, - }, - ); - List _entries = []; - for(var entry in response.data) { - _entries.add(RadarrReleaseData( - title: entry['title'] ?? 'Unknown Title', - guid: entry['guid'] ?? '', - quality: entry['quality']['quality']['name'] ?? 'Unknown', - protocol: entry['protocol'] ?? 'Unknown Protocol', - indexer: entry['indexer'] ?? 'Unknown Indexer', - infoUrl: entry['infoUrl'] ?? '', - approved: entry['approved'] ?? false, - releaseWeight: entry['releaseWeight'] ?? 0, - size: entry['size'] ?? 0, - indexerId: entry['indexerId'] ?? 0, - ageHours: entry['ageHours'] ?? 0, - rejections: entry['rejections'] ?? [], - seeders: entry['seeders'] ?? 0, - leechers: entry['leechers'] ?? 0, - customFormats: entry['quality']['customFormats'] ?? [], - )); - } - return _entries; - } on DioError catch (error, stack) { - logError('Failed to get releases ($movieID)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to get releases ($movieID)', error, stack); - return Future.error(error); - } - } - - Future downloadRelease(String guid, int indexerID) async { - try { - await _dio.post( - 'release', - data: json.encode({ - 'guid': guid, - 'indexerId': indexerID, - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to download release ($guid)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to download release ($guid)', error, stack); - return Future.error(error); - } - } - - Future automaticSearchMovie(int movieID) async { - try { - await _dio.post( - 'command', - data: json.encode({ - 'name': 'MoviesSearch', - 'movieIds': [movieID], - }), - ); - return true; - } on DioError catch (error, stack) { - logError('Failed to automatically search for movie releases ($movieID)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to automatically search for movie releases ($movieID)', error, stack); - return Future.error(error); - } - } - - Future> searchMovies(String search) async { - if(search == '') return []; - try { - Response response = await _dio.get( - 'movie/lookup', - queryParameters: { - 'term': search, - } - ); - List entries = []; - for(var entry in response.data) { - entries.add(RadarrSearchData( - title: entry['title'] ?? 'Unknown Title', - titleSlug: entry['titleSlug'] ?? ((entry['title'] as String) ?? 'Unknown Title').lunaConvertToSlug(), - overview: entry['overview'] == null || entry['overview'] == '' ? 'No summary is available.' : entry['overview'], - year: entry['year'] ?? 0, - tmdbId: entry['tmdbId'] ?? 0, - images: entry['images'] ?? [], - status: entry['status'] ?? 'Unknown Status', - )); - } - return entries; - } on DioError catch (error, stack) { - logError('Failed to search ($search)', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to search ($search)', error, stack); - return Future.error(error); - } - } - - Future> getRootFolders() async { - try { - Response response = await _dio.get('rootfolder'); - List _entries = []; - for(var entry in response.data) { - _entries.add(RadarrRootFolder( - id: entry['id'] ?? -1, - path: entry['path'] ?? 'Unknown Root Folder', - freeSpace: entry['freeSpace'] ?? 0, - )); - } - return _entries; - } on DioError catch (error, stack) { - logError('Failed to fetch root folders', error, stack); - return Future.error(error); - } catch (error, stack) { - logError('Failed to fetch root folders', error, stack); - return Future.error(error); - } - } -} diff --git a/lib/modules/radarr/core/api/data.dart b/lib/modules/radarr/core/api/data.dart deleted file mode 100644 index 1c3c2505..00000000 --- a/lib/modules/radarr/core/api/data.dart +++ /dev/null @@ -1,9 +0,0 @@ -export 'data/availability.dart'; -export 'data/catalogue.dart'; -export 'data/history.dart'; -export 'data/missing.dart'; -export 'data/qualityprofile.dart'; -export 'data/release.dart'; -export 'data/rootfolder.dart'; -export 'data/search.dart'; -export 'data/upcoming.dart'; diff --git a/lib/modules/radarr/core/api/data/catalogue.dart b/lib/modules/radarr/core/api/data/catalogue.dart deleted file mode 100644 index fdcd3e26..00000000 --- a/lib/modules/radarr/core/api/data/catalogue.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:intl/intl.dart'; -import 'package:lunasea/modules/radarr/core.dart'; - -class RadarrCatalogueData { - final Map api = Database.currentProfileObject.getRadarr(); - String title; - String sortTitle; - String studio; - String physicalRelease; - String inCinemas; - String added; - String status; - String profile; - String overview; - String path; - String minimumAvailability; - int movieID; - int year; - int sizeOnDisk; - int runtime; - int qualityProfile; - bool monitored; - bool downloaded; - bool staticPath; - Map movieFile; - String youtubeId; - String imdbId; - int tmdbId; - - RadarrCatalogueData({ - @required this.title, - @required this.sortTitle, - @required this.studio, - @required this.physicalRelease, - @required this.inCinemas, - @required this.added, - @required this.status, - @required this.year, - @required this.movieID, - @required this.monitored, - @required this.downloaded, - @required this.sizeOnDisk, - @required this.runtime, - @required this.profile, - @required this.movieFile, - @required this.overview, - @required this.path, - @required this.qualityProfile, - @required this.minimumAvailability, - @required this.youtubeId, - @required this.imdbId, - @required this.tmdbId, - @required this.staticPath, - }); - - bool get isTBA { - if(status == 'tba') { - return true; - } - return false; - } - - bool get isAnnounced { - if(status == 'announced') { - return true; - } - return false; - } - - bool get isInCinemas { - if(status == 'released' || status == 'inCinemas') { - return true; - } - return false; - } - - bool get isPhysicallyReleased { - if(status == 'released') { - return true; - } - return false; - } - - String get profileString { - if(profile == null || profile == '') { - return ''; - } - return profile; - } - - DateTime get inCinemasObject { - if(inCinemas != null) { - return DateTime.tryParse(inCinemas)?.toLocal(); - } - return null; - } - - DateTime get physicalReleaseObject { - if(physicalRelease != null) { - return DateTime.tryParse(physicalRelease)?.toLocal(); - } - return null; - } - - DateTime get dateAddedObject => DateTime.tryParse(added)?.toLocal(); - - String get dateAdded { - if(dateAddedObject != null) { - return DateFormat('MMMM dd, y').format(dateAddedObject); - } - return 'Unknown'; - } - - String get inCinemasString { - if(inCinemasObject != null) { - return DateFormat('MMMM dd, y').format(inCinemasObject); - } - return 'Unknown'; - } - - String get physicalReleaseString { - if(physicalReleaseObject != null) { - return DateFormat('MMMM dd, y').format(physicalReleaseObject); - } - return 'Unknown'; - } - - String get yearString { - if(year == null || year == 0) { - return 'Unknown Year'; - } - return '$year'; - } - - String subtitle(RadarrCatalogueSorting sorting) => '$yearString\t${Constants.TEXT_BULLET}\t${runtime.lunaRuntime()}\t•\t${_sortSubtitle(sorting)}'; - - String _sortSubtitle(RadarrCatalogueSorting sorting) { - switch(sorting) { - case RadarrCatalogueSorting.studio: return studio; - case RadarrCatalogueSorting.dateAdded: return dateAdded; - case RadarrCatalogueSorting.year: - case RadarrCatalogueSorting.runtime: - case RadarrCatalogueSorting.alphabetical: - case RadarrCatalogueSorting.quality: - case RadarrCatalogueSorting.size: return profileString; - } - throw Exception('Unknown sorting value'); - } - - TextSpan get releaseSubtitle { - String text = ''; - Color color = Colors.white; - if (downloaded) { - text = sizeOnDisk?.lunaBytesToString(); - color = monitored ? Color(LunaColours.ACCENT_COLOR) : Color(LunaColours.ACCENT_COLOR).withOpacity(0.30); - } else if(isPhysicallyReleased) { - text = ''; - } else if(isInCinemas) { - if(physicalReleaseObject != null) { - text = physicalReleaseObject.lunaUpcomingDays.toUpperCase(); - color = monitored ? Colors.blue : Colors.blue.withOpacity(0.30); - } - } else if(isAnnounced) { - if(inCinemasObject != null) { - text = inCinemasObject.lunaUpcomingDays.toUpperCase(); - color = monitored ? Colors.orange : Colors.orange.withOpacity(0.30); - } - } else if(isTBA) { - text = 'TO BE ANNOUNCED'; - color = monitored ? LunaColours.blueGrey : LunaColours.blueGrey.withOpacity(0.30); - } - return TextSpan( - text: text, - style: TextStyle( - color: color, - fontWeight: FontWeight.bold, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ); - } - - String posterURI({bool highRes = false}) { - if(api['enabled']) { - String _base = (api['host'] as String).endsWith('/') - ? '${api['host']}api/MediaCover' - : '${api['host']}/api/MediaCover'; - return highRes - ? '$_base/$movieID/poster.jpg?apikey=${api['key']}' - : '$_base/$movieID/poster-500.jpg?apikey=${api['key']}'; - } - return ''; - } - - String fanartURI({bool highRes = false}) { - if(api['enabled']) { - String _base = (api['host'] as String).endsWith('/') - ? '${api['host']}api/MediaCover' - : '${api['host']}/api/MediaCover'; - return highRes - ? '$_base/$movieID/fanart.jpg?apikey=${api['key']}' - : '$_base/$movieID/fanart-360.jpg?apikey=${api['key']}'; - } - return ''; - } -} diff --git a/lib/modules/radarr/core/api/data/history.dart b/lib/modules/radarr/core/api/data/history.dart deleted file mode 100644 index f5f02676..00000000 --- a/lib/modules/radarr/core/api/data/history.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -abstract class RadarrHistoryData { - int movieID; - String movieTitle; - String timestamp; - String eventType; - - RadarrHistoryData( - this.movieID, - this.movieTitle, - this.timestamp, - this.eventType, - ); - - DateTime get timestampObject { - return DateTime.tryParse(timestamp)?.toLocal(); - } - - String get timestampString { - if(timestampObject != null) { - Duration age = DateTime.now().difference(timestampObject); - if(age.inDays >= 1) { - return age.inDays == 1 ? '${age.inDays} Day Ago' : '${age.inDays} Days Ago'; - } - if(age.inHours >= 1) { - return age.inHours == 1 ? '${age.inHours} Hour Ago' : '${age.inHours} Hours Ago'; - } - return age.inMinutes == 1 ? '${age.inMinutes} Minute Ago' : '${age.inMinutes} Minutes Ago'; - } - return 'Unknown Date/Time'; - } - - List get subtitle; -} - -class RadarrHistoryDataGeneric extends RadarrHistoryData { - String eventType; - - RadarrHistoryDataGeneric({ - @required int movieID, - @required String movieTitle, - @required String timestamp, - @required this.eventType, - }) : super(movieID, movieTitle, timestamp, eventType); - - List get subtitle { - return [ - TextSpan( - text: '$timestampString\n', - ), - TextSpan( - text: '$eventType', - style: TextStyle( - color: LunaColours.purple, - fontWeight: FontWeight.bold, - ), - ), - ]; - } -} - -class RadarrHistoryDataFileRenamed extends RadarrHistoryData { - RadarrHistoryDataFileRenamed({ - @required int movieID, - @required String movieTitle, - @required String timestamp, - }) : super(movieID, movieTitle, timestamp, 'movieFileRenamed'); - - List get subtitle { - return [ - TextSpan( - text: '$timestampString\n', - ), - TextSpan( - text: '${RadarrConstants.EVENT_TYPE_MESSAGES[eventType]}', - style: TextStyle( - color: Color(LunaColours.ACCENT_COLOR), - fontWeight: FontWeight.bold, - ), - ), - ]; - } -} - -class RadarrHistoryDataFileDeleted extends RadarrHistoryData { - String reason; - - RadarrHistoryDataFileDeleted({ - @required int movieID, - @required String movieTitle, - @required String timestamp, - @required this.reason, - }) : super(movieID, movieTitle, timestamp, 'movieFileDeleted'); - - List get subtitle { - return [ - TextSpan( - text: '$timestampString\n', - ), - TextSpan( - text: '${RadarrConstants.EVENT_TYPE_MESSAGES[eventType]} (${Constants.historyReasonMessages[reason] ?? reason})', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ]; - } -} - -class RadarrHistoryDataDownloadImported extends RadarrHistoryData { - String quality; - - RadarrHistoryDataDownloadImported({ - @required int movieID, - @required String movieTitle, - @required String timestamp, - @required this.quality, - }) : super(movieID, movieTitle, timestamp, 'downloadFolderImported'); - - List get subtitle { - return [ - TextSpan( - text: '$timestampString\n', - ), - TextSpan( - text: '${RadarrConstants.EVENT_TYPE_MESSAGES[eventType]} ($quality)', - style: TextStyle( - color: Color(LunaColours.ACCENT_COLOR), - fontWeight: FontWeight.bold, - ), - ), - ]; - } -} - -class RadarrHistoryDataDownloadFailed extends RadarrHistoryData { - RadarrHistoryDataDownloadFailed({ - @required int movieID, - @required String movieTitle, - @required String timestamp, - }) : super(movieID, movieTitle, timestamp, 'downloadFailed'); - - List get subtitle { - return [ - TextSpan( - text: '$timestampString\n', - ), - TextSpan( - text: '${RadarrConstants.EVENT_TYPE_MESSAGES[eventType]}', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ]; - } -} - -class RadarrHistoryDataGrabbed extends RadarrHistoryData { - String indexer; - - RadarrHistoryDataGrabbed({ - @required int movieID, - @required String movieTitle, - @required String timestamp, - @required this.indexer, - }) : super(movieID, movieTitle, timestamp, 'grabbed'); - - List get subtitle { - return [ - TextSpan( - text: '$timestampString\n', - ), - TextSpan( - text: '${RadarrConstants.EVENT_TYPE_MESSAGES[eventType]} $indexer', - style: TextStyle( - color: Colors.orange, - fontWeight: FontWeight.bold, - ), - ) - ]; - } -} \ No newline at end of file diff --git a/lib/modules/radarr/core/api/data/missing.dart b/lib/modules/radarr/core/api/data/missing.dart deleted file mode 100644 index 1afda268..00000000 --- a/lib/modules/radarr/core/api/data/missing.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class RadarrMissingData { - final Map api = Database.currentProfileObject.getRadarr(); - String title; - String sortTitle; - String studio; - String physicalRelease; - String inCinemas; - String profile; - String status; - int movieID; - int year; - int runtime; - - - RadarrMissingData({ - @required this.movieID, - @required this.title, - @required this.sortTitle, - @required this.studio, - @required this.physicalRelease, - @required this.inCinemas, - @required this.profile, - @required this.year, - @required this.runtime, - @required this.status, - }); - - DateTime get inCinemasObject { - if(inCinemas != null) { - return DateTime.tryParse(inCinemas)?.toLocal(); - } - return null; - } - - DateTime get physicalReleaseObject { - if(physicalRelease != null) { - return DateTime.tryParse(physicalRelease)?.toLocal(); - } - return null; - } - - String get profileString { - if(profile == null || profile == '') { - return ''; - } - return '\t•\t$profile'; - } - - String get runtimeString => runtime.lunaRuntime().isEmpty ? '' : '\t${Constants.TEXT_BULLET}\t${runtime.lunaRuntime()}'; - - List get subtitle { - DateTime now = DateTime.now(); - return [ - TextSpan( - text: '$year$runtimeString$profileString', - ), - if(status == 'released') TextSpan( - text: physicalReleaseObject != null ? - now.difference(physicalReleaseObject).inDays == 0 ? - '\nReleasing Today' : - '\nReleased ${now.difference(physicalReleaseObject).inDays} ${now.difference(physicalReleaseObject).inDays == 1 ? "Day" : "Days"} Ago' : - '\nReleased', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - ) - ), - if(status == 'inCinemas') TextSpan( - text: physicalReleaseObject != null ? - physicalReleaseObject.difference(now).inDays == 0 ? - '\nAvailable Today' : - '\nAvailable in ${physicalReleaseObject.difference(now).inDays} ${physicalReleaseObject.difference(now).inDays == 1 ? "Day" : "Days"}' : - '\nAvailability Unknown', - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, - ), - ), - if(status == 'announced') TextSpan( - text: inCinemasObject != null ? - now.difference(inCinemasObject).inDays == 0 ? - '\nIn Cinemas Today' : - '\nIn Cinemas in ${inCinemasObject.difference(now).inDays} ${inCinemasObject.difference(now).inDays == 1 ? "Day" : "Days"}' : - '\nIn Cinemas Later', - style: TextStyle( - color: Colors.orange, - fontWeight: FontWeight.bold, - ) - ), - ]; - } - - String posterURI({bool highRes = false}) { - if(api['enabled']) { - String _base = (api['host'] as String).endsWith('/') - ? '${api['host']}api/MediaCover' - : '${api['host']}/api/MediaCover'; - return highRes - ? '$_base/$movieID/poster.jpg?apikey=${api['key']}' - : '$_base/$movieID/poster-500.jpg?apikey=${api['key']}'; - } - return ''; - } - - String fanartURI({bool highRes = false}) { - if(api['enabled']) { - String _base = (api['host'] as String).endsWith('/') - ? '${api['host']}api/MediaCover' - : '${api['host']}/api/MediaCover'; - return highRes - ? '$_base/$movieID/fanart.jpg?apikey=${api['key']}' - : '$_base/$movieID/fanart-360.jpg?apikey=${api['key']}'; - } - return ''; - } -} \ No newline at end of file diff --git a/lib/modules/radarr/core/api/data/release.dart b/lib/modules/radarr/core/api/data/release.dart deleted file mode 100644 index 7b9eecb3..00000000 --- a/lib/modules/radarr/core/api/data/release.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -class RadarrReleaseData { - String title; - String guid; - String quality; - String protocol; - String indexer; - String infoUrl; - bool approved; - int releaseWeight; - int size; - int indexerId; - int seeders; - int leechers; - double ageHours; - List rejections; - List customFormats; - - RadarrReleaseData({ - @required this.title, - @required this.guid, - @required this.quality, - @required this.protocol, - @required this.indexer, - @required this.infoUrl, - @required this.approved, - @required this.releaseWeight, - @required this.size, - @required this.indexerId, - @required this.ageHours, - @required this.rejections, - @required this.seeders, - @required this.leechers, - @required this.customFormats, - }); - - bool get isTorrent { - return protocol == 'torrent'; - } -} \ No newline at end of file diff --git a/lib/modules/radarr/core/api/data/search.dart b/lib/modules/radarr/core/api/data/search.dart deleted file mode 100644 index 47b031b6..00000000 --- a/lib/modules/radarr/core/api/data/search.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrSearchData { - String title; - String titleSlug; - String overview; - String status; - int year; - int tmdbId; - List images; - - RadarrSearchData({ - @required this.title, - @required this.titleSlug, - @required this.overview, - @required this.year, - @required this.tmdbId, - @required this.images, - @required this.status, - }); - - String get bannerURI { - for(var image in images) { - if(image['coverType'] == 'banner') { - return image['url']; - } - } - return ''; - } - - String get fanartURI { - for(var image in images) { - if(image['coverType'] == 'fanart') { - return image['url']; - } - } - return ''; - } - - String get posterURI { - for(var image in images) { - if(image['coverType'] == 'poster') { - return image['url']; - } - } - return ''; - } - - String get formattedStatus { - RadarrAvailability _result = RadarrConstants.MINIMUM_AVAILBILITIES.firstWhere((availability) => availability.id == status, orElse: () => null); - return _result?.name ?? 'Unknown Status'; - } -} \ No newline at end of file diff --git a/lib/modules/radarr/core/api/data/upcoming.dart b/lib/modules/radarr/core/api/data/upcoming.dart deleted file mode 100644 index eb31e26d..00000000 --- a/lib/modules/radarr/core/api/data/upcoming.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -class RadarrUpcomingData { - final Map api = Database.currentProfileObject.getRadarr(); - String title; - int movieID; - - RadarrUpcomingData({ - @required this.title, - @required this.movieID, - }); - - String posterURI({bool highRes = false}) { - if(api['enabled']) { - String _base = (api['host'] as String).endsWith('/') - ? '${api['host']}api/MediaCover' - : '${api['host']}/api/MediaCover'; - return highRes - ? '$_base/$movieID/poster.jpg?apikey=${api['key']}' - : '$_base/$movieID/poster-500.jpg?apikey=${api['key']}'; - } - return ''; - } - - String fanartURI({bool highRes = false}) { - if(api['enabled']) { - String _base = (api['host'] as String).endsWith('/') - ? '${api['host']}api/MediaCover' - : '${api['host']}/api/MediaCover'; - return highRes - ? '$_base/$movieID/fanart.jpg?apikey=${api['key']}' - : '$_base/$movieID/fanart-360.jpg?apikey=${api['key']}'; - } - return ''; - } -} diff --git a/lib/modules/radarr/core/api_helper.dart b/lib/modules/radarr/core/api_helper.dart new file mode 100644 index 00000000..65470a01 --- /dev/null +++ b/lib/modules/radarr/core/api_helper.dart @@ -0,0 +1,368 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAPIHelper { + /// Toggles the monitored state on a movie. + /// + /// Updates the movie catalogue list in [RadarrState]. + Future toggleMonitored({ + @required BuildContext context, + @required RadarrMovie movie, + bool showSnackbar = true, + }) async { + assert(movie != null); + if(context.read().enabled) { + RadarrMovie movieCopy = movie.clone(); + movieCopy.monitored = !movie.monitored; + return await context.read().api.movie.update(movie: movieCopy) + .then((data) async { + return await context.read().setSingleMovie(movieCopy).then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: movieCopy.monitored ? 'Monitoring' : 'No Longer Monitoring', message: movie.title); + return true; + }); + }) + .catchError((error, stack) { + LunaLogger().error('Unable to toggle monitored state: ${movie.monitored.toString()} to ${movieCopy.monitored.toString()}', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: movie.monitored ? 'Failed to Unmonitor Movie' : 'Failed to Monitor Movie', error: error); + return false; + }); + } + return false; + } + + /// Refreshes a single movie. + Future refreshMovie({ + @required BuildContext context, + @required RadarrMovie movie, + bool showSnackbar = true, + }) async { + assert(movie != null); + if(context.read().enabled) { + return await context.read().api.command.refreshMovie(movieIds: [movie.id]) + .then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: 'Refreshing...', message: movie.title); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Unable to refresh movie: ${movie.id}', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Refresh', error: error); + return false; + }); + } + return false; + } + + /// Add a new movie. + /// + /// Returns the newly added [RadarrMovie] instance. + Future addMovie({ + @required BuildContext context, + @required RadarrMovie movie, + @required RadarrRootFolder rootFolder, + @required bool monitored, + @required RadarrQualityProfile qualityProfile, + @required RadarrAvailability availability, + @required List tags, + @required bool searchForMovie, + bool showSnackbar = true, + }) async { + assert(movie != null); + assert(rootFolder != null); + assert(monitored != null); + assert(qualityProfile != null); + assert(availability != null); + assert(tags != null); + assert(searchForMovie != null); + if(context.read().enabled) { + return await context.read().api.movie.create( + movie: movie, + rootFolder: rootFolder, + monitored: monitored, + qualityProfile: qualityProfile, + minimumAvailability: availability, + tags: tags, + searchForMovie: searchForMovie, + ) + .then((movie) { + if(showSnackbar) showLunaSuccessSnackBar( + context: context, + title: [ + 'Movie Added', + if(searchForMovie) '(Searching...)', + ].join(' '), + message: movie.title, + ); + return movie; + }) + .catchError((error, stack) { + print(error); + LunaLogger().error('Failed to add movie (tmdbId: ${movie.tmdbId})', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Add Movie', error: error); + return null; + }); + } + return null; + } + + /// Backup the database. + Future backupDatabase({ + @required BuildContext context, + bool showSnackbar = true, + }) async { + if(context.read().enabled) { + return await context.read().api.command.backup() + .then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: 'Backing Up Database${Constants.TEXT_ELLIPSIS}', message: 'Backing up the database in the background'); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Unable to backup database', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Backup Database', error: error); + return false; + }); + } + return false; + } + + /// Search for all missing movies. + Future missingMovieSearch({ + @required BuildContext context, + bool showSnackbar = true, + }) async { + if(context.read().enabled) { + return await context.read().api.command.missingMovieSearch() + .then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: 'Searching${Constants.TEXT_ELLIPSIS}', message: 'Searching for all missing movies'); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Unable to search for all missing movies', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Search', error: error); + return false; + }); + } + return false; + } + + /// Run an RSS sync. + Future runRSSSync({ + @required BuildContext context, + bool showSnackbar = true, + }) async { + if(context.read().enabled) { + return await context.read().api.command.rssSync() + .then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: 'Running RSS Sync${Constants.TEXT_ELLIPSIS}', message: 'Running RSS sync in the background'); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Unable to run RSS sync', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Run RSS Sync', error: error); + return false; + }); + } + return false; + } + + /// Update all movies in your library. + Future updateLibrary({ + @required BuildContext context, + bool showSnackbar = true, + }) async { + if(context.read().enabled) { + return await context.read().api.command.refreshMovie() + .then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: 'Updating Library${Constants.TEXT_ELLIPSIS}', message: 'Updating library in the background'); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Unable to update library', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Update Library', error: error); + return false; + }); + } + return false; + } + + /// Delete a movie file. + Future deleteMovieFile({ + @required BuildContext context, + @required RadarrMovieFile movieFile, + bool showSnackbar = true, + }) async { + assert(movieFile != null); + if(context.read().enabled) { + return await context.read().api.movieFile.delete(movieFileId: movieFile.id) + .then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: 'File Deleted', message: movieFile.relativePath); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Failed to delete movie file: ${movieFile.id}', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Delete File', error: error); + return false; + }); + } + return false; + } + + /// Update a movie in Radarr. + Future updateMovie({ + @required BuildContext context, + @required RadarrMovie movie, + bool showSnackbar = true, + }) async { + assert(movie != null); + if(context.read().enabled) { + return await context.read().api.movie.update(movie: movie) + .then((_) async { + return await context.read().setSingleMovie(movie).then((_) { + if(showSnackbar) showLunaSuccessSnackBar(title: 'Updated Movie', message: movie.title); + return true; + }); + }) + .catchError((error, stack) { + LunaLogger().error('Failed to update movie: ${movie.id}', error, stack); + showLunaErrorSnackBar(title: 'Failed to Update Movie', error: error); + return false; + }); + } + return false; + } + + /// Remove a movie. + Future removeMovie({ + @required BuildContext context, + @required RadarrMovie movie, + bool showSnackbar = true, + }) async { + assert(movie != null); + if(context.read().enabled) { + return await context.read().api.movie.delete( + movieId: movie.id, + addImportExclusion: RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST.data, + deleteFiles: RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.data, + ).then((_) async { + // If the user deletes a movie right after adding it, this updates the lookup list + if(context.read().lookup != null) (await context.read().lookup)?.firstWhere( + (lookup) => lookup.id == movie.id, + orElse: () => null, + )?.id = null; + if(showSnackbar) showLunaSuccessSnackBar( + context: context, + title: [ + 'Removed Movie', + if(RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.data) '(With Files)', + ].join(' '), + message: movie.title, + ); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Failed to remove movie: ${movie.id}', error, stack); + showLunaErrorSnackBar(title: 'Failed to Remove Movie', error: error); + return false; + }); + } + return false; + } + + /// Trigger an automatic search of a movie. + Future automaticSearch({ + @required BuildContext context, + @required int movieId, + @required String title, + bool showSnackbar = true, + }) async { + assert(movieId != null); + assert(title != null); + if(context.read().enabled) { + return await context.read().api.command.moviesSearch(movieIds: [movieId]) + .then((_) async { + if(showSnackbar) showLunaSuccessSnackBar( + context: context, + title: 'Searching for Movie...', + message: title, + ); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Failed to search for movie: $movieId', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Search', error: error); + return false; + }); + } + return false; + } + + /// Push a release to the download client(s) connected to Radarr. + Future pushRelease({ + @required BuildContext context, + @required RadarrRelease release, + bool showSnackbar = true, + }) async { + assert(release != null); + if(context.read().enabled) { + return await context.read().api.release.push(indexerId: release.indexerId, guid: release.guid) + .then((value) { + if(showSnackbar) showLunaSuccessSnackBar( + context: context, + title: 'Downloading Release...', + message: release.title, + ); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Failed to download release: ${release.guid}', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Download Release', error: error); + return false; + }); + } + return false; + } + + /// Add a new tag. + Future addTag({ + @required BuildContext context, + @required String label, + bool showSnackbar = true, + }) async { + assert(label != null); + if(context.read().enabled) { + return await context.read().api.tag.create(label: label) + .then((tag) { + showLunaSuccessSnackBar(title: 'Added Tag', message: tag.label); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Failed to add tag: $label', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Add Tag', error: error); + return false; + }); + } + return false; + } + + /// Delete a tag. + Future deleteTag({ + @required BuildContext context, + @required RadarrTag tag, + bool showSnackbar = true, + }) async { + assert(tag != null); + if(context.read().enabled) { + return await context.read().api.tag.delete(id: tag.id) + .then((_) { + showLunaSuccessSnackBar(title: 'Deleted Tag', message: tag.label); + return true; + }) + .catchError((error, stack) { + LunaLogger().error('Failed to add tag: ${tag.id}', error, stack); + if(showSnackbar) showLunaErrorSnackBar(title: 'Failed to Delete Tag', error: error); + return false; + }); + } + return false; + } +} diff --git a/lib/modules/radarr/core/constants.dart b/lib/modules/radarr/core/constants.dart index 95de2bc7..152eca1a 100644 --- a/lib/modules/radarr/core/constants.dart +++ b/lib/modules/radarr/core/constants.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/radarr.dart'; -import 'package:quick_actions/quick_actions.dart'; class RadarrConstants { RadarrConstants._(); @@ -16,27 +14,10 @@ class RadarrConstants { settingsDescription: 'Configure Radarr', helpMessage: 'Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.', icon: CustomIcons.radarr, - route: '/radarr', + route: RadarrHomeRouter().route(), color: Color(0xFFFEC333), website: 'https://radarr.video', github: 'https://github.com/Radarr/Radarr', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(Radarr.ROUTE_NAME, (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Radarr'), ); - - static const Map EVENT_TYPE_MESSAGES = { - 'movieFileRenamed': 'Movie File Renamed', - 'movieFileDeleted': 'Movie File Deleted', - 'downloadFolderImported': 'Imported Movie File', - 'downloadFailed': 'Download Failed', - 'grabbed': 'Grabbed From', - }; - - // ignore: non_constant_identifier_names - static final List MINIMUM_AVAILBILITIES = [ - RadarrAvailability(id: 'preDB', name: 'PreDB'), - RadarrAvailability(id: 'announced', name: 'Announced'), - RadarrAvailability(id: 'inCinemas', name: 'In Cinemas'), - RadarrAvailability(id: 'released', name: 'Physical/Web'), - ]; -} \ No newline at end of file +} diff --git a/lib/modules/radarr/core/database.dart b/lib/modules/radarr/core/database.dart index f812866b..09242437 100644 --- a/lib/modules/radarr/core/database.dart +++ b/lib/modules/radarr/core/database.dart @@ -1,14 +1,23 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart' hide RadarrDatabaseValueExtension; +import 'package:lunasea/modules/radarr.dart'; +import 'package:lunasea/modules/radarr/core/deprecated/availability.dart'; +import 'package:lunasea/modules/radarr/core/deprecated/qualityprofile.dart'; +import 'package:lunasea/modules/radarr/core/deprecated/rootfolder.dart'; class RadarrDatabase extends LunaModuleDatabase { @override void registerAdapters() { - Hive.registerAdapter(RadarrQualityProfileAdapter()); - Hive.registerAdapter(RadarrRootFolderAdapter()); - Hive.registerAdapter(RadarrAvailabilityAdapter()); + // Deprecated, not in use but necessary to avoid Hive read errors + Hive.registerAdapter(DeprecatedRadarrQualityProfileAdapter()); + Hive.registerAdapter(DeprecatedRadarrRootFolderAdapter()); + Hive.registerAdapter(DeprecatedRadarrAvailabilityAdapter()); + // Active adapters + Hive.registerAdapter(RadarrMoviesSortingAdapter()); + Hive.registerAdapter(RadarrMoviesFilterAdapter()); + Hive.registerAdapter(RadarrReleasesSortingAdapter()); + Hive.registerAdapter(RadarrReleasesFilterAdapter()); } @override @@ -16,13 +25,25 @@ class RadarrDatabase extends LunaModuleDatabase { Map data = {}; for(RadarrDatabaseValue value in RadarrDatabaseValue.values) { switch(value) { + // Non-primative values + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES: data[value.key] = (RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.data as RadarrMoviesSorting).key; break; + case RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES: data[value.key] = (RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES.data as RadarrMoviesFilter).key; break; + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES: data[value.key] = (RadarrDatabaseValue.DEFAULT_SORTING_RELEASES.data as RadarrReleasesSorting).key; break; + case RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: data[value.key] = (RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES.data as RadarrReleasesFilter).key; break; // Primitive values - case RadarrDatabaseValue.NAVIGATION_INDEX: data[value.key] = value.data; break; - // Non-exported values - case RadarrDatabaseValue.ADD_MONITORED: - case RadarrDatabaseValue.ADD_QUALITY_PROFILE: - case RadarrDatabaseValue.ADD_ROOT_FOLDER: - case RadarrDatabaseValue.ADD_AVAILABILITY: break; + case RadarrDatabaseValue.NAVIGATION_INDEX: + case RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS: + case RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE: + case RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS: + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING: + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID: + case RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST: + case RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES: + case RadarrDatabaseValue.CONTENT_PAGE_SIZE: data[value.key] = value.data; break; } } return data; @@ -33,13 +54,25 @@ class RadarrDatabase extends LunaModuleDatabase { for(String key in config.keys) { RadarrDatabaseValue value = valueFromKey(key); if(value != null) switch(value) { + // Non-primative values + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES: value.put(RadarrMoviesSorting.ALPHABETICAL.fromKey(config[key])); break; + case RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES: value.put(RadarrMoviesFilter.ALL.fromKey(config[key])); break; + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES: value.put(RadarrReleasesSorting.ALPHABETICAL.fromKey(config[key])); break; + case RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: value.put(RadarrReleasesFilter.ALL.fromKey(config[key])); break; // Primitive values - case RadarrDatabaseValue.NAVIGATION_INDEX: value.put(config[key]); break; - // Non-imported values - case RadarrDatabaseValue.ADD_MONITORED: - case RadarrDatabaseValue.ADD_QUALITY_PROFILE: - case RadarrDatabaseValue.ADD_AVAILABILITY: - case RadarrDatabaseValue.ADD_ROOT_FOLDER: break; + case RadarrDatabaseValue.NAVIGATION_INDEX: + case RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS: + case RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE: + case RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS: + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING: + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID: + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID: + case RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES: + case RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST: + case RadarrDatabaseValue.CONTENT_PAGE_SIZE: value.put(config[key]); break; } } } @@ -48,10 +81,22 @@ class RadarrDatabase extends LunaModuleDatabase { RadarrDatabaseValue valueFromKey(String key) { switch(key) { case 'RADARR_NAVIGATION_INDEX': return RadarrDatabaseValue.NAVIGATION_INDEX; - case 'RADARR_ADD_MONITORED': return RadarrDatabaseValue.ADD_MONITORED; - case 'RADARR_ADD_QUALITY_PROFILE': return RadarrDatabaseValue.ADD_QUALITY_PROFILE; - case 'RADARR_ADD_ROOT_FOLDER': return RadarrDatabaseValue.ADD_ROOT_FOLDER; - case 'RADARR_ADD_AVAILABILITY': return RadarrDatabaseValue. ADD_AVAILABILITY; + case 'RADARR_NAVIGATION_INDEX_MOVIE_DETAILS': return RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS; + case 'RADARR_NAVIGATION_INDEX_ADD_MOVIE': return RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE; + case 'RADARR_NAVIGATION_INDEX_SYSTEM_STATUS': return RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS; + case 'RADARR_DEFAULT_SORTING_MOVIES': return RadarrDatabaseValue.DEFAULT_SORTING_MOVIES; + case 'RADARR_DEFAULT_SORTING_MOVIES_ASCENDING': return RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING; + case 'RADARR_DEFAULT_FILTERING_MOVIES': return RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES; + case 'RADARR_DEFAULT_SORTING_RELEASES': return RadarrDatabaseValue.DEFAULT_SORTING_RELEASES; + case 'RADARR_DEFAULT_SORTING_RELEASES_ASCENDING': return RadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING; + case 'RADARR_DEFAULT_FILTERING_RELEASES': return RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES; + case 'RADARR_ADD_MOVIE_DEFAULT_MONITORED_STATE': return RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE; + case 'RADARR_ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID': return RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID; + case 'RADARR_ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID': return RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID; + case 'RADARR_ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID': return RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID; + case 'RADARR_REMOVE_MOVIE_IMPORT_LIST': return RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST; + case 'RADARR_REMOVE_MOVIE_DELETE_FILES': return RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES; + case 'RADARR_CONTENT_PAGE_SIZE': return RadarrDatabaseValue.CONTENT_PAGE_SIZE; default: return null; } } @@ -59,20 +104,44 @@ class RadarrDatabase extends LunaModuleDatabase { enum RadarrDatabaseValue { NAVIGATION_INDEX, - ADD_MONITORED, - ADD_QUALITY_PROFILE, - ADD_ROOT_FOLDER, - ADD_AVAILABILITY, + NAVIGATION_INDEX_MOVIE_DETAILS, + NAVIGATION_INDEX_ADD_MOVIE, + NAVIGATION_INDEX_SYSTEM_STATUS, + DEFAULT_SORTING_MOVIES, + DEFAULT_SORTING_MOVIES_ASCENDING, + DEFAULT_FILTERING_MOVIES, + DEFAULT_SORTING_RELEASES, + DEFAULT_SORTING_RELEASES_ASCENDING, + DEFAULT_FILTERING_RELEASES, + ADD_MOVIE_DEFAULT_MONITORED_STATE, + ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID, + ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID, + ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID, + REMOVE_MOVIE_IMPORT_LIST, + REMOVE_MOVIE_DELETE_FILES, + CONTENT_PAGE_SIZE, } extension RadarrDatabaseValueExtension on RadarrDatabaseValue { String get key { switch(this) { case RadarrDatabaseValue.NAVIGATION_INDEX: return 'RADARR_NAVIGATION_INDEX'; - case RadarrDatabaseValue.ADD_MONITORED: return 'RADARR_ADD_MONITORED'; - case RadarrDatabaseValue.ADD_QUALITY_PROFILE: return 'RADARR_ADD_QUALITY_PROFILE'; - case RadarrDatabaseValue.ADD_ROOT_FOLDER: return 'RADARR_ADD_ROOT_FOLDER'; - case RadarrDatabaseValue.ADD_AVAILABILITY: return 'RADARR_ADD_AVAILABILITY'; + case RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS: return 'RADARR_NAVIGATION_INDEX_MOVIE_DETAILS'; + case RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE: return 'RADARR_NAVIGATION_INDEX_ADD_MOVIE'; + case RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS: return 'RADARR_NAVIGATION_INDEX_SYSTEM_STATUS'; + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES: return 'RADARR_DEFAULT_SORTING_MOVIES'; + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING: return 'RADARR_DEFAULT_SORTING_MOVIES_ASCENDING'; + case RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES: return 'RADARR_DEFAULT_FILTERING_MOVIES'; + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES: return 'RADARR_DEFAULT_SORTING_RELEASES'; + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING: return 'RADARR_DEFAULT_SORTING_RELEASES_ASCENDING'; + case RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: return 'RADARR_DEFAULT_FILTERING_RELEASES'; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE: return 'RADARR_ADD_MOVIE_DEFAULT_MONITORED_STATE'; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID: return 'RADARR_ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID'; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID: return 'RADARR_ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID'; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID: return 'RADARR_ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID'; + case RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST: return 'RADARR_REMOVE_MOVIE_IMPORT_LIST'; + case RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES: return 'RADARR_REMOVE_MOVIE_DELETE_FILES'; + case RadarrDatabaseValue.CONTENT_PAGE_SIZE: return 'RADARR_CONTENT_PAGE_SIZE'; } throw Exception('key not found'); } @@ -81,10 +150,22 @@ extension RadarrDatabaseValueExtension on RadarrDatabaseValue { final _box = Database.lunaSeaBox; switch(this) { case RadarrDatabaseValue.NAVIGATION_INDEX: return _box.get(this.key, defaultValue: 0); - case RadarrDatabaseValue.ADD_MONITORED: return _box.get(this.key, defaultValue: true); - case RadarrDatabaseValue.ADD_QUALITY_PROFILE: return _box.get(this.key); - case RadarrDatabaseValue.ADD_ROOT_FOLDER: return _box.get(this.key); - case RadarrDatabaseValue.ADD_AVAILABILITY: return _box.get(this.key); + case RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS: return _box.get(this.key, defaultValue: 0); + case RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE: return _box.get(this.key, defaultValue: 0); + case RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS: return _box.get(this.key, defaultValue: 0); + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES: return _box.get(this.key, defaultValue: RadarrMoviesSorting.ALPHABETICAL); + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING: return _box.get(this.key, defaultValue: true); + case RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES: return _box.get(this.key, defaultValue: RadarrMoviesFilter.ALL); + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES: return _box.get(this.key, defaultValue: RadarrReleasesSorting.WEIGHT); + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING: return _box.get(this.key, defaultValue: true); + case RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: return _box.get(this.key, defaultValue: RadarrReleasesFilter.ALL); + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE: return _box.get(this.key, defaultValue: true); + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID: return _box.get(this.key, defaultValue: null); + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID: return _box.get(this.key, defaultValue: null); + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID: return _box.get(this.key, defaultValue: 'announced'); + case RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES: return _box.get(this.key, defaultValue: false); + case RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST: return _box.get(this.key, defaultValue: false); + case RadarrDatabaseValue.CONTENT_PAGE_SIZE: return _box.get(this.key, defaultValue: 25); } throw Exception('data not found'); } @@ -93,15 +174,27 @@ extension RadarrDatabaseValueExtension on RadarrDatabaseValue { final box = Database.lunaSeaBox; switch(this) { case RadarrDatabaseValue.NAVIGATION_INDEX: if(value.runtimeType == int) box.put(this.key, value); return; - case RadarrDatabaseValue.ADD_MONITORED: if(value.runtimeType == bool) box.put(this.key, value); return; - case RadarrDatabaseValue.ADD_QUALITY_PROFILE: if(value.runtimeType == RadarrQualityProfile) box.put(this.key, value); return; - case RadarrDatabaseValue.ADD_ROOT_FOLDER: if(value.runtimeType == RadarrRootFolder) box.put(this.key, value); return; - case RadarrDatabaseValue.ADD_AVAILABILITY: if(value.runtimeType == RadarrAvailability) box.put(this.key, value); return; + case RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS: if(value.runtimeType == int) box.put(this.key, value); return; + case RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE: if(value.runtimeType == int) box.put(this.key, value); return; + case RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS: if(value.runtimeType == int) box.put(this.key, value); return; + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES: if(value.runtimeType == RadarrMoviesSorting) box.put(this.key, value); return; + case RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING: if(value.runtimeType == bool) box.put(this.key, value); return; + case RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES: if(value.runtimeType == RadarrMoviesFilter) box.put(this.key, value); return; + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES: if(value.runtimeType == RadarrReleasesSorting) box.put(this.key, value); return; + case RadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING: if(value.runtimeType == bool) box.put(this.key, value); return; + case RadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: if(value.runtimeType == RadarrReleasesFilter) box.put(this.key, value); return; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE: if(value.runtimeType == bool) box.put(this.key, value); return; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID: if(value.runtimeType == int) box.put(this.key, value); return; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID: if(value.runtimeType == int) box.put(this.key, value); return; + case RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID: if(value.runtimeType == String) box.put(this.key, value); return; + case RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES: if(value.runtimeType == bool) box.put(this.key, value); return; + case RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST: if(value.runtimeType == bool) box.put(this.key, value); return; + case RadarrDatabaseValue.CONTENT_PAGE_SIZE: if(value.runtimeType == int) box.put(this.key, value); return; } LunaLogger().warning('RadarrDatabaseValueExtension', 'put', 'Attempted to enter data for invalid RadarrDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/radarr/core/api/data/availability.dart b/lib/modules/radarr/core/deprecated/availability.dart similarity index 53% rename from lib/modules/radarr/core/api/data/availability.dart rename to lib/modules/radarr/core/deprecated/availability.dart index 9ec28478..e2d019ec 100644 --- a/lib/modules/radarr/core/api/data/availability.dart +++ b/lib/modules/radarr/core/deprecated/availability.dart @@ -3,19 +3,19 @@ import 'package:lunasea/core.dart'; part 'availability.g.dart'; -@HiveType(typeId: 7, adapterName: 'RadarrAvailabilityAdapter') -class RadarrAvailability extends HiveObject { +@HiveType(typeId: 7, adapterName: 'DeprecatedRadarrAvailabilityAdapter') +class DeprecatedRadarrAvailability extends HiveObject { @HiveField(0) String id; @HiveField(1) String name; - factory RadarrAvailability.empty() => RadarrAvailability( + factory DeprecatedRadarrAvailability.empty() => DeprecatedRadarrAvailability( id: '', name: '', ); - RadarrAvailability({ + DeprecatedRadarrAvailability({ @required this.id, @required this.name, }); diff --git a/lib/modules/radarr/core/api/data/availability.g.dart b/lib/modules/radarr/core/deprecated/availability.g.dart similarity index 73% rename from lib/modules/radarr/core/api/data/availability.g.dart rename to lib/modules/radarr/core/deprecated/availability.g.dart index 5694f387..7a5d7e89 100644 --- a/lib/modules/radarr/core/api/data/availability.g.dart +++ b/lib/modules/radarr/core/deprecated/availability.g.dart @@ -6,24 +6,25 @@ part of 'availability.dart'; // TypeAdapterGenerator // ************************************************************************** -class RadarrAvailabilityAdapter extends TypeAdapter { +class DeprecatedRadarrAvailabilityAdapter + extends TypeAdapter { @override final int typeId = 7; @override - RadarrAvailability read(BinaryReader reader) { + DeprecatedRadarrAvailability read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return RadarrAvailability( + return DeprecatedRadarrAvailability( id: fields[0] as String, name: fields[1] as String, ); } @override - void write(BinaryWriter writer, RadarrAvailability obj) { + void write(BinaryWriter writer, DeprecatedRadarrAvailability obj) { writer ..writeByte(2) ..writeByte(0) @@ -38,7 +39,7 @@ class RadarrAvailabilityAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is RadarrAvailabilityAdapter && + other is DeprecatedRadarrAvailabilityAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/modules/radarr/core/api/data/qualityprofile.dart b/lib/modules/radarr/core/deprecated/qualityprofile.dart similarity index 52% rename from lib/modules/radarr/core/api/data/qualityprofile.dart rename to lib/modules/radarr/core/deprecated/qualityprofile.dart index 0959bd8c..f7423c24 100644 --- a/lib/modules/radarr/core/api/data/qualityprofile.dart +++ b/lib/modules/radarr/core/deprecated/qualityprofile.dart @@ -3,19 +3,19 @@ import 'package:lunasea/core.dart'; part 'qualityprofile.g.dart'; -@HiveType(typeId: 6, adapterName: 'RadarrQualityProfileAdapter') -class RadarrQualityProfile extends HiveObject { +@HiveType(typeId: 6, adapterName: 'DeprecatedRadarrQualityProfileAdapter') +class DeprecatedRadarrQualityProfile extends HiveObject { @HiveField(0) int id; @HiveField(1) String name; - factory RadarrQualityProfile.empty() => RadarrQualityProfile( + factory DeprecatedRadarrQualityProfile.empty() => DeprecatedRadarrQualityProfile( id: -1, name: '', ); - RadarrQualityProfile({ + DeprecatedRadarrQualityProfile({ @required this.id, @required this.name, }); diff --git a/lib/modules/radarr/core/api/data/qualityprofile.g.dart b/lib/modules/radarr/core/deprecated/qualityprofile.g.dart similarity index 72% rename from lib/modules/radarr/core/api/data/qualityprofile.g.dart rename to lib/modules/radarr/core/deprecated/qualityprofile.g.dart index 41b295b3..fa037dad 100644 --- a/lib/modules/radarr/core/api/data/qualityprofile.g.dart +++ b/lib/modules/radarr/core/deprecated/qualityprofile.g.dart @@ -6,24 +6,25 @@ part of 'qualityprofile.dart'; // TypeAdapterGenerator // ************************************************************************** -class RadarrQualityProfileAdapter extends TypeAdapter { +class DeprecatedRadarrQualityProfileAdapter + extends TypeAdapter { @override final int typeId = 6; @override - RadarrQualityProfile read(BinaryReader reader) { + DeprecatedRadarrQualityProfile read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return RadarrQualityProfile( + return DeprecatedRadarrQualityProfile( id: fields[0] as int, name: fields[1] as String, ); } @override - void write(BinaryWriter writer, RadarrQualityProfile obj) { + void write(BinaryWriter writer, DeprecatedRadarrQualityProfile obj) { writer ..writeByte(2) ..writeByte(0) @@ -38,7 +39,7 @@ class RadarrQualityProfileAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is RadarrQualityProfileAdapter && + other is DeprecatedRadarrQualityProfileAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/modules/radarr/core/api/data/rootfolder.dart b/lib/modules/radarr/core/deprecated/rootfolder.dart similarity index 61% rename from lib/modules/radarr/core/api/data/rootfolder.dart rename to lib/modules/radarr/core/deprecated/rootfolder.dart index edfa7ad9..73f6428f 100644 --- a/lib/modules/radarr/core/api/data/rootfolder.dart +++ b/lib/modules/radarr/core/deprecated/rootfolder.dart @@ -3,8 +3,8 @@ import 'package:lunasea/core.dart'; part 'rootfolder.g.dart'; -@HiveType(typeId: 5, adapterName: 'RadarrRootFolderAdapter') -class RadarrRootFolder extends HiveObject { +@HiveType(typeId: 5, adapterName: 'DeprecatedRadarrRootFolderAdapter') +class DeprecatedRadarrRootFolder extends HiveObject { @HiveField(0) int id; @HiveField(1) @@ -12,13 +12,13 @@ class RadarrRootFolder extends HiveObject { @HiveField(2) int freeSpace; - factory RadarrRootFolder.empty() => RadarrRootFolder( + factory DeprecatedRadarrRootFolder.empty() => DeprecatedRadarrRootFolder( id: -1, path: '', freeSpace: 0, ); - RadarrRootFolder({ + DeprecatedRadarrRootFolder({ @required this.id, @required this.path, @required this.freeSpace, diff --git a/lib/modules/radarr/core/api/data/rootfolder.g.dart b/lib/modules/radarr/core/deprecated/rootfolder.g.dart similarity index 75% rename from lib/modules/radarr/core/api/data/rootfolder.g.dart rename to lib/modules/radarr/core/deprecated/rootfolder.g.dart index 799372b2..e7773f14 100644 --- a/lib/modules/radarr/core/api/data/rootfolder.g.dart +++ b/lib/modules/radarr/core/deprecated/rootfolder.g.dart @@ -6,17 +6,18 @@ part of 'rootfolder.dart'; // TypeAdapterGenerator // ************************************************************************** -class RadarrRootFolderAdapter extends TypeAdapter { +class DeprecatedRadarrRootFolderAdapter + extends TypeAdapter { @override final int typeId = 5; @override - RadarrRootFolder read(BinaryReader reader) { + DeprecatedRadarrRootFolder read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return RadarrRootFolder( + return DeprecatedRadarrRootFolder( id: fields[0] as int, path: fields[1] as String, freeSpace: fields[2] as int, @@ -24,7 +25,7 @@ class RadarrRootFolderAdapter extends TypeAdapter { } @override - void write(BinaryWriter writer, RadarrRootFolder obj) { + void write(BinaryWriter writer, DeprecatedRadarrRootFolder obj) { writer ..writeByte(3) ..writeByte(0) @@ -41,7 +42,7 @@ class RadarrRootFolderAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is RadarrRootFolderAdapter && + other is DeprecatedRadarrRootFolderAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/modules/radarr/core/dialogs.dart b/lib/modules/radarr/core/dialogs.dart index a06a1f20..70a1a444 100644 --- a/lib/modules/radarr/core/dialogs.dart +++ b/lib/modules/radarr/core/dialogs.dart @@ -3,19 +3,223 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/radarr.dart'; class RadarrDialogs { - RadarrDialogs._(); - - static Future> deleteMovieFile(BuildContext context) async { + Future> globalSettings(BuildContext context) async { bool _flag = false; - - void _setValues(bool flag) { + RadarrGlobalSettingsType _value; + + void _setValues(bool flag, RadarrGlobalSettingsType value) { _flag = flag; - Navigator.of(context).pop(); + _value = value; + Navigator.of(context, rootNavigator: true).pop(); } await LSDialog.dialog( context: context, - title: 'Delete Movie File', + title: 'Radarr Settings', + content: List.generate( + RadarrGlobalSettingsType.values.length, + (index) => LSDialog.tile( + text: RadarrGlobalSettingsType.values[index].name, + icon: RadarrGlobalSettingsType.values[index].icon, + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, RadarrGlobalSettingsType.values[index]), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _value); + } + + Future> movieSettings(BuildContext context, RadarrMovie movie) async { + bool _flag = false; + RadarrMovieSettingsType _value; + + void _setValues(bool flag, RadarrMovieSettingsType value) { + _flag = flag; + _value = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: movie.title, + content: List.generate( + RadarrMovieSettingsType.values.length, + (index) => LSDialog.tile( + text: RadarrMovieSettingsType.values[index].name(movie), + icon: RadarrMovieSettingsType.values[index].icon(movie), + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, RadarrMovieSettingsType.values[index]), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _value); + } + + Future> setDefaultPage(BuildContext context, { + @required List titles, + @required List icons, + }) async { + bool _flag = false; + int _index = 0; + + void _setValues(bool flag, int index) { + _flag = flag; + _index = index; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Page', + content: List.generate( + titles.length, + (index) => LSDialog.tile( + text: titles[index], + icon: icons[index], + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, index), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + + return Tuple2(_flag, _index); + } + + Future> setDefaultSortingOrFiltering(BuildContext context, { + @required List titles, + }) async { + bool _flag = false; + int _index = 0; + + void _setValues(bool flag, int index) { + _flag = flag; + _index = index; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Sorting & Filtering', + content: List.generate( + titles.length, + (index) => LSDialog.tile( + text: titles[index], + icon: Icons.sort, + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, index), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + + return Tuple2(_flag, _index); + } + + Future> addNewTag(BuildContext context) async { + bool _flag = false; + final _formKey = GlobalKey(); + final _textController = TextEditingController(); + + void _setValues(bool flag) { + if(_formKey.currentState.validate()) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + } + + await LSDialog.dialog( + context: context, + title: 'Add Tag', + buttons: [ + LSDialog.button( + text: 'Add', + onPressed: () => _setValues(true), + ), + ], + content: [ + Form( + key: _formKey, + child: LSDialog.textFormInput( + controller: _textController, + title: 'Tag Label', + onSubmitted: (_) => _setValues(true), + validator: (value) { + if(value == null || value.isEmpty) return 'Label cannot be empty'; + return null; + }, + ), + ), + ], + contentPadding: LSDialog.inputDialogContentPadding(), + ); + return Tuple2(_flag, _textController.text); + } + + Future deleteTag(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Delete Tag', + buttons: [ + LSDialog.button( + text: 'Delete', + textColor: LunaColours.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + LSDialog.textContent(text: 'Are you sure you want to delete this tag?'), + ], + contentPadding: LSDialog.textDialogContentPadding(), + ); + return _flag; + } + + Future searchAllMissingMovies(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Missing Movies', + buttons: [ + LSDialog.button( + text: 'Search', + onPressed: () => _setValues(true), + ), + ], + content: [ + LSDialog.textContent(text: 'Are you sure you want to search for all missing movies?'), + ], + contentPadding: LSDialog.textDialogContentPadding(), + ); + return _flag; + } + + Future deleteMovieFile(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Delete File', buttons: [ LSDialog.button( text: 'Delete', @@ -28,16 +232,205 @@ class RadarrDialogs { ], contentPadding: LSDialog.textDialogContentPadding(), ); - return [_flag]; + return _flag; } - static Future> deleteMovie(BuildContext context) async { + Future> editMinimumAvailability(BuildContext context) async { bool _flag = false; - void _setValues(bool flag) { + RadarrAvailability availability; + + void _setValues(bool flag, RadarrAvailability value) { _flag = flag; - Navigator.of(context).pop(); + availability = value; + Navigator.of(context, rootNavigator: true).pop(); } + await LSDialog.dialog( + context: context, + title: 'Minimum Availability', + content: List.generate( + RadarrAvailability.values.length, + (index) => LSDialog.tile( + text: RadarrAvailability.values[index].readable, + icon: Icons.folder, + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, RadarrAvailability.values[index]), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, availability); + } + + Future> editQualityProfile(BuildContext context, List profiles) async { + bool _flag = false; + RadarrQualityProfile profile; + + void _setValues(bool flag, RadarrQualityProfile value) { + _flag = flag; + profile = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Quality Profile', + content: List.generate( + profiles.length, + (index) => LSDialog.tile( + text: profiles[index].name, + icon: Icons.portrait, + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, profiles[index]), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, profile); + } + + Future setEditTags(BuildContext context) async { + await showDialog( + context: context, + builder: (dContext) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => AlertDialog( + actions: [ + RadarrTagsAppBarActionAddTag(asDialogButton: true), + LSDialog.button( + text: 'Close', + onPressed: () => Navigator.of(context, rootNavigator: true).pop(), + ), + + ], + title: LSDialog.title(text: 'Tags'), + content: Selector>>( + selector: (_, state) => state.tags, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot> snapshot) => LSDialog.content( + children: (snapshot.data?.length ?? 0) == 0 + ? [ LSDialog.textContent(text: 'No Tags Found') ] + : List.generate( + snapshot.data.length, + (index) => CheckboxListTile( + title: Text( + snapshot.data[index].label, + style: TextStyle( + fontSize: LSDialog.BODY_SIZE, + color: Colors.white, + ), + ), + value: context.watch().tags.where((tag) => tag.id == snapshot.data[index].id).length != 0, + onChanged: (selected) { + List _tags = context.read().tags; + selected ? _tags.add(snapshot.data[index]) : _tags.removeWhere((tag) => tag.id == snapshot.data[index].id); + context.read().tags = _tags; + }, + contentPadding: LSDialog.tileContentPadding(), + ), + ), + ), + ), + ), + contentPadding: LSDialog.textDialogContentPadding(), + shape: LunaUI.shapeBorder, + ), + ), + ); + } + + Future setAddTags(BuildContext context) async { + await showDialog( + context: context, + builder: (dContext) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => AlertDialog( + actions: [ + RadarrTagsAppBarActionAddTag(asDialogButton: true), + LSDialog.button( + text: 'Close', + onPressed: () => Navigator.of(context, rootNavigator: true).pop(), + ), + + ], + title: LSDialog.title(text: 'Tags'), + content: Selector>>( + selector: (_, state) => state.tags, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot> snapshot) => LSDialog.content( + children: (snapshot.data?.length ?? 0) == 0 + ? [ LSDialog.textContent(text: 'No Tags Found') ] + : List.generate( + snapshot.data.length, + (index) => CheckboxListTile( + title: Text( + snapshot.data[index].label, + style: TextStyle( + fontSize: LSDialog.BODY_SIZE, + color: Colors.white, + ), + ), + value: context.watch().tags.where((tag) => tag.id == snapshot.data[index].id).length != 0, + onChanged: (selected) { + List _tags = context.read().tags; + selected ? _tags.add(snapshot.data[index]) : _tags.removeWhere((tag) => tag.id == snapshot.data[index].id); + context.read().tags = _tags; + }, + contentPadding: LSDialog.tileContentPadding(), + ), + ), + ), + ), + ), + contentPadding: LSDialog.textDialogContentPadding(), + shape: LunaUI.shapeBorder, + ), + ), + ); + } + + Future> editRootFolder(BuildContext context, List folders) async { + bool _flag = false; + RadarrRootFolder _folder; + + void _setValues(bool flag, RadarrRootFolder value) { + _flag = flag; + _folder = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Root Folder', + content: List.generate( + folders.length, + (index) => LSDialog.tile( + text: folders[index].path, + subtitle: LSDialog.richText( + children: [ + LSDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()) + ], + ), + icon: Icons.folder, + iconColor: LunaColours.list(index), + onTap: () => _setValues(true, folders[index]), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _folder); + } + + Future removeMovie(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + await LSDialog.dialog( context: context, title: 'Remove Movie', @@ -50,272 +443,23 @@ class RadarrDialogs { ], content: [ LSDialog.textContent(text: 'Are you sure you want to remove the movie from Radarr?\n'), - Selector( - selector: (_, state) => state.removeDeleteFiles, - builder: (context, value, text) => CheckboxListTile( - title: text, - value: value, - onChanged: (selected) => Provider.of(context, listen: false).removeDeleteFiles = selected, - contentPadding: LSDialog.tileContentPadding(), - ), - child: Text( - 'Delete Files', - style: TextStyle( - fontSize: LSDialog.BODY_SIZE, - color: Colors.white, - ), + RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST.listen( + builder: (context, value, _) => LSDialog.checkbox( + title: 'Add to Exclusion List', + value: RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST.data, + onChanged: (value) => RadarrDatabaseValue.REMOVE_MOVIE_IMPORT_LIST.put(value), ), ), - Selector( - selector: (_, state) => state.removeAddExclusion, - builder: (context, value, text) => CheckboxListTile( - title: text, - value: value, - onChanged: (selected) => Provider.of(context, listen: false).removeAddExclusion = selected, - contentPadding: LSDialog.tileContentPadding(), - ), - child: Text( - 'Add to Exclusion List', - style: TextStyle( - fontSize: LSDialog.BODY_SIZE, - color: Colors.white, - ), + RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.listen( + builder: (context, value, _) => LSDialog.checkbox( + title: 'Delete Files', + value: RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.data, + onChanged: (value) => RadarrDatabaseValue.REMOVE_MOVIE_DELETE_FILES.put(value), ), ), ], contentPadding: LSDialog.textDialogContentPadding(), ); - return [_flag]; - } - - static Future> searchAllMissing(BuildContext context) async { - bool _flag = false; - - void _setValues(bool flag) { - _flag = flag; - Navigator.of(context).pop(); - } - - await LSDialog.dialog( - context: context, - title: 'Search All Missing', - buttons: [ - LSDialog.button( - text: 'Search', - textColor: LunaColours.accent, - onPressed: () => _setValues(true), - ), - ], - content: [ - LSDialog.textContent(text: 'Are you sure you want to search for all missing movies?'), - ], - contentPadding: LSDialog.textDialogContentPadding(), - ); - return [_flag]; - } - - static Future> editMovie(BuildContext context, RadarrCatalogueData entry) async { - List> _options = [ - ['Edit Movie', Icons.edit, 'edit_movie'], - ['Refresh Movie', Icons.refresh, 'refresh_movie'], - ['Remove Movie', Icons.delete, 'remove_movie'], - ]; - bool _flag = false; - String _value = ''; - - void _setValues(bool flag, String value) { - _flag = flag; - _value = value; - Navigator.of(context).pop(); - } - - await LSDialog.dialog( - context: context, - title: entry.title, - content: List.generate( - _options.length, - (index) => LSDialog.tile( - text: _options[index][0], - icon: _options[index][1], - iconColor: LunaColours.list(index), - onTap: () => _setValues(true, _options[index][2]), - ), - ), - contentPadding: LSDialog.listDialogContentPadding(), - ); - return [_flag, _value]; - } - - static Future> editMinimumAvailability(BuildContext context, List availability) async { - bool _flag = false; - RadarrAvailability _entry; - - void _setValues(bool flag, RadarrAvailability entry) { - _flag = flag; - _entry = entry; - Navigator.of(context).pop(); - } - - await LSDialog.dialog( - context: context, - title: 'Minimum Availability', - content: List.generate( - availability.length, - (index) => LSDialog.tile( - text: availability[index].name, - icon: Icons.folder, - iconColor: LunaColours.list(index), - onTap: () => _setValues(true, availability[index]), - ), - ), - contentPadding: LSDialog.listDialogContentPadding(), - ); - return [_flag, _entry]; - } - - static Future> editRootFolder(BuildContext context, List folders) async { - bool _flag = false; - RadarrRootFolder _folder; - - void _setValues(bool flag, RadarrRootFolder folder) { - _flag = flag; - _folder = folder; - Navigator.of(context).pop(); - } - - await LSDialog.dialog( - context: context, - title: 'Root Folder', - content: List.generate( - folders.length, - (index) => LSDialog.tile( - text: folders[index].path, - subtitle: LSDialog.richText( - children: [ - LSDialog.bolded(text: folders[index].freeSpace.lunaBytesToString()), - ], - ), - icon: Icons.folder, - iconColor: LunaColours.list(index), - onTap: () => _setValues(true, folders[index]), - ), - ), - contentPadding: LSDialog.listDialogContentPadding(), - ); - return [_flag, _folder]; - } - - static Future> editQualityProfile(BuildContext context, List qualities) async { - bool _flag = false; - RadarrQualityProfile _quality; - - void _setValues(bool flag, RadarrQualityProfile quality) { - _flag = flag; - _quality = quality; - Navigator.of(context).pop(); - } - - await LSDialog.dialog( - context: context, - title: 'Quality Profile', - content: List.generate( - qualities.length, - (index) => LSDialog.tile( - text: qualities[index].name, - icon: Icons.portrait, - iconColor: LunaColours.list(index), - onTap: () => _setValues(true, qualities[index]), - ), - ), - contentPadding: LSDialog.listDialogContentPadding(), - ); - return [_flag, _quality]; - } - - static Future> downloadWarning(BuildContext context) async { - bool _flag = false; - - void _setValues(bool flag) { - _flag = flag; - Navigator.of(context).pop(); - } - - await LSDialog.dialog( - context: context, - title: 'Download Release', - buttons: [ - LSDialog.button( - text: 'Download', - onPressed: () => _setValues(true), - ), - ], - content: [ - LSDialog.textContent(text: 'Are you sure you want to download this release? It has been marked as a rejected release by Radarr.') - ], - contentPadding: LSDialog.textDialogContentPadding(), - ); - return [_flag]; - } - - static Future> globalSettings(BuildContext context) async { - List> _options = [ - ['View Web GUI', Icons.language, 'web_gui'], - ['Update Library', Icons.autorenew, 'update_library'], - ['Run RSS Sync', Icons.rss_feed, 'rss_sync'], - ['Search All Missing', Icons.search, 'missing_search'], - ['Backup Database', Icons.save, 'backup'], - ]; - bool _flag = false; - String _value = ''; - - void _setValues(bool flag, String value) { - _flag = flag; - _value = value; - Navigator.of(context).pop(); - } - - await LSDialog.dialog( - context: context, - title: 'Radarr Settings', - content: List.generate( - _options.length, - (index) => LSDialog.tile( - text: _options[index][0], - icon: _options[index][1], - iconColor: LunaColours.list(index), - onTap: () => _setValues(true, _options[index][2]), - ), - ), - contentPadding: LSDialog.listDialogContentPadding(), - ); - return [_flag, _value]; - } - - static Future> defaultPage(BuildContext context) async { - bool _flag = false; - int _index = 0; - - void _setValues(bool flag, int index) { - _flag = flag; - _index = index; - Navigator.of(context, rootNavigator: true).pop(); - } - - await LSDialog.dialog( - context: context, - title: 'Default Page', - content: List.generate( - RadarrNavigationBar.titles.length, - (index) => LSDialog.tile( - text: RadarrNavigationBar.titles[index], - icon: RadarrNavigationBar.icons[index], - iconColor: LunaColours.list(index), - onTap: () => _setValues(true, index), - ), - ), - contentPadding: LSDialog.listDialogContentPadding(), - ); - - return [_flag, _index]; + return _flag; } } diff --git a/lib/modules/radarr/core/extensions.dart b/lib/modules/radarr/core/extensions.dart new file mode 100644 index 00000000..134fc6de --- /dev/null +++ b/lib/modules/radarr/core/extensions.dart @@ -0,0 +1,8 @@ +export 'extensions/radarr_event_type.dart'; +export 'extensions/radarr_extra_file.dart'; +export 'extensions/radarr_history_record.dart'; +export 'extensions/radarr_movie_file.dart'; +export 'extensions/radarr_movie.dart'; +export 'extensions/radarr_quality_profile.dart'; +export 'extensions/radarr_release.dart'; +export 'extensions/radarr_system_status.dart'; diff --git a/lib/modules/radarr/core/extensions/radarr_event_type.dart b/lib/modules/radarr/core/extensions/radarr_event_type.dart new file mode 100644 index 00000000..7294dcb7 --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_event_type.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +extension lunaRadarrEventType on RadarrEventType { + // Get LunaSea associated colour of the event type. + Color get lunaColour { + switch(this) { + case RadarrEventType.GRABBED: return LunaColours.orange; + case RadarrEventType.DOWNLOAD_FAILED: return LunaColours.red; + case RadarrEventType.DOWNLOAD_FOLDER_IMPORTED: return LunaColours.accent; + case RadarrEventType.DOWNLOAD_IGNORED: return LunaColours.purple; + case RadarrEventType.MOVIE_FILE_DELETED: return LunaColours.red; + case RadarrEventType.MOVIE_FILE_RENAMED: return LunaColours.blue; + case RadarrEventType.MOVIE_FOLDER_IMPORTED: return LunaColours.accent; + default: return LunaColours.blueGrey; + } + } + + IconData get lunaIcon { + switch(this) { + case RadarrEventType.GRABBED: return Icons.cloud_download; + case RadarrEventType.DOWNLOAD_FAILED: return Icons.cloud_download; + case RadarrEventType.DOWNLOAD_FOLDER_IMPORTED: return Icons.download_rounded; + case RadarrEventType.MOVIE_FOLDER_IMPORTED: return Icons.download_rounded; + case RadarrEventType.MOVIE_FILE_DELETED: return Icons.delete; + case RadarrEventType.DOWNLOAD_IGNORED: return Icons.cancel; + case RadarrEventType.MOVIE_FILE_RENAMED: return Icons.drive_file_rename_outline; + default: return Icons.help; + } + } + + Color get lunaIconColour { + switch(this) { + case RadarrEventType.GRABBED: return Colors.white; + case RadarrEventType.DOWNLOAD_FAILED: return Colors.red; + case RadarrEventType.DOWNLOAD_FOLDER_IMPORTED: return Colors.white; + case RadarrEventType.DOWNLOAD_IGNORED: return Colors.white; + case RadarrEventType.MOVIE_FILE_DELETED: return Colors.white; + case RadarrEventType.MOVIE_FILE_RENAMED: return Colors.white; + case RadarrEventType.MOVIE_FOLDER_IMPORTED: return Colors.white; + default: return Colors.white; + } + } + + String lunaReadable(RadarrHistoryRecord record) { + switch(this) { + case RadarrEventType.GRABBED: return 'Grabbed from ${record.data['indexer'] ?? LunaUI.TEXT_EMDASH}'; + case RadarrEventType.DOWNLOAD_FAILED: return 'Download Failed'; + case RadarrEventType.DOWNLOAD_FOLDER_IMPORTED: return 'Movie Imported (${record?.quality?.quality?.name ?? LunaUI.TEXT_EMDASH})'; + case RadarrEventType.DOWNLOAD_IGNORED: return 'Download Ignored'; + case RadarrEventType.MOVIE_FILE_DELETED: return 'Movie File Deleted'; + case RadarrEventType.MOVIE_FILE_RENAMED: return 'Movie File Renamed'; + case RadarrEventType.MOVIE_FOLDER_IMPORTED: return 'Movie Imported (${record?.quality?.quality?.name ?? LunaUI.TEXT_EMDASH})'; + } + return null; + } + + List lunaTableContent(RadarrHistoryRecord record, { bool movieHistory = false }) { + switch(this) { + case RadarrEventType.GRABBED: return _grabbedTableContent(record, !movieHistory); + case RadarrEventType.DOWNLOAD_FAILED: return _downloadFailedTableContent(record, !movieHistory); + case RadarrEventType.DOWNLOAD_FOLDER_IMPORTED: return _downloadFolderImportedTableContent(record); + case RadarrEventType.DOWNLOAD_IGNORED: return _downloadIgnoredTableContent(record, !movieHistory); + case RadarrEventType.MOVIE_FILE_DELETED: return _movieFileDeletedTableContent(record, !movieHistory); + case RadarrEventType.MOVIE_FILE_RENAMED: return _movieFileRenamedTableContent(record); + case RadarrEventType.MOVIE_FOLDER_IMPORTED: return _movieFolderImportedTableContent(record); + default: return []; + } + } + + List _grabbedTableContent(RadarrHistoryRecord record, bool showSourceTitle) { + return [ + if(showSourceTitle) LunaTableContent(title: 'source title', body: record.sourceTitle ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'quality', body: record?.quality?.quality?.name ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'languages', body: record?.languages?.map((language) => language.name)?.join('\n') ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'indexer', body: record.data['indexer'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'group', body: record.data['releaseGroup'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'client', body: record.data['downloadClientName'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent( + title: 'age', + body: record.data['ageHours'] != null + ? double?.tryParse((record.data['ageHours'] as String))?.lunaHoursToAge() ?? LunaUI.TEXT_EMDASH + : LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'published date', + body: DateTime.tryParse(record.data['publishedDate']) != null + ? DateTime.tryParse(record.data['publishedDate'])?.lunaDateTimeReadable(timeOnNewLine: true) ?? LunaUI.TEXT_EMDASH + : LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'info url', + body: record.data['nzbInfoUrl'] ?? LunaUI.TEXT_EMDASH, + bodyIsUrl: record.data['nzbInfoUrl'] != null, + ), + ]; + } + + List _downloadFailedTableContent(RadarrHistoryRecord record, bool showSourceTitle) { + return [ + if(showSourceTitle) LunaTableContent(title: 'source title', body: record.sourceTitle ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'client', body: record.data['downloadClientName'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'message', body: record.data['message'] ?? LunaUI.TEXT_EMDASH), + ]; + } + + List _downloadFolderImportedTableContent(RadarrHistoryRecord record) { + return [ + LunaTableContent(title: 'quality', body: record?.quality?.quality?.name ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'languages', body: record?.languages?.map((language) => language.name)?.join('\n') ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'client', body: record.data['downloadClientName'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'name', body: record.sourceTitle ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'source', body: record.data['droppedPath'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'imported to', body: record.data['importedPath'] ?? LunaUI.TEXT_EMDASH), + ]; + } + + List _downloadIgnoredTableContent(RadarrHistoryRecord record, bool showSourceTitle) { + return [ + if(showSourceTitle) LunaTableContent(title: 'source title', body: record.sourceTitle ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'message', body: record.data['message'] ?? LunaUI.TEXT_EMDASH), + ]; + } + + List _movieFileDeletedTableContent(RadarrHistoryRecord record, bool showSourceTitle) { + return [ + if(showSourceTitle) LunaTableContent(title: 'source title', body: record.sourceTitle ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'reason', body: record?.lunaFileDeletedReasonMessage ?? LunaUI.TEXT_EMDASH), + ]; + } + + List _movieFileRenamedTableContent(RadarrHistoryRecord record) { + return [ + LunaTableContent(title: 'source', body: record.data['sourceRelativePath'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'destination', body: record.data['relativePath'] ?? LunaUI.TEXT_EMDASH), + ]; + } + + List _movieFolderImportedTableContent(RadarrHistoryRecord record) { + return [ + LunaTableContent(title: 'quality', body: record?.quality?.quality?.name ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'languages', body: record?.languages?.map((language) => language.name)?.join('\n') ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'client', body: record.data['downloadClientName'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'name', body: record.sourceTitle ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'source', body: record.data['droppedPath'] ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'imported to', body: record.data['importedPath'] ?? LunaUI.TEXT_EMDASH), + ]; + } +} diff --git a/lib/modules/radarr/core/extensions/radarr_extra_file.dart b/lib/modules/radarr/core/extensions/radarr_extra_file.dart new file mode 100644 index 00000000..913427e7 --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_extra_file.dart @@ -0,0 +1,19 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +extension LunaRadarrExtraFileExtension on RadarrExtraFile { + String get lunaRelativePath { + if(this?.relativePath != null && this.relativePath.isNotEmpty) return this.relativePath; + return LunaUI.TEXT_EMDASH; + } + + String get lunaExtension { + if(this?.extension != null && this.extension.isNotEmpty) return this.extension; + return LunaUI.TEXT_EMDASH; + } + + String get lunaType { + if(this?.type != null && this.type.isNotEmpty) return this.type.lunaCapitalizeFirstLetters(); + return LunaUI.TEXT_EMDASH; + } +} \ No newline at end of file diff --git a/lib/modules/radarr/core/extensions/radarr_history_record.dart b/lib/modules/radarr/core/extensions/radarr_history_record.dart new file mode 100644 index 00000000..7e21e5a6 --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_history_record.dart @@ -0,0 +1,14 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +extension lunaRadarrHistoryRecord on RadarrHistoryRecord { + String get lunaFileDeletedReasonMessage { + if(this.eventType != RadarrEventType.MOVIE_FILE_DELETED || this.data['reason'] == null) return LunaUI.TEXT_EMDASH; + switch(this.data['reason']) { + case 'Manual': return 'File was deleted manually'; + case 'MissingFromDisk': return 'Unable to find the file on disk'; + case 'Upgrade': return 'File was deleted to import an upgrade'; + default: return LunaUI.TEXT_EMDASH; + } + } +} diff --git a/lib/modules/radarr/core/extensions/radarr_movie.dart b/lib/modules/radarr/core/extensions/radarr_movie.dart new file mode 100644 index 00000000..5d94c833 --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_movie.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +extension LunaRadarrMovieExtension on RadarrMovie { + String get lunaRuntime { + if(this.runtime != null && this.runtime != 0) return this.runtime.lunaRuntime(); + return LunaUI.TEXT_EMDASH; + } + + String get lunaAlternateTitles { + if(this.alternateTitles != null && this.alternateTitles.length != 0) return this.alternateTitles.map((title) => title.title).join('\n'); + return LunaUI.TEXT_EMDASH; + } + + String get lunaGenres { + if(this.genres != null && this.genres.length != 0) return this.genres.join('\n'); + return LunaUI.TEXT_EMDASH; + } + + String get lunaStudio { + if(this.studio != null && this.studio.isNotEmpty) return this.studio; + return LunaUI.TEXT_EMDASH; + } + + String get lunaYear { + if(this.year != null && this.year != 0) return this.year.toString(); + return LunaUI.TEXT_EMDASH; + } + + String get lunaMinimumAvailability { + if(this.minimumAvailability != null) return this.minimumAvailability.readable; + return LunaUI.TEXT_EMDASH; + } + + String get lunaDateAdded { + if(this.added != null) return this.added.lunaDateReadable; + return LunaUI.TEXT_EMDASH; + } + + bool get lunaIsInCinemas { + if(this.inCinemas != null) return this.inCinemas.toLocal().isBefore(DateTime.now()); + return false; + } + + String get lunaInCinemasOn { + if(this.inCinemas != null) return this.inCinemas.lunaDateReadable; + return LunaUI.TEXT_EMDASH; + } + + String get lunaPhysicalReleaseDate { + if(this.physicalRelease != null) return this.physicalRelease.lunaDateReadable; + return LunaUI.TEXT_EMDASH; + } + + String get lunaDigitalReleaseDate { + if(this.digitalRelease != null) return this.digitalRelease.lunaDateReadable; + return LunaUI.TEXT_EMDASH; + } + + String get lunaReleaseDate { + if(this.lunaEarlierReleaseDate != null) return this.lunaEarlierReleaseDate.lunaDateReadable; + return LunaUI.TEXT_EMDASH; + } + + String lunaTags(List tags) { + if(tags != null && tags.length != 0) return tags.map((tag) => tag.label).join('\n'); + return LunaUI.TEXT_EMDASH; + } + + bool get lunaIsReleased { + if(this.status == RadarrAvailability.RELEASED) return true; + if(this.digitalRelease != null) return this.digitalRelease.toLocal().isBefore(DateTime.now()); + if(this.physicalRelease != null) return this.physicalRelease.toLocal().isBefore(DateTime.now()); + return false; + } + + String get lunaFileSize { + if(!this.hasFile) return LunaUI.TEXT_EMDASH; + return this.sizeOnDisk.lunaBytesToString(); + } + + Text lunaHasFileTextObject(bool isMonitored) { + if(this.hasFile) return Text( + lunaFileSize, + style: TextStyle( + color: isMonitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30), + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ); + return Text(''); + } + + Text lunaNextReleaseTextObject(bool isMonitored) { + DateTime now = DateTime.now(); + // If we already have a file or it is released + if(this.hasFile || lunaIsReleased) return Text(''); + // In Cinemas + if(this.inCinemas != null && this.inCinemas.toLocal().isAfter(now)) { + String _date = this.inCinemas.lunaDaysDifference?.toUpperCase() ?? LunaUI.TEXT_EMDASH; + return Text( + _date == 'TODAY' ? _date : 'IN $_date', + style: TextStyle( + color: isMonitored ? LunaColours.orange : LunaColours.orange.withOpacity(0.30), + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ); + } + DateTime _release = lunaEarlierReleaseDate; + // Releases + if(_release != null) { + String _date = _release.lunaDaysDifference?.toUpperCase() ?? LunaUI.TEXT_EMDASH; + return Text( + _date == 'TODAY' ? _date : 'IN $_date', + style: TextStyle( + color: isMonitored ? LunaColours.blue : LunaColours.blue.withOpacity(0.30), + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ); + } + // Unknown case + return Text(''); + } + + /// Compare two movies by their release dates. Returns an integer value compatible with `.sort()` in arrays. + /// + /// Compares and uses the earlier date between `phyiscalRelease` and `digitalRelease`. + int lunaCompareToByReleaseDate(RadarrMovie movie) { + if(this.physicalRelease == null && this.digitalRelease == null && movie.physicalRelease == null && movie.digitalRelease == null) + return this.sortTitle.toLowerCase().compareTo(movie.sortTitle.toLowerCase()); + if(this.physicalRelease == null && this.digitalRelease == null) return 1; + if(movie.physicalRelease == null && movie.digitalRelease == null) return -1; + DateTime a = (this.physicalRelease ?? DateTime(9999)).isBefore((this.digitalRelease ?? DateTime(9999))) ? this.physicalRelease : this.digitalRelease; + DateTime b = (movie.physicalRelease ?? DateTime(9999)).isBefore((movie.digitalRelease ?? DateTime(9999))) ? movie.physicalRelease : movie.digitalRelease; + int comparison = a.compareTo(b); + if(comparison == 0) comparison = this.sortTitle.toLowerCase().compareTo(movie.sortTitle.toLowerCase()); + return comparison; + } + + /// Compare two movies by their cinema release date. Returns an integer value compatible with `.sort()` in arrays. + int lunaCompareToByInCinemas(RadarrMovie movie) { + if(this.inCinemas == null && movie.inCinemas == null) return this.sortTitle.toLowerCase().compareTo(movie.sortTitle.toLowerCase()); + if(this.inCinemas == null) return 1; + if(movie.inCinemas == null) return -1; + int comparison = this.inCinemas.compareTo(movie.inCinemas); + if(comparison == 0) comparison = this.sortTitle.toLowerCase().compareTo(movie.sortTitle.toLowerCase()); + return comparison; + } + + /// Compares the digital and physical release dates and returns the earlier date. + /// + /// If both are null, returns null. + DateTime get lunaEarlierReleaseDate { + if(this.physicalRelease == null && this.digitalRelease == null) return null; + if(this.physicalRelease == null) return this.digitalRelease; + if(this.digitalRelease == null) return this.physicalRelease; + return this.digitalRelease.isBefore(this.physicalRelease) ? this.digitalRelease : this.physicalRelease; + } + + /// Creates a clone of the [RadarrMovie] object (deep copy). + RadarrMovie clone() => RadarrMovie.fromJson(this.toJson()); + + /// Copies changes from a [RadarrMoviesEditState] state object into a new [RadarrMovie] object. + RadarrMovie updateEdits(RadarrMoviesEditState edits) { + RadarrMovie movie = this.clone(); + movie.monitored = edits.monitored ?? this.monitored; + movie.minimumAvailability = edits.availability ?? this.minimumAvailability; + movie.qualityProfileId = edits.qualityProfile.id ?? this.qualityProfileId; + movie.path = edits.path ?? this.path; + movie.tags = edits.tags?.map((tag) => tag.id)?.toList() ?? this.tags; + return movie; + } +} diff --git a/lib/modules/radarr/core/extensions/radarr_movie_file.dart b/lib/modules/radarr/core/extensions/radarr_movie_file.dart new file mode 100644 index 00000000..ebe0d5a1 --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_movie_file.dart @@ -0,0 +1,29 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +extension LunaRadarrMovieFileExtension on RadarrMovieFile { + String get lunaRelativePath { + if(this?.relativePath != null && this.relativePath.isNotEmpty) return this.relativePath; + return LunaUI.TEXT_EMDASH; + } + + String get lunaSize { + if(this?.size != null && this.size != 0) return this.size.lunaBytesToString(decimals: 1); + return LunaUI.TEXT_EMDASH; + } + + String get lunaQuality { + if(this?.quality?.quality?.name != null) return this.quality.quality.name; + return LunaUI.TEXT_EMDASH; + } + + String get lunaDateAdded { + if(this?.dateAdded != null) return this.dateAdded.lunaDateTimeReadable(timeOnNewLine: true); + return LunaUI.TEXT_EMDASH; + } + + String get lunaCustomFormats { + if(this?.customFormats != null && this.customFormats.isNotEmpty) return this.customFormats.map((format) => format.name).join('\n'); + return LunaUI.TEXT_EMDASH; + } +} diff --git a/lib/modules/radarr/core/extensions/radarr_quality_profile.dart b/lib/modules/radarr/core/extensions/radarr_quality_profile.dart new file mode 100644 index 00000000..a1fd319e --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_quality_profile.dart @@ -0,0 +1,9 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +extension RadarrQualityProfileExtension on RadarrQualityProfile { + String get lunaName { + if(this.name != null && this.name.isNotEmpty) return this.name; + return LunaUI.TEXT_EMDASH; + } +} diff --git a/lib/modules/radarr/core/extensions/radarr_release.dart b/lib/modules/radarr/core/extensions/radarr_release.dart new file mode 100644 index 00000000..8efebde7 --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_release.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:radarr/radarr.dart'; + +extension RadarrReleaseExtension on RadarrRelease { + IconData get lunaTrailingIcon { + if(this.approved) return Icons.download_rounded; + return Icons.report; + } + + Color get lunaTrailingColor { + if(this.approved) return Colors.white; + return LunaColours.red; + } + + String get lunaProtocol { + if(this.protocol != null) return this.protocol == RadarrProtocol.TORRENT ? '${this.protocol.readable} (${this?.seeders ?? 0}/${this?.leechers ?? 0})' : this.protocol.readable; + return LunaUI.TEXT_EMDASH; + } + + Color get lunaProtocolColor { + if(this.protocol == RadarrProtocol.USENET) return LunaColours.blue; + return LunaColours.purple; + } + + String get lunaIndexer { + if(this.indexer != null && this.indexer.isNotEmpty) return this.indexer; + return LunaUI.TEXT_EMDASH; + } + + String get lunaAge { + if(this.ageHours != null) return this.ageHours.lunaHoursToAge(); + return LunaUI.TEXT_EMDASH; + } + + String get lunaQuality { + if(this.quality != null && this.quality.quality != null) return this.quality.quality.name; + return LunaUI.TEXT_EMDASH; + } + + String get lunaSize { + if(this.size != null) return this.size.lunaBytesToString(); + return LunaUI.TEXT_EMDASH; + } +} diff --git a/lib/modules/radarr/core/extensions/radarr_system_status.dart b/lib/modules/radarr/core/extensions/radarr_system_status.dart new file mode 100644 index 00000000..2ade8fdd --- /dev/null +++ b/lib/modules/radarr/core/extensions/radarr_system_status.dart @@ -0,0 +1,54 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +extension RadarrSystemStatusExtension on RadarrSystemStatus { + String get lunaVersion { + if(this.version != null && this.version.isNotEmpty) return this.version; + return LunaUI.TEXT_EMDASH; + } + + String get lunaPackageVersion { + String packageAuthor, packageVersion; + if(this.packageVersion != null && this.packageVersion.isNotEmpty) packageVersion = this.packageVersion; + if(this.packageAuthor != null && this.packageAuthor.isNotEmpty) packageAuthor = this.packageAuthor; + return '${packageVersion ?? LunaUI.TEXT_EMDASH} by ${packageAuthor ?? LunaUI.TEXT_EMDASH}'; + } + + String get lunaNetCore { + if(this.isNetCore ?? false) return 'Yes (${this.runtimeVersion ?? LunaUI.TEXT_EMDASH})'; + return 'No'; + } + + String get lunaDocker { + if(this.isDocker ?? false) return 'Yes'; + return 'No'; + } + + String get lunaDBMigration { + if(this.migrationVersion != null) return '${this.migrationVersion}'; + return LunaUI.TEXT_EMDASH; + } + + String get lunaAppDataDirectory { + if(this.appData != null && this.appData.isNotEmpty) return this.appData; + return LunaUI.TEXT_EMDASH; + } + + String get lunaStartupDirectory { + if(this.startupPath != null && this.startupPath.isNotEmpty) return this.startupPath; + return LunaUI.TEXT_EMDASH; + } + + String get lunaMode { + if(this.mode != null && this.mode.isNotEmpty) return this.mode.lunaCapitalizeFirstLetters(); + return LunaUI.TEXT_EMDASH; + } + + String get lunaUptime { + if(this.startTime != null && this.startTime.isNotEmpty) { + DateTime _start = DateTime.tryParse(this.startTime); + if(_start != null) return DateTime.now().difference(_start).lunaTimestampWords; + } + return LunaUI.TEXT_EMDASH; + } +} \ No newline at end of file diff --git a/lib/modules/radarr/core/router.dart b/lib/modules/radarr/core/router.dart new file mode 100644 index 00000000..9b0f6139 --- /dev/null +++ b/lib/modules/radarr/core/router.dart @@ -0,0 +1,20 @@ +import 'package:fluro/fluro.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrRouter extends LunaModuleRouter { + @override + void defineAllRoutes(FluroRouter router) { + RadarrAddMovieRouter().defineRoute(router); + RadarrAddMovieDetailsRouter().defineRoute(router); + RadarrHealthCheckRouter().defineRoute(router); + RadarrHomeRouter().defineRoute(router); + RadarrHistoryRouter().defineRoute(router); + RadarrMoviesDetailsRouter().defineRoute(router); + RadarrMoviesEditRouter().defineRoute(router); + RadarrReleasesRouter().defineRoute(router); + RadarrQueueRouter().defineRoute(router); + RadarrSystemStatusRouter().defineRoute(router); + RadarrTagsRouter().defineRoute(router); + } +} diff --git a/lib/modules/radarr/core/sorting.dart b/lib/modules/radarr/core/sorting.dart deleted file mode 100644 index e058f21e..00000000 --- a/lib/modules/radarr/core/sorting.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'sorting/catalogue.dart'; -export 'sorting/releases.dart'; diff --git a/lib/modules/radarr/core/sorting/catalogue.dart b/lib/modules/radarr/core/sorting/catalogue.dart deleted file mode 100644 index 47064c9e..00000000 --- a/lib/modules/radarr/core/sorting/catalogue.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -enum RadarrCatalogueSorting { - alphabetical, - dateAdded, - quality, - runtime, - size, - studio, - year, -} - -extension RadarrCatalogueSortingExtension on RadarrCatalogueSorting { - String get value { - switch(this) { - case RadarrCatalogueSorting.alphabetical: return 'abc'; - case RadarrCatalogueSorting.dateAdded: return 'date_added'; - case RadarrCatalogueSorting.size: return 'size'; - case RadarrCatalogueSorting.quality: return 'quality'; - case RadarrCatalogueSorting.studio: return 'studio'; - case RadarrCatalogueSorting.runtime: return 'runtime'; - case RadarrCatalogueSorting.year: return 'year'; - } - throw Exception('value not found'); - } - - String get readable { - switch(this) { - case RadarrCatalogueSorting.alphabetical: return 'Alphabetical'; - case RadarrCatalogueSorting.dateAdded: return 'Date Added'; - case RadarrCatalogueSorting.size: return 'Size'; - case RadarrCatalogueSorting.quality: return 'Quality Profile'; - case RadarrCatalogueSorting.studio: return 'Studio'; - case RadarrCatalogueSorting.runtime: return 'Runtime'; - case RadarrCatalogueSorting.year: return 'Year'; - } - throw Exception('readable not found'); - } - - List sort(List data, bool ascending) => _Sorter().byType(data, this, ascending); -} - -class _Sorter extends LunaSorter { - @override - List byType( - List data, - RadarrCatalogueSorting type, - bool ascending, - ) { - switch(type) { - case RadarrCatalogueSorting.alphabetical: return _alphabetical(data, ascending); - case RadarrCatalogueSorting.dateAdded: return _dateAdded(data, ascending); - case RadarrCatalogueSorting.size: return _size(data, ascending); - case RadarrCatalogueSorting.quality: return _quality(data, ascending); - case RadarrCatalogueSorting.studio: return _studio(data, ascending); - case RadarrCatalogueSorting.runtime: return _runtime(data, ascending); - case RadarrCatalogueSorting.year: return _year(data, ascending); - } - throw Exception('sorting type not found'); - } - - List _alphabetical(List data, bool ascending) { - List _data = new List.from(data, growable: false); - ascending - ? _data.sort((a,b) => a.sortTitle.compareTo(b.sortTitle)) - : _data.sort((a,b) => b.sortTitle.compareTo(a.sortTitle)); - return _data; - } - - List _dateAdded(List data, bool ascending) { - List _data = _alphabetical(data, true); - List _hasNoDate = _data.where((item) => item.dateAddedObject == null).toList(); - List _hasDate = _data.where((item) => item.dateAddedObject != null).toList(); - _hasDate.sort((a,b) { - return ascending - ? a.dateAddedObject.compareTo(b.dateAddedObject) - : b.dateAddedObject.compareTo(a.dateAddedObject); - }); - return [..._hasDate, ..._hasNoDate]; - } - - List _size(List data, bool ascending) { - List _data = new List.from(data, growable: false); - ascending - ? _data.sort((a,b) => a.sizeOnDisk.compareTo(b.sizeOnDisk)) - : _data.sort((a,b) => b.sizeOnDisk.compareTo(a.sizeOnDisk)); - return _data; - } - - List _quality(List data, bool ascending) { - List _data = _alphabetical(data, true); - ascending - ? _data.sort((a,b) => a.qualityProfile.compareTo(b.qualityProfile)) - : _data.sort((a,b) => b.qualityProfile.compareTo(a.qualityProfile)); - return _data; - } - - List _studio(List data, bool ascending) { - List _data = _alphabetical(data, true); - ascending - ? _data.sort((a,b) => a.studio.compareTo(b.studio)) - : _data.sort((a,b) => b.studio.compareTo(a.studio)); - return _data; - } - - List _runtime(List data, bool ascending) { - List _data = _alphabetical(data, true); - ascending - ? _data.sort((a,b) => a.runtime.compareTo(b.runtime)) - : _data.sort((a,b) => b.runtime.compareTo(a.runtime)); - return _data; - } - - List _year(List data, bool ascending) { - List _data = _alphabetical(data, true); - ascending - ? _data.sort((a,b) => a.year.compareTo(b.year)) - : _data.sort((a,b) => b.year.compareTo(a.year)); - return _data; - } -} diff --git a/lib/modules/radarr/core/sorting/releases.dart b/lib/modules/radarr/core/sorting/releases.dart deleted file mode 100644 index e80a9650..00000000 --- a/lib/modules/radarr/core/sorting/releases.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -enum RadarrReleasesSorting { - age, - alphabetical, - seeders, - size, - type, - weight, -} - -extension RadarrReleasesSortingExtension on RadarrReleasesSorting { - String get value { - switch(this) { - case RadarrReleasesSorting.age: return 'age'; - case RadarrReleasesSorting.alphabetical: return 'abc'; - case RadarrReleasesSorting.seeders: return 'seeders'; - case RadarrReleasesSorting.weight: return 'weight'; - case RadarrReleasesSorting.type: return 'type'; - case RadarrReleasesSorting.size: return 'size'; - } - throw Exception('value not found'); - } - - String get readable { - switch(this) { - case RadarrReleasesSorting.age: return 'Age'; - case RadarrReleasesSorting.alphabetical: return 'Alphabetical'; - case RadarrReleasesSorting.seeders: return 'Seeders'; - case RadarrReleasesSorting.weight: return 'Weight'; - case RadarrReleasesSorting.type: return 'Type'; - case RadarrReleasesSorting.size: return 'Size'; - } - throw Exception('readable not found'); - } - - List sort(List data, bool ascending) => _Sorter().byType(data, this, ascending); -} - -class _Sorter extends LunaSorter { - @override - List byType( - List data, - RadarrReleasesSorting type, - bool ascending, - ) { - switch(type) { - case RadarrReleasesSorting.age: return _age(data, ascending); - case RadarrReleasesSorting.alphabetical: return _alphabetical(data, ascending); - case RadarrReleasesSorting.seeders: return _seeders(data, ascending); - case RadarrReleasesSorting.weight: return _weight(data, ascending); - case RadarrReleasesSorting.type: return _type(data, ascending); - case RadarrReleasesSorting.size: return _size(data, ascending); - } - throw Exception('sorting type not found'); - } - - List _alphabetical(List data, bool ascending) { - List _data = new List.from(data, growable: false); - ascending - ? _data.sort((a,b) => a.title.compareTo(b.title)) - : _data.sort((a,b) => b.title.compareTo(a.title)); - return _data; - } - - List _weight(List data, bool ascending) { - List _data = new List.from(data, growable: false); - ascending - ? _data.sort((a,b) => a.releaseWeight.compareTo(b.releaseWeight)) - : _data.sort((a,b) => b.releaseWeight.compareTo(a.releaseWeight)); - return _data; - } - - List _type(List data, bool ascending) { - List _data = new List.from(data, growable: false); - List _usenet = _data.where((value) => !value.isTorrent).toList(); - List _torrent = _data.where((value) => value.isTorrent).toList(); - return ascending - ? [..._usenet, ..._torrent] - : [..._torrent, ..._usenet]; - } - - List _age(List data, bool ascending) { - List _data = new List.from(data, growable: false); - ascending - ? _data.sort((a,b) => a.ageHours.compareTo(b.ageHours)) - : _data.sort((a,b) => b.ageHours.compareTo(a.ageHours)); - return _data; - } - - List _seeders(List data, bool ascending) { - List _data = new List.from(data, growable: false); - List _usenet = _data.where((value) => !value.isTorrent).toList(); - List _torrent = _data.where((value) => value.isTorrent).toList(); - ascending - ? _torrent.sort((a,b) => b.seeders.compareTo(a.seeders)) - : _torrent.sort((a,b) => a.seeders.compareTo(b.seeders)); - return [..._torrent, ..._usenet]; - } - - List _size(List data, bool ascending) { - List _data = new List.from(data, growable: false); - ascending - ? _data.sort((a,b) => a.size.compareTo(b.size)) - : _data.sort((a,b) => b.size.compareTo(a.size)); - return _data; - } -} diff --git a/lib/modules/radarr/core/state.dart b/lib/modules/radarr/core/state.dart index ed5ebd8f..8edfb6c6 100644 --- a/lib/modules/radarr/core/state.dart +++ b/lib/modules/radarr/core/state.dart @@ -1,2 +1,285 @@ -export 'state/global.dart'; -export 'state/local.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrState extends LunaModuleState { + RadarrState() { + reset(); + } + + @override + void reset() { + // Reset stored data + _movies = null; + _upcoming = null; + _missing = null; + _rootFolders = null; + _qualityProfiles = null; + _tags = null; + lookup = null; + // Reset search query fields + _moviesSearchQuery = ''; + // Reinitialize + resetProfile(); + if(_enabled) { + fetchRootFolders(); + fetchQualityProfiles(); + fetchTags(); + fetchMovies(); + } + notifyListeners(); + } + + /////////////// + /// PROFILE /// + /////////////// + + /// API handler instance + Radarr _api; + Radarr get api => _api; + + /// Is the API enabled? + bool _enabled; + bool get enabled => _enabled; + + /// Radarr host + String _host; + String get host => _host; + + /// Radarr API key + String _apiKey; + String get apiKey => _apiKey; + + /// Headers to attach to all requests + Map _headers; + Map get headers => _headers; + + /// Reset the profile data, reinitializes API instance + void resetProfile() { + ProfileHiveObject _profile = Database.currentProfileObject; + // Copy profile into state + _enabled = _profile.radarrEnabled ?? false; + _host = _profile.radarrHost ?? ''; + _apiKey = _profile.radarrKey ?? ''; + _headers = _profile.radarrHeaders ?? {}; + // Create the API instance if Radarr is enabled + _api = !_enabled ? null : Radarr( + host: _host, + apiKey: _apiKey, + headers: Map.from(_headers), + ); + } + + ////////////// + /// MOVIES /// + ////////////// + + String _moviesSearchQuery = ''; + String get moviesSearchQuery => _moviesSearchQuery; + set moviesSearchQuery(String moviesSearchQuery) { + assert(moviesSearchQuery != null); + _moviesSearchQuery = moviesSearchQuery; + notifyListeners(); + } + + RadarrMoviesSorting _moviesSortType = RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.data; + RadarrMoviesSorting get moviesSortType => _moviesSortType; + set moviesSortType(RadarrMoviesSorting moviesSortType) { + assert(moviesSortType != null); + _moviesSortType = moviesSortType; + notifyListeners(); + } + + RadarrMoviesFilter _moviesFilterType = RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES.data; + RadarrMoviesFilter get moviesFilterType => _moviesFilterType; + set moviesFilterType(RadarrMoviesFilter moviesFilterType) { + assert(moviesFilterType != null); + _moviesFilterType = moviesFilterType; + notifyListeners(); + } + + bool _moviesSortAscending = RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING.data; + bool get moviesSortAscending => _moviesSortAscending; + set moviesSortAscending(bool moviesSortAscending) { + assert(moviesSortAscending != null); + _moviesSortAscending = moviesSortAscending; + notifyListeners(); + } + + Future> _movies; + Future> get movies => _movies; + void fetchMovies() { + if(_api != null) { + _movies = _api.movie.getAll(); + _fetchUpcoming(); + _fetchMissing(); + } + notifyListeners(); + } + + Future resetSingleMovie(int movieId) async { + assert(movieId != null); + if(_api != null) { + RadarrMovie movie = await _api.movie.get(movieId: movieId); + List allMovies = await _movies; + int index = allMovies?.indexWhere((m) => m.id == movieId) ?? -1; + if(index >= 0) { + allMovies[index] = movie; + _fetchUpcoming(); + _fetchMissing(); + } + } + notifyListeners(); + } + + Future setSingleMovie(RadarrMovie movie) async { + assert(movie != null); + List allMovies = await _movies; + int index = allMovies?.indexWhere((m) => m.id == movie.id) ?? -1; + if(index >= 0) { + allMovies[index] = movie; + _fetchUpcoming(); + _fetchMissing(); + } + notifyListeners(); + } + + //////////////////// + /// MOVIE LOOKUP /// + //////////////////// + + /// Setting [lookup] to null is okay, so there is no need to privatize and create a getter/setter. + Future> lookup; + void fetchLookup(String query) { + assert(query != null); + if(_enabled ?? false) lookup = _api.movieLookup.get(term: query); + notifyListeners(); + } + + Future> _rootFolders; + Future> get rootFolders => _rootFolders; + void fetchRootFolders() { + if(_enabled ?? false) _rootFolders = _api.rootFolder.get(); + notifyListeners(); + } + + //////////////// + /// UPCOMING /// + //////////////// + + Future> _upcoming; + Future> get upcoming => _upcoming; + void _fetchUpcoming() { + if(_movies != null) _upcoming = _movies.then((movies) { + List _missingOnly = movies.where((movie) => movie.monitored && !movie.hasFile).toList(); + // List of movies not yet released, but in cinemas, sorted by date + List _notYetReleased = []; + List _notYetInCinemas = []; + _missingOnly.forEach((movie) { + if(movie.lunaIsInCinemas && !movie.lunaIsReleased) _notYetReleased.add(movie); + if(!movie.lunaIsInCinemas && !movie.lunaIsReleased) _notYetInCinemas.add(movie); + }); + _notYetReleased.sort((a,b) => a.lunaCompareToByReleaseDate(b)); + _notYetInCinemas.sort((a,b) => a.lunaCompareToByInCinemas(b)); + // Concat and return full array + return [ + ..._notYetReleased, + ..._notYetInCinemas, + ]; + }); + } + + /////////////// + /// MISSING /// + /////////////// + + Future> _missing; + Future> get missing => _missing; + void _fetchMissing() { + if(_movies != null) _missing = _movies.then((movies) { + List _movies = movies.where((movie) { + if(!movie.monitored) return false; + if(movie.hasFile || movie.movieFile != null) return false; + if(!movie.lunaIsReleased) return false; + return true; + }).toList(); + _movies.sort((a,b) { + int _comparison; + if(a.lunaEarlierReleaseDate == null && b.lunaEarlierReleaseDate != null) return 1; + if(b.lunaEarlierReleaseDate == null && a.lunaEarlierReleaseDate != null) return -1; + if(a.lunaEarlierReleaseDate == null && b.lunaEarlierReleaseDate == null) _comparison = 0; + if(_comparison == null) _comparison = b.lunaEarlierReleaseDate.compareTo(a.lunaEarlierReleaseDate); + if(_comparison == 0) return a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()); + return _comparison; + }); + return _movies; + }); + } + + //////////////// + /// PROFILES /// + //////////////// + + Future> _qualityProfiles; + Future> get qualityProfiles => _qualityProfiles; + set qualityProfiles(Future> qualityProfiles) { + assert(qualityProfiles != null); + _qualityProfiles = qualityProfiles; + notifyListeners(); + } + + void fetchQualityProfiles() { + if(_api != null) _qualityProfiles = _api.qualityProfile.getAll(); + notifyListeners(); + } + + //////////// + /// TAGS /// + //////////// + + Future> _tags; + Future> get tags => _tags; + set tags(Future> tags) { + assert(tags != null); + _tags = tags; + notifyListeners(); + } + + void fetchTags() { + if(_api != null) _tags = _api.tag.getAll(); + notifyListeners(); + } + + ////////////// + /// IMAGES /// + ////////////// + + String getPosterURL(int movieId, { bool highRes = false }) { + if(_enabled) { + String _base = _host.endsWith('/') ? '${_host}api/v3/MediaCover' : '$_host/api/v3/MediaCover'; + return highRes + ? '$_base/$movieId/poster.jpg?apikey=$_apiKey' + : '$_base/$movieId/poster-500.jpg?apikey=$_apiKey'; + } + return null; + } + + String getBannerURL(int movieId, { bool highRes = false }) { + if(_enabled) { + String _base = _host.endsWith('/') ? '${_host}api/v3/MediaCover' : '$_host/api/v3/MediaCover'; + return highRes + ? '$_base/$movieId/banner.jpg?apikey=$_apiKey' + : '$_base/$movieId/banner-70.jpg?apikey=$_apiKey'; + } + return null; + } + + String getFanartURL(int movieId, { bool highRes = false }) { + if(_enabled) { + String _base = _host.endsWith('/') ? '${_host}api/v3/MediaCover' : '$_host/api/v3/MediaCover'; + return highRes + ? '$_base/$movieId/fanart.jpg?apikey=$_apiKey' + : '$_base/$movieId/fanart-360.jpg?apikey=$_apiKey'; + } + return null; + } +} diff --git a/lib/modules/radarr/core/state/global.dart b/lib/modules/radarr/core/state/global.dart deleted file mode 100644 index 9718277b..00000000 --- a/lib/modules/radarr/core/state/global.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrState extends LunaModuleState { - @override - void reset() {} - - ///Catalogue Sticky Header Content - - String _searchCatalogueFilter = ''; - String get searchCatalogueFilter => _searchCatalogueFilter; - set searchCatalogueFilter(String searchCatalogueFilter) { - assert(searchCatalogueFilter != null); - _searchCatalogueFilter = searchCatalogueFilter; - notifyListeners(); - } - - - RadarrCatalogueSorting _sortCatalogueType = RadarrCatalogueSorting.alphabetical; - RadarrCatalogueSorting get sortCatalogueType => _sortCatalogueType; - set sortCatalogueType(RadarrCatalogueSorting sortCatalogueType) { - assert(sortCatalogueType != null); - _sortCatalogueType = sortCatalogueType; - notifyListeners(); - } - - bool _sortCatalogueAscending = true; - bool get sortCatalogueAscending => _sortCatalogueAscending; - set sortCatalogueAscending(bool sortCatalogueAscending) { - assert(sortCatalogueAscending != null); - _sortCatalogueAscending = sortCatalogueAscending; - notifyListeners(); - } - - bool _hideUnmonitoredMovies = false; - bool get hideUnmonitoredMovies => _hideUnmonitoredMovies; - set hideUnmonitoredMovies(bool hideUnmonitoredMovies) { - assert(hideUnmonitoredMovies != null); - _hideUnmonitoredMovies = hideUnmonitoredMovies; - notifyListeners(); - } - - ///Releases Sticky Header Content - - String _searchReleasesFilter = ''; - String get searchReleasesFilter => _searchReleasesFilter; - set searchReleasesFilter(String searchReleasesFilter) { - assert(searchReleasesFilter != null); - _searchReleasesFilter = searchReleasesFilter; - notifyListeners(); - } - - RadarrReleasesSorting _sortReleasesType = RadarrReleasesSorting.weight; - RadarrReleasesSorting get sortReleasesType => _sortReleasesType; - set sortReleasesType(RadarrReleasesSorting sortReleasesType) { - assert(sortReleasesType != null); - _sortReleasesType = sortReleasesType; - notifyListeners(); - } - - bool _sortReleasesAscending = true; - bool get sortReleasesAscending => _sortReleasesAscending; - set sortReleasesAscending(bool sortReleasesAscending) { - assert(sortReleasesAscending != null); - _sortReleasesAscending = sortReleasesAscending; - notifyListeners(); - } - - bool _hideRejectedReleases = false; - bool get hideRejectedReleases => _hideRejectedReleases; - set hideRejectedReleases(bool hideRejectedReleases) { - assert(hideRejectedReleases != null); - _hideRejectedReleases = hideRejectedReleases; - notifyListeners(); - } - - ///Add New Movie Content - - String _addSearchQuery = ''; - String get addSearchQuery => _addSearchQuery; - set addSearchQuery(String addSearchQuery) { - assert(addSearchQuery != null); - _addSearchQuery = addSearchQuery; - notifyListeners(); - } - - ///Delete options - - bool _removeDeleteFiles = false; - bool get removeDeleteFiles => _removeDeleteFiles; - set removeDeleteFiles(bool removeDeleteFiles) { - assert(removeDeleteFiles != null); - _removeDeleteFiles = removeDeleteFiles; - notifyListeners(); - } - - bool _removeAddExclusion = false; - bool get removeAddExclusion => _removeAddExclusion; - set removeAddExclusion(bool removeAddExclusion) { - assert(removeAddExclusion != null); - _removeAddExclusion = removeAddExclusion; - notifyListeners(); - } - - ///Navigation Indexes - - int _navigationIndex = 0; - int get navigationIndex => _navigationIndex; - set navigationIndex(int navigationIndex) { - assert(navigationIndex != null); - _navigationIndex = navigationIndex; - notifyListeners(); - } - - int _movieNavigationIndex = 0; - int get movieNavigationIndex => _movieNavigationIndex; - set movieNavigationIndex(int movieNavigationIndex) { - assert(movieNavigationIndex != null); - _movieNavigationIndex = movieNavigationIndex; - notifyListeners(); - } -} diff --git a/lib/modules/radarr/core/state/local.dart b/lib/modules/radarr/core/state/local.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/modules/radarr/core/types.dart b/lib/modules/radarr/core/types.dart new file mode 100644 index 00000000..2896516c --- /dev/null +++ b/lib/modules/radarr/core/types.dart @@ -0,0 +1,6 @@ +export 'types/filter_movies.dart'; +export 'types/filter_releases.dart'; +export 'types/settings_global.dart'; +export 'types/settings_movie.dart'; +export 'types/sorting_movies.dart'; +export 'types/sorting_releases.dart'; diff --git a/lib/modules/radarr/core/types/filter_movies.dart b/lib/modules/radarr/core/types/filter_movies.dart new file mode 100644 index 00000000..71000abc --- /dev/null +++ b/lib/modules/radarr/core/types/filter_movies.dart @@ -0,0 +1,83 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +part 'filter_movies.g.dart'; + +@HiveType(typeId: 19, adapterName: 'RadarrMoviesFilterAdapter') +enum RadarrMoviesFilter { + @HiveField(0) + ALL, + @HiveField(1) + MONITORED, + @HiveField(2) + UNMONITORED, + @HiveField(3) + MISSING, + @HiveField(4) + WANTED, + @HiveField(5) + CUTOFF_UNMET, +} + +extension RadarrMoviesFilterExtension on RadarrMoviesFilter { + String get key { + switch(this) { + case RadarrMoviesFilter.ALL: return 'all'; + case RadarrMoviesFilter.MONITORED: return 'monitored'; + case RadarrMoviesFilter.UNMONITORED: return 'unmonitored'; + case RadarrMoviesFilter.MISSING: return 'missing'; + case RadarrMoviesFilter.WANTED: return 'wanted'; + case RadarrMoviesFilter.CUTOFF_UNMET: return 'cutoffunmet'; + default: return null; + } + } + + String get readable { + switch(this) { + case RadarrMoviesFilter.ALL: return 'All'; + case RadarrMoviesFilter.MONITORED: return 'Monitored'; + case RadarrMoviesFilter.UNMONITORED: return 'Unmonitored'; + case RadarrMoviesFilter.MISSING: return 'Missing'; + case RadarrMoviesFilter.WANTED: return 'Wanted'; + case RadarrMoviesFilter.CUTOFF_UNMET: return 'Cutoff Unmet'; + default: return null; + } + } + + RadarrMoviesFilter fromKey(String key) { + switch(key) { + case 'all': return RadarrMoviesFilter.ALL; + case 'monitored': return RadarrMoviesFilter.MONITORED; + case 'unmonitored': return RadarrMoviesFilter.UNMONITORED; + case 'missing': return RadarrMoviesFilter.MISSING; + case 'wanted': return RadarrMoviesFilter.WANTED; + case 'cutoffunmet': return RadarrMoviesFilter.CUTOFF_UNMET; + default: return null; + } + } + + List filter(List series) => _Filterer().byType(series, this); +} + +class _Filterer { + List byType( + List movies, + RadarrMoviesFilter type, + ) { + switch(type) { + case RadarrMoviesFilter.ALL: return movies; + case RadarrMoviesFilter.MONITORED: return _monitored(movies); + case RadarrMoviesFilter.UNMONITORED: return _unmonitored(movies); + case RadarrMoviesFilter.MISSING: return _missing(movies); + case RadarrMoviesFilter.WANTED: return _wanted(movies); + case RadarrMoviesFilter.CUTOFF_UNMET: return _cutoffUnmet(movies); + } + throw Exception('sorting type not found'); + } + + List _monitored(List movies) => movies.where((movie) => movie.monitored).toList(); + List _unmonitored(List movies) => movies.where((movie) => !movie.monitored).toList(); + List _missing(List movies) => movies.where((movie) => !movie.hasFile).toList(); + List _wanted(List movies) => movies.where((movie) => !movie.hasFile || movie.movieFile.qualityCutoffNotMet).toList(); + List _cutoffUnmet(List movies) => movies.where((movie) => movie.hasFile && movie.movieFile.qualityCutoffNotMet).toList(); +} diff --git a/lib/modules/radarr/core/types/filter_movies.g.dart b/lib/modules/radarr/core/types/filter_movies.g.dart new file mode 100644 index 00000000..ad48623b --- /dev/null +++ b/lib/modules/radarr/core/types/filter_movies.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'filter_movies.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class RadarrMoviesFilterAdapter extends TypeAdapter { + @override + final int typeId = 19; + + @override + RadarrMoviesFilter read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return RadarrMoviesFilter.ALL; + case 1: + return RadarrMoviesFilter.MONITORED; + case 2: + return RadarrMoviesFilter.UNMONITORED; + case 3: + return RadarrMoviesFilter.MISSING; + case 4: + return RadarrMoviesFilter.WANTED; + case 5: + return RadarrMoviesFilter.CUTOFF_UNMET; + default: + return null; + } + } + + @override + void write(BinaryWriter writer, RadarrMoviesFilter obj) { + switch (obj) { + case RadarrMoviesFilter.ALL: + writer.writeByte(0); + break; + case RadarrMoviesFilter.MONITORED: + writer.writeByte(1); + break; + case RadarrMoviesFilter.UNMONITORED: + writer.writeByte(2); + break; + case RadarrMoviesFilter.MISSING: + writer.writeByte(3); + break; + case RadarrMoviesFilter.WANTED: + writer.writeByte(4); + break; + case RadarrMoviesFilter.CUTOFF_UNMET: + writer.writeByte(5); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RadarrMoviesFilterAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/modules/radarr/core/types/filter_releases.dart b/lib/modules/radarr/core/types/filter_releases.dart new file mode 100644 index 00000000..6fb6799a --- /dev/null +++ b/lib/modules/radarr/core/types/filter_releases.dart @@ -0,0 +1,62 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +part 'filter_releases.g.dart'; + +@HiveType(typeId: 20, adapterName: 'RadarrReleasesFilterAdapter') +enum RadarrReleasesFilter { + @HiveField(0) + ALL, + @HiveField(1) + APPROVED, + @HiveField(2) + REJECTED, +} + +extension RadarrReleasesFilterExtension on RadarrReleasesFilter { + String get key { + switch(this) { + case RadarrReleasesFilter.ALL: return 'all'; + case RadarrReleasesFilter.APPROVED: return 'approved'; + case RadarrReleasesFilter.REJECTED: return 'rejected'; + default: return null; + } + } + + String get readable { + switch(this) { + case RadarrReleasesFilter.ALL: return 'All'; + case RadarrReleasesFilter.APPROVED: return 'Approved'; + case RadarrReleasesFilter.REJECTED: return 'Rejected'; + default: return null; + } + } + + RadarrReleasesFilter fromKey(String key) { + switch(key) { + case 'all': return RadarrReleasesFilter.ALL; + case 'approved': return RadarrReleasesFilter.APPROVED; + case 'rejected': return RadarrReleasesFilter.REJECTED; + default: return null; + } + } + + List filter(List releases) => _Filterer().byType(releases, this); +} + +class _Filterer { + List byType( + List releases, + RadarrReleasesFilter type, + ) { + switch(type) { + case RadarrReleasesFilter.ALL: return releases; + case RadarrReleasesFilter.APPROVED: return _approved(releases); + case RadarrReleasesFilter.REJECTED: return _rejected(releases); + } + throw Exception('sorting type not found'); + } + + List _approved(List releases) => releases.where((element) => element.approved).toList(); + List _rejected(List releases) => releases.where((element) => !element.approved).toList(); +} diff --git a/lib/modules/radarr/core/types/filter_releases.g.dart b/lib/modules/radarr/core/types/filter_releases.g.dart new file mode 100644 index 00000000..13a70097 --- /dev/null +++ b/lib/modules/radarr/core/types/filter_releases.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'filter_releases.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class RadarrReleasesFilterAdapter extends TypeAdapter { + @override + final int typeId = 20; + + @override + RadarrReleasesFilter read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return RadarrReleasesFilter.ALL; + case 1: + return RadarrReleasesFilter.APPROVED; + case 2: + return RadarrReleasesFilter.REJECTED; + default: + return null; + } + } + + @override + void write(BinaryWriter writer, RadarrReleasesFilter obj) { + switch (obj) { + case RadarrReleasesFilter.ALL: + writer.writeByte(0); + break; + case RadarrReleasesFilter.APPROVED: + writer.writeByte(1); + break; + case RadarrReleasesFilter.REJECTED: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RadarrReleasesFilterAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/modules/radarr/core/types/settings_global.dart b/lib/modules/radarr/core/types/settings_global.dart new file mode 100644 index 00000000..3c05b8ea --- /dev/null +++ b/lib/modules/radarr/core/types/settings_global.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +enum RadarrGlobalSettingsType { + WEB_GUI, + RUN_RSS_SYNC, + SEARCH_ALL_MISSING, + UPDATE_LIBRARY, + BACKUP_DATABASE, +} + +extension RadarrGlobalSettingsTypeExtension on RadarrGlobalSettingsType { + IconData get icon { + switch(this) { + case RadarrGlobalSettingsType.WEB_GUI: return Icons.language; + case RadarrGlobalSettingsType.UPDATE_LIBRARY: return Icons.autorenew; + case RadarrGlobalSettingsType.RUN_RSS_SYNC: return Icons.rss_feed; + case RadarrGlobalSettingsType.SEARCH_ALL_MISSING: return Icons.search; + case RadarrGlobalSettingsType.BACKUP_DATABASE: return Icons.save; + } + throw Exception('Invalid RadarrGlobalSettingsType'); + } + + String get name { + switch(this) { + case RadarrGlobalSettingsType.WEB_GUI: return 'View Web GUI'; + case RadarrGlobalSettingsType.UPDATE_LIBRARY: return 'Update Library'; + case RadarrGlobalSettingsType.RUN_RSS_SYNC: return 'Run RSS Sync'; + case RadarrGlobalSettingsType.SEARCH_ALL_MISSING: return 'Search All Missing'; + case RadarrGlobalSettingsType.BACKUP_DATABASE: return 'Backup Database'; + } + throw Exception('Invalid RadarrGlobalSettingsType'); + } + + Future execute(BuildContext context) async { + switch(this) { + case RadarrGlobalSettingsType.WEB_GUI: return _webGUI(context); + case RadarrGlobalSettingsType.RUN_RSS_SYNC: return _runRssSync(context); + case RadarrGlobalSettingsType.SEARCH_ALL_MISSING: return _searchAllMissing(context); + case RadarrGlobalSettingsType.UPDATE_LIBRARY: return _updateLibrary(context); + case RadarrGlobalSettingsType.BACKUP_DATABASE: return _backupDatabase(context); + } + throw Exception('Invalid RadarrGlobalSettingsType'); + } + + Future _webGUI(BuildContext context) async => Provider.of(context, listen: false).host.lunaOpenGenericLink(); + Future _backupDatabase(BuildContext context) async => RadarrAPIHelper().backupDatabase(context: context); + Future _searchAllMissing(BuildContext context) async { + bool result = await RadarrDialogs().searchAllMissingMovies(context); + if(result) RadarrAPIHelper().missingMovieSearch(context: context); + } + Future _runRssSync(BuildContext context) async => RadarrAPIHelper().runRSSSync(context: context); + Future _updateLibrary(BuildContext context) async => RadarrAPIHelper().updateLibrary(context: context); +} diff --git a/lib/modules/radarr/core/types/settings_movie.dart b/lib/modules/radarr/core/types/settings_movie.dart new file mode 100644 index 00000000..0266f369 --- /dev/null +++ b/lib/modules/radarr/core/types/settings_movie.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +enum RadarrMovieSettingsType { + EDIT, + REFRESH, + DELETE, + MONITORED, +} + +extension RadarrMovieSettingsTypeExtension on RadarrMovieSettingsType { + IconData icon(RadarrMovie movie) { + switch(this) { + case RadarrMovieSettingsType.MONITORED: return movie.monitored ? Icons.turned_in_not : Icons.turned_in; + case RadarrMovieSettingsType.EDIT: return Icons.edit; + case RadarrMovieSettingsType.REFRESH: return Icons.refresh; + case RadarrMovieSettingsType.DELETE: return Icons.delete; + } + throw Exception('Invalid RadarrMovieSettingsType'); + } + + String name(RadarrMovie movie) { + switch(this) { + case RadarrMovieSettingsType.MONITORED: return movie.monitored ? 'Unmonitor Movie' : 'Monitor Movie'; + case RadarrMovieSettingsType.EDIT: return 'Edit Movie'; + case RadarrMovieSettingsType.REFRESH: return 'Refresh Movie'; + case RadarrMovieSettingsType.DELETE: return 'Remove Movie'; + } + throw Exception('Invalid RadarrMovieSettingsType'); + } + + Future execute(BuildContext context, RadarrMovie movie) async { + switch(this) { + case RadarrMovieSettingsType.EDIT: return _edit(context, movie); + case RadarrMovieSettingsType.REFRESH: return _refresh(context, movie); + case RadarrMovieSettingsType.DELETE: return _delete(context, movie); + case RadarrMovieSettingsType.MONITORED: return _monitored(context, movie); + } + throw Exception('Invalid RadarrMovieSettingsType'); + } + + Future _edit(BuildContext context, RadarrMovie movie) async => RadarrMoviesEditRouter().navigateTo(context, movieId: movie.id); + Future _monitored(BuildContext context, RadarrMovie movie) => RadarrAPIHelper().toggleMonitored(context: context, movie: movie); + Future _refresh(BuildContext context, RadarrMovie movie) async => RadarrAPIHelper().refreshMovie(context: context, movie: movie); + Future _delete(BuildContext context, RadarrMovie movie) async { + bool result = await RadarrDialogs().removeMovie(context); + if(result) { + RadarrAPIHelper().removeMovie(context: context, movie: movie) + .then((_) => context.read().fetchMovies()); + Navigator.of(context).lunaSafetyPop(); + } + } +} diff --git a/lib/modules/radarr/core/types/sorting_movies.dart b/lib/modules/radarr/core/types/sorting_movies.dart new file mode 100644 index 00000000..31a6a622 --- /dev/null +++ b/lib/modules/radarr/core/types/sorting_movies.dart @@ -0,0 +1,204 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +part 'sorting_movies.g.dart'; + +@HiveType(typeId: 18, adapterName: 'RadarrMoviesSortingAdapter') +enum RadarrMoviesSorting { + @HiveField(0) + ALPHABETICAL, + @HiveField(1) + DATE_ADDED, + @HiveField(7) + MIN_AVAILABILITY, + @HiveField(2) + QUALITY_PROFILE, + @HiveField(3) + RUNTIME, + @HiveField(4) + SIZE, + @HiveField(5) + STUDIO, + @HiveField(6) + YEAR, +} + +extension RadarrMoviesSortingExtension on RadarrMoviesSorting { + String get key { + switch(this) { + case RadarrMoviesSorting.ALPHABETICAL: return 'abc'; + case RadarrMoviesSorting.DATE_ADDED: return 'dateadded'; + case RadarrMoviesSorting.QUALITY_PROFILE: return 'qualityprofile'; + case RadarrMoviesSorting.RUNTIME: return 'runtime'; + case RadarrMoviesSorting.SIZE: return 'size'; + case RadarrMoviesSorting.STUDIO: return 'studio'; + case RadarrMoviesSorting.YEAR: return 'year'; + case RadarrMoviesSorting.MIN_AVAILABILITY: return 'minavailability'; + default: return null; + } + } + + String get readable { + switch(this) { + case RadarrMoviesSorting.ALPHABETICAL: return 'Alphabetical'; + case RadarrMoviesSorting.DATE_ADDED: return 'Date Added'; + case RadarrMoviesSorting.QUALITY_PROFILE: return 'Quality Profile'; + case RadarrMoviesSorting.RUNTIME: return 'Runtime'; + case RadarrMoviesSorting.SIZE: return 'Size'; + case RadarrMoviesSorting.STUDIO: return 'Studio'; + case RadarrMoviesSorting.YEAR: return 'Year'; + case RadarrMoviesSorting.MIN_AVAILABILITY: return 'Minimum Availability'; + default: return null; + } + } + + RadarrMoviesSorting fromKey(String key) { + switch(key) { + case 'abc': return RadarrMoviesSorting.ALPHABETICAL; + case'dateadded': return RadarrMoviesSorting.DATE_ADDED; + case 'qualityprofile': return RadarrMoviesSorting.QUALITY_PROFILE; + case 'runtime': return RadarrMoviesSorting.RUNTIME; + case 'size': return RadarrMoviesSorting.SIZE; + case 'studio': return RadarrMoviesSorting.STUDIO; + case 'year': return RadarrMoviesSorting.YEAR; + case 'minavailability': return RadarrMoviesSorting.MIN_AVAILABILITY; + default: return null; + } + } + + List sort(List data, bool ascending) => _Sorter().byType(data, this, ascending); +} + +class _Sorter { + List byType(List data, RadarrMoviesSorting type, bool ascending) { + switch(type) { + case RadarrMoviesSorting.DATE_ADDED: return _dateAdded(data, ascending); + case RadarrMoviesSorting.QUALITY_PROFILE: return _qualityProfile(data, ascending); + case RadarrMoviesSorting.RUNTIME: return _runtime(data, ascending); + case RadarrMoviesSorting.SIZE: return _size(data, ascending); + case RadarrMoviesSorting.STUDIO: return _studio(data, ascending); + case RadarrMoviesSorting.YEAR: return _year(data, ascending); + case RadarrMoviesSorting.MIN_AVAILABILITY: return _minimumAvailability(data, ascending); + case RadarrMoviesSorting.ALPHABETICAL: return _alphabetical(data, ascending); + } + throw Exception('sorting type not found'); + } + + List _alphabetical(List series, bool ascending) { + ascending + ? series.sort((a,b) => a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase())) + : series.sort((a,b) => b.sortTitle.toLowerCase().compareTo(a.sortTitle.toLowerCase())); + return series; + } + + List _dateAdded(List movies, bool ascending) { + movies.sort((a,b) { + int _comparison; + if(a.added == null) _comparison = 1; + if(b.added == null) _comparison = -1; + if(a.added == null && b.added == null) _comparison = 0; + if(a.added != null && b.added != null) + _comparison = ascending ? (a.added).compareTo(b.added) : (b.added).compareTo(a.added); + return _comparison == 0 + ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) + : _comparison; + }); + return movies; + } + + List _minimumAvailability(List movies, bool ascending) { + movies.sort((a,b) { + int _comparison; + if(a.minimumAvailability == null)_comparison = 1; + if(b.minimumAvailability == null) _comparison = -1; + if(a.minimumAvailability == null && b.minimumAvailability == null) _comparison = 0; + if(a.minimumAvailability != null && b.minimumAvailability != null) _comparison = ascending + ? (a.minimumAvailability.value).compareTo(b.minimumAvailability.value) + : (b.minimumAvailability.value).compareTo(a.minimumAvailability.value); + return _comparison == 0 + ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) + : _comparison; + }); + return movies; + } + + List _runtime(List movies, bool ascending) { + movies.sort((a,b) { + int _comparison; + if(a.runtime == null || a.runtime == 0)_comparison = 1; + if(b.runtime == null || b.runtime == 0) _comparison = -1; + if((a.runtime == null || a.runtime == 0) && (b.runtime == null || b.runtime == 0)) _comparison = 0; + if(a.runtime != null && b.runtime != null && a.runtime != 0 && b.runtime != 0) _comparison = ascending + ? (a.runtime).compareTo(b.runtime) + : (b.runtime).compareTo(a.runtime); + return _comparison == 0 + ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) + : _comparison; + }); + return movies; + } + + List _qualityProfile(List movies, bool ascending) { + movies.sort((a,b) { + int _comparison; + if(a.qualityProfileId == null || a.qualityProfileId == 0)_comparison = 1; + if(b.qualityProfileId == null || b.qualityProfileId == 0) _comparison = -1; + if((a.qualityProfileId == null || a.qualityProfileId == 0) && (b.qualityProfileId == null || b.qualityProfileId == 0)) _comparison = 0; + if(a.qualityProfileId != null && b.qualityProfileId != null && a.qualityProfileId != 0 && b.qualityProfileId != 0) _comparison = ascending + ? (a.qualityProfileId).compareTo(b.qualityProfileId) + : (b.qualityProfileId).compareTo(a.qualityProfileId); + return _comparison == 0 + ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) + : _comparison; + }); + return movies; + } + + List _size(List movies, bool ascending) { + movies.sort((a,b) { + int _comparison; + if(a.sizeOnDisk == null || a.sizeOnDisk == 0)_comparison = 1; + if(b.sizeOnDisk == null || b.sizeOnDisk == 0) _comparison = -1; + if((a.sizeOnDisk == null || a.sizeOnDisk == 0) && (b.sizeOnDisk == null || b.sizeOnDisk == 0)) _comparison = 0; + if(a.sizeOnDisk != null && b.sizeOnDisk != null && a.sizeOnDisk != 0 && b.sizeOnDisk != 0) _comparison = ascending + ? (a.sizeOnDisk).compareTo(b.sizeOnDisk) + : (b.sizeOnDisk).compareTo(a.sizeOnDisk); + return _comparison == 0 + ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) + : _comparison; + }); + return movies; + } + + List _studio(List movies, bool ascending) { + movies.sort((a,b) { + int _comparison; + if(a.studio == null || a.studio.isEmpty)_comparison = 1; + if(b.studio == null || b.studio.isEmpty) _comparison = -1; + if((a.studio == null || a.studio.isEmpty) && (b.studio == null || b.studio.isEmpty)) _comparison = 0; + if(a.studio != null && b.studio != null && a.studio.isNotEmpty && b.studio.isNotEmpty) _comparison = ascending + ? (a.studio.toLowerCase()).compareTo(b.studio.toLowerCase()) + : (b.studio.toLowerCase()).compareTo(a.studio.toLowerCase()); + return _comparison == 0 + ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) + : _comparison; + }); + return movies; + } + + List _year(List movies, bool ascending) { + movies.sort((a,b) { + int _comparison; + if(a.year == null || a.year == 0)_comparison = 1; + if(b.year == null || b.year == 0) _comparison = -1; + if((a.year == null || a.year == 0) && (b.year == null || b.year == 0)) _comparison = 0; + if(a.year != null && b.year != null && a.year != 0 && b.year != 0) _comparison = ascending + ? (a.year).compareTo(b.year) + : (b.year).compareTo(a.year); + return _comparison == 0 + ? a.sortTitle.toLowerCase().compareTo(b.sortTitle.toLowerCase()) + : _comparison; + }); + return movies; + } +} diff --git a/lib/modules/radarr/core/types/sorting_movies.g.dart b/lib/modules/radarr/core/types/sorting_movies.g.dart new file mode 100644 index 00000000..d29339d8 --- /dev/null +++ b/lib/modules/radarr/core/types/sorting_movies.g.dart @@ -0,0 +1,76 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sorting_movies.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class RadarrMoviesSortingAdapter extends TypeAdapter { + @override + final int typeId = 18; + + @override + RadarrMoviesSorting read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return RadarrMoviesSorting.ALPHABETICAL; + case 1: + return RadarrMoviesSorting.DATE_ADDED; + case 7: + return RadarrMoviesSorting.MIN_AVAILABILITY; + case 2: + return RadarrMoviesSorting.QUALITY_PROFILE; + case 3: + return RadarrMoviesSorting.RUNTIME; + case 4: + return RadarrMoviesSorting.SIZE; + case 5: + return RadarrMoviesSorting.STUDIO; + case 6: + return RadarrMoviesSorting.YEAR; + default: + return null; + } + } + + @override + void write(BinaryWriter writer, RadarrMoviesSorting obj) { + switch (obj) { + case RadarrMoviesSorting.ALPHABETICAL: + writer.writeByte(0); + break; + case RadarrMoviesSorting.DATE_ADDED: + writer.writeByte(1); + break; + case RadarrMoviesSorting.MIN_AVAILABILITY: + writer.writeByte(7); + break; + case RadarrMoviesSorting.QUALITY_PROFILE: + writer.writeByte(2); + break; + case RadarrMoviesSorting.RUNTIME: + writer.writeByte(3); + break; + case RadarrMoviesSorting.SIZE: + writer.writeByte(4); + break; + case RadarrMoviesSorting.STUDIO: + writer.writeByte(5); + break; + case RadarrMoviesSorting.YEAR: + writer.writeByte(6); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RadarrMoviesSortingAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/modules/radarr/core/types/sorting_releases.dart b/lib/modules/radarr/core/types/sorting_releases.dart new file mode 100644 index 00000000..93a59f66 --- /dev/null +++ b/lib/modules/radarr/core/types/sorting_releases.dart @@ -0,0 +1,123 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +part 'sorting_releases.g.dart'; + +@HiveType(typeId: 21, adapterName: 'RadarrReleasesSortingAdapter') +enum RadarrReleasesSorting { + @HiveField(0) + AGE, + @HiveField(1) + ALPHABETICAL, + @HiveField(2) + SEEDERS, + @HiveField(3) + SIZE, + @HiveField(4) + TYPE, + @HiveField(5) + WEIGHT, +} + +extension RadarrReleasesSortingExtension on RadarrReleasesSorting { + String get key { + switch(this) { + case RadarrReleasesSorting.AGE: return 'age'; + case RadarrReleasesSorting.ALPHABETICAL: return 'abc'; + case RadarrReleasesSorting.SEEDERS: return 'seeders'; + case RadarrReleasesSorting.WEIGHT: return 'weight'; + case RadarrReleasesSorting.TYPE: return 'type'; + case RadarrReleasesSorting.SIZE: return 'size'; + default: return null; + } + } + + String get readable { + switch(this) { + case RadarrReleasesSorting.AGE: return 'Age'; + case RadarrReleasesSorting.ALPHABETICAL: return 'Alphabetical'; + case RadarrReleasesSorting.SEEDERS: return 'Seeders'; + case RadarrReleasesSorting.WEIGHT: return 'Weight'; + case RadarrReleasesSorting.TYPE: return 'Type'; + case RadarrReleasesSorting.SIZE: return 'Size'; + default: return null; + } + } + + RadarrReleasesSorting fromKey(String key) { + switch(key) { + case 'age': return RadarrReleasesSorting.AGE; + case 'abc': return RadarrReleasesSorting.ALPHABETICAL; + case 'seeders': return RadarrReleasesSorting.SEEDERS; + case 'size': return RadarrReleasesSorting.SIZE; + case 'type': return RadarrReleasesSorting.TYPE; + case 'weight': return RadarrReleasesSorting.WEIGHT; + default: return null; + } + } + + List sort(List releases, bool ascending) => _Sorter().byType(releases, this, ascending); +} + +class _Sorter { + List byType( + List releases, + RadarrReleasesSorting type, + bool ascending, + ) { + switch(type) { + case RadarrReleasesSorting.AGE: return _age(releases, ascending); + case RadarrReleasesSorting.ALPHABETICAL: return _alphabetical(releases, ascending); + case RadarrReleasesSorting.SEEDERS: return _seeders(releases, ascending); + case RadarrReleasesSorting.WEIGHT: return _weight(releases, ascending); + case RadarrReleasesSorting.TYPE: return _type(releases, ascending); + case RadarrReleasesSorting.SIZE: return _size(releases, ascending); + } + throw Exception('sorting type not found'); + } + + List _alphabetical(List releases, bool ascending) { + ascending + ? releases.sort((a,b) => a.title.toLowerCase().compareTo(b.title.toLowerCase())) + : releases.sort((a,b) => b.title.toLowerCase().compareTo(a.title.toLowerCase())); + return releases; + } + + List _age(List releases, bool ascending) { + ascending + ? releases.sort((a,b) => a.ageHours.compareTo(b.ageHours)) + : releases.sort((a,b) => b.ageHours.compareTo(a.ageHours)); + return releases; + } + + List _seeders(List releases, bool ascending) { + List _torrent = _weight(releases.where((release) => release.protocol == RadarrProtocol.TORRENT).toList(), true); + List _usenet = _weight(releases.where((release) => release.protocol == RadarrProtocol.USENET).toList(), true); + ascending + ? _torrent.sort((a,b) => (a.seeders ?? -1).compareTo((b.seeders ?? -1))) + : _torrent.sort((a,b) => (b.seeders ?? -1).compareTo((a.seeders ?? -1))); + return [..._torrent, ..._usenet]; + } + + List _weight(List releases, bool ascending) { + ascending + ? releases.sort((a,b) => (a.releaseWeight ?? -1).compareTo((b.releaseWeight ?? -1))) + : releases.sort((a,b) => (b.releaseWeight ?? -1).compareTo((a.releaseWeight ?? -1))); + return releases; + } + + List _type(List releases, bool ascending) { + List _torrent = _weight(releases.where((release) => release.protocol == RadarrProtocol.TORRENT).toList(), true); + List _usenet = _weight(releases.where((release) => release.protocol == RadarrProtocol.USENET).toList(), true); + return ascending + ? [..._torrent, ..._usenet] + : [..._usenet, ..._torrent]; + } + + List _size(List releases, bool ascending) { + ascending + ? releases.sort((a,b) => (a.size ?? -1).compareTo((b.size ?? -1))) + : releases.sort((a,b) => (b.size ?? -1).compareTo((a.size ?? -1))); + return releases; + } +} diff --git a/lib/modules/radarr/core/types/sorting_releases.g.dart b/lib/modules/radarr/core/types/sorting_releases.g.dart new file mode 100644 index 00000000..8228834d --- /dev/null +++ b/lib/modules/radarr/core/types/sorting_releases.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sorting_releases.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class RadarrReleasesSortingAdapter extends TypeAdapter { + @override + final int typeId = 21; + + @override + RadarrReleasesSorting read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return RadarrReleasesSorting.AGE; + case 1: + return RadarrReleasesSorting.ALPHABETICAL; + case 2: + return RadarrReleasesSorting.SEEDERS; + case 3: + return RadarrReleasesSorting.SIZE; + case 4: + return RadarrReleasesSorting.TYPE; + case 5: + return RadarrReleasesSorting.WEIGHT; + default: + return null; + } + } + + @override + void write(BinaryWriter writer, RadarrReleasesSorting obj) { + switch (obj) { + case RadarrReleasesSorting.AGE: + writer.writeByte(0); + break; + case RadarrReleasesSorting.ALPHABETICAL: + writer.writeByte(1); + break; + case RadarrReleasesSorting.SEEDERS: + writer.writeByte(2); + break; + case RadarrReleasesSorting.SIZE: + writer.writeByte(3); + break; + case RadarrReleasesSorting.TYPE: + writer.writeByte(4); + break; + case RadarrReleasesSorting.WEIGHT: + writer.writeByte(5); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RadarrReleasesSortingAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/modules/radarr/modules.dart b/lib/modules/radarr/modules.dart new file mode 100644 index 00000000..255b281f --- /dev/null +++ b/lib/modules/radarr/modules.dart @@ -0,0 +1,15 @@ +export 'modules/add_movie.dart'; +export 'modules/add_movie_details.dart'; +export 'modules/catalogue.dart'; +export 'modules/edit_movie.dart'; +export 'modules/health_check.dart'; +export 'modules/history.dart'; +export 'modules/missing.dart'; +export 'modules/more.dart'; +export 'modules/movie_details.dart'; +export 'modules/queue.dart'; +export 'modules/radarr.dart'; +export 'modules/releases.dart'; +export 'modules/system_status.dart'; +export 'modules/tags.dart'; +export 'modules/upcoming.dart'; diff --git a/lib/modules/radarr/modules/add_movie.dart b/lib/modules/radarr/modules/add_movie.dart new file mode 100644 index 00000000..736405f6 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie.dart @@ -0,0 +1,3 @@ +export 'add_movie/route.dart'; +export 'add_movie/state.dart'; +export 'add_movie/widgets.dart'; diff --git a/lib/modules/radarr/modules/add_movie/route.dart b/lib/modules/radarr/modules/add_movie/route.dart new file mode 100644 index 00000000..f54ed8e1 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/route.dart @@ -0,0 +1,60 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieRouter extends LunaPageRouter { + RadarrAddMovieRouter() : super('/radarr/addmovie'); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrAddMovieRoute()); +} + +class _RadarrAddMovieRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State<_RadarrAddMovieRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + LunaPageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = LunaPageController(initialPage: RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE.data); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), + ); + } + + Widget _bottomNavigationBar() => RadarrAddMovieNavigationBar(pageController: _pageController); + + Widget _appBar() { + return LunaAppBar( + title: 'Add Movie', + pageController: _pageController, + scrollControllers: RadarrAddMovieNavigationBar.scrollControllers, + ); + } + + Widget _body() { + return ChangeNotifierProvider( + create: (context) => RadarrAddMovieState(context), + builder: (context, _) => PageView( + controller: _pageController, + children: [ + RadarrAddMovieSearchPage(), + RadarrAddMovieDiscoverPage(), + ], + ), + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie/state.dart b/lib/modules/radarr/modules/add_movie/state.dart new file mode 100644 index 00000000..854e5d28 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/state.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieState extends ChangeNotifier { + RadarrAddMovieState(BuildContext context) { + context.read().lookup = null; + } + + String _searchQuery = ''; + String get searchQuery => _searchQuery; + set searchQuery(String searchQuery) { + assert(searchQuery != null); + _searchQuery = searchQuery; + notifyListeners(); + } + + Future> exclusions; + + void executeSearch(BuildContext context) { + if((context?.read()?.enabled ?? false)) { + context.read().fetchLookup(_searchQuery ?? ''); + exclusions = context.read().api.exclusions.getAll(); + } + notifyListeners(); + } +} diff --git a/lib/modules/radarr/modules/add_movie/widgets.dart b/lib/modules/radarr/modules/add_movie/widgets.dart new file mode 100644 index 00000000..28bcf8cd --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/widgets.dart @@ -0,0 +1,5 @@ +export 'widgets/navigation_bar.dart'; +export 'widgets/page_discover.dart'; +export 'widgets/page_search.dart'; +export 'widgets/search_results_tile.dart'; +export 'widgets/search_search_bar.dart'; diff --git a/lib/modules/radarr/modules/add_movie/widgets/navigation_bar.dart b/lib/modules/radarr/modules/add_movie/widgets/navigation_bar.dart new file mode 100644 index 00000000..ce3e63ba --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/widgets/navigation_bar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class RadarrAddMovieNavigationBar extends StatelessWidget { + final PageController pageController; + static const List icons = [Icons.search, Icons.whatshot]; + static const List titles = ['Search', 'Discover']; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + + RadarrAddMovieNavigationBar({ + Key key, + @required this.pageController, + }): super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie/widgets/page_discover.dart b/lib/modules/radarr/modules/add_movie/widgets/page_discover.dart new file mode 100644 index 00000000..cd52aa22 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/widgets/page_discover.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class RadarrAddMovieDiscoverPage extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Future loadCallback() async {} + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: LunaMessage(text: 'Coming Soon'), + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie/widgets/page_search.dart b/lib/modules/radarr/modules/add_movie/widgets/page_search.dart new file mode 100644 index 00000000..dc8e789a --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/widgets/page_search.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieSearchPage extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Future loadCallback() async { + if(context.read().searchQuery.isNotEmpty) { + context.read().executeSearch(context); + await context.read()?.lookup; + await context.read()?.exclusions; + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar.empty( + child: RadarrAddMovieSearchSearchBar(scrollController: context.watch().scrollController), + height: 62.0, + ); + } + + Widget _body() { + return Selector>>( + selector: (_, state) => state.movies, + builder: (context, movies, _) => Selector>>( + selector: (_, state) => state.exclusions, + builder: (context, exclusions, _) => Selector>>( + selector: (_, state) => state.lookup, + builder: (context, lookup, _) { + if(lookup == null) return Container(); + return _builder(movies, lookup, exclusions); + }, + ), + ), + ); + } + + Widget _builder(Future> movies, Future> lookup, Future> exclusions) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: Future.wait([movies, lookup, exclusions]), + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Radarr movie lookup', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _results(snapshot.data[0], snapshot.data[1], snapshot.data[2]); + return LunaLoader(); + }, + ), + ); + } + + Widget _results(List movies, List results, List exclusions) { + if((results?.length ?? 0) == 0) return LunaListView(children: [LunaMessage.inList(text: 'No Results Found')]); + return LunaListViewBuilder( + scrollController: RadarrAddMovieNavigationBar.scrollControllers[0], + itemCount: results.length, + itemBuilder: (context, index) { + RadarrExclusion exclusion = exclusions?.firstWhere((exclusion) => exclusion.tmdbId == results[index].tmdbId, orElse: () => null); + return RadarrAddMovieSearchResultTile( + movie: results[index], + exists: results[index].id != null, + isExcluded: !(exclusion == null), + ); + }, + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie/widgets/search_results_tile.dart b/lib/modules/radarr/modules/add_movie/widgets/search_results_tile.dart new file mode 100644 index 00000000..57704960 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/widgets/search_results_tile.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieSearchResultTile extends StatefulWidget { + final RadarrMovie movie; + final bool onTapShowOverview; + final bool exists; + final bool isExcluded; + + RadarrAddMovieSearchResultTile({ + Key key, + @required this.movie, + @required this.exists, + @required this.isExcluded, + this.onTapShowOverview = false, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaFourLineCardWithPoster( + backgroundUrl: widget.movie.remotePoster, + posterUrl: widget.movie.remotePoster, + posterHeaders: context.watch().headers, + posterPlaceholder: 'assets/images/radarr/nomovieposter.png', + title: widget.movie.title, + darken: widget.exists, + titleColor: widget.isExcluded ? LunaColours.red : Colors.white, + subtitle1: _subtitle1(), + subtitle2: _subtitle2(), + subtitle2MaxLines: 2, + onTap: _onTap, + onLongPress: _onLongPress, + ); + } + + TextSpan _subtitle1() { + return TextSpan( + children: [ + TextSpan(text: widget.movie.lunaYear), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaRuntime), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaStudio), + ], + ); + } + + TextSpan _subtitle2() { + return TextSpan( + style: TextStyle(fontStyle: FontStyle.italic), + text: widget.movie.overview == null || widget.movie.overview.isEmpty ? 'No summary is available.\n' : widget.movie.overview, + ); + } + + Future _onTap() async { + if(widget.onTapShowOverview) { + LunaDialogs().textPreview(context, widget.movie.title, widget.movie.overview ?? 'No summary is available.'); + } else if(widget.exists) { + RadarrMoviesDetailsRouter().navigateTo(context, movieId: widget.movie.id ?? -1); + } else { + RadarrAddMovieDetailsRouter().navigateTo(context, tmdbId: widget.movie.tmdbId ?? -1); + } + } + + Future _onLongPress() async => widget.movie?.tmdbId?.toString()?.lunaOpenTheMovieDBMovie(); +} diff --git a/lib/modules/radarr/modules/add_movie/widgets/search_search_bar.dart b/lib/modules/radarr/modules/add_movie/widgets/search_search_bar.dart new file mode 100644 index 00000000..4e11e911 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie/widgets/search_search_bar.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieSearchSearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; + + RadarrAddMovieSearchSearchBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + + @override + Size get preferredSize => Size.fromHeight(62.0); + + @override + State createState() => _State(); +} + +class _State extends State { + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Padding( + child: Row( + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + autofocus: context.read().searchQuery?.isEmpty ?? true, + onChanged: (value) => context.read().searchQuery = value, + onSubmitted: (value) { + if(value.isNotEmpty) context.read().executeSearch(context); + }, + margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + ), + ), + ), + ], + ), + padding: EdgeInsets.only(top: 1.0, bottom: 1.0), + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie_details.dart b/lib/modules/radarr/modules/add_movie_details.dart new file mode 100644 index 00000000..eb6396c4 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details.dart @@ -0,0 +1,3 @@ +export 'add_movie_details/route.dart'; +export 'add_movie_details/state.dart'; +export 'add_movie_details/widgets.dart'; diff --git a/lib/modules/radarr/modules/add_movie_details/route.dart b/lib/modules/radarr/modules/add_movie_details/route.dart new file mode 100644 index 00000000..7e57a3b7 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/route.dart @@ -0,0 +1,108 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsRouter extends LunaPageRouter { + RadarrAddMovieDetailsRouter() : super('/radarr/addmovie/:tmdbid'); + + @override + Future navigateTo(BuildContext context, { @required int tmdbId }) => LunaRouter.router.navigateTo(context, route(tmdbId: tmdbId)); + + @override + String route({ @required int tmdbId }) => fullRoute.replaceFirst(':tmdbid', tmdbId.toString()); + + @override + void defineRoute(FluroRouter router) => router.define( + fullRoute, + handler: Handler(handlerFunc: (context, params) { + int tmdbId = params['tmdbid'] == null || params['tmdbid'].length == 0 ? -1 : (int.tryParse(params['tmdbid'][0]) ?? -1); + return _RadarrMoviesAddDetailsRoute(tmdbId: tmdbId); + }), + transitionType: LunaRouter.transitionType, + ); +} + +class _RadarrMoviesAddDetailsRoute extends StatefulWidget { + final int tmdbId; + + _RadarrMoviesAddDetailsRoute({ + Key key, + @required this.tmdbId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_RadarrMoviesAddDetailsRoute> with LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + Future loadCallback() async { + context.read().fetchQualityProfiles(); + context.read().fetchRootFolders(); + context.read().fetchTags(); + } + + @override + Widget build(BuildContext context) { + if(widget.tmdbId <= 0) return LunaInvalidRoute(title: 'Add Movie', message: 'Movie Not Found'); + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(context), + ); + } + + Widget _appBar() => LunaAppBar(title: 'Add Movie', state: context.read()); + + Widget _body(BuildContext context) { + return FutureBuilder( + future: Future.wait([ + context.watch().lookup, + context.watch().rootFolders, + context.watch().qualityProfiles, + context.watch().tags, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) return LunaMessage.error(onTap: _refreshKey.currentState.show); + if(snapshot.hasData) { + RadarrMovie _movie = (snapshot.data[0] as List)?.firstWhere((movie) => movie.tmdbId == widget.tmdbId, orElse: () => null); + if(_movie == null) return LunaLoader(); + return _content(_movie, snapshot.data[1], snapshot.data[2], snapshot.data[3]); + } + return LunaLoader(); + }, + ); + } + + Widget _content(RadarrMovie movie, List rootFolders, List qualityProfiles, List tags) { + return ChangeNotifierProvider( + create: (_) => RadarrAddMovieDetailsState( + movie: movie, + rootFolders: rootFolders, + qualityProfiles: qualityProfiles, + tags: tags, + ), + builder: (context, _) => LunaListView( + scrollController: context.read().scrollController, + children: [ + RadarrAddMovieSearchResultTile(movie: movie, onTapShowOverview: true, exists: false, isExcluded: false), + RadarrAddMovieDetailsMonitoredTile(), + RadarrAddMovieDetailsRootFolderTile(), + RadarrAddMovieDetailsMinimumAvailabilityTile(), + RadarrAddMovieDetailsQualityProfileTile(), + RadarrAddMovieDetailsTagsTile(), + LunaButtonContainer( + children: [ + RadarrAddMovieDetailsAddButton(searchOnAdd: false), + RadarrAddMovieDetailsAddButton(searchOnAdd: true), + ], + ), + ], + ), + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie_details/state.dart b/lib/modules/radarr/modules/add_movie_details/state.dart new file mode 100644 index 00000000..ba1c497a --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/state.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsState extends ChangeNotifier { + RadarrAddMovieDetailsState({ + @required RadarrMovie movie, + @required List rootFolders, + @required List qualityProfiles, + @required List tags, + }) { + assert(movie != null); + assert(rootFolders != null); + assert(qualityProfiles != null); + assert(tags != null); + _movie = movie; + _tags = []; + _monitored = RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE.data ?? true; + _rootFolder = (rootFolders ?? []).firstWhere( + (element) => element.id == RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID.data, + orElse: () => (rootFolders?.length ?? 0) != 0 ? rootFolders[0] : RadarrRootFolder(id: -1, freeSpace: 0, path: Constants.TEXT_EMDASH), + ); + _qualityProfile = (qualityProfiles ?? []).firstWhere( + (element) => element.id == RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID.data, + orElse: () => (qualityProfiles?.length ?? 0) != 0 ? qualityProfiles[0] : RadarrQualityProfile(id: -1, name: Constants.TEXT_EMDASH), + ); + _availability = RadarrAvailability.values.firstWhere( + (element) => element.value == RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID.data, + orElse: () => RadarrAvailability.ANNOUNCED, + ); + } + + RadarrMovie _movie; + RadarrMovie get movie => _movie; + + bool _monitored = true; + bool get monitored => _monitored; + set monitored(bool monitored) { + assert(monitored != null); + _monitored = monitored; + RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MONITORED_STATE.put(_monitored); + notifyListeners(); + } + + RadarrAvailability _availability; + RadarrAvailability get availability => _availability; + set availability(RadarrAvailability availability) { + assert(availability != null); + _availability = availability; + RadarrDatabaseValue.ADD_MOVIE_DEFAULT_MINIMUM_AVAILABILITY_ID.put(_availability.value); + notifyListeners(); + } + + RadarrRootFolder _rootFolder; + RadarrRootFolder get rootFolder => _rootFolder; + set rootFolder(RadarrRootFolder rootFolder) { + assert(rootFolder != null); + _rootFolder = rootFolder; + RadarrDatabaseValue.ADD_MOVIE_DEFAULT_ROOT_FOLDER_ID.put(_rootFolder.id); + notifyListeners(); + } + + RadarrQualityProfile _qualityProfile; + RadarrQualityProfile get qualityProfile => _qualityProfile; + set qualityProfile(RadarrQualityProfile qualityProfile) { + assert(qualityProfile != null); + _qualityProfile = qualityProfile; + RadarrDatabaseValue.ADD_MOVIE_DEFAULT_QUALITY_PROFILE_ID.put(_qualityProfile.id); + notifyListeners(); + } + + List _tags = []; + List get tags => _tags; + set tags(List tags) { + assert(tags != null); + _tags = tags; + notifyListeners(); + } + + LunaLoadingState _state = LunaLoadingState.INACTIVE; + LunaLoadingState get state => _state; + set state(LunaLoadingState state) { + assert(state != null); + _state = state; + notifyListeners(); + } +} diff --git a/lib/modules/radarr/modules/add_movie_details/widgets.dart b/lib/modules/radarr/modules/add_movie_details/widgets.dart new file mode 100644 index 00000000..6c6e631b --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/widgets.dart @@ -0,0 +1,6 @@ +export 'widgets/button_add_movie.dart'; +export 'widgets/tile_minimum_availability.dart'; +export 'widgets/tile_monitored.dart'; +export 'widgets/tile_quality_profile.dart'; +export 'widgets/tile_root_folder.dart'; +export 'widgets/tile_tags.dart'; diff --git a/lib/modules/radarr/modules/add_movie_details/widgets/button_add_movie.dart b/lib/modules/radarr/modules/add_movie_details/widgets/button_add_movie.dart new file mode 100644 index 00000000..81e75bc3 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/widgets/button_add_movie.dart @@ -0,0 +1,48 @@ + +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsAddButton extends StatelessWidget { + final bool searchOnAdd; + + RadarrAddMovieDetailsAddButton({ + Key key, + @required this.searchOnAdd, + }) : super(key: key) { + assert(searchOnAdd != null); + } + + @override + Widget build(BuildContext context) { + return LunaButton( + text: searchOnAdd ? 'Add + Search' : 'Add', + backgroundColor: searchOnAdd ? LunaColours.orange : LunaColours.accent, + onTap: () async => _onTap(context), + loadingState: context.watch().state, + ); + } + + Future _onTap(BuildContext context) async { + context.read().state = LunaLoadingState.ACTIVE; + await RadarrAPIHelper().addMovie( + context: context, + movie: context.read().movie, + rootFolder: context.read().rootFolder, + monitored: context.read().monitored, + qualityProfile: context.read().qualityProfile, + availability: context.read().availability, + tags: context.read().tags, + searchForMovie: searchOnAdd, + ) + .then((movie) async { + context.read().fetchMovies(); + context.read().movie.id = movie.id; + Navigator.of(context).popAndPushNamed(RadarrMoviesDetailsRouter().route(movieId: movie.id)); + }) + .catchError((error, stack) { + context.read().state = LunaLoadingState.ERROR; + }); + context.read().state = LunaLoadingState.INACTIVE; + } +} diff --git a/lib/modules/radarr/modules/add_movie_details/widgets/tile_minimum_availability.dart b/lib/modules/radarr/modules/add_movie_details/widgets/tile_minimum_availability.dart new file mode 100644 index 00000000..a83599da --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/widgets/tile_minimum_availability.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsMinimumAvailabilityTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Minimum Availability'), + subtitle: Selector( + selector: (_, state) => state.availability, + builder: (context, availability, _) => LunaText.subtitle(text: availability?.readable ?? Constants.TEXT_EMDASH), + ), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + Tuple2 values = await RadarrDialogs().editMinimumAvailability(context); + if(values.item1) context.read().availability = values.item2; + }, + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie_details/widgets/tile_monitored.dart b/lib/modules/radarr/modules/add_movie_details/widgets/tile_monitored.dart new file mode 100644 index 00000000..fd4dce41 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/widgets/tile_monitored.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsMonitoredTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Monitored'), + subtitle: LunaText.subtitle(text: 'Monitor movie for new releases'), + trailing: Selector( + selector: (_, state) => state.monitored, + builder: (context, monitored, _) => LunaSwitch( + value: monitored, + onChanged: (value) => context.read().monitored = value, + ), + ), + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie_details/widgets/tile_quality_profile.dart b/lib/modules/radarr/modules/add_movie_details/widgets/tile_quality_profile.dart new file mode 100644 index 00000000..eaa568aa --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/widgets/tile_quality_profile.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsQualityProfileTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Quality Profile'), + subtitle: Selector( + selector: (_, state) => state.qualityProfile, + builder: (context, profile, _) => LunaText.subtitle(text: profile?.name ?? Constants.TEXT_EMDASH), + ), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + List qualityProfiles = await context.read().qualityProfiles; + Tuple2 values = await RadarrDialogs().editQualityProfile(context, qualityProfiles); + if(values.item1) context.read().qualityProfile = values.item2; + }, + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie_details/widgets/tile_root_folder.dart b/lib/modules/radarr/modules/add_movie_details/widgets/tile_root_folder.dart new file mode 100644 index 00000000..9f9aba66 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/widgets/tile_root_folder.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsRootFolderTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Root Folder'), + subtitle: Selector( + selector: (_, state) => state.rootFolder, + builder: (context, folder, _) => LunaText.subtitle(text: folder?.path ?? Constants.TEXT_EMDASH), + ), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + List folders = await context.read().rootFolders; + Tuple2 values = await RadarrDialogs().editRootFolder(context, folders); + if(values.item1) context.read().rootFolder = values.item2; + }, + ); + } +} diff --git a/lib/modules/radarr/modules/add_movie_details/widgets/tile_tags.dart b/lib/modules/radarr/modules/add_movie_details/widgets/tile_tags.dart new file mode 100644 index 00000000..8c564763 --- /dev/null +++ b/lib/modules/radarr/modules/add_movie_details/widgets/tile_tags.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAddMovieDetailsTagsTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Tags'), + subtitle: LunaText.subtitle(text: context.watch().tags.length == 0 + ? LunaUI.TEXT_EMDASH + : context.watch().tags.map((e) => e.label).join(', ')), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async => await RadarrDialogs().setAddTags(context), + ); + } +} diff --git a/lib/modules/radarr/modules/catalogue.dart b/lib/modules/radarr/modules/catalogue.dart new file mode 100644 index 00000000..d07bf1a9 --- /dev/null +++ b/lib/modules/radarr/modules/catalogue.dart @@ -0,0 +1,2 @@ +export 'catalogue/route.dart'; +export 'catalogue/widgets.dart'; diff --git a/lib/modules/radarr/modules/catalogue/route.dart b/lib/modules/radarr/modules/catalogue/route.dart new file mode 100644 index 00000000..6d5ad625 --- /dev/null +++ b/lib/modules/radarr/modules/catalogue/route.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; +import 'package:tuple/tuple.dart'; + +class RadarrCatalogueRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + Future _refresh() async { + RadarrState _state = context.read(); + _state.fetchMovies(); + _state.fetchQualityProfiles(); + _state.fetchTags(); + await Future.wait([_state.movies, _state.qualityProfiles, _state.tags]); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body(), + appBar: _appBar(), + ); + } + + Widget _appBar() { + return LunaAppBar.empty( + child: RadarrCatalogueSearchBar(scrollController: RadarrNavigationBar.scrollControllers[0]), + height: 62.0, + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: Selector>, Future>>>( + selector: (_, state) => Tuple2(state.movies, state.qualityProfiles), + builder: (context, tuple, _) { + return FutureBuilder( + future: Future.wait([tuple.item1, tuple.item2]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Radarr movies', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _movies(snapshot.data[0], snapshot.data[1]); + return LunaLoader(); + }, + ); + } + ), + ); + } + + Widget _movies(List movies, List qualityProfiles) { + if((movies?.length ?? 0) == 0) return LunaMessage( + text: 'No Movies Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return Selector( + selector: (_, state) => state.moviesSearchQuery, + builder: (context, query, _) { + List _filtered = _filterAndSort(movies, query); + return LunaListViewBuilder( + scrollController: RadarrNavigationBar.scrollControllers[0], + itemCount: (_filtered?.length ?? 0) == 0 ? 1 : _filtered.length, + itemBuilder: (context, index) { + if((_filtered.length ?? 0) == 0) return LunaMessage.inList(text: 'No Movies Found'); + return RadarrCatalogueTile( + movie: _filtered[index], + profile: qualityProfiles.firstWhere((element) => element.id == _filtered[index].qualityProfileId, orElse: null), + ); + }, + ); + } + ); + } + + List _filterAndSort(List movies, String query) { + if(movies == null || movies.length == 0) return movies; + // Pull values from state + RadarrMoviesSorting sorting = context.read().moviesSortType; + RadarrMoviesFilter filter = context.read().moviesFilterType; + bool ascending = context.read().moviesSortAscending; + // Filter + List filtered = movies.where((movie) { + if(query != null && query.isNotEmpty) return movie.title.toLowerCase().contains(query.toLowerCase()); + return movie != null; + }).toList(); + filtered = filter.filter(filtered); + // Sort + filtered = sorting.sort(filtered, ascending); + return filtered; + } +} diff --git a/lib/modules/radarr/modules/catalogue/widgets.dart b/lib/modules/radarr/modules/catalogue/widgets.dart new file mode 100644 index 00000000..23075a51 --- /dev/null +++ b/lib/modules/radarr/modules/catalogue/widgets.dart @@ -0,0 +1,4 @@ +export 'widgets/movie_tile.dart'; +export 'widgets/search_bar_filter_button.dart'; +export 'widgets/search_bar_sort_button.dart'; +export 'widgets/search_bar.dart'; diff --git a/lib/modules/radarr/modules/catalogue/widgets/movie_tile.dart b/lib/modules/radarr/modules/catalogue/widgets/movie_tile.dart new file mode 100644 index 00000000..4fb74970 --- /dev/null +++ b/lib/modules/radarr/modules/catalogue/widgets/movie_tile.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrCatalogueTile extends StatefulWidget { + final RadarrMovie movie; + final RadarrQualityProfile profile; + + RadarrCatalogueTile({ + Key key, + @required this.movie, + @required this.profile, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Selector>>( + selector: (_, state) => state.movies, + builder: (context, movies, _) => LunaFourLineCardWithPoster( + backgroundUrl: context.read().getPosterURL(widget.movie.id), + posterUrl: context.read().getPosterURL(widget.movie.id), + posterHeaders: context.read().headers, + posterPlaceholder: 'assets/images/radarr/nomovieposter.png', + darken: !widget.movie.monitored, + title: widget.movie.title, + subtitle1: _subtitle1(), + subtitle2: _subtitle2(), + customSubtitle3: _subtitle3(), + onTap: _onTap, + onLongPress: _onLongPress, + ), + ); + } + + TextSpan _buildChildTextSpan(String text, RadarrMoviesSorting sorting) { + TextStyle style; + if(context.read().moviesSortType == sorting) style = TextStyle( + color: widget.movie.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30), + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ); + return TextSpan( + text: text, + style: style, + ); + } + + TextSpan _subtitle1() { + return TextSpan( + children: [ + _buildChildTextSpan(widget.movie.lunaYear, RadarrMoviesSorting.YEAR), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + _buildChildTextSpan(widget.movie.lunaRuntime, RadarrMoviesSorting.RUNTIME), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + _buildChildTextSpan(widget.movie.lunaStudio, RadarrMoviesSorting.STUDIO), + ], + ); + } + + TextSpan _subtitle2() { + return TextSpan( + children: [ + _buildChildTextSpan(widget.profile?.name ?? LunaUI.TEXT_EMDASH, RadarrMoviesSorting.QUALITY_PROFILE), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + _buildChildTextSpan(widget.movie.lunaMinimumAvailability, RadarrMoviesSorting.MIN_AVAILABILITY), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + _buildChildTextSpan(widget.movie.lunaDateAdded, RadarrMoviesSorting.DATE_ADDED), + ], + ); + } + + Widget _buildReleaseIcon(IconData icon, Color color, bool highlight) { + Color _color = (highlight ? color : Colors.grey).withOpacity(widget.movie.monitored ? 1 : 0.30); + return Padding( + child: Icon(icon, size: 16.0, color: _color), + padding: EdgeInsets.only(right: 8.0), + ); + } + + Widget _subtitle3() { + return Row( + children: [ + _buildReleaseIcon(Icons.videocam, LunaColours.orange, widget.movie.lunaIsInCinemas), + _buildReleaseIcon(Icons.album, LunaColours.blue, widget.movie.lunaIsReleased), + _buildReleaseIcon(Icons.check_circle, LunaColours.accent, widget.movie.hasFile), + Padding( + child: widget.movie.hasFile + ? widget.movie.lunaHasFileTextObject(widget.movie.monitored) + : widget.movie.lunaNextReleaseTextObject(widget.movie.monitored), + padding: EdgeInsets.only(top: 1.5), + ), + ], + ); + } + + Future _onTap() async => RadarrMoviesDetailsRouter().navigateTo(context, movieId: widget.movie.id); + + Future _onLongPress() async { + Tuple2 values = await RadarrDialogs().movieSettings(context, widget.movie); + if(values.item1) values.item2.execute(context, widget.movie); + } +} diff --git a/lib/modules/radarr/modules/catalogue/widgets/search_bar.dart b/lib/modules/radarr/modules/catalogue/widgets/search_bar.dart new file mode 100644 index 00000000..e401f1dc --- /dev/null +++ b/lib/modules/radarr/modules/catalogue/widgets/search_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrCatalogueSearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; + + RadarrCatalogueSearchBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + + @override + Size get preferredSize => Size.fromHeight(62.0); + + @override + State createState() => _State(); +} + +class _State extends State { + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = context.read().moviesSearchQuery; + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + autofocus: false, + onChanged: (value) => context.read().moviesSearchQuery = value, + margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + ), + ), + ), + RadarrCatalogueSearchBarFilterButton(controller: widget.scrollController), + RadarrCatalogueSearchBarSortButton(controller: widget.scrollController), + ], + ); + } +} diff --git a/lib/modules/radarr/modules/catalogue/widgets/search_bar_filter_button.dart b/lib/modules/radarr/modules/catalogue/widgets/search_bar_filter_button.dart new file mode 100644 index 00000000..95d6c51f --- /dev/null +++ b/lib/modules/radarr/modules/catalogue/widgets/search_bar_filter_button.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrCatalogueSearchBarFilterButton extends StatefulWidget { + final ScrollController controller; + + RadarrCatalogueSearchBarFilterButton({ + Key key, + @required this.controller, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.filter_alt_outlined, + onSelected: (result) { + state.moviesFilterType = result; + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => List>.generate( + RadarrMoviesFilter.values.length, + (index) => PopupMenuItem( + value: RadarrMoviesFilter.values[index], + child: Text( + RadarrMoviesFilter.values[index].readable, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + color: state.moviesFilterType == RadarrMoviesFilter.values[index] ? LunaColours.accent : Colors.white, + ), + ), + ), + ), + ), + ), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + color: Theme.of(context).canvasColor, + ); + } +} \ No newline at end of file diff --git a/lib/modules/radarr/modules/catalogue/widgets/search_bar_sort_button.dart b/lib/modules/radarr/modules/catalogue/widgets/search_bar_sort_button.dart new file mode 100644 index 00000000..3b481559 --- /dev/null +++ b/lib/modules/radarr/modules/catalogue/widgets/search_bar_sort_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrCatalogueSearchBarSortButton extends StatefulWidget { + final ScrollController controller; + + RadarrCatalogueSearchBarSortButton({ + Key key, + @required this.controller, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.sort, + onSelected: (result) { + if(state.moviesSortType == result) { + state.moviesSortAscending = !state.moviesSortAscending; + } else { + state.moviesSortAscending = true; + state.moviesSortType = result; + } + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => List>.generate( + RadarrMoviesSorting.values.length, + (index) => PopupMenuItem( + value: RadarrMoviesSorting.values[index], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + RadarrMoviesSorting.values[index].readable, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_SUBTITLE, + color: state.moviesSortType == RadarrMoviesSorting.values[index] ? LunaColours.accent : Colors.white, + ), + ), + if(state.moviesSortType == RadarrMoviesSorting.values[index]) Icon( + state.moviesSortAscending ? Icons.arrow_upward : Icons.arrow_downward, + size: LunaUI.FONT_SIZE_SUBTITLE+2.0, + color: LunaColours.accent, + ), + ], + ), + ), + ), + ), + ), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + color: Theme.of(context).canvasColor, + ); + } +} \ No newline at end of file diff --git a/lib/modules/radarr/modules/edit_movie.dart b/lib/modules/radarr/modules/edit_movie.dart new file mode 100644 index 00000000..3d6d0d75 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie.dart @@ -0,0 +1,3 @@ +export 'edit_movie/route.dart'; +export 'edit_movie/state.dart'; +export 'edit_movie/widgets.dart'; diff --git a/lib/modules/radarr/modules/edit_movie/route.dart b/lib/modules/radarr/modules/edit_movie/route.dart new file mode 100644 index 00000000..cb4007f2 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/route.dart @@ -0,0 +1,100 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditRouter extends LunaPageRouter { + RadarrMoviesEditRouter() : super('/radarr/editmovie/:movieid'); + + @override + Future navigateTo(BuildContext context, { @required int movieId }) async => LunaRouter.router.navigateTo(context, route(movieId: movieId)); + + @override + String route({ @required int movieId }) => fullRoute.replaceFirst(':movieid', movieId.toString()); + + @override + void defineRoute(FluroRouter router) => router.define( + fullRoute, + handler: Handler(handlerFunc: (context, params) { + int movieId = params['movieid'] == null || params['movieid'].length == 0 ? -1 : (int.tryParse(params['movieid'][0]) ?? -1); + return _RadarrMoviesEditRoute(movieId: movieId); + }), + transitionType: LunaRouter.transitionType, + ); +} + +class _RadarrMoviesEditRoute extends StatefulWidget { + final int movieId; + + _RadarrMoviesEditRoute({ + Key key, + @required this.movieId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_RadarrMoviesEditRoute> with LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Future loadCallback() async { + context.read().fetchTags(); + context.read().fetchQualityProfiles(); + } + + @override + Widget build(BuildContext context) { + if(widget.movieId <= 0) return LunaInvalidRoute(title: 'Edit Movie', message: 'Movie Not Found'); + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } + + Widget _appBar() => LunaAppBar(title: 'Edit Movie', state: context.read()); + + Widget _body() { + return FutureBuilder( + future: Future.wait([ + context.watch().movies, + context.watch().qualityProfiles, + context.watch().tags, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) return LunaMessage.error(onTap: loadCallback); + if(snapshot.hasData) { + RadarrMovie movie = (snapshot.data[0] as List).firstWhere( + (movie) => movie?.id == widget.movieId, + orElse: () => null, + ); + if(movie == null) return LunaLoader(); + return _list(movie, snapshot.data[1], snapshot.data[2]); + } + return LunaLoader(); + }, + ); + } + + Widget _list(RadarrMovie movie, List qualityProfiles, List tags) { + return ChangeNotifierProvider( + create: (_) => RadarrMoviesEditState(movie: movie, qualityProfiles: qualityProfiles, tags: tags), + builder: (context, _) { + if(context.watch().state == LunaLoadingState.ERROR) return LunaMessage.goBack(context: context, text: 'An Error Has Occurred'); + return LunaListView( + scrollController: context.read().scrollController, + children: [ + RadarrMoviesEditMonitoredTile(), + RadarrMoviesEditMinimumAvailabilityTile(), + RadarrMoviesEditQualityProfileTile(profiles: qualityProfiles), + RadarrMoviesEditPathTile(), + RadarrMoviesEditTagsTile(), + RadarrMoviesEditUpdateMovieButton(movie: movie), + ], + ); + } + ); + } +} diff --git a/lib/modules/radarr/modules/edit_movie/state.dart b/lib/modules/radarr/modules/edit_movie/state.dart new file mode 100644 index 00000000..d153739f --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/state.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditState extends ChangeNotifier { + RadarrMoviesEditState({ + @required RadarrMovie movie, + @required List qualityProfiles, + @required List tags, + }) { + _monitored = movie.monitored ?? true; + _path = movie.path ?? ''; + _availability = movie.minimumAvailability ?? RadarrAvailability.ANNOUNCED; + _qualityProfile = qualityProfiles.firstWhere( + (profile) => profile.id == movie.qualityProfileId, + orElse: () => qualityProfiles.length == 0 ? null : qualityProfiles[0], + ); + _tags = (tags ?? []).where((tag) => (movie.tags ?? []).contains(tag.id)).toList(); + } + + LunaLoadingState _state = LunaLoadingState.INACTIVE; + LunaLoadingState get state => _state; + set state(LunaLoadingState state) { + assert(state != null); + _state = state; + notifyListeners(); + } + + bool _monitored = true; + bool get monitored => _monitored; + set monitored(bool monitored) { + assert(monitored != null); + _monitored = monitored; + notifyListeners(); + } + + String _path = ''; + String get path => _path; + set path(String path) { + assert(path != null); + _path = path; + notifyListeners(); + } + + RadarrQualityProfile _qualityProfile; + RadarrQualityProfile get qualityProfile => _qualityProfile; + set qualityProfile(RadarrQualityProfile qualityProfile) { + assert(qualityProfile != null); + _qualityProfile = qualityProfile; + notifyListeners(); + } + + RadarrAvailability _availability; + RadarrAvailability get availability => _availability; + set availability(RadarrAvailability availability) { + assert(availability != null); + _availability = availability; + notifyListeners(); + } + + List _tags; + List get tags => _tags; + set tags(List tags) { + assert(tags != null); + _tags = tags; + notifyListeners(); + } +} diff --git a/lib/modules/radarr/modules/edit_movie/widgets.dart b/lib/modules/radarr/modules/edit_movie/widgets.dart new file mode 100644 index 00000000..646b7fd5 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/widgets.dart @@ -0,0 +1,6 @@ +export 'widgets/button_update_movie.dart'; +export 'widgets/tile_monitored.dart'; +export 'widgets/tile_path.dart'; +export 'widgets/tile_quality_profile.dart'; +export 'widgets/tile_radarr_availability.dart'; +export 'widgets/tile_tags.dart'; diff --git a/lib/modules/radarr/modules/edit_movie/widgets/button_update_movie.dart b/lib/modules/radarr/modules/edit_movie/widgets/button_update_movie.dart new file mode 100644 index 00000000..99001712 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/widgets/button_update_movie.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditUpdateMovieButton extends StatelessWidget { + final RadarrMovie movie; + + RadarrMoviesEditUpdateMovieButton({ + Key key, + @required this.movie, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaButton( + text: 'Update Movie', + loadingState: context.watch().state, + onTap: () async { + context.read().state = LunaLoadingState.ACTIVE; + RadarrMovie updatedMovie = movie.updateEdits(context.read()); + bool result = await RadarrAPIHelper().updateMovie(context: context, movie: updatedMovie); + if(result) Navigator.of(context).lunaSafetyPop(); + }, + ); + } +} diff --git a/lib/modules/radarr/modules/edit_movie/widgets/tile_monitored.dart b/lib/modules/radarr/modules/edit_movie/widgets/tile_monitored.dart new file mode 100644 index 00000000..d08b69ca --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/widgets/tile_monitored.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditMonitoredTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Monitored'), + subtitle: LunaText.subtitle(text: 'Monitor movie for new releases'), + trailing: LunaSwitch( + value: context.watch().monitored, + onChanged: (value) => context.read().monitored = value, + ), + ); + } +} diff --git a/lib/modules/radarr/modules/edit_movie/widgets/tile_path.dart b/lib/modules/radarr/modules/edit_movie/widgets/tile_path.dart new file mode 100644 index 00000000..37d4d543 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/widgets/tile_path.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditPathTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Movie Path'), + subtitle: LunaText.subtitle(text: context.watch().path ?? Constants.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + List _values = await LunaDialogs().editText(context, 'Movie Path', prefill: context.read().path); + if(_values[0]) context.read().path = _values[1]; + }, + ); + } +} diff --git a/lib/modules/radarr/modules/edit_movie/widgets/tile_quality_profile.dart b/lib/modules/radarr/modules/edit_movie/widgets/tile_quality_profile.dart new file mode 100644 index 00000000..a31422b3 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/widgets/tile_quality_profile.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditQualityProfileTile extends StatelessWidget { + final List profiles; + + RadarrMoviesEditQualityProfileTile({ + Key key, + @required this.profiles, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Quality Profile'), + subtitle: LunaText.subtitle(text: context.watch().qualityProfile?.name ?? Constants.TEXT_EMDASH), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + Tuple2 values = await RadarrDialogs().editQualityProfile(context, profiles); + if(values.item1) context.read().qualityProfile = values.item2; + }, + ); + } +} diff --git a/lib/modules/radarr/modules/edit_movie/widgets/tile_radarr_availability.dart b/lib/modules/radarr/modules/edit_movie/widgets/tile_radarr_availability.dart new file mode 100644 index 00000000..1b0a62e0 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/widgets/tile_radarr_availability.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditMinimumAvailabilityTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Minimum Availability'), + subtitle: LunaText.subtitle(text: context.watch().availability.readable), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + Tuple2 _values = await RadarrDialogs().editMinimumAvailability(context); + if(_values.item1) context.read().availability = _values.item2; + }, + ); + } +} diff --git a/lib/modules/radarr/modules/edit_movie/widgets/tile_tags.dart b/lib/modules/radarr/modules/edit_movie/widgets/tile_tags.dart new file mode 100644 index 00000000..7a3b2b04 --- /dev/null +++ b/lib/modules/radarr/modules/edit_movie/widgets/tile_tags.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesEditTagsTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: 'Tags'), + subtitle: LunaText.subtitle(text: context.watch().tags.length == 0 + ? 'Not Set' + : context.watch().tags.map((e) => e.label).join(', ')), + trailing: LunaIconButton(icon: Icons.arrow_forward_ios), + onTap: () async => await RadarrDialogs().setEditTags(context), + ); + } +} diff --git a/lib/modules/radarr/modules/health_check.dart b/lib/modules/radarr/modules/health_check.dart new file mode 100644 index 00000000..6690406b --- /dev/null +++ b/lib/modules/radarr/modules/health_check.dart @@ -0,0 +1 @@ +export 'health_check/route.dart'; diff --git a/lib/modules/radarr/modules/health_check/route.dart b/lib/modules/radarr/modules/health_check/route.dart new file mode 100644 index 00000000..716126ee --- /dev/null +++ b/lib/modules/radarr/modules/health_check/route.dart @@ -0,0 +1,30 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrHealthCheckRouter extends LunaPageRouter { + RadarrHealthCheckRouter() : super('/radarr/system/health'); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrHealthCheckRoute()); +} + + +class _RadarrHealthCheckRoute extends StatefulWidget { + @override + State<_RadarrHealthCheckRoute> createState() => _State(); +} + +class _State extends State<_RadarrHealthCheckRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) => Scaffold( + key: _scaffoldKey, + appBar: _appBar, + body: LunaMessage.goBack(context: context, text: 'Coming Soon'), + ); + + Widget get _appBar => LunaAppBar(title: 'Health Check', state: context.read()); +} diff --git a/lib/modules/radarr/modules/history.dart b/lib/modules/radarr/modules/history.dart new file mode 100644 index 00000000..a89dfef1 --- /dev/null +++ b/lib/modules/radarr/modules/history.dart @@ -0,0 +1,2 @@ +export 'history/route.dart'; +export 'history/widgets.dart'; diff --git a/lib/modules/radarr/modules/history/route.dart b/lib/modules/radarr/modules/history/route.dart new file mode 100644 index 00000000..8117a267 --- /dev/null +++ b/lib/modules/radarr/modules/history/route.dart @@ -0,0 +1,93 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrHistoryRouter extends LunaPageRouter { + RadarrHistoryRouter() : super('/radarr/history'); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrHistoryRoute()); +} + + +class _RadarrHistoryRoute extends StatefulWidget { + @override + State<_RadarrHistoryRoute> createState() => _State(); +} + +class _State extends State<_RadarrHistoryRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + final PagingController _pagingController = PagingController(firstPageKey: 1); + + @override + void dispose() { + _pagingController?.dispose(); + super.dispose(); + } + + Future _fetchPage(int pageKey) async { + await context.read().api.history.get( + page: pageKey, + pageSize: RadarrDatabaseValue.CONTENT_PAGE_SIZE.data, + sortKey: RadarrHistorySortKey.DATE, + sortDirection: RadarrSortDirection.DESCENDING, + ) + .then((data) { + if(data.totalRecords > (data.page * data.pageSize)) return _pagingController.appendPage(data.records, pageKey+1); + return _pagingController.appendLastPage(data.records); + }) + .catchError((error, stack) { + LunaLogger().error('Unable to fetch Radarr history page $pageKey', error, stack); + _pagingController.error = error; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'History', + state: context.read(), + ); + } + + Widget _body() { + return FutureBuilder( + future: context.read().movies, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Radarr movies for history list', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: () => Future.sync(() => _pagingController.refresh())); + } + if(snapshot.hasData) return _paginatedList(snapshot.data); + return LunaLoader(); + }, + ); + } + + Widget _paginatedList(List movies) => LunaPagedListView( + refreshKey: _refreshKey, + pagingController: _pagingController, + scrollController: context.read().scrollController, + listener: _fetchPage, + noItemsFoundMessage: 'No History Found', + itemBuilder: (context, history, index) { + RadarrMovie _movie = movies.firstWhere((movie) => movie.id == history.movieId, orElse: () => null); + return RadarrHistoryTile(history: history, title: _movie?.title); + }, + ); +} diff --git a/lib/modules/radarr/modules/history/widgets.dart b/lib/modules/radarr/modules/history/widgets.dart new file mode 100644 index 00000000..54a3049e --- /dev/null +++ b/lib/modules/radarr/modules/history/widgets.dart @@ -0,0 +1 @@ +export 'widgets/history_tile.dart'; diff --git a/lib/modules/radarr/modules/history/widgets/history_tile.dart b/lib/modules/radarr/modules/history/widgets/history_tile.dart new file mode 100644 index 00000000..db555708 --- /dev/null +++ b/lib/modules/radarr/modules/history/widgets/history_tile.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrHistoryTile extends StatelessWidget { + final RadarrHistoryRecord history; + final bool movieHistory; + final String title; + + /// If [movieHistory] is false (default), you must supply a title or else a dash will be shown. + RadarrHistoryTile({ + Key key, + @required this.history, + this.movieHistory = false, + this.title = LunaUI.TEXT_EMDASH, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaExpandableListTile( + title: movieHistory ? history?.sourceTitle : title, + collapsedSubtitle1: TextSpan( + text: [ + history?.date?.lunaAge ?? LunaUI.TEXT_EMDASH, + history?.date?.lunaDateTimeReadable() ?? LunaUI.TEXT_EMDASH, + ].join(LunaUI.TEXT_BULLET.lunaPad()), + ), + collapsedSubtitle2: TextSpan( + text: history?.eventType?.lunaReadable(history) ?? LunaUI.TEXT_EMDASH, + style: TextStyle( + color: history?.eventType?.lunaColour ?? LunaColours.blueGrey, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + expandedHighlightedNodes: [ + LunaHighlightedNode( + text: history.eventType?.readable, + backgroundColor: history.eventType?.lunaColour, + ), + ...history.customFormats.map((format) => LunaHighlightedNode( + text: format.name, + backgroundColor: LunaColours.blueGrey, + )), + ], + expandedTableContent: history.eventType?.lunaTableContent(history, movieHistory: movieHistory) ?? [], + onLongPress: movieHistory ? null : () async => RadarrMoviesDetailsRouter().navigateTo(context, movieId: history?.movieId ?? -1), + ); + } +} diff --git a/lib/modules/radarr/modules/missing.dart b/lib/modules/radarr/modules/missing.dart new file mode 100644 index 00000000..1ef0049e --- /dev/null +++ b/lib/modules/radarr/modules/missing.dart @@ -0,0 +1,2 @@ +export 'missing/route.dart'; +export 'missing/widgets.dart'; diff --git a/lib/modules/radarr/modules/missing/route.dart b/lib/modules/radarr/modules/missing/route.dart new file mode 100644 index 00000000..a6fca8d2 --- /dev/null +++ b/lib/modules/radarr/modules/missing/route.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMissingRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Future _refresh() async { + RadarrState _state = context.read(); + _state.fetchMovies(); + _state.fetchQualityProfiles(); + await Future.wait([ + _state.missing, + _state.qualityProfiles, + ]); + } + + Widget get _body => LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: Future.wait([ + context.watch().missing, + context.watch().qualityProfiles, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Radarr upcoming', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data[0], snapshot.data[1]); + return LunaLoader(); + }, + ), + ); + + Widget _list(List movies, List qualityProfiles) { + if((movies?.length ?? 0) == 0) return LunaMessage( + text: 'No Movies Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + scrollController: RadarrNavigationBar.scrollControllers[2], + itemCount: movies.length, + itemBuilder: (context, index) => RadarrMissingTile( + movie: movies[index], + profile: qualityProfiles.firstWhere((element) => element.id == movies[index].qualityProfileId, orElse: null), + ), + ); + } +} diff --git a/lib/modules/radarr/modules/missing/widgets.dart b/lib/modules/radarr/modules/missing/widgets.dart new file mode 100644 index 00000000..c7f5c93d --- /dev/null +++ b/lib/modules/radarr/modules/missing/widgets.dart @@ -0,0 +1 @@ +export 'widgets/missing_tile.dart'; diff --git a/lib/modules/radarr/modules/missing/widgets/missing_tile.dart b/lib/modules/radarr/modules/missing/widgets/missing_tile.dart new file mode 100644 index 00000000..6f630044 --- /dev/null +++ b/lib/modules/radarr/modules/missing/widgets/missing_tile.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMissingTile extends StatefulWidget { + final RadarrMovie movie; + final RadarrQualityProfile profile; + + RadarrMissingTile({ + Key key, + @required this.movie, + @required this.profile, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Selector>>( + selector: (_, state) => state.missing, + builder: (context, missing, _) => LunaFourLineCardWithPoster( + backgroundUrl: context.read().getPosterURL(widget.movie.id), + posterUrl: context.read().getPosterURL(widget.movie.id), + posterHeaders: context.read().headers, + posterPlaceholder: 'assets/images/radarr/nomovieposter.png', + darken: !widget.movie.monitored, + title: widget.movie.title, + subtitle1: _subtitle1(), + subtitle2: _subtitle2(), + subtitle3: _subtitle3(), + trailing: _trailing(), + onTap: _onTap, + ), + ); + } + + TextSpan _subtitle1() { + return TextSpan( + children: [ + TextSpan(text: widget.movie.lunaYear), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaRuntime), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaStudio), + ], + ); + } + + TextSpan _subtitle2() { + return TextSpan( + children: [ + TextSpan(text: widget.profile.lunaName), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaMinimumAvailability), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaReleaseDate), + ], + ); + } + + TextSpan _subtitle3() { + String _days = widget.movie.lunaEarlierReleaseDate.lunaDaysDifference; + return TextSpan( + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: LunaColours.red, + ), + text: _days == null ? 'Released' : _days == 'Today' ? 'Released Today' : 'Released $_days Ago' + ); + } + + LunaIconButton _trailing() { + return LunaIconButton( + icon: Icons.search, + onPressed: () async => RadarrAPIHelper().automaticSearch(context: context, movieId: widget.movie.id, title: widget.movie.title), + onLongPress: () async => RadarrReleasesRouter().navigateTo(context, movieId: widget.movie.id), + ); + } + + Future _onTap() async => RadarrMoviesDetailsRouter().navigateTo(context, movieId: widget.movie.id); +} diff --git a/lib/modules/radarr/modules/more.dart b/lib/modules/radarr/modules/more.dart new file mode 100644 index 00000000..56a2ef26 --- /dev/null +++ b/lib/modules/radarr/modules/more.dart @@ -0,0 +1 @@ +export 'more/route.dart'; diff --git a/lib/modules/radarr/modules/more/route.dart b/lib/modules/radarr/modules/more/route.dart new file mode 100644 index 00000000..45d84f71 --- /dev/null +++ b/lib/modules/radarr/modules/more/route.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoreRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaListView( + scrollController: RadarrNavigationBar.scrollControllers[3], + children: [ + LunaListTile( + context: context, + title: LunaText.title(text: 'Health Check'), + subtitle: LunaText.subtitle(text: 'Check for Configuration Issues'), + trailing: LunaIconButton(icon: CustomIcons.monitoring, color: LunaColours.list(0)), + onTap: () async => RadarrHealthCheckRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'History'), + subtitle: LunaText.subtitle(text: 'View Recent Activity'), + trailing: LunaIconButton(icon: CustomIcons.history, color: LunaColours.list(1)), + onTap: () async => RadarrHistoryRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Queue'), + subtitle: LunaText.subtitle(text: 'View Active & Queued Content'), + trailing: LunaIconButton(icon: Icons.queue, color: LunaColours.list(2)), + onTap: () async => RadarrQueueRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'System Status'), + subtitle: LunaText.subtitle(text: 'System Status & Disk Space'), + trailing: LunaIconButton(icon: Icons.tune, color: LunaColours.list(3)), + onTap: () async => RadarrSystemStatusRouter().navigateTo(context), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Tags'), + subtitle: LunaText.subtitle(text: 'Manage Your Tags'), + trailing: LunaIconButton(icon: Icons.style, color: LunaColours.list(4)), + onTap: () async => RadarrTagsRouter().navigateTo(context), + ), + ], + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details.dart b/lib/modules/radarr/modules/movie_details.dart new file mode 100644 index 00000000..1496ed26 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details.dart @@ -0,0 +1,3 @@ +export 'movie_details/route.dart'; +export 'movie_details/state.dart'; +export 'movie_details/widgets.dart'; diff --git a/lib/modules/radarr/modules/movie_details/route.dart b/lib/modules/radarr/modules/movie_details/route.dart new file mode 100644 index 00000000..2009cd5e --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/route.dart @@ -0,0 +1,144 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMoviesDetailsRouter extends LunaPageRouter { + RadarrMoviesDetailsRouter() : super('/radarr/movie/:movieid'); + + @override + Future navigateTo(BuildContext context, { @required int movieId }) async => LunaRouter.router.navigateTo(context, route(movieId: movieId)); + + @override + String route({ @required int movieId }) => fullRoute.replaceFirst(':movieid', movieId.toString()); + + @override + void defineRoute(FluroRouter router) => router.define( + fullRoute, + handler: Handler(handlerFunc: (context, params) { + int movieid = params['movieid'] == null || params['movieid'].length == 0 ? -1 : (int.tryParse(params['movieid'][0]) ?? -1); + return _RadarrMoviesDetailsRoute(movieId: movieid); + }), + transitionType: LunaRouter.transitionType, + ); +} + +class _RadarrMoviesDetailsRoute extends StatefulWidget { + final int movieId; + + _RadarrMoviesDetailsRoute({ + Key key, + @required this.movieId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_RadarrMoviesDetailsRoute> with LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + RadarrMovie movie; + PageController _pageController; + + @override + Future loadCallback() async { + if(widget.movieId > 0) { + _findMovie(await context.read().movies); + context.read().fetchQualityProfiles(); + context.read().fetchTags(); + await context.read().resetSingleMovie(widget.movieId); + _findMovie(await context.read().movies); + } + } + + @override + void initState() { + super.initState(); + _pageController = PageController(initialPage: RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS.data); + } + + void _findMovie(List movies) { + RadarrMovie _movie = movies.firstWhere( + (movie) => movie.id == widget.movieId, + orElse: () => null, + ); + if(mounted) setState(() => movie = _movie); + } + + List _findTags(List tagIds, List tags) { + return tags.where((tag) => tagIds.contains(tag.id)).toList(); + } + + RadarrQualityProfile _findQualityProfile(int profileId, List profiles) { + return profiles.firstWhere( + (profile) => profile.id == profileId, + orElse: () => null, + ); + } + + @override + Widget build(BuildContext context) { + if(widget.movieId <= 0) return LunaInvalidRoute(title: 'Movie Details', message: 'Movie Not Found'); + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), + ); + } + + Widget _appBar() { + List _actions = movie == null ? null : [RadarrAppBarMovieSettingsAction(movieId: widget.movieId)]; + return LunaAppBar( + pageController: _pageController, + scrollControllers: RadarrMovieDetailsNavigationBar.scrollControllers, + title: 'Movie Details', + actions: _actions, + ); + } + + Widget _bottomNavigationBar() { + if(movie == null) return null; + return RadarrMovieDetailsNavigationBar(pageController: _pageController); + } + + Widget _body() { + return Consumer( + builder: (context, state, _) => FutureBuilder( + future: Future.wait([state.qualityProfiles, state.tags]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to pull Radarr movie details', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: loadCallback); + } + if(snapshot.hasData) { + if(movie == null) return LunaLoader(); + RadarrQualityProfile qualityProfile = _findQualityProfile(movie.qualityProfileId, snapshot.data[0]); + List tags = _findTags(movie.tags, snapshot.data[1]); + return _pages(qualityProfile, tags); + } + return LunaLoader(); + }, + ), + ); + } + + Widget _pages(RadarrQualityProfile qualityProfile, List tags) { + return ChangeNotifierProvider( + create: (context) => RadarrMovieDetailsState(context: context, movie: movie), + builder: (context, _) => PageView( + controller: _pageController, + children: [ + RadarrMovieDetailsOverviewPage(movie: movie, qualityProfile: qualityProfile, tags: tags), + RadarrMovieDetailsFilesPage(), + RadarrMovieDetailsHistoryPage(movie: movie), + RadarrMovieDetailsCastCrewPage(movie: movie), + ], + ), + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/state.dart b/lib/modules/radarr/modules/movie_details/state.dart new file mode 100644 index 00000000..94bd5cb6 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/state.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsState extends ChangeNotifier { + final RadarrMovie movie; + + RadarrMovieDetailsState({ + @required BuildContext context, + @required this.movie, + }) { + assert(context != null); + assert(movie != null); + fetchFiles(context); + fetchCredits(context); + fetchHistory(context); + } + + Future fetchHistory(BuildContext context) async { + RadarrState state = context.read(); + if(state.enabled) _history = state.api.history.getForMovie(movieId: movie.id); + notifyListeners(); + await _history; + } + + Future fetchCredits(BuildContext context) async { + RadarrState state = context.read(); + if(state.enabled) _credits = state.api.credits.get(movieId: movie.id); + notifyListeners(); + await _credits; + } + + Future fetchFiles(BuildContext context) async { + RadarrState state = context.read(); + if(state.enabled) { + _extraFiles = state.api.extraFile.get(movieId: movie.id); + _movieFiles = state.api.movieFile.get(movieId: movie.id); + } + notifyListeners(); + await Future.wait([ + _extraFiles, + _movieFiles, + ]); + } + + Future> _history; + Future> get history => _history; + + Future> _credits; + Future> get credits => _credits; + + Future> _extraFiles; + Future> get extraFiles => _extraFiles; + Future> _movieFiles; + Future> get movieFiles => _movieFiles; +} \ No newline at end of file diff --git a/lib/modules/radarr/modules/movie_details/widgets.dart b/lib/modules/radarr/modules/movie_details/widgets.dart new file mode 100644 index 00000000..c7d2e46b --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets.dart @@ -0,0 +1,13 @@ +export 'widgets/appbar_edit_action.dart'; +export 'widgets/cast_crew_tile.dart'; +export 'widgets/files_extra_file_block.dart'; +export 'widgets/files_file_block.dart'; +export 'widgets/navigation_bar.dart'; +export 'widgets/overview_download_buttons.dart'; +export 'widgets/overview_movie_description_tile.dart'; +export 'widgets/overview_movie_information_block.dart'; +export 'widgets/overview_movie_links_section.dart'; +export 'widgets/page_cast_crew.dart'; +export 'widgets/page_files.dart'; +export 'widgets/page_history.dart'; +export 'widgets/page_overview.dart'; diff --git a/lib/modules/radarr/modules/movie_details/widgets/appbar_edit_action.dart b/lib/modules/radarr/modules/movie_details/widgets/appbar_edit_action.dart new file mode 100644 index 00000000..f003b9f4 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/appbar_edit_action.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAppBarMovieSettingsAction extends StatelessWidget { + final int movieId; + + RadarrAppBarMovieSettingsAction({ + Key key, + @required this.movieId, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, _) => FutureBuilder( + future: state.movies, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) return Container(); + if(snapshot.hasData) { + RadarrMovie movie = snapshot.data.firstWhere((element) => element.id == movieId, orElse: () => null); + if(movie != null) return LunaIconButton( + icon: Icons.edit, + onPressed: () async { + Tuple2 values = await RadarrDialogs().movieSettings(context, movie); + if(values.item1) values.item2.execute(context, movie); + }, + ); + } + return Container(); + }, + ), + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/cast_crew_tile.dart b/lib/modules/radarr/modules/movie_details/widgets/cast_crew_tile.dart new file mode 100644 index 00000000..4f7d3455 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/cast_crew_tile.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsCastCrewTile extends StatelessWidget { + final RadarrMovieCredits credits; + + RadarrMovieDetailsCastCrewTile({ + Key key, + @required this.credits, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaTwoLineCardWithPoster( + title: credits.personName, + posterPlaceholder: 'assets/images/lidarr/noartistposter.png', + posterUrl: credits.images.length == 0 ? null : credits.images[0].url, + posterHeaders: {}, + subtitle1: TextSpan(text: credits.character ?? credits.job ?? LunaUI.TEXT_EMDASH), + subtitle2: TextSpan( + text: credits.type.readable, + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: credits.type == RadarrCreditType.CAST ? LunaColours.accent : LunaColours.orange, + ), + ), + onTap: credits.personTmdbId?.toString()?.lunaOpenTheMovieDBCredits, + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/files_extra_file_block.dart b/lib/modules/radarr/modules/movie_details/widgets/files_extra_file_block.dart new file mode 100644 index 00000000..0126f70d --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/files_extra_file_block.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsFilesExtraFileBlock extends StatelessWidget { + final RadarrExtraFile extraFile; + + RadarrMovieDetailsFilesExtraFileBlock({ + Key key, + @required this.extraFile, + }): super(key: key); + + @override + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'relative path', body: extraFile?.lunaRelativePath), + LunaTableContent(title: 'type', body: extraFile?.lunaType), + LunaTableContent(title: 'extension', body: extraFile?.lunaExtension), + ], + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/files_file_block.dart b/lib/modules/radarr/modules/movie_details/widgets/files_file_block.dart new file mode 100644 index 00000000..8a9e816e --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/files_file_block.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsFilesFileBlock extends StatefulWidget { + final RadarrMovieFile movieFile; + + RadarrMovieDetailsFilesFileBlock({ + Key key, + @required this.movieFile, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _deleteFileState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'relative path', body: widget.movieFile?.lunaRelativePath), + LunaTableContent(title: 'size', body: widget.movieFile?.lunaSize), + LunaTableContent(title: 'quality', body: widget.movieFile?.lunaQuality), + LunaTableContent(title: 'added on', body: widget.movieFile?.lunaDateAdded), + LunaTableContent(title: 'custom formats', body: widget.movieFile?.lunaCustomFormats), + ], + buttons: [ + if(widget.movieFile?.mediaInfo != null) LunaButton.slim( + text: 'Media Info', + onTap: () async => _viewMediaInfo(), + ), + LunaButton.slim( + text: 'Delete', + onTap: () async => _deleteFile(), + backgroundColor: LunaColours.red, + loadingState: _deleteFileState, + ), + ], + ); + } + + Future _deleteFile() async { + setState(() => _deleteFileState = LunaLoadingState.ACTIVE); + bool result = await RadarrDialogs().deleteMovieFile(context); + if(result) { + bool execute = await RadarrAPIHelper().deleteMovieFile(context: context, movieFile: widget.movieFile); + if(execute) context.read().fetchFiles(context); + } + setState(() => _deleteFileState = LunaLoadingState.INACTIVE); + } + + Future _viewMediaInfo() async { + LunaBottomModalSheet().showModal( + context: context, + builder: (context) => LunaListViewModal( + children: [ + LunaHeader(text: 'Audio'), + LunaTableCard( + content: [ + LunaTableContent(title: 'bitrate', body: [ + widget.movieFile.mediaInfo.audioBitrate?.lunaBytesToString(bytes: false) ?? Constants.TEXT_EMDASH, + if(widget.movieFile.mediaInfo.audioBitrate != null) '/s', + ].join()), + LunaTableContent(title: 'channels', body: widget.movieFile.mediaInfo.audioChannels?.toString() ?? Constants.TEXT_EMDASH), + LunaTableContent(title: 'codec', body: widget.movieFile.mediaInfo.audioCodec ?? Constants.TEXT_EMDASH), + LunaTableContent(title: 'features', body: widget.movieFile.mediaInfo.audioAdditionalFeatures ?? Constants.TEXT_EMDASH), + LunaTableContent(title: 'languages', body: widget.movieFile.mediaInfo.audioLanguages ?? Constants.TEXT_EMDASH), + 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: [ + LunaTableContent(title: 'runtime', body: widget.movieFile.mediaInfo.runTime ?? Constants.TEXT_EMDASH), + LunaTableContent(title: 'subtitles', body: widget.movieFile.mediaInfo.subtitles ?? Constants.TEXT_EMDASH), + ], + ), + ] + ), + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/navigation_bar.dart b/lib/modules/radarr/modules/movie_details/widgets/navigation_bar.dart new file mode 100644 index 00000000..9b89a575 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/navigation_bar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class RadarrMovieDetailsNavigationBar extends StatelessWidget { + static const List icons = [Icons.subject_rounded, Icons.insert_drive_file_outlined, Icons.history, Icons.person]; + static const List titles = ['Overview', 'Files', 'History', 'Cast & Crew']; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + final PageController pageController; + + RadarrMovieDetailsNavigationBar({ + Key key, + @required this.pageController, + }): super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/overview_download_buttons.dart b/lib/modules/radarr/modules/movie_details/widgets/overview_download_buttons.dart new file mode 100644 index 00000000..5055c7e4 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/overview_download_buttons.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsOverviewDownloadButtons extends StatefulWidget { + final RadarrMovie movie; + + RadarrMovieDetailsOverviewDownloadButtons({ + Key key, + @required this.movie, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _automaticLoadingState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) { + return LunaButtonContainer( + children: [ + LunaButton( + text: 'Automatic', + onTap: () async => _automatic(context), + loadingState: _automaticLoadingState, + ), + LunaButton( + text: 'Interactive', + backgroundColor: LunaColours.orange, + onTap: () async => _manual(context), + ), + ], + ); + } + + Future _automatic(BuildContext context) async { + setState(() => _automaticLoadingState = LunaLoadingState.ACTIVE); + RadarrAPIHelper().automaticSearch(context: context, movieId: widget.movie.id, title: widget.movie.title) + .then((value) { + if(mounted) setState(() { + _automaticLoadingState = value ? LunaLoadingState.INACTIVE : LunaLoadingState.ERROR; + }); + }); + } + + Future _manual(BuildContext context) async => RadarrReleasesRouter().navigateTo(context, movieId: widget.movie.id ?? -1); +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/overview_movie_description_tile.dart b/lib/modules/radarr/modules/movie_details/widgets/overview_movie_description_tile.dart new file mode 100644 index 00000000..312c0243 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/overview_movie_description_tile.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsOverviewDescriptionTile extends StatelessWidget { + final RadarrMovie movie; + + RadarrMovieDetailsOverviewDescriptionTile({ + Key key, + @required this.movie, + }) : super(key: key); + + Widget build(BuildContext context) { + return LunaFourLineCardWithPoster( + posterPlaceholder: 'assets/images/radarr/nomovieposter.png', + backgroundUrl: context.read().getFanartURL(movie.id), + posterUrl: context.read().getPosterURL(movie.id), + posterHeaders: context.read().headers, + title: movie.title, + subtitle1: TextSpan(text: movie.overview == null || movie.overview.isEmpty ? 'No summary is available.\n\n\n' : movie.overview + '\n\n\n'), + subtitle1MaxLines: 3, + onTap: () async => LunaDialogs().textPreview(context, movie.title, movie.overview), + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/overview_movie_information_block.dart b/lib/modules/radarr/modules/movie_details/widgets/overview_movie_information_block.dart new file mode 100644 index 00000000..1e93eac6 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/overview_movie_information_block.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsOverviewInformationBlock extends StatelessWidget { + final RadarrMovie movie; + final RadarrQualityProfile qualityProfile; + final List tags; + + RadarrMovieDetailsOverviewInformationBlock({ + Key key, + @required this.movie, + @required this.qualityProfile, + @required this.tags, + }): super(key: key); + + @override + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent(title: 'path', body: movie?.path), + LunaTableContent(title: 'quality', body: qualityProfile?.name), + LunaTableContent(title: 'availability', body: movie?.lunaMinimumAvailability), + LunaTableContent(title: 'status', body: movie?.status?.readable ?? Constants.TEXT_EMDASH), + LunaTableContent(title: 'tags', body: movie?.lunaTags(tags) ?? Constants.TEXT_EMDASH), + LunaTableContent(title: '', body: ''), + LunaTableContent(title: 'in cinemas', body: movie?.lunaInCinemasOn), + LunaTableContent(title: 'digital', body: movie?.lunaDigitalReleaseDate), + LunaTableContent(title: 'physical', body: movie?.lunaPhysicalReleaseDate), + LunaTableContent(title: 'added on', body: movie?.lunaDateAdded), + LunaTableContent(title: '', body: ''), + LunaTableContent(title: 'year', body: movie?.lunaYear), + LunaTableContent(title: 'studio', body: movie?.lunaStudio), + LunaTableContent(title: 'runtime', body: movie?.lunaRuntime), + LunaTableContent(title: 'rating', body: movie?.certification ?? Constants.TEXT_EMDASH), + LunaTableContent(title: 'genres', body: movie?.lunaGenres), + LunaTableContent(title: 'alternate titles', body: movie?.lunaAlternateTitles), + ], + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/overview_movie_links_section.dart b/lib/modules/radarr/modules/movie_details/widgets/overview_movie_links_section.dart new file mode 100644 index 00000000..9534b543 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/overview_movie_links_section.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsOverviewLinksSection extends StatelessWidget { + final RadarrMovie movie; + + RadarrMovieDetailsOverviewLinksSection({ + Key key, + @required this.movie, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaButtonContainer( + children: [ + if(movie.imdbId != null && movie.imdbId.isNotEmpty) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset( + 'assets/images/services/imdb.png', + height: 21.0, + ), + padding: EdgeInsets.all(18.0), + ), + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + onTap: () async => await movie?.imdbId?.lunaOpenIMDB(), + ), + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + if(movie.tmdbId != null && movie.tmdbId != 0) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset( + 'assets/images/services/themoviedb.png', + height: 22.0, + ), + padding: EdgeInsets.all(17.0), + ), + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + onTap: () async => await movie?.tmdbId?.toString()?.lunaOpenTheMovieDBMovie(), + ), + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + if(movie.youTubeTrailerId != null && movie.youTubeTrailerId.isNotEmpty) LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset( + 'assets/images/services/youtube.png', + height: 17.0, + ), + padding: EdgeInsets.all(20.0), + ), + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + onTap: () async => await movie?.youTubeTrailerId?.lunaOpenYouTube(), + ), + margin: EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/modules/radarr/modules/movie_details/widgets/page_cast_crew.dart b/lib/modules/radarr/modules/movie_details/widgets/page_cast_crew.dart new file mode 100644 index 00000000..98407b70 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/page_cast_crew.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsCastCrewPage extends StatefulWidget { + final RadarrMovie movie; + + RadarrMovieDetailsCastCrewPage({ + Key key, + @required this.movie, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchCredits(context), + child: FutureBuilder( + future: context.watch().credits, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + LunaLogger().error('Unable to fetch Radarr credit/crew list: ${widget.movie.id}', snapshot.error, snapshot.stackTrace); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, + ), + ); + } + + Widget _list(List credits) { + if((credits?.length ?? 0) == 0) return LunaMessage( + text: 'No Credits Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + List _cast = credits.where((credit) => credit.type == RadarrCreditType.CAST).toList(); + List _crew = credits.where((credit) => credit.type == RadarrCreditType.CREW).toList(); + return LunaListView( + scrollController: RadarrMovieDetailsNavigationBar.scrollControllers[3], + children: [ + ...List.generate(_cast.length, (index) => RadarrMovieDetailsCastCrewTile(credits: _cast[index])), + ...List.generate(_crew.length, (index) => RadarrMovieDetailsCastCrewTile(credits: _crew[index])), + ], + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/page_files.dart b/lib/modules/radarr/modules/movie_details/widgets/page_files.dart new file mode 100644 index 00000000..657d0eee --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/page_files.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsFilesPage extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchFiles(context), + child: FutureBuilder( + future: Future.wait([ + context.watch().movieFiles, + context.watch().extraFiles, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + LunaLogger().error('Unable to fetch Radarr files: ${context.read().movie.id}', snapshot.error, snapshot.stackTrace); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data[0], snapshot.data[1]); + return LunaLoader(); + }, + ), + ); + } + + Widget _list(List movieFiles, List extraFiles) { + if((movieFiles?.length ?? 0) == 0 && (extraFiles?.length ?? 0) == 0) return LunaMessage( + text: 'No Files Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListView( + scrollController: RadarrMovieDetailsNavigationBar.scrollControllers[1], + children: [ + if((movieFiles?.length ?? 0) > 0) ..._filesTiles(movieFiles), + if((extraFiles?.length ?? 0) > 0) ..._extraFilesTiles(extraFiles), + ], + ); + } + + List _filesTiles(List movieFiles) => List.generate( + movieFiles.length, + (index) => RadarrMovieDetailsFilesFileBlock(movieFile: movieFiles[index]), + ); + + List _extraFilesTiles(List extraFiles) => List.generate( + extraFiles.length, + (index) => RadarrMovieDetailsFilesExtraFileBlock(extraFile: extraFiles[index]), + ).toList(); +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/page_history.dart b/lib/modules/radarr/modules/movie_details/widgets/page_history.dart new file mode 100644 index 00000000..c2a0c937 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/page_history.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsHistoryPage extends StatefulWidget { + final RadarrMovie movie; + + RadarrMovieDetailsHistoryPage({ + Key key, + @required this.movie, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchHistory(context), + child: FutureBuilder( + future: context.watch().history, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + LunaLogger().error('Unable to fetch Radarr movie history: ${widget.movie.id}', snapshot.error, snapshot.stackTrace); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, + ), + ); + } + + Widget _list(List history) { + if((history?.length ?? 0) == 0) return LunaMessage( + text: 'No History Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + scrollController: RadarrMovieDetailsNavigationBar.scrollControllers[2], + itemCount: history.length, + itemBuilder: (context, index) => RadarrHistoryTile(history: history[index], movieHistory: true), + ); + } +} diff --git a/lib/modules/radarr/modules/movie_details/widgets/page_overview.dart b/lib/modules/radarr/modules/movie_details/widgets/page_overview.dart new file mode 100644 index 00000000..74b36f52 --- /dev/null +++ b/lib/modules/radarr/modules/movie_details/widgets/page_overview.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrMovieDetailsOverviewPage extends StatefulWidget { + final RadarrMovie movie; + final RadarrQualityProfile qualityProfile; + final List tags; + + RadarrMovieDetailsOverviewPage({ + Key key, + @required this.movie, + @required this.qualityProfile, + @required this.tags, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: Selector>>( + selector: (_, state) => state.movies, + builder: (context, movies, _) => LunaListView( + scrollController: RadarrMovieDetailsNavigationBar.scrollControllers[0], + children: [ + RadarrMovieDetailsOverviewDescriptionTile(movie: widget.movie), + RadarrMovieDetailsOverviewDownloadButtons(movie: widget.movie), + RadarrMovieDetailsOverviewInformationBlock( + movie: widget.movie, + qualityProfile: widget.qualityProfile, + tags: widget.tags, + ), + RadarrMovieDetailsOverviewLinksSection(movie: widget.movie), + ], + ), + ), + ); + } +} diff --git a/lib/modules/radarr/modules/queue.dart b/lib/modules/radarr/modules/queue.dart new file mode 100644 index 00000000..6a579cde --- /dev/null +++ b/lib/modules/radarr/modules/queue.dart @@ -0,0 +1 @@ +export 'queue/route.dart'; diff --git a/lib/modules/radarr/modules/queue/route.dart b/lib/modules/radarr/modules/queue/route.dart new file mode 100644 index 00000000..9d68d6e0 --- /dev/null +++ b/lib/modules/radarr/modules/queue/route.dart @@ -0,0 +1,36 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrQueueRouter extends LunaPageRouter { + RadarrQueueRouter() : super('/radarr/queue'); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrQueueRoute()); +} + +class _RadarrQueueRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State<_RadarrQueueRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: LunaMessage.goBack(context: context, text: 'Coming Soon'), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'Queue', + state: context.read(), + ); + } +} diff --git a/lib/modules/radarr/modules/radarr.dart b/lib/modules/radarr/modules/radarr.dart new file mode 100644 index 00000000..34ea0d77 --- /dev/null +++ b/lib/modules/radarr/modules/radarr.dart @@ -0,0 +1,2 @@ +export 'radarr/route.dart'; +export 'radarr/widgets.dart'; diff --git a/lib/modules/radarr/modules/radarr/route.dart b/lib/modules/radarr/modules/radarr/route.dart new file mode 100644 index 00000000..4b1b2217 --- /dev/null +++ b/lib/modules/radarr/modules/radarr/route.dart @@ -0,0 +1,92 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrHomeRouter extends LunaPageRouter { + RadarrHomeRouter() : super('/radarr'); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrHomeRoute()); +} + +class _RadarrHomeRoute extends StatefulWidget { + @override + State<_RadarrHomeRoute> createState() => _State(); +} + +class _State extends State<_RadarrHomeRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + LunaPageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = LunaPageController(initialPage: RadarrDatabaseValue.NAVIGATION_INDEX.data); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: _onWillPop, + child: ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [ LunaDatabaseValue.ENABLED_PROFILE.key ]), + builder: (context, box, _) => Scaffold( + key: _scaffoldKey, + drawer: _drawer(), + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), + ), + ), + ); + } + + Future _onWillPop() async { + if(_scaffoldKey.currentState.isDrawerOpen) return true; + _scaffoldKey.currentState.openDrawer(); + return false; + } + + Widget _drawer() => LSDrawer(page: 'radarr'); + + Widget _bottomNavigationBar() { + if(context.read().enabled) return RadarrNavigationBar(pageController: _pageController); + return null; + } + + Widget _appBar() { + List profiles = Database.profilesBox.keys.fold([], (value, element) { + if((Database.profilesBox.get(element) as ProfileHiveObject)?.radarrEnabled ?? false) value.add(element); + return value; + }); + List actions; + if(context.watch().enabled) actions = [ + RadarrAppBarAddMoviesAction(), + RadarrAppBarGlobalSettingsAction(), + ]; + return LunaAppBar.dropdown( + title: 'Radarr', + profiles: profiles, + actions: actions, + pageController: _pageController, + scrollControllers: RadarrNavigationBar.scrollControllers, + ); + } + + Widget _body() => Selector( + selector: (_, state) => state.enabled, + builder: (context, enabled, _) { + if(!enabled) return LunaMessage.moduleNotEnabled(context: context, module: 'Radarr'); + return PageView( + controller: _pageController, + children: [ + RadarrCatalogueRoute(), + RadarrUpcomingRoute(), + RadarrMissingRoute(), + RadarrMoreRoute(), + ], + ); + } + ); +} diff --git a/lib/modules/radarr/modules/radarr/widgets.dart b/lib/modules/radarr/modules/radarr/widgets.dart new file mode 100644 index 00000000..58f2cb4d --- /dev/null +++ b/lib/modules/radarr/modules/radarr/widgets.dart @@ -0,0 +1,3 @@ +export 'widgets/appbar_add_movie_action.dart'; +export 'widgets/appbar_global_settings_action.dart'; +export 'widgets/navigation_bar.dart'; diff --git a/lib/modules/radarr/modules/radarr/widgets/appbar_add_movie_action.dart b/lib/modules/radarr/modules/radarr/widgets/appbar_add_movie_action.dart new file mode 100644 index 00000000..60d02d8a --- /dev/null +++ b/lib/modules/radarr/modules/radarr/widgets/appbar_add_movie_action.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAppBarAddMoviesAction extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaIconButton( + icon: Icons.add, + onPressed: () async => RadarrAddMovieRouter().navigateTo(context), + ); + } +} diff --git a/lib/modules/radarr/modules/radarr/widgets/appbar_global_settings_action.dart b/lib/modules/radarr/modules/radarr/widgets/appbar_global_settings_action.dart new file mode 100644 index 00000000..4d2307fe --- /dev/null +++ b/lib/modules/radarr/modules/radarr/widgets/appbar_global_settings_action.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrAppBarGlobalSettingsAction extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LunaIconButton( + icon: Icons.more_vert, + onPressed: () async { + Tuple2 values = await RadarrDialogs().globalSettings(context); + if(values.item1) values.item2.execute(context); + }, + ); + } +} diff --git a/lib/modules/radarr/modules/radarr/widgets/navigation_bar.dart b/lib/modules/radarr/modules/radarr/widgets/navigation_bar.dart new file mode 100644 index 00000000..f2f489d2 --- /dev/null +++ b/lib/modules/radarr/modules/radarr/widgets/navigation_bar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class RadarrNavigationBar extends StatelessWidget { + final PageController pageController; + static const List icons = [CustomIcons.movies, CustomIcons.upcoming, CustomIcons.calendar_missing, Icons.more_horiz]; + static const List titles = ['Movies', 'Upcoming', 'Missing', 'More']; + static List scrollControllers = List.generate(titles.length, (_) => ScrollController()); + + RadarrNavigationBar({ + Key key, + @required this.pageController, + }): super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); + } +} diff --git a/lib/modules/radarr/modules/releases.dart b/lib/modules/radarr/modules/releases.dart new file mode 100644 index 00000000..50a4785c --- /dev/null +++ b/lib/modules/radarr/modules/releases.dart @@ -0,0 +1,3 @@ +export 'releases/route.dart'; +export 'releases/state.dart'; +export 'releases/widgets.dart'; diff --git a/lib/modules/radarr/modules/releases/route.dart b/lib/modules/radarr/modules/releases/route.dart new file mode 100644 index 00000000..f3156559 --- /dev/null +++ b/lib/modules/radarr/modules/releases/route.dart @@ -0,0 +1,121 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrReleasesRouter extends LunaPageRouter { + RadarrReleasesRouter() : super('/radarr/releases/:movieid'); + + @override + Future navigateTo(BuildContext context, { @required int movieId }) async => LunaRouter.router.navigateTo(context, route(movieId: movieId)); + + @override + String route({ @required int movieId }) => fullRoute.replaceFirst(':movieid', movieId.toString()); + + @override + void defineRoute(FluroRouter router) => router.define( + fullRoute, + handler: Handler(handlerFunc: (context, params) { + int movieId = params['movieid'] == null || params['movieid'].length == 0 ? -1 : (int.tryParse(params['movieid'][0]) ?? -1); + return _RadarrReleasesRoute(movieId: movieId); + }), + transitionType: LunaRouter.transitionType, + ); +} + +class _RadarrReleasesRoute extends StatefulWidget { + final int movieId; + + _RadarrReleasesRoute({ + Key key, + @required this.movieId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_RadarrReleasesRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + if(widget.movieId == null || widget.movieId <= 0) return LunaInvalidRoute(title: 'Releases', message: 'Movie Not Found'); + return ChangeNotifierProvider( + create: (context) => RadarrReleasesState(context, widget.movieId), + builder: (context, _) => Scaffold( + key: _scaffoldKey, + appBar: _appBar(context), + body: _body(context), + ), + ); + } + + Widget _appBar(BuildContext context) { + return LunaAppBar( + state: context.read(), + title: 'Releases', + bottom: RadarrReleasesSearchBar(scrollController: context.read().scrollController), + ); + } + + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async { + context.read().refreshReleases(context); + await context.read().releases; + }, + child: FutureBuilder( + future: context.read().releases, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Radarr releases: ${widget.movieId}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: () => _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(context, snapshot.data); + return LunaLoader(); + }, + ), + ); + } + + Widget _list(BuildContext context, List releases) { + return Consumer( + builder: (context, state, _) { + if((releases?.length ?? 0) == 0) return LunaMessage( + text: 'No Releases Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + List _processed = _filterAndSortReleases(releases ?? [], state); + return LunaListViewBuilder( + scrollController: context.read().scrollController, + itemCount: _processed.length == 0 ? 1 : _processed.length, + itemBuilder: (context, index) { + if((_processed?.length ?? 0) == 0) return LunaMessage.inList(text: 'No Releases Found'); + return RadarrReleasesTile(release: _processed[index]); + }, + ); + }, + ); + } + + List _filterAndSortReleases(List releases, RadarrReleasesState state) { + if(releases == null || releases.length == 0) return releases; + List filtered = releases.where((release) { + String _query = state.searchQuery; + if(_query != null && _query.isNotEmpty) return release.title.toLowerCase().contains(_query.toLowerCase()); + return release != null; + }).toList(); + filtered = state.filterType.filter(filtered); + filtered = state.sortType.sort(filtered, state.sortAscending); + return filtered; + } +} diff --git a/lib/modules/radarr/modules/releases/state.dart b/lib/modules/radarr/modules/releases/state.dart new file mode 100644 index 00000000..aae6d342 --- /dev/null +++ b/lib/modules/radarr/modules/releases/state.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrReleasesState extends ChangeNotifier { + Future> _releases; + int _movieId; + + RadarrReleasesState(BuildContext context, this._movieId) { + if(context.read().enabled) _releases = context.read().api.release.get(movieId: _movieId); + } + + Future> get releases => _releases; + void refreshReleases(BuildContext context) { + if(context.read().enabled) _releases = context.read().api.release.get(movieId: _movieId); + notifyListeners(); + } + + String _searchQuery = ''; + String get searchQuery => _searchQuery; + set searchQuery(String searchQuery) { + assert(searchQuery != null); + _searchQuery = searchQuery; + notifyListeners(); + } + + RadarrReleasesFilter _filterType = RadarrReleasesFilter.ALL; + RadarrReleasesFilter get filterType => _filterType; + set filterType(RadarrReleasesFilter filterType) { + assert(filterType != null); + _filterType = filterType; + notifyListeners(); + } + + RadarrReleasesSorting _sortType = RadarrDatabaseValue.DEFAULT_SORTING_RELEASES.data; + RadarrReleasesSorting get sortType => _sortType; + set sortType(RadarrReleasesSorting sortType) { + assert(sortType != null); + _sortType = sortType; + notifyListeners(); + } + + bool _sortAscending = RadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING.data; + bool get sortAscending => _sortAscending; + set sortAscending(bool sortAscending) { + assert(sortAscending != null); + _sortAscending = sortAscending; + notifyListeners(); + } +} diff --git a/lib/modules/radarr/modules/releases/widgets.dart b/lib/modules/radarr/modules/releases/widgets.dart new file mode 100644 index 00000000..893f13a5 --- /dev/null +++ b/lib/modules/radarr/modules/releases/widgets.dart @@ -0,0 +1,4 @@ +export 'widgets/appbar.dart'; +export 'widgets/appbar_filter_button.dart'; +export 'widgets/appbar_sort_button.dart'; +export 'widgets/release_tile.dart'; diff --git a/lib/modules/radarr/modules/releases/widgets/appbar.dart b/lib/modules/radarr/modules/releases/widgets/appbar.dart new file mode 100644 index 00000000..732099d9 --- /dev/null +++ b/lib/modules/radarr/modules/releases/widgets/appbar.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrReleasesSearchBar extends StatefulWidget implements PreferredSizeWidget { + final ScrollController scrollController; + + RadarrReleasesSearchBar({ + Key key, + @required this.scrollController, + }) : super(key: key); + + @override + Size get preferredSize => Size.fromHeight(62.0); + + @override + State createState() => _State(); +} + +class _State extends State { + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, _) => Row( + children: [ + Expanded( + child: LunaTextInputBar( + controller: _controller, + autofocus: false, + onChanged: (value) => context.read().searchQuery = value, + margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + ), + ), + RadarrReleasesAppBarFilterButton(controller: widget.scrollController), + RadarrReleasesAppBarSortButton(controller: widget.scrollController), + ], + ), + ); + } +} diff --git a/lib/modules/radarr/modules/releases/widgets/appbar_filter_button.dart b/lib/modules/radarr/modules/releases/widgets/appbar_filter_button.dart new file mode 100644 index 00000000..68e1284c --- /dev/null +++ b/lib/modules/radarr/modules/releases/widgets/appbar_filter_button.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrReleasesAppBarFilterButton extends StatefulWidget { + final ScrollController controller; + + RadarrReleasesAppBarFilterButton({ + Key key, + @required this.controller, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.filter_alt_outlined, + onSelected: (result) { + state.filterType = result; + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => List>.generate( + RadarrReleasesFilter.values.length, + (index) => PopupMenuItem( + value: RadarrReleasesFilter.values[index], + child: Text( + RadarrReleasesFilter.values[index].readable, + style: TextStyle( + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + color: state.filterType == RadarrReleasesFilter.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + ), + ), + ), + ), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + color: Theme.of(context).canvasColor, + ); + } +} diff --git a/lib/modules/radarr/modules/releases/widgets/appbar_sort_button.dart b/lib/modules/radarr/modules/releases/widgets/appbar_sort_button.dart new file mode 100644 index 00000000..513b7d67 --- /dev/null +++ b/lib/modules/radarr/modules/releases/widgets/appbar_sort_button.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrReleasesAppBarSortButton extends StatefulWidget { + final ScrollController controller; + + RadarrReleasesAppBarSortButton({ + Key key, + @required this.controller, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.sort, + onSelected: (result) { + if(state.sortType == result) { + state.sortAscending = !state.sortAscending; + } else { + state.sortAscending = true; + state.sortType = result; + } + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => List>.generate( + RadarrReleasesSorting.values.length, + (index) => PopupMenuItem( + value: RadarrReleasesSorting.values[index], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + RadarrReleasesSorting.values[index].readable, + style: TextStyle( + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + color: state.sortType == RadarrReleasesSorting.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + if(state.sortType == RadarrReleasesSorting.values[index]) Icon( + state.sortAscending + ? Icons.arrow_upward + : Icons.arrow_downward, + size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, + color: LunaColours.accent, + ), + ], + ), + ), + ), + ), + ), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 13.5), + color: Theme.of(context).canvasColor, + ); + } +} diff --git a/lib/modules/radarr/modules/releases/widgets/release_tile.dart b/lib/modules/radarr/modules/releases/widgets/release_tile.dart new file mode 100644 index 00000000..ee7b840a --- /dev/null +++ b/lib/modules/radarr/modules/releases/widgets/release_tile.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrReleasesTile extends StatefulWidget { + final RadarrRelease release; + + RadarrReleasesTile({ + @required this.release, + Key key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _downloadState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) => LunaExpandableListTile( + title: widget.release.title, + collapsedSubtitle1: _subtitle1(), + collapsedSubtitle2: _subtitle2(), + collapsedTrailing: _trailing(), + expandedHighlightedNodes: _highlightedNodes(), + expandedTableContent: _tableContent(), + expandedTableButtons: _tableButtons(), + ); + + Widget _trailing() { + return LunaIconButton( + icon: widget.release.lunaTrailingIcon, + color: widget.release.lunaTrailingColor, + onPressed: () async => widget.release.rejected ? _showWarnings() : _startDownload(), + onLongPress: _startDownload, + loadingState: _downloadState, + ); + } + + TextSpan _subtitle1() { + return TextSpan( + children: [ + TextSpan( + text: widget.release.lunaProtocol, + style: TextStyle( + color: widget.release.lunaProtocolColor, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.release.lunaIndexer), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.release.lunaAge), + ], + ); + } + + TextSpan _subtitle2() { + return TextSpan( + children: [ + TextSpan(text: widget.release.lunaQuality), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.release.lunaSize), + ], + ); + } + + List _highlightedNodes() { + return [ + LunaHighlightedNode( + text: widget.release.protocol.readable, + backgroundColor: widget.release.lunaProtocolColor, + ), + if((widget.release.customFormatScore ?? 0) > 0) LunaHighlightedNode(text: '+${widget.release.customFormatScore}', backgroundColor: LunaColours.orange), + ...widget.release.customFormats.map((custom) => LunaHighlightedNode(text: custom.name, backgroundColor: LunaColours.blueGrey)), + ]; + } + + List _tableContent() { + return [ + LunaTableContent(title: 'source', body: widget.release.protocol.readable), + LunaTableContent(title: 'age', body: widget.release.lunaAge), + LunaTableContent(title: 'indexer', body: widget.release.lunaIndexer), + LunaTableContent(title: 'size', body: widget.release.lunaSize), + LunaTableContent(title: 'language', body: widget.release.languages?.map((language) => language?.name ?? LunaUI.TEXT_EMDASH)?.join('\n') ?? LunaUI.TEXT_EMDASH), + LunaTableContent(title: 'quality', body: widget.release.lunaQuality), + if(widget.release.seeders != null) LunaTableContent(title: 'seeders', body: '${widget.release.seeders}'), + if(widget.release.leechers != null) LunaTableContent(title: 'leechers', body: '${widget.release.leechers}'), + ]; + } + + List _tableButtons() { + return [ + LunaButton.slim( + text: 'Download', + onTap: _startDownload, + loadingState: _downloadState, + ), + if(widget.release.rejected) LunaButton.slim( + text: 'Rejected', + backgroundColor: LunaColours.red, + onTap: _showWarnings, + ), + ]; + } + + Future _startDownload() async { + setState(() => _downloadState = LunaLoadingState.ACTIVE); + RadarrAPIHelper().pushRelease(context: context, release: widget.release) + .then((value) { + if(mounted) setState(() => _downloadState = value ? LunaLoadingState.INACTIVE : LunaLoadingState.ERROR); + }); + } + + Future _showWarnings() async { + String rejections = ''; + for(var i=0; i super.noParameterRouteDefinition(router, _RadarrSystemStatusRoute()); +} + +class _RadarrSystemStatusRoute extends StatefulWidget { + @override + State<_RadarrSystemStatusRoute> createState() => _State(); +} + +class _State extends State<_RadarrSystemStatusRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + LunaPageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = LunaPageController(initialPage: RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS.data); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'System Status', + pageController: _pageController, + scrollControllers: RadarrSystemStatusNavigationBar.scrollControllers, + ); + } + + Widget _bottomNavigationBar() => RadarrSystemStatusNavigationBar(pageController: _pageController); + + Widget _body() { + return ChangeNotifierProvider( + create: (context) => RadarrSystemStatusState(context), + builder: (context, _) => PageView( + controller: _pageController, + children: [ + RadarrSystemStatusAboutPage(scrollController: RadarrSystemStatusNavigationBar.scrollControllers[0]), + RadarrSystemStatusDiskSpacePage(scrollController: RadarrSystemStatusNavigationBar.scrollControllers[1]), + ], + ), + ); + } +} diff --git a/lib/modules/radarr/modules/system_status/state.dart b/lib/modules/radarr/modules/system_status/state.dart new file mode 100644 index 00000000..b9f03ee0 --- /dev/null +++ b/lib/modules/radarr/modules/system_status/state.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrSystemStatusState extends ChangeNotifier { + RadarrSystemStatusState(BuildContext context) { + assert(context != null); + fetchStatus(context); + fetchDiskSpace(context); + } + + Future fetchStatus(BuildContext context) async { + RadarrState state = context.read(); + if(state.enabled) _status = state.api.system.status(); + notifyListeners(); + await _status; + } + + Future fetchDiskSpace(BuildContext context) async { + RadarrState state = context.read(); + if(state.enabled) _diskSpace = state.api.diskSpace.getAll(); + notifyListeners(); + await _diskSpace; + } + + Future _status; + Future get status => _status; + + Future> _diskSpace; + Future> get diskSpace => _diskSpace; +} diff --git a/lib/modules/radarr/modules/system_status/widgets.dart b/lib/modules/radarr/modules/system_status/widgets.dart new file mode 100644 index 00000000..a67041da --- /dev/null +++ b/lib/modules/radarr/modules/system_status/widgets.dart @@ -0,0 +1,3 @@ +export 'widgets/navigation_bar.dart'; +export 'widgets/page_about.dart'; +export 'widgets/page_disk_space.dart'; diff --git a/lib/modules/radarr/modules/system_status/widgets/navigation_bar.dart b/lib/modules/radarr/modules/system_status/widgets/navigation_bar.dart new file mode 100644 index 00000000..09c001c3 --- /dev/null +++ b/lib/modules/radarr/modules/system_status/widgets/navigation_bar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class RadarrSystemStatusNavigationBar extends StatelessWidget { + final PageController pageController; + static const List icons = [Icons.subject_rounded, Icons.donut_large_rounded]; + static const List titles = ['About', 'Disk Space']; + static List scrollControllers = List.generate(icons.length, (_) => ScrollController()); + + RadarrSystemStatusNavigationBar({ + Key key, + @required this.pageController, + }): super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); + } +} diff --git a/lib/modules/radarr/modules/system_status/widgets/page_about.dart b/lib/modules/radarr/modules/system_status/widgets/page_about.dart new file mode 100644 index 00000000..1b5ec20c --- /dev/null +++ b/lib/modules/radarr/modules/system_status/widgets/page_about.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrSystemStatusAboutPage extends StatefulWidget { + final ScrollController scrollController; + + RadarrSystemStatusAboutPage({ + Key key, + @required this.scrollController, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchStatus(context), + child: FutureBuilder( + future: context.watch().status, + builder: (context, AsyncSnapshot snapshot) { + if(snapshot.hasError) { + LunaLogger().error('Unable to fetch Radarr system status', snapshot.error, snapshot.stackTrace); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, + ), + ); + } + + Widget _list(RadarrSystemStatus status) { + return LunaListView( + scrollController: widget.scrollController, + children: [ + LunaTableCard( + content: [ + LunaTableContent(title: 'Version', body: status.lunaVersion), + LunaTableContent(title: 'Package', body: status.lunaPackageVersion), + LunaTableContent(title: '.NET Core', body: status.lunaNetCore), + LunaTableContent(title: 'Docker', body: status.lunaDocker), + LunaTableContent(title: 'Migration', body: status.lunaDBMigration), + LunaTableContent(title: 'AppData', body: status.lunaAppDataDirectory), + LunaTableContent(title: 'Startup', body: status.lunaStartupDirectory), + LunaTableContent(title: 'mode', body: status.lunaMode), + LunaTableContent(title: 'uptime', body: status.lunaUptime), + ], + ), + ], + ); + } +} diff --git a/lib/modules/radarr/modules/system_status/widgets/page_disk_space.dart b/lib/modules/radarr/modules/system_status/widgets/page_disk_space.dart new file mode 100644 index 00000000..8b455ff7 --- /dev/null +++ b/lib/modules/radarr/modules/system_status/widgets/page_disk_space.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrSystemStatusDiskSpacePage extends StatefulWidget { + final ScrollController scrollController; + + RadarrSystemStatusDiskSpacePage({ + Key key, + @required this.scrollController, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => context.read().fetchDiskSpace(context), + child: FutureBuilder( + future: context.watch().diskSpace, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + LunaLogger().error('Unable to fetch Radarr disk space', snapshot.error, snapshot.stackTrace); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, + ), + ); + } + + Widget _list(List diskSpace) { + if((diskSpace?.length ?? 0) == 0) return LunaMessage( + text: 'No Disks Found', + buttonText: 'Try Again', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + scrollController: widget.scrollController, + itemCount: diskSpace.length, + itemBuilder: (context, index) { + int _percentNumerator = diskSpace[index].freeSpace; + int _percentDenominator = diskSpace[index].totalSpace; + int _percent; + Color percentColor = LunaColours.accent; + if(_percentNumerator != null && _percentDenominator != null || _percentDenominator != 0) { + int _val = ((_percentNumerator/_percentDenominator)*100).round(); + _percent = (_val-100).abs(); + if(_percent >= 70) percentColor = LunaColours.orange; + if(_percent >= 90) percentColor = LunaColours.red; + } + return LunaListTile( + context: context, + title: LunaText.title(text: diskSpace[index].path), + subtitle: LunaText.subtitle( + text: [ + diskSpace[index].freeSpace?.lunaBytesToString() ?? LunaUI.TEXT_EMDASH, + diskSpace[index].totalSpace?.lunaBytesToString() ?? LunaUI.TEXT_EMDASH, + ].join(' / '), + ), + trailing: LunaIconButton( + text: _percent == null ? LunaUI.TEXT_EMDASH : '$_percent%', + color: percentColor, + textSize: 11.0, + ), + ); + }, + ); + } +} diff --git a/lib/modules/radarr/modules/tags.dart b/lib/modules/radarr/modules/tags.dart new file mode 100644 index 00000000..3b3bba4d --- /dev/null +++ b/lib/modules/radarr/modules/tags.dart @@ -0,0 +1,2 @@ +export 'tags/route.dart'; +export 'tags/widgets.dart'; diff --git a/lib/modules/radarr/modules/tags/route.dart b/lib/modules/radarr/modules/tags/route.dart new file mode 100644 index 00000000..8778de1b --- /dev/null +++ b/lib/modules/radarr/modules/tags/route.dart @@ -0,0 +1,83 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrTagsRouter extends LunaPageRouter { + RadarrTagsRouter() : super('/radarr/tags'); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _RadarrTagsRoute()); +} + +class _RadarrTagsRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State<_RadarrTagsRoute> { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); + } + + Future _refresh() async { + context.read().fetchTags(); + await context.read().tags; + } + + @override + Widget build(BuildContext context) => Scaffold( + key: _scaffoldKey, + appBar: _appBar, + body: _body, + ); + + Widget get _appBar => LunaAppBar( + title: 'Tags', + actions: [RadarrTagsAppBarActionAddTag()], + state: context.read(), + ); + + Widget get _body { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: FutureBuilder( + future: context.watch().tags, + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error('Unable to fetch Radarr tags', snapshot.error, snapshot.stackTrace); + } + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data); + return LunaLoader(); + }, + ), + ); + } + + Widget _list(List tags) { + if((tags?.length ?? 0) == 0) return LunaMessage( + text: 'No Tags Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + scrollController: context.read().scrollController, + itemCount: tags.length, + itemBuilder: (context, index) => RadarrTagsTagTile( + key: ObjectKey(tags[index].id), + tag: tags[index], + ), + ); + } +} diff --git a/lib/modules/radarr/modules/tags/widgets.dart b/lib/modules/radarr/modules/tags/widgets.dart new file mode 100644 index 00000000..913899a3 --- /dev/null +++ b/lib/modules/radarr/modules/tags/widgets.dart @@ -0,0 +1,2 @@ +export 'widgets/appbar_action_add_tag.dart'; +export 'widgets/tag_tile.dart'; diff --git a/lib/modules/radarr/modules/tags/widgets/appbar_action_add_tag.dart b/lib/modules/radarr/modules/tags/widgets/appbar_action_add_tag.dart new file mode 100644 index 00000000..4832e510 --- /dev/null +++ b/lib/modules/radarr/modules/tags/widgets/appbar_action_add_tag.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrTagsAppBarActionAddTag extends StatelessWidget { + final bool asDialogButton; + + RadarrTagsAppBarActionAddTag({ + Key key, + this.asDialogButton = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if(asDialogButton) return LSDialog.button( + text: 'Add', + textColor: Colors.white, + onPressed: () async => _onPressed(context), + ); + return LunaIconButton( + icon: Icons.add, + onPressed: () async => _onPressed(context), + ); + } + + Future _onPressed(BuildContext context) async { + Tuple2 values = await RadarrDialogs().addNewTag(context); + if(values.item1) RadarrAPIHelper().addTag(context: context, label: values.item2) + .then((value) { + if(value) context.read().fetchTags(); + }); + } +} diff --git a/lib/modules/radarr/modules/tags/widgets/tag_tile.dart b/lib/modules/radarr/modules/tags/widgets/tag_tile.dart new file mode 100644 index 00000000..44e9322b --- /dev/null +++ b/lib/modules/radarr/modules/tags/widgets/tag_tile.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrTagsTagTile extends StatefulWidget { + final RadarrTag tag; + + RadarrTagsTagTile({ + Key key, + @required this.tag, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with LunaLoadCallbackMixin { + List movieList; + + Future loadCallback() async { + await context.read().movies.then((movies) { + List _movies = []; + movies.forEach((element) { + if(element.tags.contains(widget.tag.id)) _movies.add(element.title); + }); + _movies.sort(); + if(mounted) setState(() => movieList = _movies); + }) + .catchError((_) { + if(mounted) setState(() => movieList = null); + }); + } + + @override + Widget build(BuildContext context) { + return LunaListTile( + context: context, + title: LunaText.title(text: widget.tag.label), + subtitle: LunaText.subtitle(text: _subtitle()), + trailing: _trailing(), + onTap: _movieDialog, + ); + } + + String _subtitle() { + if(movieList == null) return 'Loading...'; + if(movieList.length == 0) return 'No Movies'; + if(movieList.length == 1) return '1 Movie'; + return '${movieList.length} Movies'; + } + + Widget _trailing() { + if(movieList == null || movieList.length != 0) return null; + return LunaIconButton( + icon: Icons.delete, + color: LunaColours.red, + onPressed: _delete, + ); + } + + Future _movieDialog() async { + String data = movieList == null || movieList.length == 0 ? 'No Movies' : movieList.join('\n'); + LunaDialogs().textPreview(context, 'Movie List', data); + } + + Future _delete() async { + if(movieList == null || movieList.length != 0) { + showLunaErrorSnackBar( + context: context, + title: 'Cannot Delete Tag', + message: 'The tag must not be attached to any movies', + ); + } else { + bool result = await RadarrDialogs().deleteTag(context); + if(result) RadarrAPIHelper().deleteTag(context: context, tag: widget.tag) + .then((value) { + if(value) context.read().fetchTags(); + }); + } + } +} diff --git a/lib/modules/radarr/modules/upcoming.dart b/lib/modules/radarr/modules/upcoming.dart new file mode 100644 index 00000000..be57f945 --- /dev/null +++ b/lib/modules/radarr/modules/upcoming.dart @@ -0,0 +1,2 @@ +export 'upcoming/route.dart'; +export 'upcoming/widgets.dart'; diff --git a/lib/modules/radarr/modules/upcoming/route.dart b/lib/modules/radarr/modules/upcoming/route.dart new file mode 100644 index 00000000..32bed7f8 --- /dev/null +++ b/lib/modules/radarr/modules/upcoming/route.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; +import 'package:tuple/tuple.dart'; + +class RadarrUpcomingRoute extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Future _refresh() async { + RadarrState _state = context.read(); + _state.fetchMovies(); + _state.fetchQualityProfiles(); + await Future.wait([ + _state.upcoming, + _state.qualityProfiles, + ]); + } + + Widget get _body => LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: Selector>, Future>>>( + selector: (_, state) => Tuple2(state.upcoming, state.qualityProfiles), + builder: (context, tuple, _) => FutureBuilder( + future: Future.wait([tuple.item1, tuple.item2]), + builder: (context, AsyncSnapshot> snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error( + 'Unable to fetch Radarr upcoming', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState.show); + } + if(snapshot.hasData) return _list(snapshot.data[0], snapshot.data[1]); + return LunaLoader(); + }, + ), + ), + ); + + Widget _list( + List movies, + List qualityProfiles, + ) { + if((movies?.length ?? 0) == 0) return LunaMessage( + text: 'No Movies Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState.show, + ); + return LunaListViewBuilder( + scrollController: RadarrNavigationBar.scrollControllers[1], + itemCount: movies.length, + itemBuilder: (context, index) => RadarrUpcomingTile( + movie: movies[index], + profile: qualityProfiles.firstWhere((element) => element.id == movies[index].qualityProfileId, orElse: null), + ), + ); + } +} diff --git a/lib/modules/radarr/modules/upcoming/widgets.dart b/lib/modules/radarr/modules/upcoming/widgets.dart new file mode 100644 index 00000000..e1c776b4 --- /dev/null +++ b/lib/modules/radarr/modules/upcoming/widgets.dart @@ -0,0 +1 @@ +export 'widgets/upcoming_tile.dart'; diff --git a/lib/modules/radarr/modules/upcoming/widgets/upcoming_tile.dart b/lib/modules/radarr/modules/upcoming/widgets/upcoming_tile.dart new file mode 100644 index 00000000..92bd52da --- /dev/null +++ b/lib/modules/radarr/modules/upcoming/widgets/upcoming_tile.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrUpcomingTile extends StatefulWidget { + final RadarrMovie movie; + final RadarrQualityProfile profile; + + RadarrUpcomingTile({ + Key key, + @required this.movie, + @required this.profile, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Selector>>( + selector: (_, state) => state.missing, + builder: (context, missing, _) => LunaFourLineCardWithPoster( + backgroundUrl: context.read().getPosterURL(widget.movie.id), + posterUrl: context.read().getPosterURL(widget.movie.id), + posterHeaders: context.read().headers, + posterPlaceholder: 'assets/images/radarr/nomovieposter.png', + darken: !widget.movie.monitored, + title: widget.movie.title, + subtitle1: _subtitle1(), + subtitle2: _subtitle2(), + subtitle3: _subtitle3(), + trailing: _trailing(), + onTap: _onTap, + ), + ); + } + + TextSpan _subtitle1() { + return TextSpan( + children: [ + TextSpan(text: widget.movie.lunaYear), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaRuntime), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaStudio), + ], + ); + } + + TextSpan _subtitle2() { + return TextSpan( + children: [ + TextSpan(text: widget.profile.lunaName), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaMinimumAvailability), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.movie.lunaReleaseDate), + ], + ); + } + + TextSpan _subtitle3() { + Color color; + String _days; + String type; + if(widget.movie.lunaIsInCinemas && !widget.movie.lunaIsReleased) { + color = widget.movie.monitored ? LunaColours.blue : LunaColours.blue.withOpacity(0.30); + _days = widget.movie.lunaEarlierReleaseDate.lunaDaysDifference; + type = 'release'; + } else if(!widget.movie.lunaIsInCinemas && !widget.movie.lunaIsReleased) { + color = widget.movie.monitored ? LunaColours.orange : LunaColours.orange.withOpacity(0.30); + _days = widget.movie.inCinemas.lunaDaysDifference; + type = 'cinema'; + } else { + color = widget.movie.monitored ? Colors.white70 : Colors.white30; + _days = LunaUI.TEXT_EMDASH; + type = 'unknown'; + } + return TextSpan( + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: color, + ), + children: [ + if(type == 'release') TextSpan(text: _days == null ? 'Availability Unknown' : _days == 'Today' ? 'Available Today' : 'Available in $_days'), + if(type == 'cinema') TextSpan(text: _days == null ? 'Cinema Date Unknown' : _days == 'Today' ? 'In Cinemas Today' : 'In Cinemas in $_days'), + if(type == 'unknown') TextSpan(text: _days), + ], + ); + } + + LunaIconButton _trailing() { + return LunaIconButton( + icon: Icons.search, + onPressed: () async => RadarrAPIHelper().automaticSearch(context: context, movieId: widget.movie.id, title: widget.movie.title), + onLongPress: () async => RadarrReleasesRouter().navigateTo(context, movieId: widget.movie.id), + ); + } + + Future _onTap() async => RadarrMoviesDetailsRouter().navigateTo(context, movieId: widget.movie.id); +} diff --git a/lib/modules/radarr/routes.dart b/lib/modules/radarr/routes.dart deleted file mode 100644 index e139e5ef..00000000 --- a/lib/modules/radarr/routes.dart +++ /dev/null @@ -1,10 +0,0 @@ -export 'routes/radarr.dart'; -export 'routes/catalogue.dart'; -export 'routes/history.dart'; -export 'routes/missing.dart'; -export 'routes/upcoming.dart'; -export 'routes/add_search.dart'; -export 'routes/add_details.dart'; -export 'routes/details_movie.dart'; -export 'routes/edit_movie.dart'; -export 'routes/search_results.dart'; diff --git a/lib/modules/radarr/routes/add_details.dart b/lib/modules/radarr/routes/add_details.dart deleted file mode 100644 index 163af27a..00000000 --- a/lib/modules/radarr/routes/add_details.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrAddDetailsArguments { - final RadarrSearchData data; - - RadarrAddDetailsArguments({ - @required this.data, - }); -} - -class RadarrAddDetails extends StatefulWidget { - static const ROUTE_NAME = '/radarr/add/details'; - - @override - State createState() => _State(); -} - -class _State extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - RadarrAddDetailsArguments _arguments; - Future _future; - List _rootFolders = []; - List _qualityProfiles = []; - - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - setState(() => _arguments = ModalRoute.of(context).settings.arguments); - _refresh(); - }); - } - - void _refresh() => setState(() { - _future = _fetchParameters(); - }); - - Future _fetchParameters() async { - RadarrAPI _api = RadarrAPI.from(Database.currentProfileObject); - return _fetchRootFolders(_api) - .then((_) => _fetchQualityProfiles(_api)) - .then((_) => _fetchAvailability()) - .then((_) {}) - .catchError((error) => error); - } - - Future _fetchRootFolders(RadarrAPI api) async { - return await api.getRootFolders() - .then((values) { - RadarrRootFolder _rootfolder = RadarrDatabaseValue.ADD_ROOT_FOLDER.data; - _rootFolders = values; - int index = _rootFolders.indexWhere((value) => - value.id == _rootfolder?.id && - value.path == _rootfolder?.path - ); - RadarrDatabaseValue.ADD_ROOT_FOLDER.put(index != -1 ? _rootFolders[index] : _rootFolders[0]); - }) - .catchError((error) => error); - } - - Future _fetchQualityProfiles(RadarrAPI api) async { - return await api.getQualityProfiles() - .then((values) { - RadarrQualityProfile _profile = RadarrDatabaseValue.ADD_QUALITY_PROFILE.data; - _qualityProfiles = values.values.toList(); - int index = _qualityProfiles.indexWhere((value) => - value.id == _profile?.id && - value.name == _profile?.name - ); - RadarrDatabaseValue.ADD_QUALITY_PROFILE.put(index != -1 ? _qualityProfiles[index] : _qualityProfiles[0]); - }) - .catchError((error) => error); - } - - Future _fetchAvailability() async { - RadarrAvailability _availability = RadarrDatabaseValue.ADD_AVAILABILITY.data; - int index = RadarrConstants.MINIMUM_AVAILBILITIES.indexWhere((value) => - value.id == _availability?.id, - ); - RadarrDatabaseValue.ADD_AVAILABILITY.put(index != -1 ? RadarrConstants.MINIMUM_AVAILBILITIES[index] : RadarrConstants.MINIMUM_AVAILBILITIES[1]); - } - - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => _arguments == null - ? null - : LunaAppBar( - context: context, - title: _arguments.data.title, - actions: [ - LSIconButton( - icon: Icons.link, - onPressed: () async => _arguments.data.tmdbId == 0 - ? LSSnackBar( - context: context, - title: 'No TMDB Page Available', - message: 'No TMDB URL is available', - ) - : _arguments.data.tmdbId.toString().lunaOpenTheMovieDB(), - ) - ], - ); - - Widget get _body => _arguments == null - ? null - : FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError) return LSErrorMessage(onTapHandler: () => _refresh()); - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ); - - Widget get _list => LSListView( - children: [ - LSDescriptionBlock( - title: _arguments.data.title ?? 'Unknown', - description: _arguments.data.overview == '' - ? 'No summary is available.' - : _arguments.data.overview, - uri: _arguments.data.posterURI ?? '', - fallbackImage: 'assets/images/radarr/nomovieposter.png', - headers: Database.currentProfileObject.getRadarr()['headers'], - ), - ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.ADD_MONITORED.key]), - builder: (context, box, widget) { - return LSCardTile( - title: LSTitle(text: 'Monitored'), - subtitle: LSSubtitle(text: 'Monitor movie for new releases'), - trailing: Switch( - value: RadarrDatabaseValue.ADD_MONITORED.data, - onChanged: (value) => RadarrDatabaseValue.ADD_MONITORED.put(value), - ), - ); - }, - ), - ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.ADD_ROOT_FOLDER.key]), - builder: (context, box, widget) { - RadarrRootFolder _rootfolder = RadarrDatabaseValue.ADD_ROOT_FOLDER.data; - return LSCardTile( - title: LSTitle(text: 'Root Folder'), - subtitle: LSSubtitle(text: _rootfolder?.path ?? 'Unknown Root Folder'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: () async { - List _values = await RadarrDialogs.editRootFolder(context, _rootFolders); - if(_values[0]) RadarrDatabaseValue.ADD_ROOT_FOLDER.put(_values[1]); - }, - ); - }, - ), - ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.ADD_QUALITY_PROFILE.key]), - builder: (context, box, widget) { - RadarrQualityProfile _profile = RadarrDatabaseValue.ADD_QUALITY_PROFILE.data; - return LSCardTile( - title: LSTitle(text: 'Quality Profile'), - subtitle: LSSubtitle(text: _profile?.name ?? 'Unknown Profile'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: () async { - List _values = await RadarrDialogs.editQualityProfile(context, _qualityProfiles); - if(_values[0]) RadarrDatabaseValue.ADD_QUALITY_PROFILE.put(_values[1]); - }, - ); - }, - ), - ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.ADD_AVAILABILITY.key]), - builder: (context, box, widget) { - RadarrAvailability _availability = RadarrDatabaseValue.ADD_AVAILABILITY.data; - return LSCardTile( - title: LSTitle(text: 'Minimum Availability'), - subtitle: LSSubtitle(text: _availability?.name ?? 'Unknown Availability'), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: () async { - List _values = await RadarrDialogs.editMinimumAvailability(context, RadarrConstants.MINIMUM_AVAILBILITIES); - if(_values[0]) RadarrDatabaseValue.ADD_AVAILABILITY.put(_values[1]); - }, - ); - }, - ), - LSContainerRow( - children: [ - Expanded( - child: LSButton( - text: 'Add', - onTap: () async => _addMovie(false), - reducedMargin: true, - ), - ), - Expanded( - child: LSButton( - text: 'Add + Search', - backgroundColor: LunaColours.orange, - onTap: () async => _addMovie(true), - reducedMargin: true, - ), - ), - ], - ), - ], - ); - - Future _addMovie(bool search) async { - RadarrAPI _api = RadarrAPI.from(Database.currentProfileObject); - await _api.addMovie( - _arguments.data, - RadarrDatabaseValue.ADD_QUALITY_PROFILE.data, - RadarrDatabaseValue.ADD_ROOT_FOLDER.data, - RadarrDatabaseValue.ADD_AVAILABILITY.data, - RadarrDatabaseValue.ADD_MONITORED.data ?? true, - search: search, - ) - .then((id) => Navigator.of(context).pop(['movie_added', _arguments.data.title, id])) - .catchError((_) => LSSnackBar(context: context, title: search ? 'Failed to Add Movie (With Search)' : 'Failed to Add Movie', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - } -} diff --git a/lib/modules/radarr/routes/add_search.dart b/lib/modules/radarr/routes/add_search.dart deleted file mode 100644 index b9bd3ecc..00000000 --- a/lib/modules/radarr/routes/add_search.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrAddSearch extends StatefulWidget { - static const ROUTE_NAME = '/radarr/add/search'; - - @override - State createState() => _State(); -} - -class _State extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - final GlobalKey _refreshKey = GlobalKey(); - final _scrollController = ScrollController(); - Future> _future; - List _results; - List _availableIDs = []; - - @override - void initState() { - super.initState(); - _fetchAvailableMovies(); - } - - Future _fetchAvailableMovies() async { - await RadarrAPI.from(Database.currentProfileObject).getAllMovieIDs() - .then((data) => _availableIDs = data) - .catchError((_) => _availableIDs = []); - } - - Future _refresh() async { - final _model = Provider.of(context, listen: false); - final _api = RadarrAPI.from(Database.currentProfileObject); - setState(() { - _future = _api.searchMovies(_model.addSearchQuery); - }); - } - - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar( - context: context, - title: 'Add Movie', - ); - - Widget get _body => LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: () => _refresh(), - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - List _data; - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) { - _data = _error; - } else { - _results = snapshot.data; - _data = _assembleResults; - } - break; - } - case ConnectionState.none: _data = []; break; - case ConnectionState.waiting: - case ConnectionState.active: - default: _data = _loading; break; - } - return _list(_data); - }, - ), - ); - - Widget _list(List data) => LSListViewStickyHeader( - controller: _scrollController, - slivers: [ - LunaSliverStickyHeader( - header: _searchBar, - children: data, - ), - ], - ); - - Widget get _searchBar => LSContainerRow( - padding: EdgeInsets.zero, - backgroundColor: Theme.of(context).primaryColor, - children: [ - RadarrAddSearchBar(callback: _refresh), - ], - ); - - List get _loading => [LSTypewriterMessage(text: 'Searching...')]; - - List get _error => [LSErrorMessage(onTapHandler: () => _refresh(), hideButton: true)]; - - List get _assembleResults => _results.length > 0 - ? List.generate( - _results.length, - (index) => RadarrAddSearchResultTile( - data: _results[index], - alreadyAdded: _availableIDs.contains(_results[index].tmdbId), - ), - ) - : [LSGenericMessage(text: 'No Results Found')]; -} diff --git a/lib/modules/radarr/routes/catalogue.dart b/lib/modules/radarr/routes/catalogue.dart deleted file mode 100644 index 376171be..00000000 --- a/lib/modules/radarr/routes/catalogue.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrCatalogue extends StatefulWidget { - static const ROUTE_NAME = '/radarr/catalogue'; - final GlobalKey refreshIndicatorKey; - final Function refreshAllPages; - - RadarrCatalogue({ - Key key, - @required this.refreshIndicatorKey, - @required this.refreshAllPages, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - final _scaffoldKey = GlobalKey(); - final _scrollController = ScrollController(); - Future> _future; - List _results = []; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - _refresh(); - } - - Future _refresh() async { - if(mounted) setState(() => _results = []); - final _api = RadarrAPI.from(Database.currentProfileObject); - if(mounted) setState(() => { _future = _api.getAllMovies() }); - //Clear the search filter using a microtask - Future.microtask(() => Provider.of(context, listen: false)?.searchCatalogueFilter = ''); - } - - void _refreshState() => setState(() {}); - - void _refreshAllPages() => widget.refreshAllPages(); - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - key: _scaffoldKey, - body: _body, - ); - } - - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: () => _refresh(), - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); - - Widget get _searchSortBar => LSContainerRow( - padding: EdgeInsets.zero, - backgroundColor: Theme.of(context).primaryColor, - children: [ - RadarrCatalogueSearchBar(), - RadarrCatalogueHideButton(controller: _scrollController), - RadarrCatalogueSortButton(controller: _scrollController), - ], - ); - - Widget get _list => _results.length == 0 - ? LSGenericMessage( - text: 'No Movies Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () => _refresh(), - ) - : Consumer( - builder: (context, model, widget) { - List _filtered = _sort(model, _filter(model.searchCatalogueFilter)); - _filtered = model.hideUnmonitoredMovies ? _hide(_filtered) : _filtered; - return _listBody(_filtered); - }, - ); - - Widget _listBody(List filtered) { - List _children = filtered.length == 0 - ? [LSGenericMessage(text: 'No Results Found')] - : List.generate( - filtered.length, - (index) => RadarrCatalogueTile( - data: filtered[index], - scaffoldKey: _scaffoldKey, - refresh: () => _refreshAllPages(), - refreshState: () => _refreshState(), - ), - ); - return LSListViewStickyHeader( - controller: _scrollController, - slivers: [ - LunaSliverStickyHeader( - header: _searchSortBar, - children: _children, - ), - ], - ); - } - - List _filter(String filter) => _results.where( - (entry) => filter == null || filter == '' - ? entry != null - : entry.title.toLowerCase().contains(filter.toLowerCase()) - ).toList(); - - List _sort(RadarrState model, List data) { - if(data != null && data.length != 0) return model.sortCatalogueType.sort(data, model.sortCatalogueAscending); - return data; - } - - List _hide(List data) => data == null || data.length == 0 - ? data - : data.where((entry) => entry.monitored).toList(); -} diff --git a/lib/modules/radarr/routes/details_movie.dart b/lib/modules/radarr/routes/details_movie.dart deleted file mode 100644 index ba251782..00000000 --- a/lib/modules/radarr/routes/details_movie.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrDetailsMovieArguments { - RadarrCatalogueData data; - final int movieID; - - RadarrDetailsMovieArguments({ - @required this.data, - @required this.movieID, - }); -} - -class RadarrDetailsMovie extends StatefulWidget { - static const ROUTE_NAME = '/radarr/details/movie'; - - @override - State createState() => _State(); -} - -class _State extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - final _pageController = PageController(initialPage: 1); - RadarrDetailsMovieArguments _arguments; - bool _error = false; - - @override - void initState() { - super.initState(); - SchedulerBinding.instance.addPostFrameCallback((_) { - _arguments = ModalRoute.of(context).settings.arguments; - Provider.of(context, listen: false).movieNavigationIndex = 1; - _fetch(); - }); - } - - Future _fetch() async { - if(mounted) setState(() => _error = false); - if(_arguments != null) await RadarrAPI.from(Database.currentProfileObject).getMovie(_arguments.movieID) - .then((data) { - if(mounted) setState(() { - _arguments.data = data; - _error = false; - }); - }) - .catchError((_) { - if(mounted) setState(() => _error = true); - }); - } - - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - bottomNavigationBar: _arguments != null && _arguments.data != null - ? _bottomNavigationBar - : null, - body: _arguments != null - ? _arguments.data != null - ? _body - : _error - ? LSErrorMessage(onTapHandler: () => _fetch()) - : LSLoader() - : null, - ); - - Widget get _appBar => LunaAppBar( - context: context, - title: _arguments == null || _arguments.data == null - ? 'Movie Details' - : _arguments.data.title, - actions: _arguments == null || _arguments.data == null - ? null - : [ - RadarrDetailsEditButton( - data: _arguments.data, - remove: () => _removeCallback(), - ), - ], - ); - - Widget get _bottomNavigationBar => RadarrMovieNavigationBar(pageController: _pageController); - - List get _tabs => [ - RadarrDetailsOverview(data: _arguments.data), - RadarrDetailsSearch(data: _arguments.data), - ]; - - Widget get _body => PageView( - controller: _pageController, - children: _tabs, - onPageChanged: _onPageChanged, - ); - - void _onPageChanged(int index) => Provider.of(context, listen: false).movieNavigationIndex = index; - - Future _removeCallback() async => Navigator.of(context).pop(['remove_movie']); -} diff --git a/lib/modules/radarr/routes/edit_movie.dart b/lib/modules/radarr/routes/edit_movie.dart deleted file mode 100644 index 671a4474..00000000 --- a/lib/modules/radarr/routes/edit_movie.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrEditMovieArguments { - final RadarrCatalogueData data; - - RadarrEditMovieArguments({ - @required this.data, - }); -} - -class RadarrEditMovie extends StatefulWidget { - static const ROUTE_NAME = '/radarr/edit/movie'; - - @override - State createState() => _State(); -} - -class _State extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - - RadarrEditMovieArguments _arguments; - Future _future; - - List _qualityProfiles = []; - RadarrQualityProfile _qualityProfile; - RadarrAvailability _minimumAvailability; - String _path; - bool _monitored; - bool _staticPath; - - @override - void initState() { - super.initState(); - SchedulerBinding.instance.addPostFrameCallback((_) { - setState(() => _arguments = ModalRoute.of(context).settings.arguments); - _refresh(); - }); - } - - Future _refresh() async => setState(() => { _future = _fetch() }); - - Future _fetch() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - return _fetchProfiles(_api) - .then((_) => _fetchMinimumAvailability()) - .then((_) { - _path = _arguments.data.path; - _monitored = _arguments.data.monitored; - _staticPath = _arguments.data.staticPath; - return true; - }) - .catchError((error) => Future.error(error)); - } - - Future _fetchProfiles(RadarrAPI api) async { - return await api.getQualityProfiles() - .then((profiles) { - _qualityProfiles = profiles?.values?.toList(); - if(_qualityProfiles != null && _qualityProfiles.length > 0) - _qualityProfile = _qualityProfiles.firstWhere((profile) => profile.id == _arguments.data.qualityProfile); - }) - .catchError((error) => Future.error(error)); - } - - Future _fetchMinimumAvailability() async { - _minimumAvailability = RadarrConstants.MINIMUM_AVAILBILITIES.firstWhere((profile) => profile.id == _arguments.data.minimumAvailability, orElse: () => RadarrConstants.MINIMUM_AVAILBILITIES[0]); - } - - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar( - context: context, - title: _arguments?.data?.title ?? 'Edit Movie', - ); - - Widget get _body => FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ); - - Widget get _list => LSListView( - children: [ - LSCardTile( - title: LSTitle(text: 'Monitored'), - subtitle: LSSubtitle(text: 'Monitor movie for new releases'), - trailing: Switch( - value: _monitored, - onChanged: (value) => setState(() => _monitored = value), - ), - ), - LSCardTile( - title: LSTitle(text: 'Static Path'), - subtitle: LSSubtitle(text: 'Prevent directory from changing'), - trailing: Switch( - value: _staticPath, - onChanged: (value) => setState(() => _staticPath = value), - ), - ), - LSCardTile( - title: LSTitle(text: 'Movie Path'), - subtitle: LSSubtitle(text: _path), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: () => _changePath(), - ), - LSCardTile( - title: LSTitle(text: 'Quality Profile'), - subtitle: LSSubtitle(text: _qualityProfile.name), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: () => _changeProfile(), - ), - LSCardTile( - title: LSTitle(text: 'Minimum Availability'), - subtitle: LSSubtitle(text: _minimumAvailability.name), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: () => _changeMinimumAvailability(), - ), - LSButton( - text: 'Update Movie', - onTap: () async => _save().catchError((_) {}), - ), - ], - ); - - Future _changePath() async { - List _values = await LunaDialogs().editText(context, 'Movie Path', prefill: _path); - if(_values[0] && mounted) setState(() => _path = _values[1]); - } - - Future _changeProfile() async { - List _values = await RadarrDialogs.editQualityProfile(context, _qualityProfiles); - if(_values[0] && mounted) setState(() => _qualityProfile = _values[1]); - } - - Future _changeMinimumAvailability() async { - List _values = await RadarrDialogs.editMinimumAvailability(context, RadarrConstants.MINIMUM_AVAILBILITIES); - if(_values[0] && mounted) setState(() => _minimumAvailability = _values[1]); - } - - Future _save() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - await _api.editMovie( - _arguments.data.movieID, - _qualityProfile, - _minimumAvailability, - _path, - _monitored, - _staticPath, - ) - .then((_) { - _arguments.data.qualityProfile = _qualityProfile.id; - _arguments.data.profile = _qualityProfile.name; - _arguments.data.minimumAvailability = _minimumAvailability.id; - _arguments.data.staticPath = _staticPath; - _arguments.data.path = _path; - _arguments.data.monitored = _monitored; - Navigator.of(context).pop([true]); - }) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Update', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - } -} diff --git a/lib/modules/radarr/routes/history.dart b/lib/modules/radarr/routes/history.dart deleted file mode 100644 index be3eb6b1..00000000 --- a/lib/modules/radarr/routes/history.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrHistory extends StatefulWidget { - static const ROUTE_NAME = '/radarr/history'; - final GlobalKey refreshIndicatorKey; - final Function refreshAllPages; - - RadarrHistory({ - Key key, - @required this.refreshIndicatorKey, - @required this.refreshAllPages, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - final _scaffoldKey = GlobalKey(); - Future> _future; - List _results = []; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - _refresh(); - } - - void _refreshAllPages() => widget.refreshAllPages(); - - Future _refresh() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - if(mounted) setState(() { - _future = _api.getHistory(); - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - key: _scaffoldKey, - body: _body, - ); - } - - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: () => _refresh(), - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); - - Widget get _list => _results.length == 0 - ? LSGenericMessage( - text: 'No History Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () => _refresh(), - ) - : LSListViewBuilder( - itemCount: _results.length, - itemBuilder: (context, index) => RadarrHistoryTile( - data: _results[index], - scaffoldKey: _scaffoldKey, - refresh: () => _refreshAllPages(), - ), - ); -} diff --git a/lib/modules/radarr/routes/missing.dart b/lib/modules/radarr/routes/missing.dart deleted file mode 100644 index cea634e8..00000000 --- a/lib/modules/radarr/routes/missing.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrMissing extends StatefulWidget { - static const ROUTE_NAME = '/radarr/missing'; - final GlobalKey refreshIndicatorKey; - final Function refreshAllPages; - - RadarrMissing({ - Key key, - @required this.refreshIndicatorKey, - @required this.refreshAllPages, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - final _scaffoldKey = GlobalKey(); - Future> _future; - List _results = []; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - _refresh(); - } - - Future _refresh() async { - _results = []; - final _api = RadarrAPI.from(Database.currentProfileObject); - if(mounted) setState(() { - _future = _api.getMissing(); - }); - } - - void _refreshAllPages() => widget.refreshAllPages(); - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - key: _scaffoldKey, - body: _body, - ); - } - - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); - - Widget get _list => _results.length == 0 - ? LSGenericMessage( - text: 'No Missing Movies', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () => widget.refreshIndicatorKey?.currentState?.show(), - ) - : LSListViewBuilder( - itemCount: _results.length, - itemBuilder: (context, index) => RadarrMissingTile( - scaffoldKey: _scaffoldKey, - data: _results[index], - refresh: () => _refreshAllPages(), - ), - ); -} diff --git a/lib/modules/radarr/routes/radarr.dart b/lib/modules/radarr/routes/radarr.dart deleted file mode 100644 index 8f5585e1..00000000 --- a/lib/modules/radarr/routes/radarr.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class Radarr extends StatefulWidget { - static const ROUTE_NAME = '/radarr'; - - @override - State createState() => _State(); -} - -class _State extends State { - final _scaffoldKey = GlobalKey(); - final _pageController = PageController(initialPage: RadarrDatabaseValue.NAVIGATION_INDEX.data); - String _profileState = Database.currentProfileObject.toString(); - RadarrAPI _api = RadarrAPI.from(Database.currentProfileObject); - - final List _refreshKeys = [ - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - ]; - - @override - void initState() { - super.initState(); - Future.microtask(() => Provider.of(context, listen: false).navigationIndex = 0); - } - - @override - Widget build(BuildContext context) => WillPopScope( - onWillPop: () async { - if(_scaffoldKey.currentState.isDrawerOpen) { - //If the drawer is open, return true to close it - return true; - } else { - //If the drawer isn't open, open the drawer - _scaffoldKey.currentState.openDrawer(); - return false; - } - }, - child: ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]), - builder: (context, box, widget) { - if(_profileState != Database.currentProfileObject.toString()) _refreshProfile(); - return Scaffold( - key: _scaffoldKey, - body: _body, - drawer: _drawer, - appBar: _appBar, - bottomNavigationBar: _bottomNavigationBar, - ); - }, - ), - ); - - Widget get _drawer => LSDrawer(page: 'radarr'); - - Widget get _bottomNavigationBar => RadarrNavigationBar(pageController: _pageController); - - List get _tabs => [ - RadarrCatalogue( - refreshIndicatorKey: _refreshKeys[0], - refreshAllPages: _refreshAllPages, - ), - RadarrUpcoming( - refreshIndicatorKey: _refreshKeys[1], - refreshAllPages: _refreshAllPages, - ), - RadarrMissing( - refreshIndicatorKey: _refreshKeys[2], - refreshAllPages: _refreshAllPages, - ), - RadarrHistory( - refreshIndicatorKey: _refreshKeys[3], - refreshAllPages: _refreshAllPages, - ), - ]; - - Widget get _body => PageView( - controller: _pageController, - children: _api.enabled ? _tabs : List.generate(_tabs.length, (_) => LSNotEnabled('Radarr')), - onPageChanged: _onPageChanged, - ); - - Widget get _appBar => LSAppBarDropdown( - context: context, - title: 'Radarr', - profiles: Database.profilesBox.keys.fold([], (value, element) { - if((Database.profilesBox.get(element) as ProfileHiveObject).radarrEnabled) - value.add(element); - return value; - }), - actions: _api.enabled - ? [ - LSIconButton( - icon: Icons.add, - onPressed: () async => _enterAddMovie(), - ), - LSIconButton( - icon: Icons.more_vert, - onPressed: () async => _handlePopup(), - ) - ] - : null, - ); - - Future _enterAddMovie() async { - final _model = Provider.of(context, listen: false); - _model.addSearchQuery = ''; - final dynamic result = await Navigator.of(context).pushNamed(RadarrAddSearch.ROUTE_NAME); - if(result != null) switch(result[0]) { - case 'movie_added': { - LSSnackBar( - context: context, - title: 'Movie Added', - message: result[1], - type: SNACKBAR_TYPE.success, - showButton: true, - buttonOnPressed: () => Navigator.of(context).pushNamed( - RadarrDetailsMovie.ROUTE_NAME, - arguments: RadarrDetailsMovieArguments( - movieID: result[2], - data: null, - ), - ) - ); - _refreshAllPages(); - break; - } - default: LunaLogger().warning('Radarr', '_enterAddMovie', 'Unknown Case: ${result[0]}'); - } - } - - Future _handlePopup() async { - List values = await RadarrDialogs.globalSettings(context); - if(values[0]) switch(values[1]) { - case 'web_gui': await _api.host?.toString()?.lunaOpenGenericLink(); break; - case 'update_library': await _api.updateLibrary() - .then((_) => LSSnackBar(context: context, title: 'Updating Library...', message: 'Updating your library in the background')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Update Library', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - break; - case 'rss_sync': await _api.triggerRssSync() - .then((_) => LSSnackBar(context: context, title: 'Running RSS Sync...', message: 'Running RSS sync in the background')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Run RSS Sync', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - break; - case 'backup': await _api.triggerBackup() - .then((_) => LSSnackBar(context: context, title: 'Backing Up Database...', message: 'Backing up database in the background')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Backup Database', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - break; - case 'missing_search': { - List values = await RadarrDialogs.searchAllMissing(context); - if(values[0]) await _api.searchAllMissing() - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: 'Search for all missing movies')) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - break; - } - default: LunaLogger().warning('Radarr', '_handlePopup', 'Unknown Case: ${values[1]}'); - } - } - - void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; - - void _refreshProfile() { - _api = RadarrAPI.from(Database.currentProfileObject); - _profileState = Database.currentProfileObject.toString(); - _refreshAllPages(); - } - - void _refreshAllPages() { - for(var key in _refreshKeys) key?.currentState?.show(); - } -} diff --git a/lib/modules/radarr/routes/search_results.dart b/lib/modules/radarr/routes/search_results.dart deleted file mode 100644 index 84e6106a..00000000 --- a/lib/modules/radarr/routes/search_results.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrSearchResultsArguments { - final int movieID; - final String title; - - RadarrSearchResultsArguments({ - @required this.movieID, - @required this.title, - }); -} - -class RadarrSearchResults extends StatefulWidget { - static const ROUTE_NAME = '/radarr/search/results'; - - @override - State createState() => _State(); -} - -class _State extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - final GlobalKey _refreshKey = GlobalKey(); - final ScrollController _scrollController = ScrollController(); - - RadarrSearchResultsArguments _arguments; - Future> _future; - List _results; - - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - setState(() => _arguments = ModalRoute.of(context).settings.arguments); - _refresh(); - }); - } - - Future _refresh() async { - if(mounted) setState(() => _results = []); - final _api = RadarrAPI.from(Database.currentProfileObject); - setState(() => { _future = _api.getReleases(_arguments.movieID) }); - //Clear the search filter using a microtask - Future.microtask(() => Provider.of(context, listen: false)?.searchReleasesFilter = ''); - } - - @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => _arguments == null - ? null - : LunaAppBar( - context: context, - title: _arguments.title, - ); - - Widget get _body => _arguments == null - ? null - : LSRefreshIndicator( - refreshKey: _refreshKey, - onRefresh: () => _refresh(), - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); - - Widget get _searchSortBar => LSContainerRow( - padding: EdgeInsets.zero, - backgroundColor: Theme.of(context).primaryColor, - children: [ - RadarrReleasesSearchBar(), - RadarrReleasesHideButton(controller: _scrollController), - RadarrReleasesSortButton(controller: _scrollController), - ], - ); - - Widget get _list => _results.length == 0 - ? LSGenericMessage( - text: 'No Results Found', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () => _refresh(), - ) - : Consumer( - builder: (context, model, widget) { - List _filtered = _sort(model, _filter(model.searchReleasesFilter)); - _filtered = model.hideRejectedReleases ? _hide(_filtered) : _filtered; - return _listBody(_filtered); - }, - ); - - Widget _listBody(List filtered) { - List _children = filtered.length == 0 - ? [LSGenericMessage(text: 'No Results Found')] - : List.generate( - filtered.length, - (index) => RadarrSearchResultTile(data: filtered[index]), - ); - return LSListViewStickyHeader( - controller: _scrollController, - slivers: [ - LunaSliverStickyHeader( - header: _searchSortBar, - children: _children, - ) - ], - ); - } - - List _filter(String filter) => _results.where( - (entry) => filter == null || filter == '' - ? entry != null - : entry.title.toLowerCase().contains(filter.toLowerCase()) - ).toList(); - - List _sort(RadarrState model, List data) { - if(data != null && data.length != 0) return model.sortReleasesType.sort(data, model.sortReleasesAscending); - return data; - } - - List _hide(List data) => data == null || data.length == 0 - ? data - : data.where((entry) => entry.approved).toList(); -} diff --git a/lib/modules/radarr/routes/upcoming.dart b/lib/modules/radarr/routes/upcoming.dart deleted file mode 100644 index e47a47ea..00000000 --- a/lib/modules/radarr/routes/upcoming.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrUpcoming extends StatefulWidget { - static const ROUTE_NAME = '/radarr/upcoming'; - final GlobalKey refreshIndicatorKey; - final Function refreshAllPages; - - RadarrUpcoming({ - Key key, - @required this.refreshIndicatorKey, - @required this.refreshAllPages, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - final _scaffoldKey = GlobalKey(); - Future> _future; - List _results = []; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - _refresh(); - } - - Future _refresh() async { - _results = []; - final _api = RadarrAPI.from(Database.currentProfileObject); - if(mounted) setState(() { - _future = _api.getUpcoming(); - }); - } - - void _refreshAllPages() => widget.refreshAllPages(); - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - key: _scaffoldKey, - body: _body, - ); - } - - Widget get _body => LSRefreshIndicator( - refreshKey: widget.refreshIndicatorKey, - onRefresh: _refresh, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - switch(snapshot.connectionState) { - case ConnectionState.done: { - if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh()); - _results = snapshot.data; - return _list; - } - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - default: return LSLoader(); - } - }, - ), - ); - - Widget get _list => _results.length == 0 - ? LSGenericMessage( - text: 'No Upcoming Movies', - showButton: true, - buttonText: 'Refresh', - onTapHandler: () => widget.refreshIndicatorKey?.currentState?.show(), - ) - : LSListViewBuilder( - itemCount: _results.length, - itemBuilder: (context, index) => RadarrUpcomingTile( - scaffoldKey: _scaffoldKey, - data: _results[index], - refresh: () => _refreshAllPages(), - ), - ); -} diff --git a/lib/modules/radarr/widgets.dart b/lib/modules/radarr/widgets.dart deleted file mode 100644 index ce1ddae4..00000000 --- a/lib/modules/radarr/widgets.dart +++ /dev/null @@ -1,20 +0,0 @@ -export 'widgets/catalogue_hide_button.dart'; -export 'widgets/catalogue_search_bar.dart'; -export 'widgets/catalogue_sorting_button.dart'; -export 'widgets/catalogue_tile.dart'; -export 'widgets/history_tile.dart'; -export 'widgets/missing_tile.dart'; -export 'widgets/upcoming_tile.dart'; -export 'widgets/details_edit_button.dart'; -export 'widgets/details_overview.dart'; -export 'widgets/details_search.dart'; -export 'widgets/details_search_tile.dart'; -export 'widgets/details_overview.dart'; -export 'widgets/add_search_bar.dart'; -export 'widgets/add_search_result_tile.dart'; -export 'widgets/navigation_bar.dart'; -export 'widgets/movie_navigation_bar.dart'; -export 'widgets/releases_sorting_button.dart'; -export 'widgets/releases_hide_button.dart'; -export 'widgets/releases_search_bar.dart'; -export 'widgets/details_file_tile.dart'; diff --git a/lib/modules/radarr/widgets/add_search_bar.dart b/lib/modules/radarr/widgets/add_search_bar.dart deleted file mode 100644 index d24b66d9..00000000 --- a/lib/modules/radarr/widgets/add_search_bar.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrAddSearchBar extends StatefulWidget { - final Function callback; - - RadarrAddSearchBar({ - @required this.callback, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - final TextEditingController _controller = TextEditingController(); - - @override - void initState() { - super.initState(); - final model = Provider.of(context, listen: false); - _controller.text = model.addSearchQuery; - } - - @override - Widget build(BuildContext context) => Expanded( - child: Consumer( - builder: (context, model, widget) => LSTextInputBar( - controller: _controller, - autofocus: true, - onChanged: (text, updateController) => _onChange(model, text, updateController), - onSubmitted: (_) => _onSubmit(), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), - ), - ), - ); - - void _onChange(RadarrState model, String text, updateController) { - model.addSearchQuery = text; - if(updateController) _controller.text = text; - } - - void _onSubmit() => widget.callback(); -} diff --git a/lib/modules/radarr/widgets/add_search_result_tile.dart b/lib/modules/radarr/widgets/add_search_result_tile.dart deleted file mode 100644 index 64a05986..00000000 --- a/lib/modules/radarr/widgets/add_search_result_tile.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrAddSearchResultTile extends StatelessWidget { - final bool alreadyAdded; - final RadarrSearchData data; - - RadarrAddSearchResultTile({ - @required this.alreadyAdded, - @required this.data, - }); - - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: data.title, darken: alreadyAdded), - subtitle: LSSubtitle( - text: '${data.year} (${data.formattedStatus})\n${data.overview.trim()}', - maxLines: 2, - darken: alreadyAdded, - ), - trailing: alreadyAdded - ? null - : LSIconButton(icon: Icons.arrow_forward_ios), - onTap: alreadyAdded - ? () => _showAlreadyAddedMessage(context) - : () async => _enterDetails(context), - padContent: true, - ); - - Future _showAlreadyAddedMessage(BuildContext context) => LSSnackBar( - context: context, - title: 'Movie Already in Radarr', - message: data.title, - ); - - Future _enterDetails(BuildContext context) async { - final dynamic result = await Navigator.of(context).pushNamed( - RadarrAddDetails.ROUTE_NAME, - arguments: RadarrAddDetailsArguments(data: data), - ); - if(result != null) switch(result[0]) { - case 'movie_added': Navigator.of(context).pop(result); break; - default: LunaLogger().warning('RadarrAddSearchResultTile', '_enterDetails', 'Unknown Case: ${result[0]}'); - } - } -} diff --git a/lib/modules/radarr/widgets/catalogue_hide_button.dart b/lib/modules/radarr/widgets/catalogue_hide_button.dart deleted file mode 100644 index 46c64cc4..00000000 --- a/lib/modules/radarr/widgets/catalogue_hide_button.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrCatalogueHideButton extends StatefulWidget { - final ScrollController controller; - - RadarrCatalogueHideButton({ - Key key, - @required this.controller, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCard( - child: Consumer( - builder: (context, model, widget) => InkWell( - child: LSIconButton( - icon: model.hideUnmonitoredMovies - ? Icons.visibility_off - : Icons.visibility, - ), - onTap: () => model.hideUnmonitoredMovies = !model.hideUnmonitoredMovies, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - ), - ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), - color: Theme.of(context).canvasColor, - ); -} \ No newline at end of file diff --git a/lib/modules/radarr/widgets/catalogue_search_bar.dart b/lib/modules/radarr/widgets/catalogue_search_bar.dart deleted file mode 100644 index 172f1a3e..00000000 --- a/lib/modules/radarr/widgets/catalogue_search_bar.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrCatalogueSearchBar extends StatefulWidget { - @override - State createState() => _State(); -} - -class _State extends State { - final _textController = TextEditingController(); - - @override - Widget build(BuildContext context) => Expanded( - child: Consumer( - builder: (context, model, widget) => LSTextInputBar( - controller: _textController, - labelText: 'Search Movies...', - onChanged: (text, update) => _onChanged(model, text, update), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), - ), - ), - ); - - void _onChanged(RadarrState model, String text, bool update) { - model.searchCatalogueFilter = text; - if(update) _textController.text = ''; - } -} \ No newline at end of file diff --git a/lib/modules/radarr/widgets/catalogue_sorting_button.dart b/lib/modules/radarr/widgets/catalogue_sorting_button.dart deleted file mode 100644 index bce60826..00000000 --- a/lib/modules/radarr/widgets/catalogue_sorting_button.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrCatalogueSortButton extends StatefulWidget { - final ScrollController controller; - - RadarrCatalogueSortButton({ - Key key, - @required this.controller, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCard( - child: Consumer( - builder: (context, model, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.sort), - onSelected: (result) { - if(model.sortCatalogueType == result) { - model.sortCatalogueAscending = !model.sortCatalogueAscending; - } else { - model.sortCatalogueAscending = true; - model.sortCatalogueType = result; - } - _scrollBack(); - }, - itemBuilder: (context) => List>.generate( - RadarrCatalogueSorting.values.length, - (index) => PopupMenuItem( - value: RadarrCatalogueSorting.values[index], - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - RadarrCatalogueSorting.values[index].readable, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - if(model.sortCatalogueType == RadarrCatalogueSorting.values[index]) Icon( - model.sortCatalogueAscending - ? Icons.arrow_upward - : Icons.arrow_downward, - size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, - color: LunaColours.accent, - ), - ], - ), - ), - ), - ), - ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), - color: Theme.of(context).canvasColor, - ); - - void _scrollBack() { - widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } -} \ No newline at end of file diff --git a/lib/modules/radarr/widgets/catalogue_tile.dart b/lib/modules/radarr/widgets/catalogue_tile.dart deleted file mode 100644 index 9aeae3da..00000000 --- a/lib/modules/radarr/widgets/catalogue_tile.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrCatalogueTile extends StatefulWidget { - final RadarrCatalogueData data; - final GlobalKey scaffoldKey; - final Function refresh; - final Function refreshState; - - RadarrCatalogueTile({ - @required this.data, - @required this.scaffoldKey, - @required this.refresh, - @required this.refreshState, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.data.title, darken: !widget.data.monitored), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Selector( - selector: (_, model) => model.sortCatalogueType, - builder: (context, type, _) => LSSubtitle( - text: widget.data.subtitle(type), - darken: !widget.data.monitored, - maxLines: 2, - ), - ), - Row( - children: [ - Padding( - child: Icon( - Icons.videocam, - size: 18.0, - color: widget.data.isInCinemas ? - widget.data.monitored ? Colors.orange : Colors.orange.withOpacity(0.30) : - widget.data.monitored ? Colors.grey : Colors.grey.withOpacity(0.30), - ), - padding: EdgeInsets.fromLTRB(0.0, 3.0, 12.0, 3.0), - ), - Padding( - child: Icon( - Icons.album, - size: 18.0, - color: widget.data.isPhysicallyReleased ? - widget.data.monitored ? Colors.blue : Colors.blue.withOpacity(0.30) : - widget.data.monitored ? Colors.grey : Colors.grey.withOpacity(0.30), - ), - padding: EdgeInsets.fromLTRB(0.0, 3.0, 12.0, 3.0), - ), - Padding( - child: Icon( - Icons.check_circle, - size: 18.0, - color: widget.data.downloaded ? - widget.data.monitored ? Color(LunaColours.ACCENT_COLOR) : Color(LunaColours.ACCENT_COLOR).withOpacity(0.30) : - widget.data.monitored ? Colors.grey : Colors.grey.withOpacity(0.30), - ), - padding: EdgeInsets.fromLTRB(0.0, 3.0, 16.0, 3.0), - ), - Padding( - child: RichText( - text: widget.data.releaseSubtitle, - ), - padding: EdgeInsets.fromLTRB(0.0, 3.0, 0.0, 3.0), - ), - ], - ), - ], - ), - trailing: LSIconButton( - icon: widget.data.monitored - ? Icons.turned_in - : Icons.turned_in_not, - color: widget.data.monitored - ? Colors.white - : Colors.white30, - onPressed: () async => _toggleMonitoredStatus(), - ), - onTap: () async => _enterMovie(), - onLongPress: () async => _handlePopup(), - customPadding: EdgeInsets.fromLTRB(12.0, 4.0, 12.0, 0.0), - decoration: LunaCardDecoration( - uri: widget.data.posterURI(), - headers: Database.currentProfileObject.getRadarr()['headers'], - ), - ); - - Future _toggleMonitoredStatus() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - await _api.toggleMovieMonitored(widget.data.movieID, !widget.data.monitored) - .then((_) { - if(mounted) setState(() => widget.data.monitored = !widget.data.monitored); - widget.refreshState(); - LSSnackBar( - context: context, - title: widget.data.monitored ? 'Monitoring' : 'No Longer Monitoring', - message: widget.data.title, - type: SNACKBAR_TYPE.success, - ); - }) - .catchError((_) { - LSSnackBar( - context: context, - title: widget.data.monitored ? 'Failed to Stop Monitoring' : 'Failed to Monitor', - message: LunaLogger.CHECK_LOGS_MESSAGE, - type: SNACKBAR_TYPE.failure, - ); - }); - } - - Future _enterMovie() async { - final dynamic result = await Navigator.of(context).pushNamed( - RadarrDetailsMovie.ROUTE_NAME, - arguments: RadarrDetailsMovieArguments( - data: widget.data, - movieID: widget.data.movieID, - ), - ); - if(result != null) switch(result[0]) { - case 'remove_movie': { - LSSnackBar( - context: context, - title: 'Removed', - message: widget.data.title, - type: SNACKBAR_TYPE.success, - ); - widget.refresh(); - break; - } - default: LunaLogger().warning('RadarrCatalogueTile', '_enterMovie', 'Unknown Case: ${result[0]}'); - } - } - - Future _handlePopup() async { - List values = await RadarrDialogs.editMovie(context, widget.data); - if(values[0]) switch(values[1]) { - case 'refresh_movie': _refreshMovie(); break; - case 'edit_movie': _editMovie(); break; - case 'remove_movie': _removeMovie(); break; - default: LunaLogger().warning('RadarrCatalogueTile', '_handlePopup', 'Invalid method passed through popup. (${values[1]})'); - } - } - - Future _refreshMovie() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - await _api.refreshMovie(widget.data.movieID) - .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 _editMovie() async { - final dynamic result = await Navigator.of(context).pushNamed( - RadarrEditMovie.ROUTE_NAME, - arguments: RadarrEditMovieArguments( - data: widget.data, - ), - ); - if(result != null && result[0]) LSSnackBar( - context: context, - title: 'Updated', - message: widget.data.title, - type: SNACKBAR_TYPE.success, - ); - } - - Future _removeMovie() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - List values = await RadarrDialogs.deleteMovie(context); - if(values[0]) { - RadarrState _state = Provider.of(context, listen: false); - await _api.removeMovie( - widget.data.movieID, - deleteFiles: _state.removeDeleteFiles, - addExclusion: _state.removeAddExclusion, - ) - .then((_) { - LSSnackBar(context: context, title: 'Removed', message: widget.data.title, type: SNACKBAR_TYPE.success); - widget.refresh(); - }) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - } - } -} \ No newline at end of file diff --git a/lib/modules/radarr/widgets/details_edit_button.dart b/lib/modules/radarr/widgets/details_edit_button.dart deleted file mode 100644 index 44613c82..00000000 --- a/lib/modules/radarr/widgets/details_edit_button.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrDetailsEditButton extends StatefulWidget { - final RadarrCatalogueData data; - final Function() remove; - - RadarrDetailsEditButton({ - @required this.data, - @required this.remove, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => Consumer( - builder: (context, model, widget) => LSIconButton( - icon: Icons.edit, - onPressed: () async => _handlePopup(context), - ), - ); - - Future _handlePopup(BuildContext context) async { - List values = await RadarrDialogs.editMovie(context, widget.data); - if(values[0]) switch(values[1]) { - case 'refresh_movie': _refreshMovie(context); break; - case 'edit_movie': _editMovie(context); break; - case 'remove_movie': _removeMovie(context); break; - default: LunaLogger().warning('RadarrDetailsEditButton', '_handlePopup', 'Invalid method passed through popup. (${values[1]})'); - } - } - - Future _editMovie(BuildContext context) async { - final dynamic result = await Navigator.of(context).pushNamed( - RadarrEditMovie.ROUTE_NAME, - arguments: RadarrEditMovieArguments(data: widget.data), - ); - if(result != null && result[0]) LSSnackBar( - context: context, - title: 'Updated', - message: widget.data.title, - type: SNACKBAR_TYPE.success, - ); - } - - Future _refreshMovie(BuildContext context) async { - final _api = RadarrAPI.from(Database.currentProfileObject); - await _api.refreshMovie(widget.data.movieID) - .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 _removeMovie(BuildContext context) async { - final _api = RadarrAPI.from(Database.currentProfileObject); - List values = await RadarrDialogs.deleteMovie(context); - if(values[0]) { - RadarrState _state = Provider.of(context, listen: false); - await _api.removeMovie( - widget.data.movieID, - deleteFiles: _state.removeDeleteFiles, - addExclusion: _state.removeAddExclusion, - ) - .then((_) { - LSSnackBar(context: context, title: 'Removed', message: widget.data.title, type: SNACKBAR_TYPE.success); - widget.remove(); - }) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Remove', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - } - } -} diff --git a/lib/modules/radarr/widgets/details_file_tile.dart b/lib/modules/radarr/widgets/details_file_tile.dart deleted file mode 100644 index d2fdb2b6..00000000 --- a/lib/modules/radarr/widgets/details_file_tile.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrDetailsFileTile extends StatefulWidget { - final RadarrCatalogueData data; - - RadarrDetailsFileTile({ - Key key, - @required this.data, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => widget.data.downloaded - ? LSCardTile( - title: LSTitle(text: widget.data.movieFile['relativePath']), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: widget.data.movieFile['mediaInfo'] != null - ? [ - TextSpan(text: '${int.tryParse(widget.data.movieFile['size'].toString())?.lunaBytesToString() ?? 'Unknown'}\t•\t'), - TextSpan(text: '${widget.data.movieFile['quality']['quality']['name'] ?? 'Unknown'}\n'), - TextSpan(text: '${widget.data.movieFile['mediaInfo']['videoFormat'] ?? 'Unknown'}/'), - TextSpan(text: '${widget.data.movieFile['mediaInfo']['audioFormat'] ?? 'Unknown'}'), - ] - : [ - TextSpan(text: '${int.tryParse(widget.data.movieFile['size'].toString())?.lunaBytesToString() ?? 'Unknown'}\t•\t'), - TextSpan(text: '${widget.data.movieFile['quality']['quality']['name'] ?? 'Unknown'}\n'), - ], - ), - overflow: TextOverflow.fade, - maxLines: 2, - softWrap: false, - ), - trailing: LSIconButton( - icon: Icons.delete, - color: LunaColours.red, - onPressed: () async => _delete().catchError((_) {}), - ), - padContent: true, - ) - : LSGenericMessage(text: 'No Files Found'); - - Future _delete() async { - List values = await RadarrDialogs.deleteMovieFile(context); - RadarrAPI _api = RadarrAPI.from(Database.currentProfileObject); - if(values[0]) _api.removeMovieFile(widget.data.movieFile['id']) - .then((_) { - LSSnackBar( - context: context, - title: 'Movie File Deleted', - message: widget.data.movieFile['relativePath'], - type: SNACKBAR_TYPE.success, - ); - setState(() { - widget.data.downloaded = false; - }); - }) - .catchError((error) { - LSSnackBar( - context: context, - title: 'Failed to Delete Movie File', - message: LunaLogger.CHECK_LOGS_MESSAGE, - type: SNACKBAR_TYPE.failure, - ); - return Future.error(error); - }); - } -} diff --git a/lib/modules/radarr/widgets/details_overview.dart b/lib/modules/radarr/widgets/details_overview.dart deleted file mode 100644 index d221bd39..00000000 --- a/lib/modules/radarr/widgets/details_overview.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrDetailsOverview extends StatefulWidget { - final RadarrCatalogueData data; - - RadarrDetailsOverview({ - Key key, - @required this.data, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return LSListView( - children: [ - LSDescriptionBlock( - title: widget?.data?.title ?? 'Unknown', - description: widget?.data?.overview == '' - ? 'No summary is available.' - : widget?.data?.overview, - uri: widget?.data?.posterURI() ?? '', - fallbackImage: 'assets/images/radarr/nomovieposter.png', - headers: Database.currentProfileObject.getRadarr()['headers'], - ), - LSCardTile( - title: LSTitle(text: 'Movie Path', centerText: true), - subtitle: LSSubtitle(text: widget?.data?.path ?? 'Unknown', centerText: true), - onTap: () => LunaDialogs().textPreview(context, widget?.data?.title, widget?.data?.path ?? 'Unknown'), - ), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Quality Profile', centerText: true), - subtitle: LSSubtitle(text: widget?.data?.profile ?? 'Unknown', centerText: true), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Min Availability', centerText: true), - subtitle: LSSubtitle( - text: RadarrConstants.MINIMUM_AVAILBILITIES.firstWhere( - (data) => data.id == widget?.data?.minimumAvailability, - orElse: () => null, - )?.name ?? 'Unknown', - centerText: true, - ), - reducedMargin: true, - ), - ), - ], - ), - LSContainerRow( - children: [ - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Studio', centerText: true), - subtitle: LSSubtitle(text: widget?.data?.studio ?? 'None', centerText: true), - reducedMargin: true, - ), - ), - Expanded( - child: LSCardTile( - title: LSTitle(text: 'Runtime', centerText: true), - subtitle: LSSubtitle(text: (widget?.data?.runtime ?? -1) > 0 ? '${widget.data.runtime} Minutes' : 'Unknown', centerText: true), - reducedMargin: true, - ), - ), - ], - ), - LSContainerRow( - children: [ - if(widget.data.imdbId != '') Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset( - 'assets/images/services/imdb.png', - height: 21.0, - ), - padding: EdgeInsets.all(18.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await widget.data?.imdbId?.lunaOpenIMDB(), - ), - reducedMargin: true, - ), - ), - if(widget.data.tmdbId != null && widget.data.tmdbId != 0) Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset( - 'assets/images/services/themoviedb.png', - height: 19.0, - ), - padding: EdgeInsets.all(19.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await widget.data?.tmdbId?.toString()?.lunaOpenTheMovieDB(), - ), - reducedMargin: true, - ), - ), - if(widget.data.youtubeId != '') Expanded( - child: LSCard( - child: InkWell( - child: Padding( - child: Image.asset( - 'assets/images/services/youtube.png', - height: 17.0, - ), - padding: EdgeInsets.all(20.0), - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () async => await widget.data?.youtubeId?.lunaOpenYouTube(), - ), - reducedMargin: true, - ), - ), - ], - ), - ], - ); - } -} diff --git a/lib/modules/radarr/widgets/details_search.dart b/lib/modules/radarr/widgets/details_search.dart deleted file mode 100644 index 06b3c294..00000000 --- a/lib/modules/radarr/widgets/details_search.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrDetailsSearch extends StatefulWidget { - final RadarrCatalogueData data; - - RadarrDetailsSearch({ - Key key, - @required this.data, - }) : super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return _body; - } - - Widget get _body => LSListView( - children: [ - _buttons, - RadarrDetailsFileTile(data: widget.data), - ], - ); - - Widget get _buttons => LSContainerRow( - children: [ - Expanded( - child: LSButton( - text: 'Automatic', - onTap: () async => _automatic().catchError((_) {}), - reducedMargin: true, - ), - ), - Expanded( - child: LSButton( - text: 'Interactive', - backgroundColor: LunaColours.orange, - onTap: () async => _manual(), - reducedMargin: true, - ), - ), - ], - ); - - Future _manual() async => Navigator.of(context).pushNamed( - RadarrSearchResults.ROUTE_NAME, - arguments: RadarrSearchResultsArguments( - movieID: widget.data.movieID, - title: widget.data.title, - ), - ); - - Future _automatic() async { - RadarrAPI _api = RadarrAPI.from(Database.currentProfileObject); - _api.automaticSearchMovie(widget.data.movieID) - .then((_) => LSSnackBar( - context: context, - title: 'Searching...', - message: widget.data.title, - type: SNACKBAR_TYPE.success, - )) - .catchError((_) => LSSnackBar( - context: context, - title: 'Failed to Search', - message: LunaLogger.CHECK_LOGS_MESSAGE, - type: SNACKBAR_TYPE.failure, - )); - } -} diff --git a/lib/modules/radarr/widgets/details_search_tile.dart b/lib/modules/radarr/widgets/details_search_tile.dart deleted file mode 100644 index a0c694a9..00000000 --- a/lib/modules/radarr/widgets/details_search_tile.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:expandable/expandable.dart'; -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; -import 'package:lunasea/modules/home.dart'; - -class RadarrSearchResultTile extends StatelessWidget { - final RadarrReleaseData data; - final ExpandableController _controller = ExpandableController(); - - RadarrSearchResultTile({ - @required this.data, - }); - - @override - Widget build(BuildContext context) => LSExpandable( - controller: _controller, - collapsed: _collapsed(context), - expanded: _expanded(context), - ); - - Widget _expanded(BuildContext context) => LSCard( - child: InkWell( - child: Row( - children: [ - Expanded( - child: Padding( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LSTitle(text: data.title, softWrap: true, maxLines: 12), - Padding( - child: Wrap( - direction: Axis.horizontal, - runSpacing: 10.0, - children: [ - LSTextHighlighted( - text: data.protocol.lunaCapitalizeFirstLetters(), - bgColor: data.isTorrent - ? LunaColours.purple - : LunaColours.blue, - ), - ...List.generate( - data.customFormats.length, - (index) => LSTextHighlighted( - text: data.customFormats[index]['name'], - bgColor: LunaColours.blueGrey, - ), - ), - ], - ), - padding: EdgeInsets.only(top: 8.0, bottom: 2.0), - ), - Padding( - child: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - if(data.isTorrent) TextSpan( - text: '${data.seeders} Seeders\t•\t${data.leechers} Leechers\n', - style: TextStyle( - color: LunaColours.purple, - fontWeight: FontWeight.bold, - ), - ), - TextSpan(text: '${data.quality ?? 'Unknown'}\t•\t'), - TextSpan(text: '${data.size.lunaBytesToString() ?? 'Unknown'}\t•\t'), - TextSpan(text: '${data.indexer}\n'), - TextSpan(text: '${data.ageHours.lunaHoursToAge() ?? 'Unknown'}'), - ], - ), - ), - padding: EdgeInsets.only(top: 6.0, bottom: 10.0), - ), - Padding( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: LSButtonSlim( - text: 'Download', - onTap: () => _startDownload(context), - margin: data.approved - ? EdgeInsets.zero - : EdgeInsets.only(right: 6.0), - ), - ), - if(!data.approved) Expanded( - child: LSButtonSlim( - text: 'Rejected', - backgroundColor: LunaColours.red, - onTap: () => _showWarnings(context), - margin: EdgeInsets.only(left: 6.0), - ), - ), - ], - ), - padding: EdgeInsets.only(bottom: 2.0), - ), - ], - ), - padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), - ), - ), - ], - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: () => _controller.toggle(), - ), - ); - - Widget _collapsed(BuildContext context) => LSCardTile( - title: LSTitle(text: data.title), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: [ - TextSpan( - style: TextStyle( - color: data.isTorrent - ? LunaColours.purple - : LunaColours.blue, - fontWeight: FontWeight.bold, - ), - text: data.protocol.lunaCapitalizeFirstLetters(), - ), - if(data.isTorrent) TextSpan( - text: ' (${data.seeders}/${data.leechers})', - style: TextStyle( - color: LunaColours.purple, - fontWeight: FontWeight.bold, - ), - ), - TextSpan(text: '\t•\t${data.indexer}\t•\t'), - TextSpan(text: '${data.ageHours.lunaHoursToAge() ?? 'Unknown'}\n'), - TextSpan(text: '${data.quality ?? 'Unknown'}\t•\t'), - TextSpan(text: '${data.size.lunaBytesToString() ?? 'Unknown'}'), - ], - ), - ), - trailing: LSIconButton( - icon: data.approved - ? Icons.file_download - : Icons.report, - color: data.approved - ? Colors.white - : LunaColours.red, - onPressed: () async => data.approved - ? _startDownload(context) - : _showWarnings(context), - ), - padContent: true, - onTap: () => _controller.toggle(), - ); - - Future _startDownload(BuildContext context) async { - RadarrAPI _api = RadarrAPI.from(Database.currentProfileObject); - await _api.downloadRelease(data.guid, data.indexerId) - .then((_) => LSSnackBar( - context: context, - title: 'Downloading...', - message: data.title, - type: SNACKBAR_TYPE.success, - showButton: true, - buttonText: 'Back', - buttonOnPressed: () => Navigator.of(context).popUntil((Route route) { - return !route.willHandlePopInternally - && route is ModalRoute - && (route.settings.name == Radarr.ROUTE_NAME || route.settings.name == Home.ROUTE_NAME); - }), - )) - .catchError((_) => LSSnackBar( - context: context, - title: 'Failed to Start Downloading', - message: LunaLogger.CHECK_LOGS_MESSAGE, - type: SNACKBAR_TYPE.failure, - )); - } - - Future _showWarnings(BuildContext context) async { - String reject = ''; - for(var i=0; i scaffoldKey; - final Function refresh; - - RadarrHistoryTile({ - @required this.data, - @required this.scaffoldKey, - @required this.refresh, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.data.movieTitle), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: widget.data.subtitle, - ), - ), - trailing: LSIconButton( - icon: Icons.arrow_forward_ios, - ), - padContent: true, - onTap: () async => _enterMovie(), - ); - - Future _enterMovie() async { - if(widget.data.movieID == null || widget.data.movieID == -1) { - LSSnackBar( - context: context, - title: 'No Movie Available', - message: 'There is no movie associated with this history entry', - ); - } else { - final dynamic result = await Navigator.of(context).pushNamed( - RadarrDetailsMovie.ROUTE_NAME, - arguments: RadarrDetailsMovieArguments( - data: null, - movieID: widget.data.movieID, - ), - ); - if(result != null) switch(result[0]) { - case 'remove_movie': { - LSSnackBar( - context: context, - title: 'Removed', - message: widget.data.movieTitle, - type: SNACKBAR_TYPE.success, - ); - widget.refresh(); - break; - } - } - } - } -} \ No newline at end of file diff --git a/lib/modules/radarr/widgets/missing_tile.dart b/lib/modules/radarr/widgets/missing_tile.dart deleted file mode 100644 index d168ce17..00000000 --- a/lib/modules/radarr/widgets/missing_tile.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrMissingTile extends StatefulWidget { - final RadarrMissingData data; - final GlobalKey scaffoldKey; - final Function refresh; - - RadarrMissingTile({ - @required this.data, - @required this.scaffoldKey, - @required this.refresh, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.data.title), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: widget.data.subtitle, - ), - ), - trailing: LSIconButton( - icon: Icons.search, - onPressed: () async => _search(), - onLongPress: () async => _interactiveSearch(), - ), - onTap: () async => _enterMovie(), - decoration: LunaCardDecoration( - uri: widget.data.posterURI(), - headers: Database.currentProfileObject.getRadarr()['headers'], - ), - padContent: true, - ); - - Future _search() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - await _api.searchMissingMovies([widget.data.movieID]) - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: widget.data.title)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - } - - Future _interactiveSearch() async => Navigator.of(context).pushNamed( - RadarrSearchResults.ROUTE_NAME, - arguments: RadarrSearchResultsArguments( - movieID: widget.data.movieID, - title: widget.data.title, - ), - ); - - Future _enterMovie() async { - final dynamic result = await Navigator.of(context).pushNamed( - RadarrDetailsMovie.ROUTE_NAME, - arguments: RadarrDetailsMovieArguments( - data: null, - movieID: widget.data.movieID, - ), - ); - if(result != null) switch(result[0]) { - case 'remove_movie': { - LSSnackBar( - context: context, - title: 'Removed', - message: widget.data.title, - type: SNACKBAR_TYPE.success, - ); - widget.refresh(); - break; - } - } - } -} diff --git a/lib/modules/radarr/widgets/movie_navigation_bar.dart b/lib/modules/radarr/widgets/movie_navigation_bar.dart deleted file mode 100644 index 6545b040..00000000 --- a/lib/modules/radarr/widgets/movie_navigation_bar.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrMovieNavigationBar extends StatefulWidget { - final PageController pageController; - - RadarrMovieNavigationBar({ - Key key, - @required this.pageController, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - static const List _navbarTitles = [ - 'Overview', - 'Search', - ]; - - static const List _navbarIcons = [ - Icons.subject, - CustomIcons.movies, - ]; - - @override - Widget build(BuildContext context) => Selector( - selector: (_, model) => model.movieNavigationIndex, - builder: (context, index, _) => LSBottomNavigationBar( - index: index, - icons: _navbarIcons, - titles: _navbarTitles, - onTap: (index) async => await _navOnTap(index), - ), - ); - - Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ).then((_) => Provider.of(context, listen: false).movieNavigationIndex = index); - } -} diff --git a/lib/modules/radarr/widgets/navigation_bar.dart b/lib/modules/radarr/widgets/navigation_bar.dart deleted file mode 100644 index d5f89673..00000000 --- a/lib/modules/radarr/widgets/navigation_bar.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrNavigationBar extends StatefulWidget { - static const List titles = [ - 'Catalogue', - 'Upcoming', - 'Missing', - 'History', - ]; - - static const List icons = [ - CustomIcons.movies, - CustomIcons.upcoming, - CustomIcons.calendar_missing, - CustomIcons.history, - ]; - - final PageController pageController; - - RadarrNavigationBar({ - Key key, - @required this.pageController, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) { - Provider.of(context, listen: false).navigationIndex = RadarrDatabaseValue.NAVIGATION_INDEX.data; - }); - } - - @override - Widget build(BuildContext context) => Selector( - selector: (_, model) => model.navigationIndex, - builder: (context, index, _) => LSBottomNavigationBar( - index: index, - icons: RadarrNavigationBar.icons, - titles: RadarrNavigationBar.titles, - onTap: _navOnTap, - ), - ); - - Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); - Provider.of(context, listen: false).navigationIndex = index; - } -} \ No newline at end of file diff --git a/lib/modules/radarr/widgets/releases_hide_button.dart b/lib/modules/radarr/widgets/releases_hide_button.dart deleted file mode 100644 index 32e8d94a..00000000 --- a/lib/modules/radarr/widgets/releases_hide_button.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrReleasesHideButton extends StatefulWidget { - final ScrollController controller; - - RadarrReleasesHideButton({ - Key key, - @required this.controller, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCard( - child: Consumer( - builder: (context, model, widget) => InkWell( - child: LSIconButton( - icon: model.hideRejectedReleases - ? Icons.visibility_off - : Icons.visibility, - ), - onTap: () => model.hideRejectedReleases = !model.hideRejectedReleases, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - ), - ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), - color: Theme.of(context).canvasColor, - ); -} diff --git a/lib/modules/radarr/widgets/releases_search_bar.dart b/lib/modules/radarr/widgets/releases_search_bar.dart deleted file mode 100644 index 32b68606..00000000 --- a/lib/modules/radarr/widgets/releases_search_bar.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrReleasesSearchBar extends StatefulWidget { - final String prefill; - - RadarrReleasesSearchBar({ - Key key, - this.prefill = '', - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - final _textController = TextEditingController(); - - void initState() { - super.initState(); - _textController.text = widget.prefill ?? ''; - } - - @override - Widget build(BuildContext context) => Expanded( - child: Consumer( - builder: (context, model, widget) => LSTextInputBar( - controller: _textController, - labelText: 'Search Releases...', - onChanged: (text, update) => _onChanged(model, text, update), - margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0), - ), - ), - ); - - void _onChanged(RadarrState model, String text, bool update) { - model.searchReleasesFilter = text; - if(update) _textController.text = ''; - } -} diff --git a/lib/modules/radarr/widgets/releases_sorting_button.dart b/lib/modules/radarr/widgets/releases_sorting_button.dart deleted file mode 100644 index 7d3621ad..00000000 --- a/lib/modules/radarr/widgets/releases_sorting_button.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrReleasesSortButton extends StatefulWidget { - final ScrollController controller; - - RadarrReleasesSortButton({ - Key key, - @required this.controller, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCard( - child: Consumer( - builder: (context, model, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.sort), - onSelected: (result) { - if(model.sortReleasesType == result) { - model.sortReleasesAscending = !model.sortReleasesAscending; - } else { - model.sortReleasesAscending = true; - model.sortReleasesType = result; - } - _scrollBack(); - }, - itemBuilder: (context) => List>.generate( - RadarrReleasesSorting.values.length, - (index) => PopupMenuItem( - value: RadarrReleasesSorting.values[index], - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - RadarrReleasesSorting.values[index].readable, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - if(model.sortReleasesType == RadarrReleasesSorting.values[index]) Icon( - model.sortReleasesAscending - ? Icons.arrow_upward - : Icons.arrow_downward, - size: Constants.UI_FONT_SIZE_SUBTITLE+2.0, - color: LunaColours.accent, - ), - ], - ), - ), - ), - ), - ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), - color: Theme.of(context).canvasColor, - ); - - void _scrollBack() { - widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } -} diff --git a/lib/modules/radarr/widgets/upcoming_tile.dart b/lib/modules/radarr/widgets/upcoming_tile.dart deleted file mode 100644 index e2d1873f..00000000 --- a/lib/modules/radarr/widgets/upcoming_tile.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrUpcomingTile extends StatefulWidget { - final RadarrMissingData data; - final GlobalKey scaffoldKey; - final Function refresh; - - RadarrUpcomingTile({ - @required this.data, - @required this.scaffoldKey, - @required this.refresh, - }); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCardTile( - title: LSTitle(text: widget.data.title), - subtitle: RichText( - text: TextSpan( - style: TextStyle( - color: Colors.white70, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - children: widget.data.subtitle, - ), - ), - trailing: LSIconButton( - icon: Icons.search, - onPressed: () async => _search(), - onLongPress: () async => _interactiveSearch(), - ), - onTap: () async => _enterMovie(), - padContent: true, - decoration: LunaCardDecoration( - uri: widget.data.posterURI(), - headers: Database.currentProfileObject.getRadarr()['headers'], - ), - ); - - Future _search() async { - final _api = RadarrAPI.from(Database.currentProfileObject); - await _api.searchMissingMovies([widget.data.movieID]) - .then((_) => LSSnackBar(context: context, title: 'Searching...', message: widget.data.title)) - .catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure)); - } - - Future _interactiveSearch() async => Navigator.of(context).pushNamed( - RadarrSearchResults.ROUTE_NAME, - arguments: RadarrSearchResultsArguments( - movieID: widget.data.movieID, - title: widget.data.title, - ), - ); - - Future _enterMovie() async { - final dynamic result = await Navigator.of(context).pushNamed( - RadarrDetailsMovie.ROUTE_NAME, - arguments: RadarrDetailsMovieArguments( - data: null, - movieID: widget.data.movieID, - ), - ); - if(result != null) switch(result[0]) { - case 'remove_movie': { - LSSnackBar( - context: context, - title: 'Removed', - message: widget.data.title, - type: SNACKBAR_TYPE.success, - ); - widget.refresh(); - break; - } - } - } -} diff --git a/lib/modules/sabnzbd/core/api/api.dart b/lib/modules/sabnzbd/core/api/api.dart index 7bea7eee..3d152b80 100644 --- a/lib/modules/sabnzbd/core/api/api.dart +++ b/lib/modules/sabnzbd/core/api/api.dart @@ -359,17 +359,17 @@ class SABnzbdAPI { List entries = []; for(var entry in response.data['history']['slots']) { entries.add(SABnzbdHistoryData( - nzoId: entry['nzo_id'] ?? Constants.EMPTY_STRING, - name: entry['name'] ?? Constants.EMPTY_STRING, + nzoId: entry['nzo_id'] ?? '', + name: entry['name'] ?? '', size: entry['bytes'] ?? 0, - status: entry['status'] ?? Constants.EMPTY_STRING, - failureMessage: entry['fail_message'] ?? Constants.EMPTY_STRING, + status: entry['status'] ?? '', + failureMessage: entry['fail_message'] ?? '', timestamp: entry['completed'] ?? 0, - actionLine: entry['action_line'] ?? Constants.EMPTY_STRING, + actionLine: entry['action_line'] ?? '', category: entry['category'] == '*' ? 'Default' : entry['category'], downloadTime: entry['download_time'] ?? 0, - stageLog: entry['stage_log'] ?? Constants.EMPTY_LIST, - storageLocation: entry['storage'] ?? Constants.EMPTY_STRING, + stageLog: entry['stage_log'] ?? [], + storageLocation: entry['storage'] ?? '', )); } return entries; diff --git a/lib/modules/sabnzbd/core/constants.dart b/lib/modules/sabnzbd/core/constants.dart index 5718333b..06f4e7fb 100644 --- a/lib/modules/sabnzbd/core/constants.dart +++ b/lib/modules/sabnzbd/core/constants.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/sabnzbd.dart'; -import 'package:quick_actions/quick_actions.dart'; class SABnzbdConstants { SABnzbdConstants._(); @@ -16,11 +14,10 @@ class SABnzbdConstants { settingsDescription: 'Configure SABnzbd', helpMessage: 'SABnzbd is a multi-platform binary newsgroup downloader. The program works in the background and simplifies the downloading verifying and extracting of files from Usenet.', icon: CustomIcons.sabnzbd, - route: '/sabnzbd', + route: SABnzbd.ROUTE_NAME, color: Color(0xFFFECC2B), website: 'https://sabnzbd.org', github: 'https://github.com/sabnzbd/sabnzbd', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(SABnzbd.ROUTE_NAME, (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'SABnzbd'), ); } diff --git a/lib/modules/sabnzbd/core/database.dart b/lib/modules/sabnzbd/core/database.dart index 7179eb54..934e4faf 100644 --- a/lib/modules/sabnzbd/core/database.dart +++ b/lib/modules/sabnzbd/core/database.dart @@ -65,7 +65,7 @@ extension SABnzbdDatabaseValueExtension on SABnzbdDatabaseValue { LunaLogger().warning('SABnzbdDatabaseValueExtension', 'put', 'Attempted to enter data for invalid SABnzbdDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/sabnzbd/core/dialogs.dart b/lib/modules/sabnzbd/core/dialogs.dart index ae866640..1b780d02 100644 --- a/lib/modules/sabnzbd/core/dialogs.dart +++ b/lib/modules/sabnzbd/core/dialogs.dart @@ -666,7 +666,7 @@ class SABnzbdDialogs { await LSDialog.dialog( context: context, - title: 'Default Page', + title: 'Page', content: List.generate( SABnzbdNavigationBar.titles.length, (index) => LSDialog.tile( diff --git a/lib/modules/sabnzbd/routes/history_stages.dart b/lib/modules/sabnzbd/routes/history_stages.dart index 375aa373..b5b05fcc 100644 --- a/lib/modules/sabnzbd/routes/history_stages.dart +++ b/lib/modules/sabnzbd/routes/history_stages.dart @@ -37,10 +37,7 @@ class _State extends State { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Stages', - ); + Widget get _appBar => LunaAppBar(title: 'Stages'); Widget get _body => _arguments == null ? null diff --git a/lib/modules/sabnzbd/routes/sabnzbd.dart b/lib/modules/sabnzbd/routes/sabnzbd.dart index 695d2480..5a71fdce 100644 --- a/lib/modules/sabnzbd/routes/sabnzbd.dart +++ b/lib/modules/sabnzbd/routes/sabnzbd.dart @@ -69,8 +69,7 @@ class _State extends State { onPageChanged: _onPageChanged, ); - Widget get _appBar => LSAppBarDropdown( - context: context, + Widget get _appBar => LunaAppBar.dropdown( title: 'SABnzbd', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).sabnzbdEnabled) diff --git a/lib/modules/sabnzbd/routes/statistics.dart b/lib/modules/sabnzbd/routes/statistics.dart index 50cee6ec..db2ccb4e 100644 --- a/lib/modules/sabnzbd/routes/statistics.dart +++ b/lib/modules/sabnzbd/routes/statistics.dart @@ -36,10 +36,7 @@ class _State extends State { }); } - Widget get _appBar => LunaAppBar( - context: context, - title: 'Server Statistics', - ); + Widget get _appBar => LunaAppBar(title: 'Server Statistics'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/sabnzbd/widgets/app_bar_stats.dart b/lib/modules/sabnzbd/widgets/app_bar_stats.dart index fbe91992..d08b06d3 100644 --- a/lib/modules/sabnzbd/widgets/app_bar_stats.dart +++ b/lib/modules/sabnzbd/widgets/app_bar_stats.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; import 'package:tuple/tuple.dart'; import 'package:lunasea/modules/sabnzbd.dart'; @@ -26,7 +27,7 @@ class SABnzbdAppBarStats extends StatelessWidget { TextSpan( text: _status(data.item1, data.item2), style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_HEADER, color: LunaColours.accent, ), @@ -53,6 +54,7 @@ class SABnzbdAppBarStats extends StatelessWidget { : speed; Future _onTap(BuildContext context, int speed) async { + HapticFeedback.lightImpact(); List values = await SABnzbdDialogs.speedLimit(context, speed); if(values[0]) switch(values[1]) { case -1: { diff --git a/lib/modules/sabnzbd/widgets/history_hide_button.dart b/lib/modules/sabnzbd/widgets/history_hide_button.dart index b45709cc..330067bd 100644 --- a/lib/modules/sabnzbd/widgets/history_hide_button.dart +++ b/lib/modules/sabnzbd/widgets/history_hide_button.dart @@ -9,16 +9,14 @@ class SABnzbdHistoryHideButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, model, widget) => InkWell( - child: LSIconButton( - icon: model.historyHideFailed - ? Icons.visibility_off - : Icons.visibility, - ), - onTap: () => model.historyHideFailed = !model.historyHideFailed, - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + builder: (context, model, widget) => LSIconButton( + icon: model.historyHideFailed + ? Icons.visibility_off + : Icons.visibility, + onPressed: () => model.historyHideFailed = !model.historyHideFailed, ), ), margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), diff --git a/lib/modules/sabnzbd/widgets/history_tile.dart b/lib/modules/sabnzbd/widgets/history_tile.dart index c6af64d5..8f68b273 100644 --- a/lib/modules/sabnzbd/widgets/history_tile.dart +++ b/lib/modules/sabnzbd/widgets/history_tile.dart @@ -127,7 +127,7 @@ class SABnzbdHistoryTile extends StatelessWidget { text: data.statusString, style: TextStyle( color: data.statusColor, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ) ], diff --git a/lib/modules/sabnzbd/widgets/navigation_bar.dart b/lib/modules/sabnzbd/widgets/navigation_bar.dart index ccfa1d5c..1bc7336d 100644 --- a/lib/modules/sabnzbd/widgets/navigation_bar.dart +++ b/lib/modules/sabnzbd/widgets/navigation_bar.dart @@ -45,11 +45,7 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); - Provider.of(context, listen: false).navigationIndex = index; + widget.pageController.lunaAnimateToPage(index) + .then((_) => Provider.of(context, listen: false).navigationIndex = index); } } diff --git a/lib/modules/sabnzbd/widgets/queue_fab.dart b/lib/modules/sabnzbd/widgets/queue_fab.dart index 63571616..c4c8f720 100644 --- a/lib/modules/sabnzbd/widgets/queue_fab.dart +++ b/lib/modules/sabnzbd/widgets/queue_fab.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; import 'package:tuple/tuple.dart'; import 'package:lunasea/modules/sabnzbd.dart'; @@ -38,14 +39,14 @@ class _State extends State with TickerProviderStateMixin { void _setupIconController() { _iconController = AnimationController( vsync: this, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), ); } void _setupHideController() { _hideController = AnimationController( vsync: this, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED), ); _hideController.forward(); widget.scrollController.addListener(() { @@ -88,6 +89,7 @@ class _State extends State with TickerProviderStateMixin { ); Future _toggle(BuildContext context, bool paused) async { + HapticFeedback.lightImpact(); SABnzbdAPI _api = SABnzbdAPI.from(Database.currentProfileObject); paused ? _resume(context, _api) @@ -95,6 +97,7 @@ class _State extends State with TickerProviderStateMixin { } Future _toggleFor(BuildContext context) async { + HapticFeedback.heavyImpact(); List values = await SABnzbdDialogs.pauseFor(context); if(values[0]) { if(values[1] == -1) { diff --git a/lib/modules/search/core/constants.dart b/lib/modules/search/core/constants.dart index 9fcf7d2c..241b8b54 100644 --- a/lib/modules/search/core/constants.dart +++ b/lib/modules/search/core/constants.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/search.dart'; -import 'package:quick_actions/quick_actions.dart'; class SearchConstants { SearchConstants._(); @@ -16,12 +14,11 @@ class SearchConstants { settingsDescription: 'Configure Search', helpMessage: '${Constants.APPLICATION_NAME} currently supports all indexers that support the newznab protocol, including NZBHydra2.', icon: Icons.search, - route: '/search', + route: Search.ROUTE_NAME, color: Color(LunaColours.ACCENT_COLOR), website: '', github: 'https://github.com/theotherp/nzbhydra2', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(Search.ROUTE_NAME, (Route route) => false), - shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Usenet Indexer Search'), + shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Search'), ); static const List ADULT_CATEGORIES = ['xxx', 'adult', 'porn']; diff --git a/lib/modules/search/core/database.dart b/lib/modules/search/core/database.dart index 83931def..50aad999 100644 --- a/lib/modules/search/core/database.dart +++ b/lib/modules/search/core/database.dart @@ -72,7 +72,7 @@ extension SearchDatabaseValueExtension on SearchDatabaseValue { LunaLogger().warning('SearchDatabaseValueExtension', 'put', 'Attempted to enter data for invalid SearchDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/search/core/dialogs.dart b/lib/modules/search/core/dialogs.dart index 72127572..553f6781 100644 --- a/lib/modules/search/core/dialogs.dart +++ b/lib/modules/search/core/dialogs.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; class SearchDialogs { @@ -28,10 +29,7 @@ class SearchDialogs { builder: (context, profilesBox, widget) => LSDialog.content( children: [ Padding( - child: PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), + child: LunaPopupMenuButton( child: Container( child: Row( mainAxisSize: MainAxisSize.max, @@ -60,7 +58,10 @@ class SearchDialogs { ), ), ), - onSelected: (result) => LunaProfile().safelyChangeProfiles(context, result), + onSelected: (result) { + HapticFeedback.selectionClick(); + LunaProfile().safelyChangeProfiles(context, result); + }, itemBuilder: (context) { return >[for(String profile in (profilesBox as Box).keys) PopupMenuItem( value: profile, diff --git a/lib/modules/search/routes/categories.dart b/lib/modules/search/routes/categories.dart index 375f5073..0ff12595 100644 --- a/lib/modules/search/routes/categories.dart +++ b/lib/modules/search/routes/categories.dart @@ -35,7 +35,6 @@ class _State extends State { } Widget get _appBar => LunaAppBar( - context: context, title: Provider.of(context, listen: false)?.indexer?.displayName ?? 'Categories', actions: [ LSIconButton( diff --git a/lib/modules/search/routes/indexers.dart b/lib/modules/search/routes/indexers.dart index 7fadf5ce..46017677 100644 --- a/lib/modules/search/routes/indexers.dart +++ b/lib/modules/search/routes/indexers.dart @@ -38,8 +38,7 @@ class _State extends State { ); Widget get _appBar => LunaAppBar( - context: context, - hideLeading: true, + useDrawer: true, title: 'Search', ); diff --git a/lib/modules/search/routes/results.dart b/lib/modules/search/routes/results.dart index 126c7ab0..627604df 100644 --- a/lib/modules/search/routes/results.dart +++ b/lib/modules/search/routes/results.dart @@ -40,7 +40,6 @@ class _State extends State { } Widget get _appBar => LunaAppBar( - context: context, title: Provider.of(context, listen: false)?.searchTitle ?? 'Results', actions: [ LSIconButton( diff --git a/lib/modules/search/routes/search.dart b/lib/modules/search/routes/search.dart index 6a328c19..19d65b0f 100644 --- a/lib/modules/search/routes/search.dart +++ b/lib/modules/search/routes/search.dart @@ -35,10 +35,7 @@ class _State extends State { }); } - Widget get _appBar => LunaAppBar( - context: context, - title: 'Search: ${Provider.of(context, listen: false)?.searchTitle ?? 'Unknown'}', - ); + Widget get _appBar => LunaAppBar(title: 'Search: ${Provider.of(context, listen: false)?.searchTitle ?? 'Unknown'}'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/search/routes/subcategories.dart b/lib/modules/search/routes/subcategories.dart index 32bdca8b..7ad52cdb 100644 --- a/lib/modules/search/routes/subcategories.dart +++ b/lib/modules/search/routes/subcategories.dart @@ -20,7 +20,6 @@ class _State extends State { ); Widget get _appBar => LunaAppBar( - context: context, title: Provider.of(context, listen: false)?.category?.name ?? 'Subcategories', actions: [ LSIconButton( diff --git a/lib/modules/search/widgets/result_sorting_button.dart b/lib/modules/search/widgets/result_sorting_button.dart index 798a1e8e..a5eeac7d 100644 --- a/lib/modules/search/widgets/result_sorting_button.dart +++ b/lib/modules/search/widgets/result_sorting_button.dart @@ -16,13 +16,11 @@ class SearchResultsSortButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, model, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.sort), + builder: (context, model, _) => LunaPopupMenuButton( + icon: Icons.sort, onSelected: (result) { if(model.sortResultsSorting == result) { model.sortResultsAscending = !model.sortResultsAscending; @@ -30,7 +28,7 @@ class _State extends State { model.sortResultsAscending = true; model.sortResultsSorting = result; } - _scrollBack(); + widget.controller.lunaAnimateToStart(); }, itemBuilder: (context) => List>.generate( SearchResultsSorting.values.length, @@ -61,14 +59,4 @@ class _State extends State { margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), color: Theme.of(context).canvasColor, ); - - void _scrollBack() { - widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } } diff --git a/lib/modules/settings/core/constants.dart b/lib/modules/settings/core/constants.dart index 19125efc..59d6f04e 100644 --- a/lib/modules/settings/core/constants.dart +++ b/lib/modules/settings/core/constants.dart @@ -1,8 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/settings.dart'; -import 'package:quick_actions/quick_actions.dart'; class SettingsConstants { SettingsConstants._(); @@ -16,11 +13,10 @@ class SettingsConstants { settingsDescription: '', helpMessage: '', icon: CustomIcons.settings, - route: '/settings', - color: Color(LunaColours.ACCENT_COLOR), + route: SettingsHomeRouter().route(), + color: LunaColours.accent, website: '', github: '', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(SettingsHomeRouter().route(), (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Settings'), ); } diff --git a/lib/modules/settings/core/database.dart b/lib/modules/settings/core/database.dart index d523f96b..d2759fe8 100644 --- a/lib/modules/settings/core/database.dart +++ b/lib/modules/settings/core/database.dart @@ -65,7 +65,7 @@ extension SettingsDatabaseValueExtension on SettingsDatabaseValue { LunaLogger().warning('SettingsDatabaseValueExtension', 'put', 'Attempted to enter data for invalid SettingsDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/settings/core/dialogs.dart b/lib/modules/settings/core/dialogs.dart index 2cf0a294..cd9e9a34 100644 --- a/lib/modules/settings/core/dialogs.dart +++ b/lib/modules/settings/core/dialogs.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/home/core.dart'; +import 'package:tuple/tuple.dart'; import 'package:wake_on_lan/wake_on_lan.dart'; class SettingsDialogs { @@ -499,7 +500,7 @@ class SettingsDialogs { return [_flag, _profile]; } - static Future> changeBrowser(BuildContext context) async { + static Future> changeBrowser(BuildContext context) async { bool _flag = false; LunaBrowser _browser; @@ -523,7 +524,7 @@ class SettingsDialogs { ), contentPadding: LSDialog.listDialogContentPadding(), ); - return [_flag, _browser]; + return Tuple2(_flag, _browser); } static Future> editCalendarStartingDay(BuildContext context) async { diff --git a/lib/modules/settings/modules/account/route.dart b/lib/modules/settings/modules/account/route.dart index f2fbfd13..7aa65362 100644 --- a/lib/modules/settings/modules/account/route.dart +++ b/lib/modules/settings/modules/account/route.dart @@ -26,7 +26,6 @@ class _State extends State<_SettingsAccountRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Account', actions: [_helpMessageButton], ); diff --git a/lib/modules/settings/modules/account/widgets/signed_out_body.dart b/lib/modules/settings/modules/account/widgets/signed_out_body.dart index c713d308..c49e8676 100644 --- a/lib/modules/settings/modules/account/widgets/signed_out_body.dart +++ b/lib/modules/settings/modules/account/widgets/signed_out_body.dart @@ -67,7 +67,7 @@ class _State extends State { backgroundColor: LunaColours.blueGrey, onTap: _register, reducedMargin: true, - isLoading: _state == LunaLoadingState.ACTIVE, + loadingState: _state, ), ), Expanded( @@ -75,7 +75,7 @@ class _State extends State { text: 'Sign In', onTap: _signIn, reducedMargin: true, - isLoading: _state == LunaLoadingState.ACTIVE, + loadingState: _state, ), ), ], @@ -87,7 +87,7 @@ class _State extends State { "Forgot Your Password?", style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, ), textAlign: TextAlign.center, diff --git a/lib/modules/settings/modules/account_password_reset/route.dart b/lib/modules/settings/modules/account_password_reset/route.dart index badde11e..7877f06e 100644 --- a/lib/modules/settings/modules/account_password_reset/route.dart +++ b/lib/modules/settings/modules/account_password_reset/route.dart @@ -25,10 +25,7 @@ class _State extends State<_SettingsAccountPasswordResetRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Password Reset', - ); + Widget get _appBar => LunaAppBar(title: 'Password Reset'); Widget get _body => LSListView( children: [ diff --git a/lib/modules/settings/modules/configuration.dart b/lib/modules/settings/modules/configuration.dart index 99a8180f..30920913 100644 --- a/lib/modules/settings/modules/configuration.dart +++ b/lib/modules/settings/modules/configuration.dart @@ -20,119 +20,102 @@ class _State extends State<_SettingsConfigurationRoute> { final GlobalKey _scaffoldKey = GlobalKey(); @override - Widget build(BuildContext context) => Scaffold( - key: _scaffoldKey, - appBar: _appBar, - body: _body, - ); - - Widget get _appBar => LunaAppBar( - context: context, - title: 'Configuration', - actions: [ - _profileButton, - ], - ); - - Widget get _profileButton { - Future _execute() async { - List values = await SettingsDialogs.enabledProfile( - context, - Database.profilesBox.keys.map((x) => x as String).toList()..sort((a,b) => a.toLowerCase().compareTo(b.toLowerCase())), - ); - if(values[0] && values[1] != LunaDatabaseValue.ENABLED_PROFILE.data) - LunaProfile().safelyChangeProfiles(context, values[1]); - } - return LSIconButton( - icon: Icons.person, - onPressed: _execute, + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: _appBar(), + body: _body(), ); } - Widget get _body => LSListView( - children: [ - ..._customization, - LSDivider(), - ..._general, - LSDivider(), - ..._automation, - LSDivider(), - ..._clients, - LSDivider(), - ..._monitoring, - ], - ); + Widget _appBar() { + return LunaAppBar( + state: context.read(), + title: 'Configuration', + actions: [_appBarProfileButton()], + ); + } - List get _customization => [ - LSCardTile( - title: LSTitle(text: 'Appearance'), - subtitle: LSSubtitle(text: 'Customize the Look & Feel'), - trailing: LSIconButton(icon: Icons.brush), - onTap: () async => SettingsConfigurationAppearanceRouter().navigateTo(context), - ), - LSCardTile( - title: LSTitle(text: 'Drawer'), - subtitle: LSSubtitle(text: 'Drawer Customizations'), - trailing: LSIconButton(icon: Icons.dehaze), - onTap: () async => SettingsConfigurationDrawerRouter().navigateTo(context), - ), - if(Platform.isIOS) _openLinksInTile, - LSCardTile( - title: LSTitle(text: 'Quick Actions'), - subtitle: LSSubtitle(text: 'Quick Actions on the Home Screen'), - trailing: LSIconButton(icon: Icons.rounded_corner), - onTap: () async => SettingsConfigurationQuickActionsRouter().navigateTo(context), - ), - ]; + Widget _appBarProfileButton() { + return LunaIconButton( + icon: Icons.person, + onPressed: () async { + List values = await SettingsDialogs.enabledProfile( + context, + Database.profilesBox.keys.map((x) => x as String).toList()..sort((a,b) => a.toLowerCase().compareTo(b.toLowerCase())), + ); + if(values[0] && values[1] != LunaDatabaseValue.ENABLED_PROFILE.data) LunaProfile().safelyChangeProfiles(context, values[1]); + }, + ); + } - List get _general => [ - LSCardTile( - title: LSTitle(text: 'Home'), - subtitle: LSSubtitle(text: 'Configure the Home Screen'), - trailing: LSIconButton(icon: CustomIcons.home), - onTap: () async => SettingsConfigurationHomeRouter().navigateTo(context), - ), - _tileFromModuleMap(SearchConstants.MODULE_METADATA, () async => SettingsConfigurationSearchRouter().navigateTo(context)), - _tileFromModuleMap(WakeOnLANConstants.MODULE_METADATA, () async => SettingsConfigurationWakeOnLANRouter().navigateTo(context)), - ]; - - List get _automation => [ - _tileFromModuleMap(LidarrConstants.MODULE_METADATA, () async => SettingsConfigurationLidarrRouter().navigateTo(context)), - _tileFromModuleMap(RadarrConstants.MODULE_METADATA, () async => SettingsConfigurationRadarrRouter().navigateTo(context)), - _tileFromModuleMap(SonarrConstants.MODULE_METADATA, () async => SettingsConfigurationSonarrRouter().navigateTo(context)), - ]; - - List get _clients => [ - _tileFromModuleMap(NZBGetConstants.MODULE_METADATA, () async => SettingsConfigurationNZBGetRouter().navigateTo(context)), - _tileFromModuleMap(SABnzbdConstants.MODULE_METADATA, () async => SettingsConfigurationSABnzbdRouter().navigateTo(context)), - ]; - - List get _monitoring => [ - _tileFromModuleMap(TautulliConstants.MODULE_METADATA, () async => SettingsConfigurationTautulliRouter().navigateTo(context)), - ]; - - Widget _tileFromModuleMap(LunaModuleMetadata map, Function onTap) => LSCardTile( - title: LSTitle(text: map.name), - subtitle: LSSubtitle(text: map.settingsDescription), - trailing: LSIconButton(icon: map.icon), - onTap: onTap, - ); - - Widget get _openLinksInTile { - Future _execute() async { - List _values = await SettingsDialogs.changeBrowser(context); - if(_values[0]) LunaDatabaseValue.SELECTED_BROWSER.put(_values[1]); - } - return ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.SELECTED_BROWSER.key]), - builder: (context, box, widget) => LSCardTile( - title: LSTitle(text: 'Open Links In...'), - subtitle: LSSubtitle(text: (LunaDatabaseValue.SELECTED_BROWSER.data as LunaBrowser).name), - trailing: LSIconButton( - icon: (LunaDatabaseValue.SELECTED_BROWSER.data as LunaBrowser).icon, + Widget _body() { + return LunaListView( + scrollController: context.read().scrollController, + children: [ + LunaListTile( + context: context, + title: LunaText.title(text: 'Appearance'), + subtitle: LunaText.subtitle(text: 'Customize the Look & Feel'), + trailing: LunaIconButton(icon: Icons.brush), + onTap: () async => SettingsConfigurationAppearanceRouter().navigateTo(context), ), - onTap: _execute, - ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Drawer'), + subtitle: LunaText.subtitle(text: 'Drawer Customizations'), + trailing: LunaIconButton(icon: Icons.dehaze), + onTap: () async => SettingsConfigurationDrawerRouter().navigateTo(context), + ), + if(Platform.isIOS) LunaDatabaseValue.SELECTED_BROWSER.listen( + builder: (context, box, widget) => LunaListTile( + context: context, + title: LunaText.title(text: 'Open Links In...'), + subtitle: LunaText.subtitle(text: (LunaDatabaseValue.SELECTED_BROWSER.data as LunaBrowser).name), + trailing: LunaIconButton(icon: (LunaDatabaseValue.SELECTED_BROWSER.data as LunaBrowser).icon), + onTap: () async { + Tuple2 result = await SettingsDialogs.changeBrowser(context); + if(result.item1) LunaDatabaseValue.SELECTED_BROWSER.put(result.item2); + }, + ), + ), + LunaListTile( + context: context, + title: LunaText.title(text: 'Quick Actions'), + subtitle: LunaText.subtitle(text: 'Quick Actions on the Home Screen'), + trailing: LunaIconButton(icon: Icons.rounded_corner), + onTap: () async => SettingsConfigurationQuickActionsRouter().navigateTo(context), + ), + LunaDivider(), + LunaListTile( + context: context, + title: LunaText.title(text: 'Home'), + subtitle: LunaText.subtitle(text: 'Configure the Home Screen'), + trailing: LunaIconButton(icon: CustomIcons.home), + onTap: () async => SettingsConfigurationHomeRouter().navigateTo(context), + ), + _tileFromModuleMap(SearchConstants.MODULE_METADATA, () async => SettingsConfigurationSearchRouter().navigateTo(context)), + _tileFromModuleMap(WakeOnLANConstants.MODULE_METADATA, () async => SettingsConfigurationWakeOnLANRouter().navigateTo(context)), + LunaDivider(), + _tileFromModuleMap(LidarrConstants.MODULE_METADATA, () async => SettingsConfigurationLidarrRouter().navigateTo(context)), + _tileFromModuleMap(RadarrConstants.MODULE_METADATA, () async => SettingsConfigurationRadarrRouter().navigateTo(context)), + _tileFromModuleMap(SonarrConstants.MODULE_METADATA, () async => SettingsConfigurationSonarrRouter().navigateTo(context)), + LunaDivider(), + _tileFromModuleMap(NZBGetConstants.MODULE_METADATA, () async => SettingsConfigurationNZBGetRouter().navigateTo(context)), + _tileFromModuleMap(SABnzbdConstants.MODULE_METADATA, () async => SettingsConfigurationSABnzbdRouter().navigateTo(context)), + LunaDivider(), + _tileFromModuleMap(TautulliConstants.MODULE_METADATA, () async => SettingsConfigurationTautulliRouter().navigateTo(context)), + ], + ); + } + + Widget _tileFromModuleMap(LunaModuleMetadata map, Function onTap) { + return LunaListTile( + context: context, + title: LunaText.title(text: map.name), + subtitle: LunaText.subtitle(text: map.settingsDescription), + trailing: LunaIconButton(icon: map.icon), + onTap: onTap, ); } } diff --git a/lib/modules/settings/modules/configuration_appearance.dart b/lib/modules/settings/modules/configuration_appearance.dart index 3a64b710..7d58d61a 100644 --- a/lib/modules/settings/modules/configuration_appearance.dart +++ b/lib/modules/settings/modules/configuration_appearance.dart @@ -24,10 +24,7 @@ class _State extends State<_SettingsConfigurationAppearanceRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Appearance', - ); + Widget get _appBar => LunaAppBar(title: 'Appearance'); Widget get _body => LSListView( children: [ @@ -43,7 +40,7 @@ class _State extends State<_SettingsConfigurationAppearanceRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'AMOLED Dark Theme'), subtitle: LSSubtitle(text: 'Pure Black Dark Theme'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.THEME_AMOLED.data, onChanged: (value) => LunaDatabaseValue.THEME_AMOLED.put(value), ), @@ -58,7 +55,7 @@ class _State extends State<_SettingsConfigurationAppearanceRoute> { builder: (context, box, _) => LSCardTile( title: LSTitle(text: 'AMOLED Borders'), subtitle: LSSubtitle(text: 'Add Subtle Borders Across the UI'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.THEME_AMOLED_BORDER.data, onChanged: LunaDatabaseValue.THEME_AMOLED.data ? (value) => LunaDatabaseValue.THEME_AMOLED_BORDER.put(value) @@ -91,7 +88,7 @@ class _State extends State<_SettingsConfigurationAppearanceRoute> { builder: (context, box, _) => LSCardTile( title: LSTitle(text: 'Use 24 Hour Time'), subtitle: LSSubtitle(text: 'Show Timestamps in 24 Hour Style'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.USE_24_HOUR_TIME.data, onChanged: (value) => LunaDatabaseValue.USE_24_HOUR_TIME.put(value), ), diff --git a/lib/modules/settings/modules/configuration_drawer.dart b/lib/modules/settings/modules/configuration_drawer.dart index 4d2c5c37..3336a380 100644 --- a/lib/modules/settings/modules/configuration_drawer.dart +++ b/lib/modules/settings/modules/configuration_drawer.dart @@ -23,10 +23,7 @@ class _State extends State<_SettingsConfigurationDrawerRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Drawer', - ); + Widget get _appBar => LunaAppBar(title: 'Drawer'); Widget get _body => LSListView( children: [ @@ -46,7 +43,7 @@ class _State extends State<_SettingsConfigurationDrawerRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Use Folders'), subtitle: LSSubtitle(text: 'Group Modules into Categories'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.DRAWER_GROUP_MODULES.data, onChanged: (value) => LunaDatabaseValue.DRAWER_GROUP_MODULES.put(value), ), @@ -61,7 +58,7 @@ class _State extends State<_SettingsConfigurationDrawerRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Expand Automation'), subtitle: LSSubtitle(text: 'Expand Automation Folder Initially'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.DRAWER_EXPAND_AUTOMATION.data, onChanged: LunaDatabaseValue.DRAWER_GROUP_MODULES.data ? (value) => LunaDatabaseValue.DRAWER_EXPAND_AUTOMATION.put(value) @@ -78,7 +75,7 @@ class _State extends State<_SettingsConfigurationDrawerRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Expand Clients'), subtitle: LSSubtitle(text: 'Expand Clients Folder Initially'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.DRAWER_EXPAND_CLIENTS.data, onChanged: LunaDatabaseValue.DRAWER_GROUP_MODULES.data ? (value) => LunaDatabaseValue.DRAWER_EXPAND_CLIENTS.put(value) @@ -95,7 +92,7 @@ class _State extends State<_SettingsConfigurationDrawerRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Expand Monitoring'), subtitle: LSSubtitle(text: 'Expand Monitoring Folder Initially'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.DRAWER_EXPAND_MONITORING.data, onChanged: LunaDatabaseValue.DRAWER_GROUP_MODULES.data ? (value) => LunaDatabaseValue.DRAWER_EXPAND_MONITORING.put(value) diff --git a/lib/modules/settings/modules/configuration_home.dart b/lib/modules/settings/modules/configuration_home.dart index 01f4b543..7f0e8c07 100644 --- a/lib/modules/settings/modules/configuration_home.dart +++ b/lib/modules/settings/modules/configuration_home.dart @@ -25,10 +25,7 @@ class _State extends State<_SettingsConfigurationHomeRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Home', - ); + Widget get _appBar => LunaAppBar(title: 'Home'); Widget get _body => LSListView( children: [ @@ -55,7 +52,7 @@ class _State extends State<_SettingsConfigurationHomeRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Use Brand Colours'), subtitle: LSSubtitle(text: 'For Module Icons'), - trailing: Switch( + trailing: LunaSwitch( value: HomeDatabaseValue.MODULES_BRAND_COLOURS.data, onChanged: (value) => HomeDatabaseValue.MODULES_BRAND_COLOURS.put(value), ), @@ -112,7 +109,7 @@ class _State extends State<_SettingsConfigurationHomeRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Lidarr'), subtitle: LSSubtitle(text: HomeDatabaseValue.CALENDAR_ENABLE_LIDARR.data ? 'Showing Lidarr Entries' : 'Hiding Lidarr Entries'), - trailing: Switch( + trailing: LunaSwitch( value: HomeDatabaseValue.CALENDAR_ENABLE_LIDARR.data, onChanged: (value) => HomeDatabaseValue.CALENDAR_ENABLE_LIDARR.put(value), ), @@ -124,7 +121,7 @@ class _State extends State<_SettingsConfigurationHomeRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Radarr'), subtitle: LSSubtitle(text: HomeDatabaseValue.CALENDAR_ENABLE_RADARR.data ? 'Showing Radarr Entries' : 'Hiding Radarr Entries'), - trailing: Switch( + trailing: LunaSwitch( value: HomeDatabaseValue.CALENDAR_ENABLE_RADARR.data, onChanged: (value) => HomeDatabaseValue.CALENDAR_ENABLE_RADARR.put(value), ), @@ -136,7 +133,7 @@ class _State extends State<_SettingsConfigurationHomeRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Sonarr'), subtitle: LSSubtitle(text: HomeDatabaseValue.CALENDAR_ENABLE_SONARR.data ? 'Showing Sonarr Entries' : 'Hiding Sonarr Entries'), - trailing: Switch( + trailing: LunaSwitch( value: HomeDatabaseValue.CALENDAR_ENABLE_SONARR.data, onChanged: (value) => HomeDatabaseValue.CALENDAR_ENABLE_SONARR.put(value), ), diff --git a/lib/modules/settings/modules/configuration_lidarr.dart b/lib/modules/settings/modules/configuration_lidarr.dart index adf67ffd..cc5272d5 100644 --- a/lib/modules/settings/modules/configuration_lidarr.dart +++ b/lib/modules/settings/modules/configuration_lidarr.dart @@ -27,7 +27,6 @@ class _State extends State<_SettingsConfigurationLidarrRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Lidarr', actions: [_helpMessageButton], ); @@ -68,7 +67,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> { Widget get _enabledTile => LSCardTile( title: LSTitle(text: 'Enable Lidarr'), - trailing: Switch( + trailing: LunaSwitch( value: Database.currentProfileObject.lidarrEnabled ?? false, onChanged: (value) { Database.currentProfileObject.lidarrEnabled = value; @@ -161,7 +160,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> { return ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [LidarrDatabaseValue.NAVIGATION_INDEX.key]), builder: (context, box, _) => LSCardTile( - title: LSTitle(text: 'Default Page'), + title: LSTitle(text: 'Home'), subtitle: LSSubtitle(text: LidarrNavigationBar.titles[LidarrDatabaseValue.NAVIGATION_INDEX.data]), trailing: LSIconButton(icon: LidarrNavigationBar.icons[LidarrDatabaseValue.NAVIGATION_INDEX.data]), onTap: _execute, diff --git a/lib/modules/settings/modules/configuration_lidarr_headers.dart b/lib/modules/settings/modules/configuration_lidarr_headers.dart index 39abcc05..5e55bb59 100644 --- a/lib/modules/settings/modules/configuration_lidarr_headers.dart +++ b/lib/modules/settings/modules/configuration_lidarr_headers.dart @@ -26,10 +26,7 @@ class _State extends State<_SettingsConfigurationLidarrHeadersRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => ValueListenableBuilder( valueListenable: Database.profilesBox.listenable(), @@ -44,7 +41,7 @@ class _State extends State<_SettingsConfigurationLidarrHeadersRoute> { _addHeaderTile, ]; - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (Database.currentProfileObject.lidarrHeaders ?? {}).cast(); diff --git a/lib/modules/settings/modules/configuration_nzbget.dart b/lib/modules/settings/modules/configuration_nzbget.dart index 2586e6d9..2df62f9b 100644 --- a/lib/modules/settings/modules/configuration_nzbget.dart +++ b/lib/modules/settings/modules/configuration_nzbget.dart @@ -27,7 +27,6 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'NZBGet', actions: [_helpMessageButton], ); @@ -64,7 +63,7 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> { Widget get _enabledTile => LSCardTile( title: LSTitle(text: 'Enable NZBGet'), - trailing: Switch( + trailing: LunaSwitch( value: Database.currentProfileObject.nzbgetEnabled ?? false, onChanged: (value) { Database.currentProfileObject.nzbgetEnabled = value; diff --git a/lib/modules/settings/modules/configuration_nzbget_headers.dart b/lib/modules/settings/modules/configuration_nzbget_headers.dart index 60e7117c..340d0535 100644 --- a/lib/modules/settings/modules/configuration_nzbget_headers.dart +++ b/lib/modules/settings/modules/configuration_nzbget_headers.dart @@ -26,10 +26,7 @@ class _State extends State<_SettingsConfigurationNZBGetHeadersRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => ValueListenableBuilder( valueListenable: Database.profilesBox.listenable(), @@ -44,7 +41,7 @@ class _State extends State<_SettingsConfigurationNZBGetHeadersRoute> { _addHeaderTile, ]; - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (Database.currentProfileObject.nzbgetHeaders ?? {}).cast(); diff --git a/lib/modules/settings/modules/configuration_quickactions.dart b/lib/modules/settings/modules/configuration_quickactions.dart index 98d5067c..e4974638 100644 --- a/lib/modules/settings/modules/configuration_quickactions.dart +++ b/lib/modules/settings/modules/configuration_quickactions.dart @@ -28,7 +28,6 @@ class _State extends State<_SettingsConfigurationQuickActionsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Quick Actions', actions: [ LSIconButton( @@ -57,11 +56,11 @@ class _State extends State<_SettingsConfigurationQuickActionsRoute> { title: LSTitle(text: title), trailing: ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [action.key]), - builder: (context, box, _) => Switch( + builder: (context, box, _) => LunaSwitch( value: action.data, onChanged: (value) { action.put(value); - LunaQuickActions.setShortcutItems(); + LunaQuickActions().setShortcutItems(); } ), ), diff --git a/lib/modules/settings/modules/configuration_radarr.dart b/lib/modules/settings/modules/configuration_radarr.dart index 18b36f56..ebe4b954 100644 --- a/lib/modules/settings/modules/configuration_radarr.dart +++ b/lib/modules/settings/modules/configuration_radarr.dart @@ -27,7 +27,6 @@ class _State extends State<_SettingsConfigurationRadarrRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Radarr', actions: [_helpMessageButton], ); @@ -63,88 +62,91 @@ class _State extends State<_SettingsConfigurationRadarrRoute> { List get _customization => [ LSHeader(text: 'Default Pages'), - _configDefaultPageTile, + _defaultPageHomeTile, + _defaultPageMovieDetailsTile, + _defaultPageAddMovieTile, + _defaultPageSystemStatusTile, + LSHeader(text: 'Default Sorting & Filtering'), + _defaultSortingMoviesTile, + _defaultSortingMoviesDirectionTile, + _defaultFilteringMoviesTile, ]; Widget get _enabledTile => LSCardTile( title: LSTitle(text: 'Enable Radarr'), - trailing: Switch( + trailing: LunaSwitch( value: Database.currentProfileObject.radarrEnabled ?? false, onChanged: (value) { Database.currentProfileObject.radarrEnabled = value; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); }, ), ); - Widget get _hostTile { - Future _execute() async { - List _values = await SettingsDialogs.editHost( - context, - 'Radarr Host', - prefill: Database.currentProfileObject.radarrHost ?? '', - ); + Widget get _hostTile => LSCardTile( + title: LSTitle(text: 'Host'), + subtitle: LSSubtitle(text: Database.currentProfileObject.radarrHost == null || Database.currentProfileObject.radarrHost == '' ? 'Not Set' : Database.currentProfileObject.radarrHost), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + List _values = await SettingsDialogs.editHost(context, 'Radarr Host', prefill: Database.currentProfileObject.radarrHost ?? ''); if(_values[0]) { Database.currentProfileObject.radarrHost = _values[1]; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } - } - return LSCardTile( - title: LSTitle(text: 'Host'), - subtitle: LSSubtitle( - text: Database.currentProfileObject.radarrHost == null || Database.currentProfileObject.radarrHost == '' - ? 'Not Set' - : Database.currentProfileObject.radarrHost - ), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: _execute, - ); - } + }, + ); - Widget get _apiKeyTile { - Future _execute() async { - List _values = await LunaDialogs().editText( - context, - 'Radarr API Key', - prefill: Database.currentProfileObject.radarrKey ?? '', - ); + Widget get _apiKeyTile => LSCardTile( + title: LSTitle(text: 'API Key'), + subtitle: LSSubtitle(text: Database.currentProfileObject.radarrKey == null || Database.currentProfileObject.radarrKey == '' ? 'Not Set' : '••••••••••••'), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + List _values = await LunaDialogs().editText(context, 'Radarr API Key', prefill: Database.currentProfileObject.radarrKey ?? ''); if(_values[0]) { Database.currentProfileObject.radarrKey = _values[1]; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } - } - return LSCardTile( - title: LSTitle(text: 'API Key'), - subtitle: LSSubtitle( - text: Database.currentProfileObject.radarrKey == null || Database.currentProfileObject.radarrKey == '' - ? 'Not Set' - : '••••••••••••' - ), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: _execute, - ); - } + }, + ); - Widget get _testConnectionTile { - Future _testConnection(BuildContext context) async => await RadarrAPI.from(Database.currentProfileObject).testConnection() - .then((_) => showLunaSuccessSnackBar( - context: context, - title: 'Connected Successfully', - message: 'Radarr is ready to use with ${Constants.APPLICATION_NAME}', - )) - .catchError((error, stack) { - LunaLogger().error('Connection Test Failed', error, stack); - showLunaErrorSnackBar( + Widget get _testConnectionTile => LSButton( + text: 'Test Connection', + onTap: () async { + RadarrState state = Provider.of(context, listen: false); + if(state.host == null || state.host.isEmpty) { + showLunaErrorSnackBar( + context: context, + title: 'Host Required', + message: 'Host is required to connect to Radarr', + ); + return; + } + if(state.apiKey == null || state.apiKey.isEmpty) { + showLunaErrorSnackBar( + context: context, + title: 'API Key Required', + message: 'API key is required to connect to Radarr', + ); + return; + } + Radarr(host: state.host, apiKey: state.apiKey, headers: Map.from(state.headers)).system.status() + .then((_) => showLunaSuccessSnackBar( context: context, - title: 'Connection Test Failed', - error: error, - ); - }); - return LSButton( - text: 'Test Connection', - onTap: () async => _testConnection(context), - ); - } + title: 'Connected Successfully', + message: 'Radarr is ready to use with ${Constants.APPLICATION_NAME}', + )).catchError((error, trace) { + LunaLogger().error('Connection Test Failed', error, trace); + showLunaErrorSnackBar( + context: context, + title: 'Connection Test Failed', + error: error, + ); + }); + }, + ); Widget get _customHeadersTile => LSCardTile( title: LSTitle(text: 'Custom Headers'), @@ -153,19 +155,102 @@ class _State extends State<_SettingsConfigurationRadarrRoute> { onTap: () async => SettingsConfigurationRadarrHeadersRouter().navigateTo(context), ); - Widget get _configDefaultPageTile { - Future _execute() async { - List _values = await RadarrDialogs.defaultPage(context); - if(_values[0]) RadarrDatabaseValue.NAVIGATION_INDEX.put(_values[1]); - } - return ValueListenableBuilder( - valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.NAVIGATION_INDEX.key]), - builder: (context, box, _) => LSCardTile( - title: LSTitle(text: 'Default Page'), - subtitle: LSSubtitle(text: RadarrNavigationBar.titles[RadarrDatabaseValue.NAVIGATION_INDEX.data]), - trailing: LSIconButton(icon: RadarrNavigationBar.icons[RadarrDatabaseValue.NAVIGATION_INDEX.data]), - onTap: _execute, + Widget get _defaultPageHomeTile => RadarrDatabaseValue.NAVIGATION_INDEX.listen( + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'Home'), + subtitle: LSSubtitle(text: RadarrNavigationBar.titles[RadarrDatabaseValue.NAVIGATION_INDEX.data]), + trailing: LSIconButton(icon: RadarrNavigationBar.icons[RadarrDatabaseValue.NAVIGATION_INDEX.data]), + onTap: () async { + Tuple2 values = await RadarrDialogs().setDefaultPage(context, titles: RadarrNavigationBar.titles, icons: RadarrNavigationBar.icons); + if(values.item1) RadarrDatabaseValue.NAVIGATION_INDEX.put(values.item2); + }, + ), + ); + + Widget get _defaultPageMovieDetailsTile => RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS.listen( + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'Movie Details'), + subtitle: LSSubtitle(text: RadarrMovieDetailsNavigationBar.titles[RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS.data]), + trailing: LSIconButton(icon: RadarrMovieDetailsNavigationBar.icons[RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS.data]), + onTap: () async { + Tuple2 values = await RadarrDialogs().setDefaultPage(context, titles: RadarrMovieDetailsNavigationBar.titles, icons: RadarrMovieDetailsNavigationBar.icons); + if(values.item1) RadarrDatabaseValue.NAVIGATION_INDEX_MOVIE_DETAILS.put(values.item2); + }, + ), + ); + + Widget get _defaultPageAddMovieTile => RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE.listen( + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'Add Movie'), + subtitle: LSSubtitle(text: RadarrAddMovieNavigationBar.titles[RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE.data]), + trailing: LSIconButton(icon: RadarrAddMovieNavigationBar.icons[RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE.data]), + onTap: () async { + Tuple2 values = await RadarrDialogs().setDefaultPage(context, titles: RadarrAddMovieNavigationBar.titles, icons: RadarrAddMovieNavigationBar.icons); + if(values.item1) RadarrDatabaseValue.NAVIGATION_INDEX_ADD_MOVIE.put(values.item2); + }, + ), + ); + + Widget get _defaultPageSystemStatusTile => RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS.listen( + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'System Status'), + subtitle: LSSubtitle(text: RadarrSystemStatusNavigationBar.titles[RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS.data]), + trailing: LSIconButton(icon: RadarrSystemStatusNavigationBar.icons[RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS.data]), + onTap: () async { + Tuple2 values = await RadarrDialogs().setDefaultPage(context, titles: RadarrSystemStatusNavigationBar.titles, icons: RadarrSystemStatusNavigationBar.icons); + if(values.item1) RadarrDatabaseValue.NAVIGATION_INDEX_SYSTEM_STATUS..put(values.item2); + }, + ), + ); + + Widget get _defaultSortingMoviesTile => ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.key]), + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'Movies Sort Category'), + subtitle: LSSubtitle(text: (RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.data as RadarrMoviesSorting).readable), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + List titles = RadarrMoviesSorting.values.map((e) => e.readable).toList(); + Tuple2 values = await RadarrDialogs().setDefaultSortingOrFiltering(context, titles: titles); + if(values.item1) { + RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.put(RadarrMoviesSorting.values[values.item2]); + context.read().moviesSortType = RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.data; + context.read().moviesSortAscending = RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING.data; + } + }, + ), + ); + + Widget get _defaultSortingMoviesDirectionTile => ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING.key]), + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'Movies Sort Direction'), + subtitle: LSSubtitle(text: RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING.data ? 'Ascending' : 'Descending'), + trailing: LunaSwitch( + value: RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING.data, + onChanged: (value) { + RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING.put(value); + context.read().moviesSortType = RadarrDatabaseValue.DEFAULT_SORTING_MOVIES.data; + context.read().moviesSortAscending = RadarrDatabaseValue.DEFAULT_SORTING_MOVIES_ASCENDING.data; + }, ), - ); - } + ), + ); + + Widget get _defaultFilteringMoviesTile => ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES.key]), + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'Movies Filter Category'), + subtitle: LSSubtitle(text: (RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES.data as RadarrMoviesFilter).readable), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { + List titles = RadarrMoviesFilter.values.map((e) => e.readable).toList(); + Tuple2 values = await RadarrDialogs().setDefaultSortingOrFiltering(context, titles: titles); + if(values.item1) { + RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES.put(RadarrMoviesFilter.values[values.item2]); + context.read().moviesFilterType = RadarrDatabaseValue.DEFAULT_FILTERING_MOVIES.data; + } + }, + ), + ); } diff --git a/lib/modules/settings/modules/configuration_radarr_headers.dart b/lib/modules/settings/modules/configuration_radarr_headers.dart index c882e95b..2ed58d35 100644 --- a/lib/modules/settings/modules/configuration_radarr_headers.dart +++ b/lib/modules/settings/modules/configuration_radarr_headers.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/radarr.dart'; import 'package:lunasea/modules/settings.dart'; class SettingsConfigurationRadarrHeadersRouter extends LunaPageRouter { @@ -26,10 +27,7 @@ class _State extends State<_SettingsConfigurationRadarrHeadersRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => ValueListenableBuilder( valueListenable: Database.profilesBox.listenable(), @@ -44,7 +42,7 @@ class _State extends State<_SettingsConfigurationRadarrHeadersRoute> { _addHeaderTile, ]; - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (Database.currentProfileObject.radarrHeaders ?? {}).cast(); @@ -90,6 +88,7 @@ class _State extends State<_SettingsConfigurationRadarrHeadersRoute> { _headers.addAll({'Authorization': 'Basic $_auth'}); Database.currentProfileObject.radarrHeaders = _headers; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } break; case 100: @@ -99,6 +98,7 @@ class _State extends State<_SettingsConfigurationRadarrHeadersRoute> { _headers.addAll({results[1]: results[2]}); Database.currentProfileObject.radarrHeaders = _headers; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } break; default: diff --git a/lib/modules/settings/modules/configuration_sabnzbd.dart b/lib/modules/settings/modules/configuration_sabnzbd.dart index d4c62b92..4ca79f71 100644 --- a/lib/modules/settings/modules/configuration_sabnzbd.dart +++ b/lib/modules/settings/modules/configuration_sabnzbd.dart @@ -28,7 +28,6 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'SABnzbd', actions: [_helpMessageButton], ); @@ -64,7 +63,7 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> { Widget get _enabledTile => LSCardTile( title: LSTitle(text: 'Enable SABnzbd'), - trailing: Switch( + trailing: LunaSwitch( value: Database.currentProfileObject.sabnzbdEnabled ?? false, onChanged: (value) { Database.currentProfileObject.sabnzbdEnabled = value; diff --git a/lib/modules/settings/modules/configuration_sabnzbd_headers.dart b/lib/modules/settings/modules/configuration_sabnzbd_headers.dart index 3a68486a..6c8cd950 100644 --- a/lib/modules/settings/modules/configuration_sabnzbd_headers.dart +++ b/lib/modules/settings/modules/configuration_sabnzbd_headers.dart @@ -26,10 +26,7 @@ class _State extends State<_SettingsConfigurationSABnzbdHeadersRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => ValueListenableBuilder( valueListenable: Database.profilesBox.listenable(), @@ -44,7 +41,7 @@ class _State extends State<_SettingsConfigurationSABnzbdHeadersRoute> { _addHeaderTile, ]; - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (Database.currentProfileObject.sabnzbdHeaders ?? {}).cast(); diff --git a/lib/modules/settings/modules/configuration_search.dart b/lib/modules/settings/modules/configuration_search.dart index c57a69aa..594a12ae 100644 --- a/lib/modules/settings/modules/configuration_search.dart +++ b/lib/modules/settings/modules/configuration_search.dart @@ -30,7 +30,6 @@ class _State extends State<_SettingsConfigurationSearchRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Search', actions: [_helpMessageButton], ); @@ -84,7 +83,7 @@ class _State extends State<_SettingsConfigurationSearchRoute> { builder: (context, box, widget) => LSCardTile( title: LSTitle(text: 'Hide Adult Categories'), subtitle: LSSubtitle(text: 'Hide Adult Content'), - trailing: Switch( + trailing: LunaSwitch( value: SearchDatabaseValue.HIDE_XXX.data, onChanged: (value) => SearchDatabaseValue.HIDE_XXX.put(value), ), diff --git a/lib/modules/settings/modules/configuration_search_add.dart b/lib/modules/settings/modules/configuration_search_add.dart index 5469f9b2..9090ace9 100644 --- a/lib/modules/settings/modules/configuration_search_add.dart +++ b/lib/modules/settings/modules/configuration_search_add.dart @@ -25,10 +25,7 @@ class _State extends State<_SettingsConfigurationSearchAddRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Add Indexer', - ); + Widget get _appBar => LunaAppBar(title: 'Add Indexer'); Widget get _body => LSListView( children: [ diff --git a/lib/modules/settings/modules/configuration_search_add_headers.dart b/lib/modules/settings/modules/configuration_search_add_headers.dart index 9237f81f..bb8e652e 100644 --- a/lib/modules/settings/modules/configuration_search_add_headers.dart +++ b/lib/modules/settings/modules/configuration_search_add_headers.dart @@ -25,10 +25,7 @@ class _State extends State { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => LSListView( children: [ @@ -38,7 +35,7 @@ class _State extends State { ], ); - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (widget.indexer.headers ?? {}).cast(); diff --git a/lib/modules/settings/modules/configuration_search_edit.dart b/lib/modules/settings/modules/configuration_search_edit.dart index d82127cc..2fb254f3 100644 --- a/lib/modules/settings/modules/configuration_search_edit.dart +++ b/lib/modules/settings/modules/configuration_search_edit.dart @@ -63,10 +63,7 @@ class _State extends State<_SettingsConfigurationSearchEditRoute> { body: _indexer != null ? _body : _indexerNotFound, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Edit Indexer', - ); + Widget get _appBar => LunaAppBar(title: 'Edit Indexer'); Widget get _body => LSListView( children: [ diff --git a/lib/modules/settings/modules/configuration_search_edit_headers.dart b/lib/modules/settings/modules/configuration_search_edit_headers.dart index 691210f4..14dc656b 100644 --- a/lib/modules/settings/modules/configuration_search_edit_headers.dart +++ b/lib/modules/settings/modules/configuration_search_edit_headers.dart @@ -25,10 +25,7 @@ class _State extends State { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => LSListView( children: [ @@ -38,7 +35,7 @@ class _State extends State { ], ); - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (widget.indexer.headers ?? {}).cast(); diff --git a/lib/modules/settings/modules/configuration_sonarr.dart b/lib/modules/settings/modules/configuration_sonarr.dart index 506b45f2..17760d76 100644 --- a/lib/modules/settings/modules/configuration_sonarr.dart +++ b/lib/modules/settings/modules/configuration_sonarr.dart @@ -26,7 +26,6 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Sonarr', actions: [_helpMessageButton], ); @@ -63,7 +62,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { Widget get _enabledTile => LSCardTile( title: LSTitle(text: 'Enable Sonarr'), - trailing: Switch( + trailing: LunaSwitch( value: Database.currentProfileObject.sonarrEnabled ?? false, onChanged: (value) { Database.currentProfileObject.sonarrEnabled = value; @@ -73,6 +72,19 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { ), ); + Widget get _enableVersion3Tile => LSCardTile( + title: LSTitle(text: 'Sonarr v3'), + subtitle: LSSubtitle(text: 'Enable Support for Sonarr v3'), + trailing: LunaSwitch( + value: Database.currentProfileObject.sonarrVersion3 ?? false, + onChanged: (value) { + Database.currentProfileObject.sonarrVersion3 = value; + Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); + }, + ), + ); + Widget get _hostTile => LSCardTile( title: LSTitle(text: 'Host'), subtitle: LSSubtitle(text: Database.currentProfileObject.sonarrHost == null || Database.currentProfileObject.sonarrHost == '' ? 'Not Set' : Database.currentProfileObject.sonarrHost), @@ -108,19 +120,6 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { onTap: () async => SettingsConfigurationSonarrHeadersRouter().navigateTo(context), ); - Widget get _enableVersion3Tile => LSCardTile( - title: LSTitle(text: 'Sonarr v3 Features'), - subtitle: LSSubtitle(text: 'Enable Version 3 Specific Features'), - trailing: Switch( - value: Database.currentProfileObject.sonarrVersion3 ?? false, - onChanged: (value) { - Database.currentProfileObject.sonarrVersion3 = value; - Database.currentProfileObject.save(); - Provider.of(context, listen: false).reset(); - }, - ), - ); - Widget get _testConnectionTile => LSButton( text: 'Test Connection', onTap: () async { @@ -161,11 +160,11 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { LSHeader(text: 'Default Pages'), _defaultPageHomeTile, _defaultPageSeriesDetailsTile, - LSHeader(text: 'Default Sorting'), - _defaultSortingReleasesTile, - _defaultSortingReleasesDirectionTile, + LSHeader(text: 'Default Sorting & Filtering'), _defaultSortingSeriesTile, _defaultSortingSeriesDirectionTile, + _defaultSortingReleasesTile, + _defaultSortingReleasesDirectionTile, ]; Widget get _defaultPageHomeTile => ValueListenableBuilder( @@ -197,12 +196,12 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { Widget get _defaultSortingReleasesTile => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [SonarrDatabaseValue.DEFAULT_SORTING_RELEASES.key]), builder: (context, box, _) => LSCardTile( - title: LSTitle(text: 'Releases Category'), + title: LSTitle(text: 'Releases Sort Category'), subtitle: LSSubtitle(text: (SonarrDatabaseValue.DEFAULT_SORTING_RELEASES.data as SonarrReleasesSorting).readable), trailing: LSIconButton(icon: Icons.arrow_forward_ios), onTap: () async { List _titles = SonarrReleasesSorting.values.map((e) => e.readable).toList(); - List _values = await SonarrDialogs.setDefaultSorting(context, titles: _titles); + List _values = await SonarrDialogs.setDefaultSortingOrFiltering(context, titles: _titles); if(_values[0]) { SonarrDatabaseValue.DEFAULT_SORTING_RELEASES.put(SonarrReleasesSorting.values[_values[1]]); context.read().releasesSortType = SonarrDatabaseValue.DEFAULT_SORTING_RELEASES.data; @@ -217,7 +216,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { builder: (context, box, _) => LSCardTile( title: LSTitle(text: 'Releases Sort Direction'), subtitle: LSSubtitle(text: SonarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING.data ? 'Ascending' : 'Descending'), - trailing: Switch( + trailing: LunaSwitch( value: SonarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING.data, onChanged: (value) { SonarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING.put(value); @@ -231,12 +230,12 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { Widget get _defaultSortingSeriesTile => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [SonarrDatabaseValue.DEFAULT_SORTING_SERIES.key]), builder: (context, box, _) => LSCardTile( - title: LSTitle(text: 'Series Category'), + title: LSTitle(text: 'Series Sort Category'), subtitle: LSSubtitle(text: (SonarrDatabaseValue.DEFAULT_SORTING_SERIES.data as SonarrSeriesSorting).readable), trailing: LSIconButton(icon: Icons.arrow_forward_ios), onTap: () async { List _titles = SonarrSeriesSorting.values.map((e) => e.readable).toList(); - List _values = await SonarrDialogs.setDefaultSorting(context, titles: _titles); + List _values = await SonarrDialogs.setDefaultSortingOrFiltering(context, titles: _titles); if(_values[0]) { SonarrDatabaseValue.DEFAULT_SORTING_SERIES.put(SonarrSeriesSorting.values[_values[1]]); context.read().seriesSortType = SonarrDatabaseValue.DEFAULT_SORTING_SERIES.data; @@ -251,7 +250,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> { builder: (context, box, _) => LSCardTile( title: LSTitle(text: 'Series Sort Direction'), subtitle: LSSubtitle(text: SonarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING.data ? 'Ascending' : 'Descending'), - trailing: Switch( + trailing: LunaSwitch( value: SonarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING.data, onChanged: (value) { SonarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING.put(value); diff --git a/lib/modules/settings/modules/configuration_sonarr_headers.dart b/lib/modules/settings/modules/configuration_sonarr_headers.dart index d18757dc..02dbabd8 100644 --- a/lib/modules/settings/modules/configuration_sonarr_headers.dart +++ b/lib/modules/settings/modules/configuration_sonarr_headers.dart @@ -3,6 +3,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/modules/sonarr.dart'; class SettingsConfigurationSonarrHeadersRouter extends LunaPageRouter { SettingsConfigurationSonarrHeadersRouter() : super('/settings/configuration/sonarr/headers'); @@ -26,10 +27,7 @@ class _State extends State<_SettingsConfigurationSonarrHeadersRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => ValueListenableBuilder( valueListenable: Database.profilesBox.listenable(), @@ -44,7 +42,7 @@ class _State extends State<_SettingsConfigurationSonarrHeadersRoute> { _addHeaderTile, ]; - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (Database.currentProfileObject.sonarrHeaders ?? {}).cast(); @@ -90,6 +88,7 @@ class _State extends State<_SettingsConfigurationSonarrHeadersRoute> { _headers.addAll({'Authorization': 'Basic $_auth'}); Database.currentProfileObject.sonarrHeaders = _headers; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } break; case 100: @@ -99,6 +98,7 @@ class _State extends State<_SettingsConfigurationSonarrHeadersRoute> { _headers.addAll({results[1]: results[2]}); Database.currentProfileObject.sonarrHeaders = _headers; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } break; default: diff --git a/lib/modules/settings/modules/configuration_tautulli.dart b/lib/modules/settings/modules/configuration_tautulli.dart index 950b522e..6e5e0f94 100644 --- a/lib/modules/settings/modules/configuration_tautulli.dart +++ b/lib/modules/settings/modules/configuration_tautulli.dart @@ -27,7 +27,6 @@ class _State extends State<_SettingsConfigurationTautulliRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Tautulli', actions: [_helpMessageButton], ); @@ -63,7 +62,7 @@ class _State extends State<_SettingsConfigurationTautulliRoute> { Widget get _enabledTile => LSCardTile( title: LSTitle(text: 'Enable Tautulli'), - trailing: Switch( + trailing: LunaSwitch( value: Database.currentProfileObject.tautulliEnabled ?? false, onChanged: (value) { Database.currentProfileObject.tautulliEnabled = value; @@ -73,47 +72,33 @@ class _State extends State<_SettingsConfigurationTautulliRoute> { ), ); - Widget get _hostTile { - Future _execute() async { + Widget get _hostTile => LSCardTile( + title: LSTitle(text: 'Host'), + subtitle: LSSubtitle(text: Database.currentProfileObject.tautulliHost == null || Database.currentProfileObject.tautulliHost == '' ? 'Not Set' : Database.currentProfileObject.tautulliHost), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { List _values = await SettingsDialogs.editHost(context, 'Tautulli Host', prefill: Database.currentProfileObject.tautulliHost ?? ''); if(_values[0]) { Database.currentProfileObject.tautulliHost = _values[1]; Database.currentProfileObject.save(); Provider.of(context, listen: false).reset(); } - } - return LSCardTile( - title: LSTitle(text: 'Host'), - subtitle: LSSubtitle( - text: Database.currentProfileObject.tautulliHost == null || Database.currentProfileObject.tautulliHost == '' - ? 'Not Set' - : Database.currentProfileObject.tautulliHost - ), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: _execute, - ); - } + }, + ); - Widget get _apiKeyTile { - Future _execute() async { + Widget get _apiKeyTile => LSCardTile( + title: LSTitle(text: 'API Key'), + subtitle: LSSubtitle(text: Database.currentProfileObject.tautulliKey == null || Database.currentProfileObject.tautulliKey == '' ? 'Not Set' : '••••••••••••'), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async { List _values = await LunaDialogs().editText(context, 'Tautulli API Key', prefill: Database.currentProfileObject.tautulliKey ?? ''); if(_values[0]) { Database.currentProfileObject.tautulliKey = _values[1]; Database.currentProfileObject.save(); Provider.of(context, listen: false).reset(); } - } - return LSCardTile( - title: LSTitle(text: 'API Key'), - subtitle: LSSubtitle( - text: Database.currentProfileObject.tautulliKey == null || Database.currentProfileObject.tautulliKey == '' - ? 'Not Set' - : '••••••••••••' - ), - trailing: LSIconButton(icon: Icons.arrow_forward_ios), - onTap: _execute, - ); - } + }, + ); Widget get _customHeadersTile => LSCardTile( title: LSTitle(text: 'Custom Headers'), diff --git a/lib/modules/settings/modules/configuration_tautulli_headers.dart b/lib/modules/settings/modules/configuration_tautulli_headers.dart index 33e942dd..66462dc9 100644 --- a/lib/modules/settings/modules/configuration_tautulli_headers.dart +++ b/lib/modules/settings/modules/configuration_tautulli_headers.dart @@ -3,6 +3,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/modules/tautulli.dart'; class SettingsConfigurationTautulliHeadersRouter extends LunaPageRouter { SettingsConfigurationTautulliHeadersRouter() : super('/settings/configuration/tautulli/headers'); @@ -26,10 +27,7 @@ class _State extends State<_SettingsConfigurationTautulliHeadersRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Custom Headers', - ); + Widget get _appBar => LunaAppBar(title: 'Custom Headers'); Widget get _body => ValueListenableBuilder( valueListenable: Database.profilesBox.listenable(), @@ -44,7 +42,7 @@ class _State extends State<_SettingsConfigurationTautulliHeadersRoute> { _addHeaderTile, ]; - Widget get _noHeaders => LSGenericMessage(text: 'No Custom Headers Added'); + Widget get _noHeaders => LunaMessage.inList(text: 'No Custom Headers Added'); List get _list { Map headers = (Database.currentProfileObject.tautulliHeaders ?? {}).cast(); @@ -90,6 +88,7 @@ class _State extends State<_SettingsConfigurationTautulliHeadersRoute> { _headers.addAll({'Authorization': 'Basic $_auth'}); Database.currentProfileObject.tautulliHeaders = _headers; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } break; case 100: @@ -99,6 +98,7 @@ class _State extends State<_SettingsConfigurationTautulliHeadersRoute> { _headers.addAll({results[1]: results[2]}); Database.currentProfileObject.tautulliHeaders = _headers; Database.currentProfileObject.save(); + Provider.of(context, listen: false).reset(); } break; default: diff --git a/lib/modules/settings/modules/configuration_wakeonlan.dart b/lib/modules/settings/modules/configuration_wakeonlan.dart index dff63d19..e934abfd 100644 --- a/lib/modules/settings/modules/configuration_wakeonlan.dart +++ b/lib/modules/settings/modules/configuration_wakeonlan.dart @@ -26,7 +26,6 @@ class _State extends State<_SettingsConfigurationWakeOnLANRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Wake on LAN', actions: [_helpMessageButton], ); @@ -59,7 +58,7 @@ class _State extends State<_SettingsConfigurationWakeOnLANRoute> { Widget get _enabledTile => LSCardTile( title: LSTitle(text: 'Enable Wake on LAN'), - trailing: Switch( + trailing: LunaSwitch( value: Database.currentProfileObject.wakeOnLANEnabled ?? false, onChanged: (value) { Database.currentProfileObject.wakeOnLANEnabled = value; diff --git a/lib/modules/settings/modules/donations.dart b/lib/modules/settings/modules/donations.dart index 25d6dd43..905b7d40 100644 --- a/lib/modules/settings/modules/donations.dart +++ b/lib/modules/settings/modules/donations.dart @@ -60,10 +60,7 @@ class _State extends State<_SettingsDonationsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Donations', - ); + Widget get _appBar => LunaAppBar(title: 'Donations'); Widget get _body => LunaInAppPurchases.isAvailable && LunaInAppPurchases.donationIAPs.length != 0 ? LSListViewBuilder( diff --git a/lib/modules/settings/modules/donations_thankyou.dart b/lib/modules/settings/modules/donations_thankyou.dart index 7e85f805..a058061c 100644 --- a/lib/modules/settings/modules/donations_thankyou.dart +++ b/lib/modules/settings/modules/donations_thankyou.dart @@ -23,10 +23,7 @@ class _State extends State<_SettingsDonationsThankYouRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Donations', - ); + Widget get _appBar => LunaAppBar(title: 'Donations'); Widget get _body => Padding( child: Column( diff --git a/lib/modules/settings/modules/profiles/route.dart b/lib/modules/settings/modules/profiles/route.dart index d07f792c..8bb2fa52 100644 --- a/lib/modules/settings/modules/profiles/route.dart +++ b/lib/modules/settings/modules/profiles/route.dart @@ -29,7 +29,6 @@ class _State extends State<_SettingsProfilesRoute> with AutomaticKeepAliveClient } Widget get _appBar => LunaAppBar( - context: context, title: 'Profiles', actions: [_helpMessageButton], ); diff --git a/lib/modules/settings/modules/resources.dart b/lib/modules/settings/modules/resources.dart index 14fcc77c..4c39d286 100644 --- a/lib/modules/settings/modules/resources.dart +++ b/lib/modules/settings/modules/resources.dart @@ -25,10 +25,7 @@ class _State extends State<_SettingsResourcesRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Resources', - ); + Widget get _appBar => LunaAppBar(title: 'Resources'); Widget get _body => LSListView( children: [ diff --git a/lib/modules/settings/modules/settings.dart b/lib/modules/settings/modules/settings.dart index 7bcc6f88..29d9de9c 100644 --- a/lib/modules/settings/modules/settings.dart +++ b/lib/modules/settings/modules/settings.dart @@ -41,8 +41,7 @@ class _State extends State<_SettingsHomeRoute> { ); Widget get _appBar => LunaAppBar( - context: context, - hideLeading: true, + useDrawer: true, title: SettingsConstants.MODULE_METADATA.name, ); diff --git a/lib/modules/settings/modules/system/route.dart b/lib/modules/settings/modules/system/route.dart index 9492f08c..496a03d3 100644 --- a/lib/modules/settings/modules/system/route.dart +++ b/lib/modules/settings/modules/system/route.dart @@ -32,10 +32,7 @@ class _State extends State<_SettingsSystemRoute> with AutomaticKeepAliveClientMi ); } - Widget get _appBar => LunaAppBar( - context: context, - title: 'System', - ); + Widget get _appBar => LunaAppBar(title: 'System'); Widget get _body => LSListView( children: [ @@ -83,7 +80,7 @@ class _State extends State<_SettingsSystemRoute> with AutomaticKeepAliveClientMi builder: (context, box, _) => LSCardTile( title: LSTitle(text: 'Sentry'), subtitle: LSSubtitle(text: 'Crash and Error Tracking'), - trailing: Switch( + trailing: LunaSwitch( value: LunaDatabaseValue.ENABLED_SENTRY.data, onChanged: (value) async { List _values = value @@ -99,7 +96,7 @@ class _State extends State<_SettingsSystemRoute> with AutomaticKeepAliveClientMi Future _execute() async { List values = await SettingsDialogs.clearConfiguration(context); if(values[0]) { - Database.setDefaults(clearAlerts: true); + Database().setDefaults(clearAlerts: true); LunaFirebaseAuth().signOut(); LunaState.reset(context); showLunaSuccessSnackBar( diff --git a/lib/modules/settings/modules/system_logs.dart b/lib/modules/settings/modules/system_logs.dart index b3f3fd0e..3804c838 100644 --- a/lib/modules/settings/modules/system_logs.dart +++ b/lib/modules/settings/modules/system_logs.dart @@ -27,10 +27,7 @@ class _State extends State<_SettingsSystemLogsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Logs'); Widget get _body => LSListView( children: [ diff --git a/lib/modules/settings/modules/system_logs_details/route.dart b/lib/modules/settings/modules/system_logs_details/route.dart index c071b0ec..162581bb 100644 --- a/lib/modules/settings/modules/system_logs_details/route.dart +++ b/lib/modules/settings/modules/system_logs_details/route.dart @@ -60,10 +60,7 @@ class _State extends State<_SettingsSystemLogsDetailsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: '${widget.type ?? 'Unknown'} Logs', - ); + Widget get _appBar => LunaAppBar(title: '${widget.type ?? 'Unknown'} Logs'); Widget get _body => FutureBuilder( future: FLog.FLog.getAllLogsByFilter(logLevels: levels), diff --git a/lib/modules/settings/modules/system_logs_details/widgets/log_tile.dart b/lib/modules/settings/modules/system_logs_details/widgets/log_tile.dart index 1057f548..d7e2ad6b 100644 --- a/lib/modules/settings/modules/system_logs_details/widgets/log_tile.dart +++ b/lib/modules/settings/modules/system_logs_details/widgets/log_tile.dart @@ -32,7 +32,7 @@ class SettingsLogsDetailsLogTile extends StatelessWidget { TextSpan( style: TextStyle( color: log.logLevel.lunaColour, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), text: log.logLevel.lunaName, ) diff --git a/lib/modules/sonarr/core/constants.dart b/lib/modules/sonarr/core/constants.dart index 8ba137ad..277a3c74 100644 --- a/lib/modules/sonarr/core/constants.dart +++ b/lib/modules/sonarr/core/constants.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/sonarr.dart'; -import 'package:quick_actions/quick_actions.dart'; class SonarrConstants { SonarrConstants._(); @@ -16,11 +14,10 @@ class SonarrConstants { settingsDescription: 'Configure Sonarr', helpMessage: 'Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.', icon: CustomIcons.television, - route: '/sonarr', + route: SonarrHomeRouter.route(), color: Color(0xFF3FC6F4), website: 'https://sonarr.tv', github: 'https://github.com/Sonarr/Sonarr', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(SonarrHomeRouter.route(), (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Sonarr'), ); } diff --git a/lib/modules/sonarr/core/database.dart b/lib/modules/sonarr/core/database.dart index 558d92df..78b1c0df 100644 --- a/lib/modules/sonarr/core/database.dart +++ b/lib/modules/sonarr/core/database.dart @@ -182,7 +182,7 @@ extension SonarrDatabaseValueExtension on SonarrDatabaseValue { LunaLogger().warning('SonarrDatabaseValueExtension', 'put', 'Attempted to enter data for invalid SonarrDatabaseValue: ${this?.toString() ?? 'null'}'); }//=> Database.lunaSeaBox.put(this.key, value); - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/sonarr/core/dialogs.dart b/lib/modules/sonarr/core/dialogs.dart index ecfd6c9b..5ac196dd 100644 --- a/lib/modules/sonarr/core/dialogs.dart +++ b/lib/modules/sonarr/core/dialogs.dart @@ -132,7 +132,7 @@ class SonarrDialogs { await LSDialog.dialog( context: context, - title: 'Default Page', + title: 'Page', content: List.generate( titles.length, (index) => LSDialog.tile( @@ -294,7 +294,7 @@ class SonarrDialogs { return [_flag, _textController.text]; } - static Future> setDefaultSorting(BuildContext context, { + static Future> setDefaultSortingOrFiltering(BuildContext context, { @required List titles, }) async { bool _flag = false; @@ -308,7 +308,7 @@ class SonarrDialogs { await LSDialog.dialog( context: context, - title: 'Default Sorting', + title: 'Sorting & Filtering', content: List.generate( titles.length, (index) => LSDialog.tile( diff --git a/lib/modules/sonarr/core/state.dart b/lib/modules/sonarr/core/state.dart index 3e64cc24..1f0546a8 100644 --- a/lib/modules/sonarr/core/state.dart +++ b/lib/modules/sonarr/core/state.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; @@ -27,7 +26,7 @@ class SonarrState extends LunaModuleState { _tags = null; _episodes = {}; _selectedEpisodes = []; - // Reset search query fields (except the home screen) + // Reset search query fields _addSearchQuery = ''; _releasesSearchQuery = ''; // Reinitialize @@ -43,6 +42,8 @@ class SonarrState extends LunaModuleState { notifyListeners(); } + void notify() => notifyListeners(); + /////////////// /// PROFILE /// /////////////// @@ -206,9 +207,9 @@ class SonarrState extends LunaModuleState { notifyListeners(); } - SonarrReleasesHiding _releasesHidingType = SonarrReleasesHiding.ALL; - SonarrReleasesHiding get releasesHidingType => _releasesHidingType; - set releasesHidingType(SonarrReleasesHiding releasesHidingType) { + SonarrReleasesFilter _releasesHidingType = SonarrReleasesFilter.ALL; + SonarrReleasesFilter get releasesHidingType => _releasesHidingType; + set releasesHidingType(SonarrReleasesFilter releasesHidingType) { assert(releasesHidingType != null); _releasesHidingType = releasesHidingType; notifyListeners(); @@ -222,7 +223,7 @@ class SonarrState extends LunaModuleState { notifyListeners(); } - bool _releasesSortAscending = SonarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING.data; + bool _releasesSortAscending = SonarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING.data; bool get releasesSortAscending => _releasesSortAscending; set releasesSortAscending(bool releasesSortAscending) { assert(releasesSortAscending != null); @@ -250,9 +251,9 @@ class SonarrState extends LunaModuleState { notifyListeners(); } - SonarrSeriesHiding _seriesHidingType = SonarrSeriesHiding.ALL; - SonarrSeriesHiding get seriesHidingType => _seriesHidingType; - set seriesHidingType(SonarrSeriesHiding seriesHidingType) { + SonarrSeriesFilter _seriesHidingType = SonarrSeriesFilter.ALL; + SonarrSeriesFilter get seriesHidingType => _seriesHidingType; + set seriesHidingType(SonarrSeriesFilter seriesHidingType) { assert(seriesHidingType != null); _seriesHidingType = seriesHidingType; notifyListeners(); diff --git a/lib/modules/sonarr/core/types.dart b/lib/modules/sonarr/core/types.dart index 72ccd11e..27d0938a 100644 --- a/lib/modules/sonarr/core/types.dart +++ b/lib/modules/sonarr/core/types.dart @@ -1,5 +1,5 @@ -export 'types/hiding_releases.dart'; -export 'types/hiding_series.dart'; +export 'types/filter_releases.dart'; +export 'types/filter_series.dart'; export 'types/monitor_status.dart'; export 'types/settings_episode.dart'; export 'types/settings_global.dart'; diff --git a/lib/modules/sonarr/core/types/hiding_releases.dart b/lib/modules/sonarr/core/types/filter_releases.dart similarity index 56% rename from lib/modules/sonarr/core/types/hiding_releases.dart rename to lib/modules/sonarr/core/types/filter_releases.dart index b2c786ff..f73ded8a 100644 --- a/lib/modules/sonarr/core/types/hiding_releases.dart +++ b/lib/modules/sonarr/core/types/filter_releases.dart @@ -1,26 +1,26 @@ import 'package:lunasea/modules/sonarr.dart'; -enum SonarrReleasesHiding { +enum SonarrReleasesFilter { ALL, APPROVED, REJECTED, } -extension SonarrReleasesHidingExtension on SonarrReleasesHiding { +extension SonarrReleasesFilterExtension on SonarrReleasesFilter { String get value { switch(this) { - case SonarrReleasesHiding.ALL: return 'all'; - case SonarrReleasesHiding.APPROVED: return 'approved'; - case SonarrReleasesHiding.REJECTED: return 'rejected'; + case SonarrReleasesFilter.ALL: return 'all'; + case SonarrReleasesFilter.APPROVED: return 'approved'; + case SonarrReleasesFilter.REJECTED: return 'rejected'; } throw Exception('value not found'); } String get readable { switch(this) { - case SonarrReleasesHiding.ALL: return 'All'; - case SonarrReleasesHiding.APPROVED: return 'Approved'; - case SonarrReleasesHiding.REJECTED: return 'Rejected'; + case SonarrReleasesFilter.ALL: return 'All'; + case SonarrReleasesFilter.APPROVED: return 'Approved'; + case SonarrReleasesFilter.REJECTED: return 'Rejected'; } throw Exception('readable not found'); } @@ -31,12 +31,12 @@ extension SonarrReleasesHidingExtension on SonarrReleasesHiding { class _Sorter { List byType( List releases, - SonarrReleasesHiding type, + SonarrReleasesFilter type, ) { switch(type) { - case SonarrReleasesHiding.ALL: return releases; - case SonarrReleasesHiding.APPROVED: return _approved(releases); - case SonarrReleasesHiding.REJECTED: return _rejected(releases); + case SonarrReleasesFilter.ALL: return releases; + case SonarrReleasesFilter.APPROVED: return _approved(releases); + case SonarrReleasesFilter.REJECTED: return _rejected(releases); } throw Exception('sorting type not found'); } diff --git a/lib/modules/sonarr/core/types/hiding_series.dart b/lib/modules/sonarr/core/types/filter_series.dart similarity index 56% rename from lib/modules/sonarr/core/types/hiding_series.dart rename to lib/modules/sonarr/core/types/filter_series.dart index 7a5cd500..ab011bed 100644 --- a/lib/modules/sonarr/core/types/hiding_series.dart +++ b/lib/modules/sonarr/core/types/filter_series.dart @@ -1,26 +1,26 @@ import 'package:lunasea/modules/sonarr.dart'; -enum SonarrSeriesHiding { +enum SonarrSeriesFilter { ALL, MONITORED, UNMONITORED, } -extension SonarrSeriesHidingExtension on SonarrSeriesHiding { +extension SonarrSeriesFilterExtension on SonarrSeriesFilter { String get value { switch(this) { - case SonarrSeriesHiding.ALL: return 'all'; - case SonarrSeriesHiding.MONITORED: return 'monitored'; - case SonarrSeriesHiding.UNMONITORED: return 'unmonitored'; + case SonarrSeriesFilter.ALL: return 'all'; + case SonarrSeriesFilter.MONITORED: return 'monitored'; + case SonarrSeriesFilter.UNMONITORED: return 'unmonitored'; } throw Exception('value not found'); } String get readable { switch(this) { - case SonarrSeriesHiding.ALL: return 'All'; - case SonarrSeriesHiding.MONITORED: return 'Monitored'; - case SonarrSeriesHiding.UNMONITORED: return 'Unmonitored'; + case SonarrSeriesFilter.ALL: return 'All'; + case SonarrSeriesFilter.MONITORED: return 'Monitored'; + case SonarrSeriesFilter.UNMONITORED: return 'Unmonitored'; } throw Exception('readable not found'); } @@ -31,12 +31,12 @@ extension SonarrSeriesHidingExtension on SonarrSeriesHiding { class _Sorter { List byType( List series, - SonarrSeriesHiding type, + SonarrSeriesFilter type, ) { switch(type) { - case SonarrSeriesHiding.ALL: return series; - case SonarrSeriesHiding.MONITORED: return _monitored(series); - case SonarrSeriesHiding.UNMONITORED: return _unmonitored(series); + case SonarrSeriesFilter.ALL: return series; + case SonarrSeriesFilter.MONITORED: return _monitored(series); + case SonarrSeriesFilter.UNMONITORED: return _unmonitored(series); } throw Exception('sorting type not found'); } diff --git a/lib/modules/sonarr/core/types/sorting_series.dart b/lib/modules/sonarr/core/types/sorting_series.dart index 42d29768..26a766a7 100644 --- a/lib/modules/sonarr/core/types/sorting_series.dart +++ b/lib/modules/sonarr/core/types/sorting_series.dart @@ -70,11 +70,7 @@ extension SonarrSeriesSortingExtension on SonarrSeriesSorting { } class _Sorter { - List byType( - List data, - SonarrSeriesSorting type, - bool ascending, - ) { + List byType(List data, SonarrSeriesSorting type, bool ascending) { switch(type) { case SonarrSeriesSorting.DATE_ADDED: return _dateAdded(data, ascending); case SonarrSeriesSorting.EPISODES: return _episodes(data, ascending); diff --git a/lib/modules/sonarr/modules/history/widgets/history_tile.dart b/lib/modules/sonarr/modules/history/widgets/history_tile.dart index 6135e006..4f70e0b6 100644 --- a/lib/modules/sonarr/modules/history/widgets/history_tile.dart +++ b/lib/modules/sonarr/modules/history/widgets/history_tile.dart @@ -88,7 +88,7 @@ class _State extends State { style: TextStyle( fontSize: Constants.UI_FONT_SIZE_SUBTITLE, color: widget.record.eventType.lunaColour, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), text: widget.record.eventType.lunaMessage(widget.record), ), diff --git a/lib/modules/sonarr/modules/missing/widgets/missing_tile.dart b/lib/modules/sonarr/modules/missing/widgets/missing_tile.dart index f8778709..03f6cf8b 100644 --- a/lib/modules/sonarr/modules/missing/widgets/missing_tile.dart +++ b/lib/modules/sonarr/modules/missing/widgets/missing_tile.dart @@ -124,7 +124,7 @@ class _State extends State { style: TextStyle( fontSize: Constants.UI_FONT_SIZE_SUBTITLE, color: LunaColours.red, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), children: [ TextSpan(text: widget.record.airDateUtc == null diff --git a/lib/modules/sonarr/modules/queue/route.dart b/lib/modules/sonarr/modules/queue/route.dart index d5710b37..2c9e208a 100644 --- a/lib/modules/sonarr/modules/queue/route.dart +++ b/lib/modules/sonarr/modules/queue/route.dart @@ -50,10 +50,7 @@ class _State extends State<_SonarrQueueRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Queue', - ); + Widget get _appBar => LunaAppBar(title: 'Queue'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/sonarr/modules/queue/widgets/queue_tile.dart b/lib/modules/sonarr/modules/queue/widgets/queue_tile.dart index 65453783..ee826a11 100644 --- a/lib/modules/sonarr/modules/queue/widgets/queue_tile.dart +++ b/lib/modules/sonarr/modules/queue/widgets/queue_tile.dart @@ -40,7 +40,7 @@ class _State extends State { TextSpan( text: '${widget.record?.quality?.quality?.name ?? 'Unknown'} ${Constants.TEXT_EMDASH} ${widget.record?.lunaPercentageComplete ?? Constants.TEXT_EMDASH}%', style: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.accent, ), ), @@ -106,7 +106,7 @@ class _State extends State { TextSpan( text: '${widget.record.series.title}\n', style: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, color: Colors.white, ), @@ -117,7 +117,7 @@ class _State extends State { : 'Season ${widget.record.episode.seasonNumber} ${Constants.TEXT_EMDASH} Episode ${widget.record.episode.episodeNumber}\n', style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, ), ), diff --git a/lib/modules/sonarr/modules/releases/route.dart b/lib/modules/sonarr/modules/releases/route.dart index f94afb38..3769ec44 100644 --- a/lib/modules/sonarr/modules/releases/route.dart +++ b/lib/modules/sonarr/modules/releases/route.dart @@ -100,7 +100,7 @@ class _State extends State<_SonarrReleasesRoute> { body: _body, ); - Widget get _appBar => SonarrReleasesAppBar(context: context, scrollController: _scrollController); + Widget get _appBar => SonarrReleasesAppBar(scrollController: _scrollController); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/sonarr/modules/releases/widgets.dart b/lib/modules/sonarr/modules/releases/widgets.dart index 2374b22e..65c0bf10 100644 --- a/lib/modules/sonarr/modules/releases/widgets.dart +++ b/lib/modules/sonarr/modules/releases/widgets.dart @@ -1,4 +1,4 @@ export 'widgets/appbar.dart'; -export 'widgets/appbar_hide_button.dart'; +export 'widgets/appbar_filter_button.dart'; export 'widgets/appbar_sort_button.dart'; export 'widgets/tile_release.dart'; diff --git a/lib/modules/sonarr/modules/releases/widgets/appbar.dart b/lib/modules/sonarr/modules/releases/widgets/appbar.dart index d222ae8c..f900ff00 100644 --- a/lib/modules/sonarr/modules/releases/widgets/appbar.dart +++ b/lib/modules/sonarr/modules/releases/widgets/appbar.dart @@ -5,10 +5,8 @@ import 'package:lunasea/modules/sonarr.dart'; // ignore: non_constant_identifier_names Widget SonarrReleasesAppBar({ - @required BuildContext context, @required ScrollController scrollController, }) => LunaAppBar( - context: context, title: 'Releases', bottom: _SearchBar(scrollController: scrollController), ); @@ -55,7 +53,7 @@ class _State extends State<_SearchBar> { margin: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), ), ), - SonarrReleasesAppBarHideButton(controller: scrollController), + SonarrReleasesAppBarFilterButton(controller: scrollController), SonarrReleasesAppBarSortButton(controller: scrollController), ], ), diff --git a/lib/modules/sonarr/modules/releases/widgets/appbar_filter_button.dart b/lib/modules/sonarr/modules/releases/widgets/appbar_filter_button.dart new file mode 100644 index 00000000..50668d08 --- /dev/null +++ b/lib/modules/sonarr/modules/releases/widgets/appbar_filter_button.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/sonarr.dart'; + +class SonarrReleasesAppBarFilterButton extends StatefulWidget { + final ScrollController controller; + + SonarrReleasesAppBarFilterButton({ + Key key, + @required this.controller, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) => LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.filter_alt_outlined, + onSelected: (result) { + state.releasesHidingType = result; + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => List>.generate( + SonarrReleasesFilter.values.length, + (index) => PopupMenuItem( + value: SonarrReleasesFilter.values[index], + child: Text( + SonarrReleasesFilter.values[index].readable, + style: TextStyle( + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + color: state.releasesHidingType == SonarrReleasesFilter.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + ), + ), + ), + ), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + color: Theme.of(context).canvasColor, + ); +} \ No newline at end of file diff --git a/lib/modules/sonarr/modules/releases/widgets/appbar_hide_button.dart b/lib/modules/sonarr/modules/releases/widgets/appbar_hide_button.dart deleted file mode 100644 index eae96114..00000000 --- a/lib/modules/sonarr/modules/releases/widgets/appbar_hide_button.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/sonarr.dart'; - -class SonarrReleasesAppBarHideButton extends StatefulWidget { - final ScrollController controller; - - SonarrReleasesAppBarHideButton({ - Key key, - @required this.controller, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCard( - child: Consumer( - builder: (context, state, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.visibility), - onSelected: (result) { - state.releasesHidingType = result; - _scrollBack(); - }, - itemBuilder: (context) => List>.generate( - SonarrReleasesHiding.values.length, - (index) => PopupMenuItem( - value: SonarrReleasesHiding.values[index], - child: Text( - SonarrReleasesHiding.values[index].readable, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: state.releasesHidingType == SonarrReleasesHiding.values[index] - ? LunaColours.accent - : Colors.white, - ), - ), - ), - ), - ), - ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), - color: Theme.of(context).canvasColor, - ); - - void _scrollBack() { - if(widget.controller.hasClients) widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } -} \ No newline at end of file diff --git a/lib/modules/sonarr/modules/releases/widgets/appbar_sort_button.dart b/lib/modules/sonarr/modules/releases/widgets/appbar_sort_button.dart index db16044c..f2c0fb91 100644 --- a/lib/modules/sonarr/modules/releases/widgets/appbar_sort_button.dart +++ b/lib/modules/sonarr/modules/releases/widgets/appbar_sort_button.dart @@ -16,13 +16,11 @@ class SonarrReleasesAppBarSortButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, state, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.sort), + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.sort, onSelected: (result) { if(state.releasesSortType == result) { state.releasesSortAscending = !state.releasesSortAscending; @@ -30,7 +28,7 @@ class _State extends State { state.releasesSortAscending = true; state.releasesSortType = result; } - _scrollBack(); + widget.controller.lunaAnimateToStart(); }, itemBuilder: (context) => List>.generate( SonarrReleasesSorting.values.length, @@ -61,17 +59,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 13.5), color: Theme.of(context).canvasColor, ); - - void _scrollBack() { - if(widget.controller.hasClients) widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } } \ No newline at end of file diff --git a/lib/modules/sonarr/modules/releases/widgets/tile_release.dart b/lib/modules/sonarr/modules/releases/widgets/tile_release.dart index b7aa2f1c..2adda199 100644 --- a/lib/modules/sonarr/modules/releases/widgets/tile_release.dart +++ b/lib/modules/sonarr/modules/releases/widgets/tile_release.dart @@ -43,7 +43,7 @@ class _State extends State { color: widget.release.protocol == 'torrent' ? LunaColours.purple : LunaColours.blue, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), text: widget.release.protocol.lunaCapitalizeFirstLetters(), ), @@ -51,7 +51,7 @@ class _State extends State { text: ' (${widget.release.seeders}/${widget.release.leechers})', style: TextStyle( color: LunaColours.purple, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), TextSpan(text: '\t•\t${widget.release.indexer}\t•\t'), diff --git a/lib/modules/sonarr/modules/series/route.dart b/lib/modules/sonarr/modules/series/route.dart index a1c01d45..e4cc5c15 100644 --- a/lib/modules/sonarr/modules/series/route.dart +++ b/lib/modules/sonarr/modules/series/route.dart @@ -100,9 +100,8 @@ class _State extends State with AutomaticKeepAliveClientMixin List _filterAndSort(List series, List profiles, String query) { if(series == null || series.length == 0) return series; SonarrState _state = Provider.of(context, listen: false); - List _filtered = new List.from(series); // Filter - _filtered = _filtered.where((show) { + List _filtered = series.where((show) { if(query != null && query.isNotEmpty) return show.title.toLowerCase().contains(query.toLowerCase()); return show != null; }).toList(); diff --git a/lib/modules/sonarr/modules/series/widgets.dart b/lib/modules/sonarr/modules/series/widgets.dart index f37dad01..9579836b 100644 --- a/lib/modules/sonarr/modules/series/widgets.dart +++ b/lib/modules/sonarr/modules/series/widgets.dart @@ -1,4 +1,4 @@ export 'widgets/search_bar.dart'; export 'widgets/series_tile.dart'; -export 'widgets/search_bar_hide_button.dart'; +export 'widgets/search_bar_filter_button.dart'; export 'widgets/search_bar_sort_button.dart'; diff --git a/lib/modules/sonarr/modules/series/widgets/search_bar.dart b/lib/modules/sonarr/modules/series/widgets/search_bar.dart index a17e5d76..b4a7f76e 100644 --- a/lib/modules/sonarr/modules/series/widgets/search_bar.dart +++ b/lib/modules/sonarr/modules/series/widgets/search_bar.dart @@ -40,7 +40,7 @@ class _State extends State { ), ), ), - SonarrSeriesSearchBarHideButton(controller: widget.scrollController), + SonarrSeriesSearchBarFilterButton(controller: widget.scrollController), SonarrSeriesSearchBarSortButton(controller: widget.scrollController), ], ), diff --git a/lib/modules/sonarr/modules/series/widgets/search_bar_filter_button.dart b/lib/modules/sonarr/modules/series/widgets/search_bar_filter_button.dart new file mode 100644 index 00000000..1f377a4c --- /dev/null +++ b/lib/modules/sonarr/modules/series/widgets/search_bar_filter_button.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/sonarr.dart'; + +class SonarrSeriesSearchBarFilterButton extends StatefulWidget { + final ScrollController controller; + + SonarrSeriesSearchBarFilterButton({ + Key key, + @required this.controller, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) => LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.filter_alt_outlined, + onSelected: (result) { + state.seriesHidingType = result; + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => List>.generate( + SonarrSeriesFilter.values.length, + (index) => PopupMenuItem( + value: SonarrSeriesFilter.values[index], + child: Text( + SonarrSeriesFilter.values[index].readable, + style: TextStyle( + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + color: state.seriesHidingType == SonarrSeriesFilter.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + ), + ), + ), + ), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + color: Theme.of(context).canvasColor, + ); +} \ No newline at end of file diff --git a/lib/modules/sonarr/modules/series/widgets/search_bar_hide_button.dart b/lib/modules/sonarr/modules/series/widgets/search_bar_hide_button.dart deleted file mode 100644 index c8e88bc6..00000000 --- a/lib/modules/sonarr/modules/series/widgets/search_bar_hide_button.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/sonarr.dart'; - -class SonarrSeriesSearchBarHideButton extends StatefulWidget { - final ScrollController controller; - - SonarrSeriesSearchBarHideButton({ - Key key, - @required this.controller, - }): super(key: key); - - @override - State createState() => _State(); -} - -class _State extends State { - @override - Widget build(BuildContext context) => LSCard( - child: Consumer( - builder: (context, state, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.visibility), - onSelected: (result) { - state.seriesHidingType = result; - _scrollBack(); - }, - itemBuilder: (context) => List>.generate( - SonarrSeriesHiding.values.length, - (index) => PopupMenuItem( - value: SonarrSeriesHiding.values[index], - child: Text( - SonarrSeriesHiding.values[index].readable, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: state.seriesHidingType == SonarrSeriesHiding.values[index] - ? LunaColours.accent - : Colors.white, - ), - ), - ), - ), - ), - ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), - color: Theme.of(context).canvasColor, - ); - - void _scrollBack() { - if(widget.controller.hasClients) widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } -} \ No newline at end of file diff --git a/lib/modules/sonarr/modules/series/widgets/search_bar_sort_button.dart b/lib/modules/sonarr/modules/series/widgets/search_bar_sort_button.dart index f1c04e15..d00300a2 100644 --- a/lib/modules/sonarr/modules/series/widgets/search_bar_sort_button.dart +++ b/lib/modules/sonarr/modules/series/widgets/search_bar_sort_button.dart @@ -16,13 +16,11 @@ class SonarrSeriesSearchBarSortButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => LSCard( + Widget build(BuildContext context) => LunaCard( + context: context, child: Consumer( - builder: (context, state, widget) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.sort), + builder: (context, state, _) => LunaPopupMenuButton( + icon: Icons.sort, onSelected: (result) { if(state.seriesSortType == result) { state.seriesSortAscending = !state.seriesSortAscending; @@ -30,7 +28,7 @@ class _State extends State { state.seriesSortAscending = true; state.seriesSortType = result; } - _scrollBack(); + widget.controller.lunaAnimateToStart(); }, itemBuilder: (context) => List>.generate( SonarrSeriesSorting.values.length, @@ -61,17 +59,7 @@ class _State extends State { ), ), ), - margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 12.0), + margin: EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 13.5), color: Theme.of(context).canvasColor, ); - - void _scrollBack() { - if(widget.controller.hasClients) widget.controller.animateTo( - 1.00, - duration: Duration( - milliseconds: Constants.UI_NAVIGATION_SPEED*2, - ), - curve: Curves.easeOutSine, - ); - } } \ No newline at end of file diff --git a/lib/modules/sonarr/modules/series/widgets/series_tile.dart b/lib/modules/sonarr/modules/series/widgets/series_tile.dart index 86337fae..458acf32 100644 --- a/lib/modules/sonarr/modules/series/widgets/series_tile.dart +++ b/lib/modules/sonarr/modules/series/widgets/series_tile.dart @@ -87,7 +87,7 @@ class _State extends State { : LunaColours.accent.withOpacity(0.30) : null, fontWeight: Provider.of(context).seriesSortType == SonarrSeriesSorting.EPISODES - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -103,7 +103,7 @@ class _State extends State { : LunaColours.accent.withOpacity(0.30) : null, fontWeight: Provider.of(context).seriesSortType == SonarrSeriesSorting.SIZE - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -130,7 +130,7 @@ class _State extends State { : LunaColours.accent.withOpacity(0.30) : null, fontWeight: Provider.of(context).seriesSortType == SonarrSeriesSorting.TYPE - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -144,7 +144,7 @@ class _State extends State { : LunaColours.accent.withOpacity(0.30) : null, fontWeight: Provider.of(context).seriesSortType == SonarrSeriesSorting.QUALITY - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -175,7 +175,7 @@ class _State extends State { fontWeight: Provider.of(context).seriesSortType == SonarrSeriesSorting.NETWORK || Provider.of(context).seriesSortType == SonarrSeriesSorting.NEXT_AIRING - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -198,7 +198,7 @@ class _State extends State { : LunaColours.accent.withOpacity(0.30) : null, fontWeight: Provider.of(context).seriesSortType == SonarrSeriesSorting.DATE_ADDED - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -208,7 +208,7 @@ class _State extends State { color: widget.series.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30), - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ], diff --git a/lib/modules/sonarr/modules/series_add/route.dart b/lib/modules/sonarr/modules/series_add/route.dart index ca3b627d..fae631cb 100644 --- a/lib/modules/sonarr/modules/series_add/route.dart +++ b/lib/modules/sonarr/modules/series_add/route.dart @@ -1,6 +1,5 @@ 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'; @@ -31,23 +30,10 @@ class _SonarrSeriesAddRoute extends StatefulWidget { class _State extends State<_SonarrSeriesAddRoute> { final GlobalKey _scaffoldKey = GlobalKey(); - @override - void initState() { - super.initState(); - SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh()); - } - - Future _refresh() async { - context.read().fetchRootFolders(); - context.read().resetQualityProfiles(); - context.read().resetLanguageProfiles(); - context.read().resetTags(); - } - @override Widget build(BuildContext context) => Scaffold( key: _scaffoldKey, - appBar: SonarrSeriesAddAppBar(context: context), + appBar: SonarrSeriesAddAppBar(), body: SonarrSeriesAddSearchResults(), ); } diff --git a/lib/modules/sonarr/modules/series_add/widgets/appbar.dart b/lib/modules/sonarr/modules/series_add/widgets/appbar.dart index e996b618..54735fa1 100644 --- a/lib/modules/sonarr/modules/series_add/widgets/appbar.dart +++ b/lib/modules/sonarr/modules/series_add/widgets/appbar.dart @@ -3,10 +3,7 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/sonarr.dart'; // ignore: non_constant_identifier_names -Widget SonarrSeriesAddAppBar({ - @required BuildContext context, -}) => LunaAppBar( - context: context, +Widget SonarrSeriesAddAppBar() => LunaAppBar( title: 'Add Series', bottom: _SearchBar(), ); diff --git a/lib/modules/sonarr/modules/series_add_details/route.dart b/lib/modules/sonarr/modules/series_add_details/route.dart index 30d5ac62..691fe788 100644 --- a/lib/modules/sonarr/modules/series_add_details/route.dart +++ b/lib/modules/sonarr/modules/series_add_details/route.dart @@ -66,7 +66,6 @@ class _State extends State<_SonarrSeriesAddDetailsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Add Series', actions: [ SonarrSeriesAddDetailsAppbarLinkAction(tvdbId: widget.tvdbId), diff --git a/lib/modules/sonarr/modules/series_add_details/widgets/button_add_series.dart b/lib/modules/sonarr/modules/series_add_details/widgets/button_add_series.dart index a025dd13..9b237c5f 100644 --- a/lib/modules/sonarr/modules/series_add_details/widgets/button_add_series.dart +++ b/lib/modules/sonarr/modules/series_add_details/widgets/button_add_series.dart @@ -23,7 +23,7 @@ class _State extends State { 'Add', style: TextStyle( color: Colors.white, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, ), textAlign: TextAlign.center, @@ -40,7 +40,7 @@ class _State extends State { ), color: LunaColours.accent, margin: EdgeInsets.all(6.0), - elevation: Constants.UI_ELEVATION, + elevation: LunaUI.ELEVATION, shape: LSRoundedShape(), ), ), @@ -53,7 +53,7 @@ class _State extends State { 'Add + Search', style: TextStyle( color: Colors.white, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, ), textAlign: TextAlign.center, @@ -70,7 +70,7 @@ class _State extends State { ), color: LunaColours.orange, margin: EdgeInsets.all(6.0), - elevation: Constants.UI_ELEVATION, + elevation: LunaUI.ELEVATION, shape: LSRoundedShape(), ), ), diff --git a/lib/modules/sonarr/modules/series_add_details/widgets/tile_monitored.dart b/lib/modules/sonarr/modules/series_add_details/widgets/tile_monitored.dart index ac4dfc36..97e970c1 100644 --- a/lib/modules/sonarr/modules/series_add_details/widgets/tile_monitored.dart +++ b/lib/modules/sonarr/modules/series_add_details/widgets/tile_monitored.dart @@ -7,7 +7,7 @@ class SonarrSeriesAddDetailsMonitoredTile extends StatelessWidget { Widget build(BuildContext context) => LSCardTile( title: LSTitle(text: 'Monitored'), subtitle: LSSubtitle(text: 'Monitor series for new releases'), - trailing: Switch( + trailing: LunaSwitch( value: context.watch().monitored, onChanged: (value) { context.read().monitored = value; diff --git a/lib/modules/sonarr/modules/series_add_details/widgets/tile_use_season_folders.dart b/lib/modules/sonarr/modules/series_add_details/widgets/tile_use_season_folders.dart index d2cd33a1..63af8119 100644 --- a/lib/modules/sonarr/modules/series_add_details/widgets/tile_use_season_folders.dart +++ b/lib/modules/sonarr/modules/series_add_details/widgets/tile_use_season_folders.dart @@ -7,7 +7,7 @@ class SonarrSeriesAddDetailsUseSeasonFoldersTile extends StatelessWidget { Widget build(BuildContext context) => LSCardTile( title: LSTitle(text: 'Use Season Folders'), subtitle: LSSubtitle(text: 'Sort episodes into season folders'), - trailing: Switch( + trailing: LunaSwitch( value: context.watch().useSeasonFolders, onChanged: (value) { context.read().useSeasonFolders = value; diff --git a/lib/modules/sonarr/modules/series_details/route.dart b/lib/modules/sonarr/modules/series_details/route.dart index 806a6ab7..8a5e907f 100644 --- a/lib/modules/sonarr/modules/series_details/route.dart +++ b/lib/modules/sonarr/modules/series_details/route.dart @@ -97,7 +97,6 @@ class _State extends State<_SonarrSeriesDetailsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Series Details', actions: [ SonarrAppBarSeriesSettingsAction(seriesId: widget.seriesId), diff --git a/lib/modules/sonarr/modules/series_details/widgets/navigation_bar.dart b/lib/modules/sonarr/modules/series_details/widgets/navigation_bar.dart index 0555bd8a..24130fe4 100644 --- a/lib/modules/sonarr/modules/series_details/widgets/navigation_bar.dart +++ b/lib/modules/sonarr/modules/series_details/widgets/navigation_bar.dart @@ -4,7 +4,7 @@ import 'package:lunasea/modules/sonarr.dart'; class SonarrSeriesDetailsNavigationBar extends StatefulWidget { static const List icons = [ - Icons.subject, + Icons.subject_rounded, CustomIcons.television, ]; @@ -53,10 +53,6 @@ class _State extends State { ); Future _navOnTap(int index) async { - if(widget.pageController.hasClients) widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); + widget.pageController.lunaAnimateToPage(index); } } diff --git a/lib/modules/sonarr/modules/series_details/widgets/overview.dart b/lib/modules/sonarr/modules/series_details/widgets/overview.dart index 8544f4a4..ebb64026 100644 --- a/lib/modules/sonarr/modules/series_details/widgets/overview.dart +++ b/lib/modules/sonarr/modules/series_details/widgets/overview.dart @@ -67,7 +67,7 @@ class SonarrSeriesDetailsOverview extends StatelessWidget { children: [ LSTitle(text: series.title, maxLines: 1), Text( - series.overview, + series.overview == null || series.overview.isEmpty ? 'No summary is available.\n\n\n' : series.overview, maxLines: 4, overflow: TextOverflow.fade, style: TextStyle( diff --git a/lib/modules/sonarr/modules/series_details/widgets/season_all_tile.dart b/lib/modules/sonarr/modules/series_details/widgets/season_all_tile.dart index e48e7adc..b3bb8647 100644 --- a/lib/modules/sonarr/modules/series_details/widgets/season_all_tile.dart +++ b/lib/modules/sonarr/modules/series_details/widgets/season_all_tile.dart @@ -27,7 +27,7 @@ class SonarrSeriesDetailsSeasonAllTile extends StatelessWidget { color: series.lunaPercentageComplete == 100 ? series.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30) : series.monitored ? LunaColours.red : LunaColours.red.withOpacity(0.30), - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), text: '${series.lunaPercentageComplete}% ${Constants.TEXT_EMDASH} ${series.episodeFileCount ?? 0}/${series.episodeCount ?? 0} Episodes Available', ), diff --git a/lib/modules/sonarr/modules/series_details/widgets/season_tile.dart b/lib/modules/sonarr/modules/series_details/widgets/season_tile.dart index 9afca377..736d919e 100644 --- a/lib/modules/sonarr/modules/series_details/widgets/season_tile.dart +++ b/lib/modules/sonarr/modules/series_details/widgets/season_tile.dart @@ -36,7 +36,7 @@ class _State extends State { color: widget.season.lunaPercentageComplete == 100 ? widget.season.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30) : widget.season.monitored ? LunaColours.red : LunaColours.red.withOpacity(0.30), - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), text: '${widget.season.lunaPercentageComplete}% ${Constants.TEXT_EMDASH} ${widget?.season?.statistics?.episodeFileCount ?? 0}/${widget?.season?.statistics?.totalEpisodeCount ?? 0} Episodes Available', ), diff --git a/lib/modules/sonarr/modules/series_edit/route.dart b/lib/modules/sonarr/modules/series_edit/route.dart index a33b30f4..c76da2fb 100644 --- a/lib/modules/sonarr/modules/series_edit/route.dart +++ b/lib/modules/sonarr/modules/series_edit/route.dart @@ -64,10 +64,7 @@ class _State extends State<_SonarrSeriesEditRoute> { body: _initialLoad ? _body : LSLoader(), ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Edit Series', - ); + Widget get _appBar => LunaAppBar(title: 'Edit Series'); Widget get _body => FutureBuilder( future: Future.wait([ diff --git a/lib/modules/sonarr/modules/series_edit/widgets/button_update_series.dart b/lib/modules/sonarr/modules/series_edit/widgets/button_update_series.dart index 10bbdaaf..983f2738 100644 --- a/lib/modules/sonarr/modules/series_edit/widgets/button_update_series.dart +++ b/lib/modules/sonarr/modules/series_edit/widgets/button_update_series.dart @@ -22,7 +22,7 @@ class SonarrSeriesEditUpdateSeriesButton extends StatelessWidget { 'Update Series', style: TextStyle( color: Colors.white, - fontWeight: FontWeight.bold, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, ), textAlign: TextAlign.center, @@ -38,8 +38,8 @@ class SonarrSeriesEditUpdateSeriesButton extends StatelessWidget { : null, ), color: LunaColours.accent, - margin: Constants.UI_CARD_MARGIN, - elevation: Constants.UI_ELEVATION, + margin: LunaUI.MARGIN_CARD, + elevation: LunaUI.ELEVATION, shape: LSRoundedShape(), ), ), diff --git a/lib/modules/sonarr/modules/series_edit/widgets/tile_monitored.dart b/lib/modules/sonarr/modules/series_edit/widgets/tile_monitored.dart index 61ec8147..0c60a024 100644 --- a/lib/modules/sonarr/modules/series_edit/widgets/tile_monitored.dart +++ b/lib/modules/sonarr/modules/series_edit/widgets/tile_monitored.dart @@ -7,7 +7,7 @@ class SonarrSeriesEditMonitoredTile extends StatelessWidget { Widget build(BuildContext context) => LSCardTile( title: LSTitle(text: 'Monitored'), subtitle: LSSubtitle(text: 'Monitor series for new releases'), - trailing: Switch( + trailing: LunaSwitch( value: context.watch().monitored, onChanged: (value) => context.read().monitored = value, ), diff --git a/lib/modules/sonarr/modules/series_edit/widgets/tile_series_path.dart b/lib/modules/sonarr/modules/series_edit/widgets/tile_series_path.dart index 170d5242..b9613110 100644 --- a/lib/modules/sonarr/modules/series_edit/widgets/tile_series_path.dart +++ b/lib/modules/sonarr/modules/series_edit/widgets/tile_series_path.dart @@ -14,7 +14,7 @@ class SonarrSeriesEditSeriesPathTile extends StatelessWidget { Future _onTap(BuildContext context) async { List _values = await LunaDialogs().editText( context, - 'Series Type', + 'Series Path', prefill: context.read().seriesPath, ); if(_values[0]) context.read().seriesPath = _values[1]; diff --git a/lib/modules/sonarr/modules/series_edit/widgets/tile_use_season_folders.dart b/lib/modules/sonarr/modules/series_edit/widgets/tile_use_season_folders.dart index 39c10cd9..c38382b1 100644 --- a/lib/modules/sonarr/modules/series_edit/widgets/tile_use_season_folders.dart +++ b/lib/modules/sonarr/modules/series_edit/widgets/tile_use_season_folders.dart @@ -7,7 +7,7 @@ class SonarrSeriesEditSeasonFoldersTile extends StatelessWidget { Widget build(BuildContext context) => LSCardTile( title: LSTitle(text: 'Use Season Folders'), subtitle: LSSubtitle(text: 'Sort episodes into season folders'), - trailing: Switch( + trailing: LunaSwitch( value: context.watch().useSeasonFolders, onChanged: (value) => context.read().useSeasonFolders = value, ), diff --git a/lib/modules/sonarr/modules/series_season_details/route.dart b/lib/modules/sonarr/modules/series_season_details/route.dart index 3ec01769..f016cbf3 100644 --- a/lib/modules/sonarr/modules/series_season_details/route.dart +++ b/lib/modules/sonarr/modules/series_season_details/route.dart @@ -76,10 +76,7 @@ class _State extends State<_SonarrSeriesSeasonDetailsRoute> { floatingActionButton: _floatingActionButton, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Season Details', - ); + Widget get _appBar => LunaAppBar(title: 'Season Details'); Widget get _floatingActionButton => context.watch().selectedEpisodes.length == 0 ? null diff --git a/lib/modules/sonarr/modules/series_season_details/widgets/tile_episode.dart b/lib/modules/sonarr/modules/series_season_details/widgets/tile_episode.dart index 3675ad3b..b647e600 100644 --- a/lib/modules/sonarr/modules/series_season_details/widgets/tile_episode.dart +++ b/lib/modules/sonarr/modules/series_season_details/widgets/tile_episode.dart @@ -96,7 +96,7 @@ class _State extends State { : 'Season ${widget.episode.seasonNumber} ${Constants.TEXT_EMDASH} Episode ${widget.episode.episodeNumber}\n', style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_STICKYHEADER, ), ), @@ -196,7 +196,7 @@ class _State extends State { TextSpan(text: '\n'), if(_queue != null) TextSpan( style: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: LunaColours.purple, ), children: [ @@ -207,7 +207,7 @@ class _State extends State { ), if(_queue == null && widget.episode.hasFile) TextSpan( style: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: widget.episode.episodeFile.qualityCutoffNotMet ? widget.episode.monitored ? LunaColours.orange : LunaColours.orange.withOpacity(0.30) : widget.episode.monitored ? LunaColours.accent : LunaColours.accent.withOpacity(0.30), @@ -220,14 +220,14 @@ class _State extends State { ), if(_queue == null && !widget.episode.hasFile && (widget.episode?.airDateUtc?.toLocal()?.isAfter(DateTime.now()) ?? true)) TextSpan( style: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: widget.episode.monitored ? LunaColours.blue : LunaColours.blue.withOpacity(0.30), ), text: 'Unaired', ), if(_queue == null && !widget.episode.hasFile && (widget.episode?.airDateUtc?.toLocal()?.isBefore(DateTime.now()) ?? false)) TextSpan( style: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, color: widget.episode.monitored ? LunaColours.red : LunaColours.red.withOpacity(0.30), ), text: 'Missing', @@ -251,7 +251,7 @@ class _State extends State { color: widget.episode.monitored ? Colors.white : Colors.white30, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, fontSize: Constants.UI_FONT_SIZE_SUBTITLE, ), ), diff --git a/lib/modules/sonarr/modules/sonarr/route.dart b/lib/modules/sonarr/modules/sonarr/route.dart index d2f95b75..84bed1cd 100644 --- a/lib/modules/sonarr/modules/sonarr/route.dart +++ b/lib/modules/sonarr/modules/sonarr/route.dart @@ -80,8 +80,8 @@ class _State extends State<_SonarrHomeRoute> { ), ); - Widget get _appBar => SonarrAppBar( - context: context, + Widget get _appBar => LunaAppBar.dropdown( + title: 'Sonarr', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject)?.sonarrEnabled ?? false) value.add(element); return value; diff --git a/lib/modules/sonarr/modules/sonarr/widgets.dart b/lib/modules/sonarr/modules/sonarr/widgets.dart index 359ff914..9e2e86d4 100644 --- a/lib/modules/sonarr/modules/sonarr/widgets.dart +++ b/lib/modules/sonarr/modules/sonarr/widgets.dart @@ -1,4 +1,3 @@ -export 'widgets/appbar.dart'; export 'widgets/appbar_add_series_action.dart'; export 'widgets/appbar_global_settings_action.dart'; export 'widgets/navigation_bar.dart'; diff --git a/lib/modules/sonarr/modules/sonarr/widgets/appbar.dart b/lib/modules/sonarr/modules/sonarr/widgets/appbar.dart deleted file mode 100644 index 1c54ae04..00000000 --- a/lib/modules/sonarr/modules/sonarr/widgets/appbar.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lunasea/core.dart'; - -// ignore: non_constant_identifier_names -Widget SonarrAppBar({ - @required BuildContext context, - @required List profiles, - @required List actions, -}) => profiles != null && profiles.length < 2 - ? LunaAppBar( - context: context, - title: 'Sonarr', - actions: actions, - hideLeading: true, - ) - : AppBar( - title: PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - child: Wrap( - direction: Axis.horizontal, - children: [ - Text( - 'Sonarr', - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_HEADER, - ), - ), - LSIcon( - icon: Icons.arrow_drop_down, - ), - ], - ), - onSelected: (result) => LunaProfile().safelyChangeProfiles(context, result), - itemBuilder: (context) { - return >[for(String profile in profiles) PopupMenuItem( - value: profile, - child: Text( - profile, - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - ), - ), - )]; - }, - ), - centerTitle: false, - elevation: 0, - actions: actions, - ); \ No newline at end of file diff --git a/lib/modules/sonarr/modules/sonarr/widgets/navigation_bar.dart b/lib/modules/sonarr/modules/sonarr/widgets/navigation_bar.dart index 90b9d58e..5a714c29 100644 --- a/lib/modules/sonarr/modules/sonarr/widgets/navigation_bar.dart +++ b/lib/modules/sonarr/modules/sonarr/widgets/navigation_bar.dart @@ -57,10 +57,6 @@ class _State extends State { ); Future _navOnTap(int index) async { - if(widget.pageController.hasClients) widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); + widget.pageController.lunaAnimateToPage(index); } } diff --git a/lib/modules/sonarr/modules/tags/route.dart b/lib/modules/sonarr/modules/tags/route.dart index 8caa2911..ebd6d8eb 100644 --- a/lib/modules/sonarr/modules/tags/route.dart +++ b/lib/modules/sonarr/modules/tags/route.dart @@ -51,7 +51,6 @@ class _State extends State<_SonarrTagsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Tags', actions: [ SonarrTagsAppBarActionAddTag(), diff --git a/lib/modules/sonarr/modules/upcoming/widgets/upcoming_tile.dart b/lib/modules/sonarr/modules/upcoming/widgets/upcoming_tile.dart index 1e6d9a66..c2487077 100644 --- a/lib/modules/sonarr/modules/upcoming/widgets/upcoming_tile.dart +++ b/lib/modules/sonarr/modules/upcoming/widgets/upcoming_tile.dart @@ -15,153 +15,73 @@ class SonarrUpcomingTile extends StatefulWidget { } class _State extends State { - final double _height = 90.0; - final double _width = 60.0; - final double _padding = 8.0; - @override - Widget build(BuildContext context) => Selector>( - selector: (_, state) => state.missing, - builder: (context, series, _) => LSCard( - child: InkWell( - child: Row( - children: [ - _poster, - Expanded(child: _information), - _trailing, - ], - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - ), - borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), - onTap: _tileOnTap, - onLongPress: _tileOnLongPress, + Widget build(BuildContext context) { + return Selector>( + selector: (_, state) => state.missing, + builder: (context, missing, _) => LunaFourLineCardWithPoster( + backgroundUrl: context.read().getBannerURL(widget.record.seriesId), + posterUrl: context.read().getPosterURL(widget.record.seriesId), + posterHeaders: context.read().headers, + posterPlaceholder: 'assets/images/sonarr/noseriesposter.png', + title: widget.record.series.title, + subtitle1: _subtitle1(), + subtitle2: _subtitle2(), + subtitle3: _subtitle3(), + darken: !widget.record.monitored, + onTap: _onTap, + onLongPress: _onLongPress, + trailing: _trailing(), ), - decoration: LunaCardDecoration( - uri: Provider.of(context, listen: false).getBannerURL(widget.record.seriesId), - headers: Provider.of(context, listen: false).headers, - ), - ), + ); + } + + Widget _trailing() => LunaIconButton( + text: widget.record.lunaAirTime, + onPressed: _trailingOnPressed, + onLongPress: _trailingOnLongPress, ); - Widget get _poster => LSNetworkImage( - url: Provider.of(context, listen: false).getPosterURL(widget.record.seriesId), - placeholder: 'assets/images/sonarr/noseriesposter.png', - height: _height, - width: _width, - headers: Provider.of(context, listen: false).headers.cast(), - ); - - Widget get _information => Padding( - child: Container( - child: Column( - children: [ - LSTitle(text: widget.record.series.title, darken: !widget.record.monitored, maxLines: 1), - _subtitleOne, - _subtitleTwo, - _subtitleThree, - ], - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - ), - height: (_height-(_padding*2)), - ), - padding: EdgeInsets.all(_padding), - ); - - Widget get _trailing => Container( - child: Padding( - child: InkWell( - child: IconButton( - icon: Text( - widget.record.lunaAirTime, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: Constants.UI_FONT_SIZE_SUBHEADER-2.0, - ), - ), - onPressed: _trailingOnPressed, - ), - onLongPress: _trailingOnLongPress, - ), - padding: EdgeInsets.only(right: 12.0), - ), - height: _height, - ); - - Widget get _subtitleOne => RichText( - text: TextSpan( - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: widget.record.monitored ? Colors.white70 : Colors.white30, - ), + TextSpan _subtitle1() { + return TextSpan( children: [ TextSpan(text: widget.record.seasonNumber == 0 ? 'Specials ' : 'Season ${widget.record.seasonNumber} '), TextSpan(text: Constants.TEXT_EMDASH), TextSpan(text: ' Episode ${widget.record.episodeNumber}'), ], - ), - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, - ); + ); + } - Widget get _subtitleTwo => RichText( - text: TextSpan( + TextSpan _subtitle2() { + return TextSpan( + style: TextStyle(fontStyle: FontStyle.italic), + children: [ + TextSpan(text: widget.record.title ?? 'Unknown Title'), + ], + ); + } + + TextSpan _subtitle3() { + Color color = widget.record.hasFile ? LunaColours.accent : widget.record.lunaHasAired ? LunaColours.red : LunaColours.blue; + return TextSpan( style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: widget.record.monitored ? Colors.white70 : Colors.white30, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: color, ), children: [ - TextSpan( - style: TextStyle( - fontStyle: FontStyle.italic, - ), - text: widget.record.title ?? 'Unknown Title', - ), + if(!widget.record.hasFile) TextSpan(text: widget.record.lunaHasAired ? 'Missing' : 'Unaired'), + if(widget.record.hasFile) TextSpan(text: 'Downloaded (${widget?.record?.episodeFile?.quality?.quality?.name ?? 'Unknown'})'), ], - ), - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, - ); + ); + } - Widget get _subtitleThree => RichText( - text: TextSpan( - style: TextStyle( - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - fontWeight: FontWeight.w600, - ), - children: [ - if(!widget.record.hasFile) TextSpan( - style: TextStyle( - color: widget.record.lunaHasAired ? LunaColours.red : LunaColours.blue, - ), - text: widget.record.lunaHasAired ? 'Missing' : 'Unaired', - ), - if(widget.record.hasFile) TextSpan( - style: TextStyle( - color: LunaColours.accent, - ), - text: 'Downloaded (${widget?.record?.episodeFile?.quality?.quality?.name ?? 'Unknown'})', - ), - ], - ), - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, - ); - - Future _tileOnTap() async => SonarrSeriesSeasonDetailsRouter.navigateTo( + Future _onTap() async => SonarrSeriesSeasonDetailsRouter.navigateTo( context, seriesId: widget.record.seriesId, seasonNumber: widget.record.seasonNumber, ); - Future _tileOnLongPress() async => SonarrSeriesDetailsRouter.navigateTo( + Future _onLongPress() async => SonarrSeriesDetailsRouter.navigateTo( context, seriesId: widget.record.seriesId, ); diff --git a/lib/modules/tautulli/core/constants.dart b/lib/modules/tautulli/core/constants.dart index 5250d4d4..c6c8627e 100644 --- a/lib/modules/tautulli/core/constants.dart +++ b/lib/modules/tautulli/core/constants.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; -import 'package:lunasea/main.dart'; import 'package:lunasea/modules/tautulli.dart'; -import 'package:quick_actions/quick_actions.dart'; class TautulliConstants { TautulliConstants._(); @@ -16,11 +14,10 @@ class TautulliConstants { settingsDescription: 'Configure Tautulli', helpMessage: 'Tautulli is an application that you can run alongside your Plex Media Server to monitor activity and track various statistics. Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched.', icon: CustomIcons.tautulli, - route: '/tautulli', + route: TautulliHomeRouter.route(), color: Color(0xFFDBA23A), website: 'https://tautulli.com', github: 'https://github.com/Tautulli/Tautulli', - pushBaseRoute: () => LunaBIOS.navigatorKey.currentState.pushNamedAndRemoveUntil(TautulliHomeRouter.route(), (Route route) => false), shortcutItem: ShortcutItem(type: MODULE_KEY, localizedTitle: 'Tautulli'), ); } diff --git a/lib/modules/tautulli/core/database.dart b/lib/modules/tautulli/core/database.dart index d4859db6..ff41b52e 100644 --- a/lib/modules/tautulli/core/database.dart +++ b/lib/modules/tautulli/core/database.dart @@ -142,7 +142,7 @@ extension TautulliDatabaseValueExtension on TautulliDatabaseValue { LunaLogger().warning('TautulliDatabaseValueExtension', 'put', 'Attempted to enter data for invalid TautulliDatabaseValue: ${this?.toString() ?? 'null'}'); } - void listen(Widget Function(BuildContext, Box, Widget) builder) => ValueListenableBuilder( + ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]), builder: builder, ); diff --git a/lib/modules/tautulli/core/dialogs.dart b/lib/modules/tautulli/core/dialogs.dart index 2e31f568..c7505246 100644 --- a/lib/modules/tautulli/core/dialogs.dart +++ b/lib/modules/tautulli/core/dialogs.dart @@ -47,7 +47,7 @@ class TautulliDialogs { await LSDialog.dialog( context: context, - title: 'Default Page', + title: 'Page', content: List.generate( titles.length, (index) => LSDialog.tile( diff --git a/lib/modules/tautulli/core/extensions/tautulli_session.dart b/lib/modules/tautulli/core/extensions/tautulli_session.dart index 80fede5c..e5c51699 100644 --- a/lib/modules/tautulli/core/extensions/tautulli_session.dart +++ b/lib/modules/tautulli/core/extensions/tautulli_session.dart @@ -89,6 +89,13 @@ extension TautulliSessionExtension on TautulliSession { ')', ].join(); + String get lsSubtitle => [ + this.streamSubtitleDecision == TautulliTranscodeDecision.NULL ? 'Direct Play' : this.streamSubtitleDecision.name, + ' (', + this.subtitleCodec.toUpperCase(), + ')', + ].join(); + String lsArtworkPath(BuildContext context) { switch(this.mediaType) { case TautulliMediaType.EPISODE: return context.watch().getImageURLFromRatingKey(this.grandparentRatingKey); diff --git a/lib/modules/tautulli/modules/activity/widgets/activity_status.dart b/lib/modules/tautulli/modules/activity/widgets/activity_status.dart index 1aaf4667..59b11862 100644 --- a/lib/modules/tautulli/modules/activity/widgets/activity_status.dart +++ b/lib/modules/tautulli/modules/activity/widgets/activity_status.dart @@ -13,6 +13,29 @@ class TautulliActivityStatus extends StatelessWidget { @override Widget build(BuildContext context) => LSHeader( text: '${activity.streamCount} ${activity.streamCount == 1 ? 'Session' : 'Sessions'}', - subtitle: 'Total Bandwidth Usage: ${activity.totalBandwidth.lunaKilobytesToString(bytes: false, decimals: 1)}ps', + subtitle: [ + sessions, + bandwidth, + ].join('\n'), ); + + String get sessions { + String value = 'Sessions: '; + value += [ + if((activity.streamCountDirectPlay ?? 0) > 0) '${activity.streamCountDirectPlay} ${activity.streamCountDirectPlay == 1 ? 'Direct Play' : 'Direct Plays'}', + if((activity.streamCountDirectStream ?? 0) > 0) '${activity.streamCountDirectStream} ${activity.streamCountDirectStream == 1 ? 'Direct Stream' : 'Direct Streams'}', + if((activity.streamCountTranscode ?? 0) > 0) '${activity.streamCountTranscode} ${activity.streamCountTranscode == 1 ? 'Transcode' : 'Transcodes'}', + ].join(', '); + return value; + } + + String get bandwidth { + String value = 'Bandwidth: ${activity.totalBandwidth?.lunaKilobytesToString(bytes: false, decimals: 1) ?? '0.0'}ps ('; + value += [ + if((activity.lanBandwidth ?? 0) > 0) 'LAN: ${activity.lanBandwidth.lunaKilobytesToString(bytes: false, decimals: 1)}ps', + if((activity.wanBandwidth ?? 0) > 0) 'WAN: ${activity.wanBandwidth.lunaKilobytesToString(bytes: false, decimals: 1)}ps', + ].join(', '); + value += ')'; + return value; + } } diff --git a/lib/modules/tautulli/modules/activity_details/route.dart b/lib/modules/tautulli/modules/activity_details/route.dart index 92ae4c04..2e4f765e 100644 --- a/lib/modules/tautulli/modules/activity_details/route.dart +++ b/lib/modules/tautulli/modules/activity_details/route.dart @@ -60,7 +60,6 @@ class _State extends State<_TautulliActivityDetailsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Activity Details', actions: [ TautulliActivityDetailsUser(sessionId: widget.sessionId), diff --git a/lib/modules/tautulli/modules/activity_details/widgets/information.dart b/lib/modules/tautulli/modules/activity_details/widgets/information.dart index c9db0382..b94d1847 100644 --- a/lib/modules/tautulli/modules/activity_details/widgets/information.dart +++ b/lib/modules/tautulli/modules/activity_details/widgets/information.dart @@ -38,11 +38,13 @@ class TautulliActivityDetailsInformation extends StatelessWidget { title: 'Player', children: [ LSTableContent(title: 'location', body: session.ipAddress), - LSTableContent(title: 'device', body: session.device), - LSTableContent(title: 'platform', body: session.platform), + LSTableContent(title: 'platform', body: [ + session.platform, + if(session.platformVersion != null && session.platformVersion.isNotEmpty) '(${session.platformVersion})', + ].join(' ')), LSTableContent(title: 'product', body: session.product), LSTableContent(title: 'player', body: session.player), - LSTableContent(title: 'version', body: session.platformVersion), + LSTableContent(title: 'quality', body: session.lsQuality), ], ); @@ -50,13 +52,14 @@ class TautulliActivityDetailsInformation extends StatelessWidget { title: 'Stream', children: [ LSTableContent(title: 'bandwidth', body: session.lsBandwidth), - LSTableContent(title: 'quality', body: session.lsQuality), LSTableContent(title: 'stream', body: session.lsStream), LSTableContent(title: 'container', body: session.lsContainer), if(session.streamVideoDecision != null && session.streamVideoDecision != TautulliTranscodeDecision.NULL) LSTableContent(title: 'video', body: session.lsVideo), if(session.streamAudioDecision != null && session.streamAudioDecision != TautulliTranscodeDecision.NULL) LSTableContent(title: 'audio', body: session.lsAudio), + if(session.subtitles != null && session.subtitles) + LSTableContent(title: 'subtitle', body: session.lsSubtitle), ], ); } diff --git a/lib/modules/tautulli/modules/check_for_updates/route.dart b/lib/modules/tautulli/modules/check_for_updates/route.dart index 7576dc8a..838a57dc 100644 --- a/lib/modules/tautulli/modules/check_for_updates/route.dart +++ b/lib/modules/tautulli/modules/check_for_updates/route.dart @@ -58,10 +58,7 @@ class _State extends State<_TautulliCheckForUpdatesRoute> { body: _initialLoad ? _body : LSLoader(), ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Check for Updates', - ); + Widget get _appBar => LunaAppBar(title: 'Check for Updates'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/tautulli/modules/check_for_updates/widgets/pms_tile.dart b/lib/modules/tautulli/modules/check_for_updates/widgets/pms_tile.dart index 53524f24..bf747599 100644 --- a/lib/modules/tautulli/modules/check_for_updates/widgets/pms_tile.dart +++ b/lib/modules/tautulli/modules/check_for_updates/widgets/pms_tile.dart @@ -40,7 +40,7 @@ class TautulliCheckForUpdatesPMSTile extends StatelessWidget { text: 'No Updates Available\n', style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), if(!update.updateAvailable) TextSpan(text: 'Current Version: ${update.version}'), @@ -48,7 +48,7 @@ class TautulliCheckForUpdatesPMSTile extends StatelessWidget { text: 'Update Available\n', style: TextStyle( color: LunaColours.orange, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), if(update.updateAvailable) TextSpan(text: 'Latest Version: ${update.version}'), diff --git a/lib/modules/tautulli/modules/check_for_updates/widgets/tautulli_tile.dart b/lib/modules/tautulli/modules/check_for_updates/widgets/tautulli_tile.dart index ec10ec65..a3be5712 100644 --- a/lib/modules/tautulli/modules/check_for_updates/widgets/tautulli_tile.dart +++ b/lib/modules/tautulli/modules/check_for_updates/widgets/tautulli_tile.dart @@ -40,14 +40,14 @@ class TautulliCheckForUpdatesTautulliTile extends StatelessWidget { text: 'No Updates Available\n', style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), if(update.update) TextSpan( text: 'Update Available\n', style: TextStyle( color: LunaColours.orange, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), if(update.update) TextSpan( diff --git a/lib/modules/tautulli/modules/graphs/route.dart b/lib/modules/tautulli/modules/graphs/route.dart index 506799db..5f7c5810 100644 --- a/lib/modules/tautulli/modules/graphs/route.dart +++ b/lib/modules/tautulli/modules/graphs/route.dart @@ -48,7 +48,6 @@ class _State extends State<_TautulliGraphsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Graphs', actions: [ TautulliGraphsTypeButton(), diff --git a/lib/modules/tautulli/modules/graphs/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/graphs/widgets/navigation_bar.dart index 2f4a3e27..53e005bd 100644 --- a/lib/modules/tautulli/modules/graphs/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/modules/graphs/widgets/navigation_bar.dart @@ -53,10 +53,6 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); + widget.pageController.lunaAnimateToPage(index); } } diff --git a/lib/modules/tautulli/modules/graphs/widgets/type_button.dart b/lib/modules/tautulli/modules/graphs/widgets/type_button.dart index 86eb6810..f72abcc8 100644 --- a/lib/modules/tautulli/modules/graphs/widgets/type_button.dart +++ b/lib/modules/tautulli/modules/graphs/widgets/type_button.dart @@ -7,11 +7,8 @@ class TautulliGraphsTypeButton extends StatelessWidget { @override Widget build(BuildContext context) => Selector( selector: (_, state) => state.graphYAxis, - builder: (context, type, _) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.merge_type), + builder: (context, type, _) => LunaPopupMenuButton( + icon: Icons.merge_type, onSelected: (value) { context.read().graphYAxis = value; context.read().resetAllPlayPeriodGraphs(); diff --git a/lib/modules/tautulli/modules/graphs_stream_information/route.dart b/lib/modules/tautulli/modules/graphs_stream_information/route.dart index 2910cd98..f558719f 100644 --- a/lib/modules/tautulli/modules/graphs_stream_information/route.dart +++ b/lib/modules/tautulli/modules/graphs_stream_information/route.dart @@ -73,7 +73,7 @@ class _State extends State with AutomaticK subtitle: [ 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', '\n\n', - 'The combined total of television and movies by their original resolution (pre-transcoding).', + 'The combined total of television and movies by their streamed resolution (post-transcoding).', ].join(), ), TautulliGraphsPlayCountByStreamResolutionGraph(), @@ -82,7 +82,7 @@ class _State extends State with AutomaticK subtitle: [ 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', '\n\n', - 'The combined total of television and movies by their original resolution (pre-transcoding).', + 'The combined total of television, movies, and music by platform and stream type.', ].join(), ), TautulliGraphsPlayCountByPlatformStreamTypeGraph(), @@ -91,7 +91,7 @@ class _State extends State with AutomaticK subtitle: [ 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days', '\n\n', - 'The combined total of television and movies by their original resolution (pre-transcoding).', + 'The combined total of television, movies, and music by user and stream type.', ].join(), ), TautulliGraphsPlayCountByUserStreamTypeGraph(), diff --git a/lib/modules/tautulli/modules/history_details/route.dart b/lib/modules/tautulli/modules/history_details/route.dart index 8c279dc5..30771681 100644 --- a/lib/modules/tautulli/modules/history_details/route.dart +++ b/lib/modules/tautulli/modules/history_details/route.dart @@ -88,7 +88,6 @@ class _State extends State<_TautulliHistoryDetailsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'History Details', actions: [ TautulliHistoryDetailsUser(ratingKey: widget.ratingKey, sessionKey: widget.sessionKey, referenceId: widget.referenceId), diff --git a/lib/modules/tautulli/modules/ipaddress_details/route.dart b/lib/modules/tautulli/modules/ipaddress_details/route.dart index 348c2d80..a2530397 100644 --- a/lib/modules/tautulli/modules/ipaddress_details/route.dart +++ b/lib/modules/tautulli/modules/ipaddress_details/route.dart @@ -70,10 +70,7 @@ class _State extends State<_TautulliIPAddressRoute> { body: _initialLoad ? _body : LSLoader(), ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'IP Address Details', - ); + Widget get _appBar => LunaAppBar(title: 'IP Address Details'); Widget get _body => FutureBuilder( future: Future.wait([ diff --git a/lib/modules/tautulli/modules/ipaddress_details/widgets/geolocation_tile.dart b/lib/modules/tautulli/modules/ipaddress_details/widgets/geolocation_tile.dart index 210f8330..e5453a5c 100644 --- a/lib/modules/tautulli/modules/ipaddress_details/widgets/geolocation_tile.dart +++ b/lib/modules/tautulli/modules/ipaddress_details/widgets/geolocation_tile.dart @@ -14,13 +14,13 @@ class TautulliIPAddressDetailsGeolocationTile extends StatelessWidget { Widget build(BuildContext context) => LSTableBlock( title: 'Location Details', children: [ - LSTableContent(title: 'country', body: geolocation.country), - LSTableContent(title: 'region', body: geolocation.region), - LSTableContent(title: 'city', body: geolocation.city), - LSTableContent(title: 'postal', body: geolocation.postalCode), - LSTableContent(title: 'timezone', body: geolocation.timezone), - LSTableContent(title: 'latitude', body: '${geolocation.latitude}'), - LSTableContent(title: 'longitude', body: '${geolocation.longitude}'), + LSTableContent(title: 'country', body: geolocation?.country ?? Constants.TEXT_EMDASH), + LSTableContent(title: 'region', body: geolocation?.region ?? Constants.TEXT_EMDASH), + LSTableContent(title: 'city', body: geolocation?.city ?? Constants.TEXT_EMDASH), + LSTableContent(title: 'postal', body: geolocation?.postalCode ?? Constants.TEXT_EMDASH), + LSTableContent(title: 'timezone', body: geolocation?.timezone ?? Constants.TEXT_EMDASH), + LSTableContent(title: 'latitude', body: '${geolocation?.latitude ?? Constants.TEXT_EMDASH}'), + LSTableContent(title: 'longitude', body: '${geolocation?.longitude ?? Constants.TEXT_EMDASH}'), ], ); } \ No newline at end of file diff --git a/lib/modules/tautulli/modules/ipaddress_details/widgets/whois_tile.dart b/lib/modules/tautulli/modules/ipaddress_details/widgets/whois_tile.dart index c4b6ceab..888619b6 100644 --- a/lib/modules/tautulli/modules/ipaddress_details/widgets/whois_tile.dart +++ b/lib/modules/tautulli/modules/ipaddress_details/widgets/whois_tile.dart @@ -14,16 +14,16 @@ class TautulliIPAddressDetailsWHOISTile extends StatelessWidget { Widget build(BuildContext context) => LSTableBlock( title: 'Connection Details', children: [ - LSTableContent(title: 'host', body: whois.host), - ...whois.subnets.fold>([], (list, subnet) { + LSTableContent(title: 'host', body: whois?.host ?? Constants.TEXT_EMDASH), + ...whois?.subnets?.fold>([], (list, subnet) { list.add(LSTableContent( title: 'isp', body: [ - subnet.description, - '\n\n${subnet.address}', - '\n${subnet.city}, ${subnet.state}', - '\n${subnet.postalCode}', - '\n${subnet.country}', + subnet?.description ?? Constants.TEXT_EMDASH, + '\n\n${subnet?.address ?? Constants.TEXT_EMDASH}', + '\n${subnet?.city}, ${subnet?.state ?? Constants.TEXT_EMDASH}', + '\n${subnet?.postalCode ?? Constants.TEXT_EMDASH}', + '\n${subnet?.country ?? Constants.TEXT_EMDASH}', ].join(), )); return list; diff --git a/lib/modules/tautulli/modules/libraries/route.dart b/lib/modules/tautulli/modules/libraries/route.dart index 2fec47d6..6d42b3c3 100644 --- a/lib/modules/tautulli/modules/libraries/route.dart +++ b/lib/modules/tautulli/modules/libraries/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliLibrariesRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Libraries', - ); + Widget get _appBar => LunaAppBar(title: 'Libraries'); Widget get _body => LSRefreshIndicator( onRefresh: _refresh, diff --git a/lib/modules/tautulli/modules/libraries/widgets/library_tile.dart b/lib/modules/tautulli/modules/libraries/widgets/library_tile.dart index 79f25383..bde7867e 100644 --- a/lib/modules/tautulli/modules/libraries/widgets/library_tile.dart +++ b/lib/modules/tautulli/modules/libraries/widgets/library_tile.dart @@ -30,7 +30,7 @@ class TautulliLibrariesLibraryTile extends StatelessWidget { TextSpan( style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), text: '${library.lastAccessed?.lunaAge ?? 'Unknown'}', ), diff --git a/lib/modules/tautulli/modules/libraries_details/route.dart b/lib/modules/tautulli/modules/libraries_details/route.dart index 42e07e53..f993240d 100644 --- a/lib/modules/tautulli/modules/libraries_details/route.dart +++ b/lib/modules/tautulli/modules/libraries_details/route.dart @@ -60,10 +60,7 @@ class _State extends State<_TautulliLibrariesDetailsRoute> { bottomNavigationBar: TautulliLibrariesDetailsNavigationBar(pageController: _pageController), ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Library Details', - ); + Widget get _appBar => LunaAppBar(title: 'Library Details'); Widget get _body => PageView( controller: _pageController, diff --git a/lib/modules/tautulli/modules/libraries_details/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/libraries_details/widgets/navigation_bar.dart index 873577df..1d31a3a6 100644 --- a/lib/modules/tautulli/modules/libraries_details/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/modules/libraries_details/widgets/navigation_bar.dart @@ -53,10 +53,6 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); + widget.pageController.lunaAnimateToPage(index); } } diff --git a/lib/modules/tautulli/modules/logs/route.dart b/lib/modules/tautulli/modules/logs/route.dart index 18b03aaa..2f8cf4a9 100644 --- a/lib/modules/tautulli/modules/logs/route.dart +++ b/lib/modules/tautulli/modules/logs/route.dart @@ -39,10 +39,7 @@ class _State extends State<_TautulliLogsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Logs'); Widget get _body => LSListView( children: [ diff --git a/lib/modules/tautulli/modules/logs_logins/route.dart b/lib/modules/tautulli/modules/logs_logins/route.dart index 3aef7b8e..2772cf4f 100644 --- a/lib/modules/tautulli/modules/logs_logins/route.dart +++ b/lib/modules/tautulli/modules/logs_logins/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliLogsLoginsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Login Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Login Logs'); Widget get _body => LSRefreshIndicator( onRefresh: _refresh, diff --git a/lib/modules/tautulli/modules/logs_logins/widgets/log_tile.dart b/lib/modules/tautulli/modules/logs_logins/widgets/log_tile.dart index ccbb28ae..772bc68e 100644 --- a/lib/modules/tautulli/modules/logs_logins/widgets/log_tile.dart +++ b/lib/modules/tautulli/modules/logs_logins/widgets/log_tile.dart @@ -35,7 +35,7 @@ class TautulliLogsLoginsLogTile extends StatelessWidget { : DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} hh:mm a').format(login.timestamp), style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ], diff --git a/lib/modules/tautulli/modules/logs_newsletters/route.dart b/lib/modules/tautulli/modules/logs_newsletters/route.dart index 69acfa7a..aa45e538 100644 --- a/lib/modules/tautulli/modules/logs_newsletters/route.dart +++ b/lib/modules/tautulli/modules/logs_newsletters/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliLogsNewslettersRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Newsletter Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Newsletter Logs'); Widget get _body => LSRefreshIndicator( onRefresh: _refresh, diff --git a/lib/modules/tautulli/modules/logs_newsletters/widgets/log_tile.dart b/lib/modules/tautulli/modules/logs_newsletters/widgets/log_tile.dart index f7cb9eb9..47f87aed 100644 --- a/lib/modules/tautulli/modules/logs_newsletters/widgets/log_tile.dart +++ b/lib/modules/tautulli/modules/logs_newsletters/widgets/log_tile.dart @@ -35,7 +35,7 @@ class TautulliLogsNewsletterLogTile extends StatelessWidget { : DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} hh:mm a').format(newsletter.timestamp), style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ], diff --git a/lib/modules/tautulli/modules/logs_notifications/route.dart b/lib/modules/tautulli/modules/logs_notifications/route.dart index ff411256..2369975e 100644 --- a/lib/modules/tautulli/modules/logs_notifications/route.dart +++ b/lib/modules/tautulli/modules/logs_notifications/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliLogsNotificationsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Notification Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Notification Logs'); Widget get _body => LSRefreshIndicator( onRefresh: _refresh, diff --git a/lib/modules/tautulli/modules/logs_notifications/widgets/log_tile.dart b/lib/modules/tautulli/modules/logs_notifications/widgets/log_tile.dart index ad0c7add..38021f49 100644 --- a/lib/modules/tautulli/modules/logs_notifications/widgets/log_tile.dart +++ b/lib/modules/tautulli/modules/logs_notifications/widgets/log_tile.dart @@ -35,7 +35,7 @@ class TautulliLogsNotificationLogTile extends StatelessWidget { : DateFormat('MMMM dd, yyyy ${Constants.TEXT_EMDASH} hh:mm a').format(notification.timestamp), style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), ], diff --git a/lib/modules/tautulli/modules/logs_plex_media_scanner/route.dart b/lib/modules/tautulli/modules/logs_plex_media_scanner/route.dart index ecbd6b51..a74f6726 100644 --- a/lib/modules/tautulli/modules/logs_plex_media_scanner/route.dart +++ b/lib/modules/tautulli/modules/logs_plex_media_scanner/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliLogsPlexMediaScannerRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Plex Media Scanner Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Plex Media Scanner Logs'); Widget get _body => LSRefreshIndicator( onRefresh: _refresh, diff --git a/lib/modules/tautulli/modules/logs_plex_media_scanner/widgets/log_tile.dart b/lib/modules/tautulli/modules/logs_plex_media_scanner/widgets/log_tile.dart index ff634c98..441170cd 100644 --- a/lib/modules/tautulli/modules/logs_plex_media_scanner/widgets/log_tile.dart +++ b/lib/modules/tautulli/modules/logs_plex_media_scanner/widgets/log_tile.dart @@ -38,7 +38,7 @@ class TautulliLogsPlexMediaScannerLogTile extends StatelessWidget { text: log.timestamp, style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ) ), ], diff --git a/lib/modules/tautulli/modules/logs_plex_media_server/route.dart b/lib/modules/tautulli/modules/logs_plex_media_server/route.dart index 037ac19e..daad535b 100644 --- a/lib/modules/tautulli/modules/logs_plex_media_server/route.dart +++ b/lib/modules/tautulli/modules/logs_plex_media_server/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliLogsPlexMediaServerRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Plex Media Server Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Plex Media Server Logs'); Widget get _body => LSRefreshIndicator( onRefresh: _refresh, diff --git a/lib/modules/tautulli/modules/logs_plex_media_server/widgets/log_tile.dart b/lib/modules/tautulli/modules/logs_plex_media_server/widgets/log_tile.dart index 96652dfc..05866176 100644 --- a/lib/modules/tautulli/modules/logs_plex_media_server/widgets/log_tile.dart +++ b/lib/modules/tautulli/modules/logs_plex_media_server/widgets/log_tile.dart @@ -38,7 +38,7 @@ class TautulliLogsPlexMediaServerLogTile extends StatelessWidget { text: log.timestamp, style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ) ), ], diff --git a/lib/modules/tautulli/modules/logs_tautulli/route.dart b/lib/modules/tautulli/modules/logs_tautulli/route.dart index 99b96a02..4e2ddea1 100644 --- a/lib/modules/tautulli/modules/logs_tautulli/route.dart +++ b/lib/modules/tautulli/modules/logs_tautulli/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliLogsTautulliRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Tautulli Logs', - ); + Widget get _appBar => LunaAppBar(title: 'Tautulli Logs'); Widget get _body => LSRefreshIndicator( onRefresh: _refresh, diff --git a/lib/modules/tautulli/modules/logs_tautulli/widgets/log_tile.dart b/lib/modules/tautulli/modules/logs_tautulli/widgets/log_tile.dart index ae2db5e9..d89c15cd 100644 --- a/lib/modules/tautulli/modules/logs_tautulli/widgets/log_tile.dart +++ b/lib/modules/tautulli/modules/logs_tautulli/widgets/log_tile.dart @@ -38,7 +38,7 @@ class TautulliLogsTautulliLogTile extends StatelessWidget { text: log.timestamp, style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ) ), ], diff --git a/lib/modules/tautulli/modules/media_details/route.dart b/lib/modules/tautulli/modules/media_details/route.dart index 244ad81f..c56c5b94 100644 --- a/lib/modules/tautulli/modules/media_details/route.dart +++ b/lib/modules/tautulli/modules/media_details/route.dart @@ -74,10 +74,7 @@ class _State extends State<_TautulliMediaDetailsRoute> { : _contentNotFound, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Media Details', - ); + Widget get _appBar => LunaAppBar(title: 'Media Details'); Widget get _bottomNavigationBar { if( diff --git a/lib/modules/tautulli/modules/media_details/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/media_details/widgets/navigation_bar.dart index 4036070e..61d2a5d6 100644 --- a/lib/modules/tautulli/modules/media_details/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/modules/media_details/widgets/navigation_bar.dart @@ -53,10 +53,6 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); + widget.pageController.lunaAnimateToPage(index); } } diff --git a/lib/modules/tautulli/modules/recently_added/route.dart b/lib/modules/tautulli/modules/recently_added/route.dart index e843f6c9..cff1cc4d 100644 --- a/lib/modules/tautulli/modules/recently_added/route.dart +++ b/lib/modules/tautulli/modules/recently_added/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliRecentlyAddedRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Recently Added', - ); + Widget get _appBar => LunaAppBar(title: 'Recently Added'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/tautulli/modules/search/route.dart b/lib/modules/tautulli/modules/search/route.dart index 8032f59c..2cc32159 100644 --- a/lib/modules/tautulli/modules/search/route.dart +++ b/lib/modules/tautulli/modules/search/route.dart @@ -37,7 +37,7 @@ class _State extends State<_TautulliSearchRoute> { @override Widget build(BuildContext context) => Scaffold( key: _scaffoldKey, - appBar: TautulliSearchAppBar(context: context), + appBar: TautulliSearchAppBar(), body: TautulliSearchSearchResults(), ); } \ No newline at end of file diff --git a/lib/modules/tautulli/modules/search/widgets/search_appbar.dart b/lib/modules/tautulli/modules/search/widgets/search_appbar.dart index 5112a4b8..2a6500db 100644 --- a/lib/modules/tautulli/modules/search/widgets/search_appbar.dart +++ b/lib/modules/tautulli/modules/search/widgets/search_appbar.dart @@ -3,10 +3,7 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; // ignore: non_constant_identifier_names -Widget TautulliSearchAppBar({ - @required BuildContext context, -}) => LunaAppBar( - context: context, +Widget TautulliSearchAppBar() => LunaAppBar( title: 'Search', bottom: _SearchBar(), ); diff --git a/lib/modules/tautulli/modules/search/widgets/search_result_tile.dart b/lib/modules/tautulli/modules/search/widgets/search_result_tile.dart index 9a19dbdb..8776d6b1 100644 --- a/lib/modules/tautulli/modules/search/widgets/search_result_tile.dart +++ b/lib/modules/tautulli/modules/search/widgets/search_result_tile.dart @@ -91,7 +91,7 @@ class TautulliSearchResultTile extends StatelessWidget { softWrap: false, style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ); diff --git a/lib/modules/tautulli/modules/statistics/route.dart b/lib/modules/tautulli/modules/statistics/route.dart index 658a71e5..42ee2f1a 100644 --- a/lib/modules/tautulli/modules/statistics/route.dart +++ b/lib/modules/tautulli/modules/statistics/route.dart @@ -54,7 +54,6 @@ class _State extends State<_TautulliStatisticsRoute> { ); Widget get _appBar => LunaAppBar( - context: context, title: 'Statistics', actions: [ TautulliStatisticsTypeButton(), diff --git a/lib/modules/tautulli/modules/statistics/widgets/media_tile.dart b/lib/modules/tautulli/modules/statistics/widgets/media_tile.dart index 1169badf..c5e25643 100644 --- a/lib/modules/tautulli/modules/statistics/widgets/media_tile.dart +++ b/lib/modules/tautulli/modules/statistics/widgets/media_tile.dart @@ -83,7 +83,7 @@ class TautulliStatisticsMediaTile extends StatelessWidget { ? LunaColours.accent : null, fontWeight: context.watch().statisticsType == TautulliStatsType.PLAYS - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -96,7 +96,7 @@ class TautulliStatisticsMediaTile extends StatelessWidget { ? LunaColours.accent : null, fontWeight: context.watch().statisticsType == TautulliStatsType.DURATION - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ) diff --git a/lib/modules/tautulli/modules/statistics/widgets/platform_tile.dart b/lib/modules/tautulli/modules/statistics/widgets/platform_tile.dart index a8fc788f..283b9ce9 100644 --- a/lib/modules/tautulli/modules/statistics/widgets/platform_tile.dart +++ b/lib/modules/tautulli/modules/statistics/widgets/platform_tile.dart @@ -72,7 +72,7 @@ class TautulliStatisticsPlatformTile extends StatelessWidget { ? LunaColours.accent : null, fontWeight: context.watch().statisticsType == TautulliStatsType.PLAYS - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -85,7 +85,7 @@ class TautulliStatisticsPlatformTile extends StatelessWidget { ? LunaColours.accent : null, fontWeight: context.watch().statisticsType == TautulliStatsType.DURATION - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ) diff --git a/lib/modules/tautulli/modules/statistics/widgets/stream_tile.dart b/lib/modules/tautulli/modules/statistics/widgets/stream_tile.dart index 1d375ec8..8f79f69e 100644 --- a/lib/modules/tautulli/modules/statistics/widgets/stream_tile.dart +++ b/lib/modules/tautulli/modules/statistics/widgets/stream_tile.dart @@ -68,7 +68,7 @@ class TautulliStatisticsStreamTile extends StatelessWidget { text: data['count'].toString() + (data['count'] == 1 ? ' Play' : ' Plays'), style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), TextSpan(text: '\n'), diff --git a/lib/modules/tautulli/modules/statistics/widgets/time_range_button.dart b/lib/modules/tautulli/modules/statistics/widgets/time_range_button.dart index efb71753..3fb1cca8 100644 --- a/lib/modules/tautulli/modules/statistics/widgets/time_range_button.dart +++ b/lib/modules/tautulli/modules/statistics/widgets/time_range_button.dart @@ -6,11 +6,8 @@ class TautulliStatisticsTimeRangeButton extends StatelessWidget { @override Widget build(BuildContext context) => Selector( selector: (_, state) => state.statisticsTimeRange, - builder: (context, range, _) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.access_time), + builder: (context, range, _) => LunaPopupMenuButton( + icon: Icons.access_time, onSelected: (value) { context.read().statisticsTimeRange = value; context.read().resetStatistics(); diff --git a/lib/modules/tautulli/modules/statistics/widgets/type_button.dart b/lib/modules/tautulli/modules/statistics/widgets/type_button.dart index ad54dc73..fef25ef6 100644 --- a/lib/modules/tautulli/modules/statistics/widgets/type_button.dart +++ b/lib/modules/tautulli/modules/statistics/widgets/type_button.dart @@ -7,11 +7,8 @@ class TautulliStatisticsTypeButton extends StatelessWidget { @override Widget build(BuildContext context) => Selector( selector: (_, state) => state.statisticsType, - builder: (context, type, _) => PopupMenuButton( - shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data - ? LSRoundedShapeWithBorder() - : LSRoundedShape(), - icon: LSIcon(icon: Icons.merge_type), + builder: (context, type, _) => LunaPopupMenuButton( + icon: Icons.merge_type, onSelected: (value) { context.read().statisticsType = value; context.read().resetStatistics(); diff --git a/lib/modules/tautulli/modules/statistics/widgets/user_tile.dart b/lib/modules/tautulli/modules/statistics/widgets/user_tile.dart index 43c2a0b0..c65a2bed 100644 --- a/lib/modules/tautulli/modules/statistics/widgets/user_tile.dart +++ b/lib/modules/tautulli/modules/statistics/widgets/user_tile.dart @@ -72,7 +72,7 @@ class TautulliStatisticsUserTile extends StatelessWidget { ? LunaColours.accent : null, fontWeight: context.watch().statisticsType == TautulliStatsType.PLAYS - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ), @@ -85,7 +85,7 @@ class TautulliStatisticsUserTile extends StatelessWidget { ? LunaColours.accent : null, fontWeight: context.watch().statisticsType == TautulliStatsType.DURATION - ? FontWeight.w600 + ? LunaUI.FONT_WEIGHT_BOLD : null, ), ) diff --git a/lib/modules/tautulli/modules/synced_items/route.dart b/lib/modules/tautulli/modules/synced_items/route.dart index 6b487112..34b0ed7a 100644 --- a/lib/modules/tautulli/modules/synced_items/route.dart +++ b/lib/modules/tautulli/modules/synced_items/route.dart @@ -53,10 +53,7 @@ class _State extends State<_TautulliSyncedItemsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'Synced Items', - ); + Widget get _appBar => LunaAppBar(title: 'Synced Items'); Widget get _body => LSRefreshIndicator( refreshKey: _refreshKey, diff --git a/lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart b/lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart index 266c6e27..36a2e845 100644 --- a/lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart +++ b/lib/modules/tautulli/modules/synced_items/widgets/synced_item_tile.dart @@ -25,7 +25,7 @@ class TautulliSyncedItemTile extends StatelessWidget { text: (syncedItem.state ?? 'Unknown').lunaCapitalizeFirstLetters(), style: TextStyle( color: LunaColours.accent, - fontWeight: FontWeight.w600, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, ), ), TextSpan(text: '\t${Constants.TEXT_EMDASH}\t'), diff --git a/lib/modules/tautulli/modules/tautulli/route.dart b/lib/modules/tautulli/modules/tautulli/route.dart index 1d8bcf51..28be747a 100644 --- a/lib/modules/tautulli/modules/tautulli/route.dart +++ b/lib/modules/tautulli/modules/tautulli/route.dart @@ -79,8 +79,7 @@ class _State extends State<_TautulliHomeRoute> { ), ); - Widget get _appBar => LSAppBarDropdown( - context: context, + Widget get _appBar => LunaAppBar.dropdown( title: 'Tautulli', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject)?.tautulliEnabled ?? false) value.add(element); diff --git a/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart index 9470f21b..88d07b88 100644 --- a/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart @@ -1,6 +1,4 @@ -import 'package:badges/badges.dart'; import 'package:flutter/material.dart'; -import 'package:google_nav_bar/google_nav_bar.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/tautulli.dart'; import 'package:tautulli/tautulli.dart'; @@ -53,104 +51,29 @@ class _State extends State { @override Widget build(BuildContext context) => Consumer( - builder: (context, state, _) => Container( - child: SafeArea( - top: false, - child: Padding( - child: GNav( - gap: 8.0, - iconSize: 24.0, - padding: EdgeInsets.fromLTRB(18.0, 5.0, 12.0, 5.0), - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - tabBackgroundColor: Theme.of(context).canvasColor, - activeColor: LunaColours.accent, - tabs: [ - GButton( - icon: TautulliNavigationBar.icons[0], - text: TautulliNavigationBar.titles[0], - iconSize: 22.0, - textStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: LunaColours.accent, - ), - leading: FutureBuilder( - future: state.activity, - builder: (BuildContext context, AsyncSnapshot snapshot) => Badge( - badgeColor: LunaColours.accent.withOpacity(0.65), - elevation: 0, - animationDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - animationType: BadgeAnimationType.fade, - shape: BadgeShape.circle, - position: BadgePosition.topEnd( - top: -15, - end: -15, - ), - badgeContent: Text( - snapshot.hasData - ? snapshot.data.streamCount.toString() - : '?', - style: TextStyle(color: Colors.white), - ), - child: Icon( - TautulliNavigationBar.icons[0], - color: _index == 0 - ? LunaColours.accent - : Colors.white, - ), - showBadge: state.enabled && _index != 0 && snapshot.hasData && snapshot.data.streamCount > 0, - ), - ), - ), - GButton( - icon: TautulliNavigationBar.icons[1], - text: TautulliNavigationBar.titles[1], - iconSize: 22.0, - textStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: LunaColours.accent, - ), - ), - GButton( - icon: TautulliNavigationBar.icons[2], - text: TautulliNavigationBar.titles[2], - iconSize: 22.0, - textStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: LunaColours.accent, - ), - ), - GButton( - icon: TautulliNavigationBar.icons[3], - text: TautulliNavigationBar.titles[3], - iconSize: 22.0, - textStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: Constants.UI_FONT_SIZE_SUBTITLE, - color: LunaColours.accent, - ), - ), - ], - selectedIndex: _index, - onTabChange: _navOnTap, + builder: (context, state, _) => LSBottomNavigationBar( + index: _index, + onTap: _navOnTap, + icons: TautulliNavigationBar.icons, + titles: TautulliNavigationBar.titles, + leadingOnGButton: [ + FutureBuilder( + future: state.activity, + builder: (BuildContext context, AsyncSnapshot snapshot) => LunaNavigationBarBadge( + text: snapshot.hasData ? snapshot.data.streamCount.toString() : '?', + icon: TautulliNavigationBar.icons[0], + isActive: _index == 0, + showBadge: state.enabled && _index != 0 && snapshot.hasData && snapshot.data.streamCount > 0, ), - padding: EdgeInsets.all(12.0), ), - ), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - //LunaColours.secondary, - ), + null, + null, + null, + ], ), ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); + widget.pageController.lunaAnimateToPage(index); } } diff --git a/lib/modules/tautulli/modules/users_details/route.dart b/lib/modules/tautulli/modules/users_details/route.dart index d7e999a8..d841eab4 100644 --- a/lib/modules/tautulli/modules/users_details/route.dart +++ b/lib/modules/tautulli/modules/users_details/route.dart @@ -73,10 +73,7 @@ class _State extends State<_TautulliUserDetailsRoute> { body: _body, ); - Widget get _appBar => LunaAppBar( - context: context, - title: 'User Details', - ); + Widget get _appBar => LunaAppBar(title: 'User Details'); Widget get _bottomNavigationBar => TautulliUserDetailsNavigationBar(pageController: _pageController); diff --git a/lib/modules/tautulli/modules/users_details/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/users_details/widgets/navigation_bar.dart index acc6ef17..05c0ab5f 100644 --- a/lib/modules/tautulli/modules/users_details/widgets/navigation_bar.dart +++ b/lib/modules/tautulli/modules/users_details/widgets/navigation_bar.dart @@ -57,10 +57,6 @@ class _State extends State { ); Future _navOnTap(int index) async { - await widget.pageController.animateToPage( - index, - duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), - curve: Curves.easeOutSine, - ); + widget.pageController.lunaAnimateToPage(index); } } diff --git a/lib/modules/wake_on_lan/core/constants.dart b/lib/modules/wake_on_lan/core/constants.dart index f6ad2096..5c95cb90 100644 --- a/lib/modules/wake_on_lan/core/constants.dart +++ b/lib/modules/wake_on_lan/core/constants.dart @@ -17,7 +17,6 @@ class WakeOnLANConstants { color: Color(LunaColours.ACCENT_COLOR), website: '', github: '', - pushBaseRoute: null, shortcutItem: null, ); } diff --git a/pubspec.lock b/pubspec.lock index 760222be..255c9223 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.41.1" + version: "0.41.2" archive: dependency: transitive description: @@ -42,7 +42,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.2" + version: "2.5.0" badges: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.1" + version: "1.6.2" build_config: dependency: transitive description: @@ -70,56 +70,56 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "2.1.6" + version: "2.1.7" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.2" + version: "1.5.3" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.10.13" + version: "1.11.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.6" + version: "6.1.7" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "4.3.2" + version: "5.0.0" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "7.1.0" + version: "8.0.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0" checked_yaml: dependency: transitive description: @@ -133,14 +133,14 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" cloud_firestore: dependency: "direct main" description: @@ -175,7 +175,49 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.15.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" + connectivity_plus_linux: + dependency: transitive + description: + name: connectivity_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + connectivity_plus_macos: + dependency: transitive + description: + name: connectivity_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + connectivity_plus_web: + dependency: transitive + description: + name: connectivity_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" + connectivity_plus_windows: + dependency: transitive + description: + name: connectivity_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" convert: dependency: "direct main" description: @@ -203,7 +245,7 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.11" + version: "1.3.12" dartx: dependency: transitive description: @@ -211,6 +253,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.5.0" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" dio: dependency: "direct main" description: @@ -231,7 +280,7 @@ packages: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "1.2.5" + version: "1.2.6" expandable: dependency: "direct main" description: @@ -266,7 +315,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "2.1.5+1" + version: "2.1.6" firebase_auth: dependency: "direct main" description: @@ -357,7 +406,7 @@ packages: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "0.10.11" + version: "1.0.0" fl_chart: dependency: "direct main" description: @@ -371,7 +420,7 @@ packages: name: flash url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.2" fluro: dependency: "direct main" description: @@ -397,7 +446,7 @@ packages: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "0.2.5" + version: "0.3.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -437,7 +486,7 @@ packages: name: google_nav_bar url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "4.0.2" graphs: dependency: transitive description: @@ -508,6 +557,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.5+1" + infinite_scroll_pagination: + dependency: "direct main" + description: + name: infinite_scroll_pagination + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" intl: dependency: "direct main" description: @@ -528,7 +584,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.2" + version: "0.6.3" json_annotation: dependency: transitive description: @@ -542,21 +598,21 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.0" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.9" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" mime: dependency: transitive description: @@ -578,6 +634,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.4" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+1" + network_info_plus_linux: + dependency: transitive + description: + name: network_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + network_info_plus_macos: + dependency: transitive + description: + name: network_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + network_info_plus_web: + dependency: transitive + description: + name: network_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + network_info_plus_windows: + dependency: transitive + description: + name: network_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" node_interop: dependency: transitive description: @@ -605,14 +703,14 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.3+2" + version: "0.4.3+4" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" path_drawing: dependency: transitive description: @@ -668,28 +766,28 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.9.2" + version: "1.10.0" percent_indicator: dependency: "direct main" description: name: percent_indicator url: "https://pub.dartlang.org" source: hosted - version: "2.1.9" + version: "2.1.9+1" permission_handler: dependency: "direct main" description: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "5.0.1+1" + version: "5.1.0+2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" petitparser: dependency: transitive description: @@ -703,7 +801,7 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.0.0" plugin_platform_interface: dependency: transitive description: @@ -717,14 +815,14 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" pool: dependency: transitive description: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0" process: dependency: transitive description: @@ -738,21 +836,21 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.2+4" + version: "4.3.3" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" + version: "2.0.0" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.7" + version: "0.1.8" quick_actions: dependency: "direct main" description: @@ -767,6 +865,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + radarr: + dependency: "direct main" + description: + name: radarr + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" recursive_regex: dependency: transitive description: @@ -780,21 +885,21 @@ packages: name: sembast url: "https://pub.dartlang.org" source: hosted - version: "2.4.8+1" + version: "2.4.9" sentry: dependency: transitive description: name: sentry url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.4" sentry_flutter: dependency: "direct main" description: name: sentry_flutter url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.4" service_worker: dependency: transitive description: @@ -850,7 +955,7 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "0.2.4" simple_gesture_detector: dependency: transitive description: @@ -863,6 +968,13 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.10+1" sonarr: dependency: "direct main" description: @@ -876,42 +988,42 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+1" + version: "0.9.10+2" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.1" stack_trace: dependency: "direct main" description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.6" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0" synchronized: dependency: transitive description: @@ -932,14 +1044,14 @@ packages: name: tautulli url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" time: dependency: transitive description: @@ -967,7 +1079,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" url_launcher: dependency: "direct main" description: @@ -1002,7 +1114,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.5+1" + version: "0.1.5+3" url_launcher_windows: dependency: transitive description: @@ -1030,35 +1142,35 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" wake_on_lan: dependency: "direct main" description: name: wake_on_lan url: "https://pub.dartlang.org" source: hosted - version: "1.1.1+5" + version: "1.1.1+6" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+15" + version: "1.0.0" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4" + version: "1.7.4+1" xdg_directories: dependency: transitive description: @@ -1095,5 +1207,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.10.0 <2.11.0" - flutter: ">=1.22.0 <2.0.0" + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=1.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index aa3b57b0..a25c4fb2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lunasea description: Self-Hosted Controller -version: 4.2.1+40201001 +version: 5.0.0+50000001 publish_to: 'none' environment: sdk: ">=2.7.0 <3.0.0" @@ -8,16 +8,17 @@ environment: dependencies: badges: ^1.1.6 cloud_firestore: ^0.16.0 + connectivity_plus: ^0.8.0 convert: ^2.1.1 dio: ^3.0.10 encrypt: ^4.1.0 expandable: ^4.1.4 - file_picker: ^2.1.5+1 + file_picker: ^2.1.6 firebase_core: ^0.7.0 firebase_auth: ^0.20.0+1 firebase_messaging: ^8.0.0-dev.14 firebase_storage: ^7.0.0 - flash: ^1.3.1 + flash: ^1.3.2 fluro: ^1.7.8 flutter: sdk: flutter @@ -25,37 +26,40 @@ dependencies: flutter_sticky_header: ^0.5.0 fl_chart: ^0.12.2 f_logs: ^1.3.0-alpha-02 - google_nav_bar: ^3.2.0 + google_nav_bar: ^4.0.2 hive: ^1.4.4+1 hive_flutter: ^0.3.1 in_app_purchase: ^0.3.5+1 + infinite_scroll_pagination: ^2.3.0 intl: ^0.16.1 modal_bottom_sheet: ^1.0.0+1 - package_info: ^0.4.3+2 + network_info_plus: ^0.1.1+1 + package_info: ^0.4.3+4 path_provider: ^1.6.27 - percent_indicator: ^2.1.9 - permission_handler: ^5.0.1+1 - provider: ^4.3.2+4 + percent_indicator: ^2.1.9+1 + permission_handler: ^5.1.0+2 + provider: ^4.3.3 quick_actions: ^0.4.0+10 - sentry_flutter: ^4.0.1 + sentry_flutter: ^4.0.4 share_plus: ^1.2.0 - stack_trace: ^1.9.6 + stack_trace: ^1.10.0 table_calendar: ^2.3.3 tuple: ^1.0.3 url_launcher: ^5.7.10 uuid: ^2.2.2 xml_parser: ^0.1.2 # Comet.Tools Packages + radarr: ^1.0.1 sonarr: ^1.1.0+1 - tautulli: ^1.2.0 - wake_on_lan: ^1.1.1+5 + tautulli: ^1.2.1 + wake_on_lan: ^1.1.1+6 dev_dependencies: - analyzer: ^0.41.1 - build_runner: ^1.10.13 + analyzer: ^0.41.2 + build_runner: ^1.11.1 dapackages: ^1.4.0 flutter_launcher_icons: ^0.8.1 - flutter_native_splash: ^0.2.5 + flutter_native_splash: ^0.3.0 hive_generator: ^0.8.2 flutter: diff --git a/scripts/build_ipa b/scripts/build_ipa index e189009d..6cc45a04 100755 --- a/scripts/build_ipa +++ b/scripts/build_ipa @@ -2,18 +2,14 @@ cd .. # Remove old builds +rm -rf build rm -rf output/*.ipa # Clean and build flutter clean -flutter build ios -# Export build to IPA -cd ios -xcodebuild -workspace Runner.xcworkspace -scheme Runner -sdk iphoneos -configuration Release archive -archivePath build/LunaSea.xcarchive -xcodebuild -exportArchive -archivePath build/LunaSea.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath build/LunaSea.app +flutter build ipa --export-options-plist=ios/ExportOptions.plist # Copy IPA to root of project -cd .. mkdir -p output -cp ios/build/LunaSea.app/LunaSea.ipa output/LunaSea-arm64-release.ipa +cp build/ios/ipa/LunaSea.ipa output/LunaSea-arm64-release.ipa # Remove build files rm -rf build -rm -rf build ios/build +rm -rf ios/build