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 @@
-[](https://www.lunasea.app/appstore)
-[](https://www.lunasea.app/playstore)
-[](https://codemagic.io/apps/5ef3424e03ad6b478a268d4c/5ef3424e03ad6b478a268d4b/latest_build)
-[](https://www.lunasea.app/reddit)
-[](https://www.lunasea.app/discord)
-
+#
LunaSea
+
+[](https://www.lunasea.app/appstore)
+[](https://www.lunasea.app/playstore)
+[](https://www.lunasea.app/feedback)
+[](https://www.lunasea.app/discord)
+[](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