diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93f6c391..e2009961 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,13 @@
# LunaSea Changelog
-## [Beta/TestFlight] v4.2.0 (40200003)
+## [Beta] v4.2.0 (40200004)
#### NEW
+- `[Accounts]` Added LunaSea accounts
+- `[Accounts]` Ability to backup, restore, and delete cloud configurations
+- `[Backups]` All backups now contain all customization and configuration options in LunaSea
+- `[Changelog]` Show changelog on launch if a new version is installed
+- `[Changelog]` Use a new bottom sheet UI for the changelog
- `[Settings/Modules]` Add an information/help button with module descriptions and links
#### TWEAKS
@@ -13,10 +18,11 @@
- `[Settings/Tautulli]` Removed the need to enable Tautulli to test the connection
#### FIXES
-- `[IAPs]` Ensure all IAPs are marked as "consumed"
+- `[In App Purchases]` Ensure all in app purchases are marked as "consumed"
- `[Flutter]` Updated packages
- `[Logging]` Updated Sentry to v4 framework to improve capturing fatal/crashing bugs
- `[Settings/Logs]` Hide exception and stack trace buttons when an error is not available
+- `[Settings/Resources]` Updated URL endpoints
- `[Sonarr/History]` Fixed history fetching the oldest entries, not the newest
- `[State]` Correctly clear state when clearing LunaSea's configuration
- `[Tautulli/Activity]` Fixed consistency of hardware transcoding indicator compared to the web UI
@@ -24,7 +30,7 @@
---
-## [Stable/Production] v4.1.1 (40101001)
+## v4.1.1 (40101001)
#### NEW
- None
@@ -38,7 +44,7 @@
---
-## [Stable/Production] v4.1.0 (40100007)
+## v4.1.0 (40100007)
#### NEW
- `[Backup & Restore]` Backup files now use the .lunasea extension (older .json backups are still supported)
@@ -89,7 +95,7 @@
---
-## [Stable/Production] v4.0.0 (400102)
+## v4.0.0 (400102)
#### NEW
- `[Module]` Tautulli support
@@ -123,7 +129,7 @@
---
-## [Stable/Production] v3.3.0 (330002)
+## v3.3.0 (330002)
#### NEW
- `[Home/Calendar]` New "schedule" view
@@ -141,7 +147,7 @@
---
-## [Stable/Production] v3.2.0 (320003)
+## v3.2.0 (320003)
#### NEW
- `[Drawer]` Add the LunaSea logo to the header
@@ -159,7 +165,7 @@
---
-## [Stable/Production] v3.1.0 (310004)
+## v3.1.0 (310004)
#### NEW
@@ -182,7 +188,7 @@
---
-## [Stable/Production] v3.0.0 (300100)
+## v3.0.0 (300100)
#### NEW
@@ -222,7 +228,7 @@
---
-## [Stable/Production] v2.3.0 (230004)
+## v2.3.0 (230004)
#### NEW
@@ -247,7 +253,7 @@
---
-## [Stable/Production] v2.2.0 (220003)
+## v2.2.0 (220003)
#### NEW
@@ -270,7 +276,7 @@
---
-## [Stable/Production] v2.1.0 (66)
+## v2.1.0 (66)
#### NEW
@@ -297,7 +303,7 @@
---
-## [Stable/Production] v2.0.1 (55)
+## v2.0.1 (55)
#### NEW
@@ -328,7 +334,7 @@
---
-## [Stable/Production] v2.0.0 (333)
+## v2.0.0 (333)
#### NEW
@@ -381,7 +387,7 @@
---
-## [Stable/Production] v1.1.0 (44)
+## v1.1.0 (44)
#### NEW
@@ -408,7 +414,7 @@
---
-## [Stable/Production] v1.0.0 (33)
+## v1.0.0 (33)
#### NEW
diff --git a/assets/changelog.json b/assets/changelog.json
new file mode 100644
index 00000000..39282b06
--- /dev/null
+++ b/assets/changelog.json
@@ -0,0 +1,116 @@
+{
+ "motd": "This update introduces some exciting new features, including a brand new cloud account system! You can register for a free LunaSea account to backup an unlimited amount of configurations and easily restore configurations from previous devices.\n\nBackups also now support backing up the entire database, including customization details for each module.",
+ "version": "4.2.0",
+ "build": "40200004",
+ "new": [
+ {
+ "module": "Accounts",
+ "changes": [
+ "Added LunaSea accounts",
+ "Ability to backup, restore, and delete cloud configurations"
+ ]
+ },
+ {
+ "module": "Backups",
+ "changes": [
+ "All backups now contain all customization and configuration options in LunaSea"
+ ]
+ },
+ {
+ "module": "Changelog",
+ "changes": [
+ "Show changelog on launch if a new version is installed",
+ "Use a new bottom sheet UI for the changelog"
+ ]
+ },
+ {
+ "module": "Settings/Modules",
+ "changes": [
+ "Add an information/help button with module descriptions and links"
+ ]
+ }
+ ],
+ "tweaks": [
+ {
+ "module": "Radarr",
+ "changes": [
+ "Always show the amount of days content will be available instead of limiting it to only content in the next 30 days"
+ ]
+ },
+ {
+ "module": "Settings",
+ "changes": [
+ "Moved \"Backup & Restore\" and \"Logs\" to \"System\" section",
+ "Merged \"Customization\" and \"Modules\" sections into \"Configuration\""
+ ]
+ },
+ {
+ "module": "Settings/Sonarr",
+ "changes": [
+ "Removed the need to enable Sonarr to test the connection"
+ ]
+ },
+ {
+ "module": "Settings/Tautulli",
+ "changes": [
+ "Removed the need to enable Tautulli to test the connection"
+ ]
+ }
+ ],
+ "fixes": [
+ {
+ "module": "In App Purchases",
+ "changes": [
+ "Ensure all in app purchases are marked as \"consumed\""
+ ]
+ },
+ {
+ "module": "Flutter",
+ "changes": [
+ "Updated packages"
+ ]
+ },
+ {
+ "module": "Logging",
+ "changes": [
+ "Updated Sentry to v4 framework to improve capturing fatal/crashing bugs"
+ ]
+ },
+ {
+ "module": "Settings/Logs",
+ "changes": [
+ "Hide exception and stack trace buttons when an error is not available"
+ ]
+ },
+ {
+ "module": "Settings/Resources",
+ "changes": [
+ "Updated URL endpoints"
+ ]
+ },
+ {
+ "module": "Sonarr/History",
+ "changes": [
+ "Fixed history fetching the oldest entries, not the newest"
+ ]
+ },
+ {
+ "module": "State",
+ "changes": [
+ "Correctly clear state when clearing LunaSea's configuration"
+ ]
+ },
+ {
+ "module": "Tautulli/Activity",
+ "changes": [
+ "Fixed consistency of hardware transcoding indicator compared to the web UI"
+ ]
+ },
+ {
+ "module": "UI/Divider",
+ "changes": [
+ "Fix consistency of divider width across regular and AMOLED dark theme"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 202700e7..052851b4 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -46,6 +46,9 @@ PODS:
- Firebase/Firestore (6.33.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 1.18.0)
+ - Firebase/Messaging (6.33.0):
+ - Firebase/CoreOnly
+ - FirebaseMessaging (~> 4.7.0)
- Firebase/Storage (6.33.0):
- Firebase/CoreOnly
- FirebaseStorage (~> 3.9.0)
@@ -57,6 +60,11 @@ PODS:
- firebase_core (0.5.3):
- Firebase/CoreOnly (~> 6.33.0)
- Flutter
+ - firebase_messaging (8.0.0-dev.11):
+ - Firebase/CoreOnly (~> 6.33.0)
+ - Firebase/Messaging (~> 6.33.0)
+ - firebase_core
+ - Flutter
- firebase_storage (5.2.0):
- Firebase/CoreOnly (~> 6.33.0)
- Firebase/Storage (~> 6.33.0)
@@ -77,6 +85,24 @@ PODS:
- GoogleUtilities/Logger (~> 6.7)
- nanopb (~> 1.30906.0)
- FirebaseFirestore (1.18.0)
+ - FirebaseInstallations (1.7.0):
+ - FirebaseCore (~> 6.10)
+ - GoogleUtilities/Environment (~> 6.7)
+ - GoogleUtilities/UserDefaults (~> 6.7)
+ - PromisesObjC (~> 1.2)
+ - FirebaseInstanceID (4.8.0):
+ - FirebaseCore (~> 6.10)
+ - FirebaseInstallations (~> 1.6)
+ - GoogleUtilities/Environment (~> 6.7)
+ - GoogleUtilities/UserDefaults (~> 6.7)
+ - FirebaseMessaging (4.7.1):
+ - FirebaseCore (~> 6.10)
+ - FirebaseInstanceID (~> 4.7)
+ - GoogleUtilities/AppDelegateSwizzler (~> 6.7)
+ - GoogleUtilities/Environment (~> 6.7)
+ - GoogleUtilities/Reachability (~> 6.7)
+ - GoogleUtilities/UserDefaults (~> 6.7)
+ - Protobuf (>= 3.9.2, ~> 3.9)
- FirebaseStorage (3.9.1):
- FirebaseCore (~> 6.10)
- GTMSessionFetcher/Core (~> 1.1)
@@ -98,6 +124,8 @@ PODS:
- "GoogleUtilities/NSData+zlib (6.7.2)"
- GoogleUtilities/Reachability (6.7.2):
- GoogleUtilities/Logger
+ - GoogleUtilities/UserDefaults (6.7.2):
+ - GoogleUtilities/Logger
- GTMSessionFetcher/Core (1.5.0)
- in_app_purchase (0.0.1):
- Flutter
@@ -113,6 +141,7 @@ PODS:
- "permission_handler (5.0.1+1)":
- Flutter
- PromisesObjC (1.2.11)
+ - Protobuf (3.14.0)
- quick_actions (0.0.1):
- Flutter
- SDWebImage (5.10.0):
@@ -124,7 +153,7 @@ PODS:
- sentry_flutter (0.0.1):
- Flutter
- Sentry (~> 6.0.10)
- - share (0.0.1):
+ - share_plus (0.0.1):
- Flutter
- SwiftyGif (5.3.0)
- url_launcher (0.0.1):
@@ -135,6 +164,7 @@ DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
+ - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_storage (from `.symlinks/plugins/firebase_storage/ios`)
- FirebaseFirestore (from `https://github.com/invertase/firestore-ios-sdk-frameworks.git`, tag `6.33.0`)
- Flutter (from `Flutter`)
@@ -144,7 +174,7 @@ DEPENDENCIES:
- permission_handler (from `.symlinks/plugins/permission_handler/ios`)
- quick_actions (from `.symlinks/plugins/quick_actions/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- - share (from `.symlinks/plugins/share/ios`)
+ - share_plus (from `.symlinks/plugins/share_plus/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
SPEC REPOS:
@@ -155,12 +185,16 @@ SPEC REPOS:
- FirebaseAuth
- FirebaseCore
- FirebaseCoreDiagnostics
+ - FirebaseInstallations
+ - FirebaseInstanceID
+ - FirebaseMessaging
- FirebaseStorage
- GoogleDataTransport
- GoogleUtilities
- GTMSessionFetcher
- nanopb
- PromisesObjC
+ - Protobuf
- SDWebImage
- Sentry
- SwiftyGif
@@ -174,6 +208,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
+ firebase_messaging:
+ :path: ".symlinks/plugins/firebase_messaging/ios"
firebase_storage:
:path: ".symlinks/plugins/firebase_storage/ios"
FirebaseFirestore:
@@ -193,8 +229,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/quick_actions/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
- share:
- :path: ".symlinks/plugins/share/ios"
+ share_plus:
+ :path: ".symlinks/plugins/share_plus/ios"
url_launcher:
:path: ".symlinks/plugins/url_launcher/ios"
@@ -211,11 +247,15 @@ SPEC CHECKSUMS:
Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5
firebase_auth: d5159db3873478d1ac839af7b10d2f831516136a
firebase_core: 5d6a02f3d85acd5f8321c2d6d62877626a670659
+ firebase_messaging: 738982b5b4110cdc7a72449544123f6fcdb65604
firebase_storage: a023e199edb807d8481c8aa722c8516f462ffab2
FirebaseAuth: c92d49ada7948d1a23466e3db17bc4c2039dddc3
FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd
FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1
FirebaseFirestore: 4e00bcfe64c4cb493124b87e249abe6c580403c7
+ FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2
+ FirebaseInstanceID: bd3ffc24367f901a43c063b36c640b345a4a5dd1
+ FirebaseMessaging: 5eca4ef173de76253352511aafef774caa1cba2a
FirebaseStorage: 15e0f15ef3c7fec3d1899d68623e47d4447066b4
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833
@@ -227,11 +267,12 @@ SPEC CHECKSUMS:
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6
PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f
+ Protobuf: 0cde852566359049847168e51bd1c690e0f70056
quick_actions: 6cb2390c4dab0e737c94573c27e18d9666710720
SDWebImage: 9169792e9eec3e45bba2a0c02f74bf8bd922d1ee
Sentry: 8ffcfe3416e0a58b90bb1ae319e8e8a13022a03e
sentry_flutter: e57e33f56d5791e4effa48e85e44505367686b27
- share: 0b2c3e82132f5888bccca3351c504d0003b3b410
+ share_plus: 2e6276f752c279a84cd68a9fc27dfbbe2a74b072
SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 4125f5f7..15fc88d9 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -173,6 +173,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
+ LastSwiftUpdateCheck = 1230;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
@@ -299,6 +300,7 @@
"${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
"${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
+ "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
@@ -309,7 +311,7 @@
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
"${BUILT_PRODUCTS_DIR}/quick_actions/quick_actions.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
- "${BUILT_PRODUCTS_DIR}/share/share.framework",
+ "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework",
);
name = "[CP] Embed Pods Frameworks";
@@ -320,6 +322,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.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",
@@ -330,7 +333,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/quick_actions.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework",
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 393dea53..e989fcee 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -35,12 +35,22 @@
LSSupportsOpeningDocumentsInPlace
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSLocationWhenInUseUsageDescription
+ 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
LunaSea would like to access your photo gallery for uploading images to LunaSea
- NSLocationWhenInUseUsageDescription
- LunaSea would like to access your location for determining local-remote host switching
+ UIBackgroundModes
+
+ fetch
+ remote-notification
+
UIFileSharingEnabled
UILaunchStoryboardName
@@ -62,10 +72,5 @@
UIViewControllerBasedStatusBarAppearance
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 0c67376e..903def2a 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -1,5 +1,8 @@
-
+
+ aps-environment
+ development
+
diff --git a/lib/core.dart b/lib/core.dart
index c51f311d..ab447915 100644
--- a/lib/core.dart
+++ b/lib/core.dart
@@ -1,4 +1,5 @@
export 'core/adapters.dart';
+export 'core/bottom_modal_sheet.dart';
export 'core/configuration.dart';
export 'core/constants.dart';
export 'core/database.dart';
diff --git a/lib/core/bottom_modal_sheet.dart b/lib/core/bottom_modal_sheet.dart
new file mode 100644
index 00000000..ed182c35
--- /dev/null
+++ b/lib/core/bottom_modal_sheet.dart
@@ -0,0 +1,77 @@
+import 'dart:convert';
+import 'package:flutter/material.dart';
+import 'package:lunasea/core.dart';
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
+
+class LunaBottomModalSheet {
+ /// Show a bottom modal sheet modal using the supplied builder to show the content.
+ Future showModal({
+ @required BuildContext context,
+ @required Widget Function(BuildContext) builder,
+ bool expand = true,
+ bool useRootNavigator = true,
+ }) async => showBarModalBottomSheet(
+ context: context,
+ useRootNavigator: useRootNavigator,
+ expand: expand,
+ backgroundColor: LunaDatabaseValue.THEME_AMOLED.data ? Colors.black : LunaColours.secondary,
+ shape: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data ? LSRoundedShapeWithBorder() : LSRoundedShape(),
+ builder: builder,
+ );
+
+ /// Show the changelog screen using a bottom modal sheet.
+ Future showChangelog(BuildContext context) async {
+ await DefaultAssetBundle.of(context).loadString("assets/changelog.json")
+ .then((data) {
+ Map changelog = json.decode(data);
+ LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN.put(changelog['build']);
+ showModal(
+ context: context,
+ builder: (context) => LSListView(
+ children: [
+ LSHeader(text: '${changelog['version']} (${changelog['build']})', subtitle: changelog['motd']),
+ if((changelog['new'] as List).length != 0) LSTableBlock(
+ title: 'New',
+ children: List.generate(
+ (changelog['new'] as List).length,
+ (index) => LSTableContent(
+ 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(
+ (changelog['tweaks'] as List).length,
+ (index) => LSTableContent(
+ 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(
+ (changelog['fixes'] as List).length,
+ (index) => LSTableContent(
+ title: changelog['fixes'][index]['module'],
+ body: (changelog['fixes'][index]['changes'] as List).fold('', (data, object) => data += '${Constants.TEXT_BULLET}\t$object\n').trim(),
+ ),
+ ),
+ ),
+ LSDivider(),
+ LSButton(
+ text: 'Full Changelog',
+ onTap: () async => Constants.URL_CHANGELOG.lunaOpenGenericLink(),
+ ),
+ ],
+ ),
+ );
+ })
+ .catchError((error, stack) {
+ LunaLogger().error('Failed to fetch changelog', error, stack);
+ showLunaErrorSnackBar(context: context, title: 'Failed to Fetch Changelog', error: error);
+ });
+ }
+}
diff --git a/lib/core/constants.dart b/lib/core/constants.dart
index cc4318a0..059b3401 100644
--- a/lib/core/constants.dart
+++ b/lib/core/constants.dart
@@ -50,15 +50,15 @@ class Constants {
static const NO_SERVICES_ENABLED = '<>';
- static const URL_DOCUMENTATION = 'https://docs.lunasea.app';
- static const URL_GITHUB = 'https://github.com/CometTools/LunaSea';
- static const URL_REDDIT = 'https://www.reddit.com/r/LunaSeaApp';
+ static const URL_DOCUMENTATION = 'https://www.lunasea.app/docs';
+ 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://feedback.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://discord.com/invite/8MH2N3h';
+ static const URL_DISCORD = 'https://www.lunasea.app/discord';
static const URL_SENTRY = 'https://sentry.io';
- static const URL_TESTFLIGHT = 'https://testflight.apple.com/join/WWXaybra';
+ static const URL_TESTFLIGHT = 'https://www.lunasea.app/testflight';
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/luna_database.dart b/lib/core/database/luna_database.dart
index f05ddad5..47adfe9e 100644
--- a/lib/core/database/luna_database.dart
+++ b/lib/core/database/luna_database.dart
@@ -31,7 +31,8 @@ class LunaDatabase extends LunaModuleDatabase {
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD:
case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI:
case LunaDatabaseValue.QUICK_ACTIONS_SEARCH:
- case LunaDatabaseValue.USE_24_HOUR_TIME: data[value.key] = value.data; break;
+ case LunaDatabaseValue.USE_24_HOUR_TIME:
+ case LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN: data[value.key] = value.data; break;
}
}
return data;
@@ -61,7 +62,8 @@ class LunaDatabase extends LunaModuleDatabase {
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD:
case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI:
case LunaDatabaseValue.QUICK_ACTIONS_SEARCH:
- case LunaDatabaseValue.USE_24_HOUR_TIME: value.put(config[key]); break;
+ case LunaDatabaseValue.USE_24_HOUR_TIME:
+ case LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN: value.put(config[key]); break;
}
}
}
@@ -86,6 +88,7 @@ class LunaDatabase extends LunaModuleDatabase {
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;
+ case 'LUNASEA_LATEST_CHANGELOG_BUILD_SHOWN': return LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN;
default: return null;
}
}
@@ -110,6 +113,7 @@ enum LunaDatabaseValue {
QUICK_ACTIONS_TAUTULLI,
QUICK_ACTIONS_SEARCH,
USE_24_HOUR_TIME,
+ LATEST_CHANGELOG_BUILD_SHOWN,
}
extension LunaDatabaseValueExtension on LunaDatabaseValue {
@@ -133,6 +137,7 @@ extension LunaDatabaseValueExtension on LunaDatabaseValue {
case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI: return 'LUNASEA_QUICK_ACTIONS_TAUTULLI';
case LunaDatabaseValue.QUICK_ACTIONS_SEARCH: return 'LUNASEA_QUICK_ACTIONS_SEARCH';
case LunaDatabaseValue.USE_24_HOUR_TIME: return 'LUNASEA_USE_24_HOUR_TIME';
+ case LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN: return 'LUNASEA_LATEST_CHANGELOG_BUILD_SHOWN';
}
throw Exception('key not found');
}
@@ -158,6 +163,7 @@ extension LunaDatabaseValueExtension on LunaDatabaseValue {
case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI: 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);
+ case LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN: return box.get(this.key, defaultValue: null);
}
throw Exception('data not found');
}
@@ -183,6 +189,7 @@ extension LunaDatabaseValueExtension on LunaDatabaseValue {
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;
+ case LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN: if(value.runtimeType == String) box.put(this.key, value); return;
}
LunaLogger().warning('LunaDatabaseValueExtension', 'put', 'Attempted to enter data for invalid LunaDatabaseValue: ${this?.toString() ?? 'null'}');
}
diff --git a/lib/core/extensions/iap_product_details.dart b/lib/core/extensions/iap_product_details.dart
index 003a32de..79be5fe3 100644
--- a/lib/core/extensions/iap_product_details.dart
+++ b/lib/core/extensions/iap_product_details.dart
@@ -22,7 +22,7 @@ extension ProductDetailsExtension on ProductDetails {
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.IAP_UPGRADE_PRO: return 'Upgrade to LunaSea Pro';
+ case LunaInAppPurchases.IAP_UPGRADE_PRO: return 'Upgrade to ${Constants.APPLICATION_NAME} Pro';
default: return 'Unknown In-App Purchase';
}
}
diff --git a/lib/core/filesystem.dart b/lib/core/filesystem.dart
index d0b07aa2..2360e139 100644
--- a/lib/core/filesystem.dart
+++ b/lib/core/filesystem.dart
@@ -1,5 +1,5 @@
import 'dart:io';
-import 'package:share/share.dart';
+import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
class LunaFileSystem {
diff --git a/lib/core/firebase.dart b/lib/core/firebase.dart
index a33f33c4..2b0b0877 100644
--- a/lib/core/firebase.dart
+++ b/lib/core/firebase.dart
@@ -1,13 +1,6 @@
-import 'package:firebase_core/firebase_core.dart';
-
-class LunaFirebase {
- /// Return an instance of [FirebaseApp].
- ///
- /// Throws an error if [LunaFirebase.initialize] has not been called.
- FirebaseApp get app => Firebase.app();
-
- /// Initialize Firebase: Calls [Firebase.initializeApp()].
- ///
- /// This must be called before anything accesses Firebase services, or an exception will be thrown.
- static Future initialize({ String name }) async => Firebase.initializeApp();
-}
+export 'firebase/auth.dart';
+export 'firebase/core.dart';
+export 'firebase/firestore.dart';
+export 'firebase/messaging.dart';
+export 'firebase/storage.dart';
+export 'firebase/types.dart';
diff --git a/lib/core/firebase/auth.dart b/lib/core/firebase/auth.dart
new file mode 100644
index 00000000..75be141e
--- /dev/null
+++ b/lib/core/firebase/auth.dart
@@ -0,0 +1,64 @@
+import 'package:firebase_auth/firebase_auth.dart';
+import 'package:lunasea/core.dart';
+
+class LunaFirebaseAuth {
+ /// Return an instance of [FirebaseAuth].
+ ///
+ /// Throws an error if [LunaFirebase.initialize] has not been called.
+ static FirebaseAuth get instance => FirebaseAuth.instance;
+
+ /// Returns the [User] object.
+ ///
+ /// If the user is not signed in, returns null.
+ User get user => instance.currentUser;
+
+ /// Returns if a user is signed in.
+ bool get isSignedIn => instance.currentUser != null;
+
+ /// Returns the user's UID.
+ ///
+ /// If the user is not signed in, returns null.
+ String get uid => instance.currentUser?.uid;
+
+ /// Return the user's email.
+ ///
+ /// If the user is not signed in, returns null.
+ String get email => instance.currentUser?.email;
+
+ /// Sign out a logged in user.
+ ///
+ /// If the user is not signed in, this is a non-op.
+ Future signOut() async => instance.signOut();
+
+ /// Register a new user using Firebase Authentication.
+ ///
+ /// Returns a [LunaFirebaseAuthResponse] which contains the state (true on success, false on failure), the [User] object, and [FirebaseAuthException] if applicable.
+ Future registerUser(String email, String password) async {
+ try {
+ assert(email != null && password != null);
+ UserCredential _user = await instance.createUserWithEmailAndPassword(email: email, password: password);
+ return LunaFirebaseAuthResponse(state: true, user: _user.user, error: null);
+ } on FirebaseAuthException catch (error) {
+ return LunaFirebaseAuthResponse(state: false, user: null, error: error);
+ } catch (error, stack) {
+ LunaLogger().error("Failed to register user: $email", error, stack);
+ return LunaFirebaseAuthResponse(state: false, user: null, error: null);
+ }
+ }
+
+ /// Sign in a user using Firebase Authentication.
+ ///
+ /// Returns a [LunaFirebaseAuthResponse] which contains the state (true on success, false on failure), the [User] object, and [FirebaseAuthException] if applicable.
+ Future signInUser(String email, String password) async {
+ try {
+ assert(email != null && password != null);
+ UserCredential _user = await instance.signInWithEmailAndPassword(email: email, password: password);
+ return LunaFirebaseAuthResponse(state: true, user: _user.user, error: null);
+ } on FirebaseAuthException catch (error) {
+ return LunaFirebaseAuthResponse(state: false, user: null, error: error);
+ } catch (error, stack) {
+ LunaLogger().error("Failed to login user: $email", error, stack);
+ return LunaFirebaseAuthResponse(state: false, user: null, error: null);
+ }
+ }
+}
diff --git a/lib/core/firebase/core.dart b/lib/core/firebase/core.dart
new file mode 100644
index 00000000..7f6ddc99
--- /dev/null
+++ b/lib/core/firebase/core.dart
@@ -0,0 +1,13 @@
+import 'package:firebase_core/firebase_core.dart';
+
+class LunaFirebase {
+ /// Return an instance of [FirebaseApp].
+ ///
+ /// Throws an error if [LunaFirebase.initialize] has not been called.
+ static FirebaseApp get instance => Firebase.app();
+
+ /// 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();
+}
diff --git a/lib/core/firebase/firestore.dart b/lib/core/firebase/firestore.dart
new file mode 100644
index 00000000..f8505816
--- /dev/null
+++ b/lib/core/firebase/firestore.dart
@@ -0,0 +1,52 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:lunasea/core.dart';
+
+class LunaFirebaseFirestore {
+ /// Returns an instance of [FirebaseFirestore].
+ ///
+ /// Throws an errof if [LunaFirebase.initialize] has not been called.
+ static FirebaseFirestore get instance => FirebaseFirestore.instance;
+
+ /// Add a backup entry to Firestore. Returns true if successful, and false on any error.
+ ///
+ /// If the user is not signed in, returns false.
+ Future addBackupEntry(String id, int timestamp, { String title = '', String description = '' }) async {
+ if(LunaFirebaseAuth().user == null) return false;
+ try {
+ LunaFirebaseBackupDocument entry = LunaFirebaseBackupDocument(id: id, title: title, description: description, timestamp: timestamp);
+ instance.doc('users/${LunaFirebaseAuth().uid}/backups/$id').set(entry.toJSON());
+ return true;
+ } catch (error, stack) {
+ LunaLogger().error('Failed to add backup entry', error, stack);
+ return false;
+ }
+ }
+
+ /// Delete a backup entry from Firestore. Returns true if successful, and false on any error.
+ ///
+ /// If the user is not signed in, returns false.
+ Future deleteBackupEntry(String id) async {
+ if(LunaFirebaseAuth().user == null) return false;
+ try {
+ await instance.doc('users/${LunaFirebaseAuth().uid}/backups/$id').delete();
+ return true;
+ } catch (error, stack) {
+ LunaLogger().error('Failed to delete backup entry', error, stack);
+ return false;
+ }
+ }
+
+ /// Returns a list of all backups available for this account.
+ ///
+ /// If the user is not signed in, returns an empty list.
+ Future> getBackupEntries() async {
+ if(LunaFirebaseAuth().user == null) return [];
+ try {
+ QuerySnapshot snapshot = await instance.collection('users/${LunaFirebaseAuth().uid}/backups').orderBy('timestamp', descending: true).get();
+ return snapshot.docs.map((document) => LunaFirebaseBackupDocument.fromQueryDocumentSnapshot(document)).toList();
+ } catch (error, stack) {
+ LunaLogger().error('Failed to get backup list', error, stack);
+ return [];
+ }
+ }
+}
diff --git a/lib/core/firebase/messaging.dart b/lib/core/firebase/messaging.dart
new file mode 100644
index 00000000..d59dd9a6
--- /dev/null
+++ b/lib/core/firebase/messaging.dart
@@ -0,0 +1,24 @@
+import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:lunasea/core.dart';
+
+class LunaFirebaseMessaging {
+ /// Returns an instance of [FirebaseMessaging].
+ ///
+ /// Throws an error if [LunaFirebase.initialize] has not been called.
+ static FirebaseMessaging get instance => FirebaseMessaging.instance;
+
+ /// Request for permission to send a user notifications.
+ ///
+ /// Returns true if permissions are allowed at either a full or provisional level.
+ /// Returns false if permissions are denied or not determined.
+ Future requestNotificationPermissions() async {
+ NotificationSettings settings = await instance.requestPermission();
+ switch(settings.authorizationStatus) {
+ case AuthorizationStatus.authorized:
+ case AuthorizationStatus.provisional: return true;
+ case AuthorizationStatus.denied:
+ case AuthorizationStatus.notDetermined:
+ default: return false;
+ }
+ }
+}
diff --git a/lib/core/firebase/storage.dart b/lib/core/firebase/storage.dart
new file mode 100644
index 00000000..4d5d6561
--- /dev/null
+++ b/lib/core/firebase/storage.dart
@@ -0,0 +1,59 @@
+import 'dart:typed_data';
+import 'package:firebase_storage/firebase_storage.dart';
+import 'package:lunasea/core.dart';
+
+class LunaFirebaseStorage {
+ static const String _BACKUP_BUCKET = 'backup.lunasea.app';
+
+ /// Returns an instance of [FirebaseStorage] for the default bucket.
+ ///
+ /// Throws an error if [LunaFirebase.initialize] has not been called.
+ static FirebaseStorage get instanceDefault => FirebaseStorage.instance;
+
+ /// Returns an instance of [FirebaseStorage] for the backup bucket.
+ ///
+ /// Throws an error if [LunaFirebase.initialize] has not been called.
+ static FirebaseStorage get instanceBackup => FirebaseStorage.instanceFor(bucket: _BACKUP_BUCKET);
+
+ /// Upload a backup configuration to Firebase storage.
+ ///
+ /// If the user is not signed in, returns null.
+ Future uploadBackup(String data, String id) async {
+ if(!LunaFirebaseAuth().isSignedIn) return false;
+ try {
+ await instanceBackup.ref('${LunaFirebaseAuth().uid}/$id.lunasea').putString(data);
+ return true;
+ } catch (error, stack) {
+ LunaLogger().error('Failed to backup to Firebase', error, stack);
+ return false;
+ }
+ }
+
+ /// Delete a backup configuration from Firebase storage.
+ ///
+ /// If the user is not signed in, returns null.
+ Future deleteBackup(String id) async {
+ if(!LunaFirebaseAuth().isSignedIn) return false;
+ try {
+ await instanceBackup.ref('${LunaFirebaseAuth().uid}/$id.lunasea').delete();
+ return true;
+ } catch (error, stack) {
+ LunaLogger().error('Failed to delete backup from Firebase', error, stack);
+ return false;
+ }
+ }
+
+ /// Download a backup configuration from Firebase storage.
+ ///
+ /// If the user is not signed in, returns null.
+ Future downloadBackup(String id) async {
+ if(!LunaFirebaseAuth().isSignedIn) return null;
+ try {
+ Uint8List data = await instanceBackup.ref('${LunaFirebaseAuth().uid}/$id.lunasea').getData();
+ return String.fromCharCodes(data);
+ } catch (error, stack) {
+ LunaLogger().error('Failed to download backup from Firebase', error, stack);
+ return null;
+ }
+ }
+}
diff --git a/lib/core/firebase/types.dart b/lib/core/firebase/types.dart
new file mode 100644
index 00000000..44fd47e0
--- /dev/null
+++ b/lib/core/firebase/types.dart
@@ -0,0 +1,2 @@
+export 'types/auth_response.dart';
+export 'types/backup_document.dart';
diff --git a/lib/core/firebase/types/auth_response.dart b/lib/core/firebase/types/auth_response.dart
new file mode 100644
index 00000000..795f663f
--- /dev/null
+++ b/lib/core/firebase/types/auth_response.dart
@@ -0,0 +1,14 @@
+import 'package:firebase_auth/firebase_auth.dart';
+import 'package:flutter/foundation.dart';
+
+class LunaFirebaseAuthResponse {
+ final bool state;
+ final User user;
+ final FirebaseAuthException error;
+
+ LunaFirebaseAuthResponse({
+ @required this.state,
+ @required this.user,
+ @required this.error,
+ });
+}
diff --git a/lib/core/firebase/types/backup_document.dart b/lib/core/firebase/types/backup_document.dart
new file mode 100644
index 00000000..f365d21a
--- /dev/null
+++ b/lib/core/firebase/types/backup_document.dart
@@ -0,0 +1,31 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:flutter/foundation.dart';
+
+class LunaFirebaseBackupDocument {
+ final String id;
+ final int timestamp;
+ final String title;
+ final String description;
+
+
+ LunaFirebaseBackupDocument({
+ @required this.id,
+ @required this.timestamp,
+ @required this.title,
+ @required this.description,
+ });
+
+ factory LunaFirebaseBackupDocument.fromQueryDocumentSnapshot(QueryDocumentSnapshot document) => LunaFirebaseBackupDocument(
+ id: document.data()['id'],
+ timestamp: document.data()['timestamp'],
+ title: document.data()['title'],
+ description: document.data()['description'],
+ );
+
+ Map toJSON() => {
+ 'id': id,
+ 'title': title,
+ 'timestamp': timestamp,
+ 'description': description,
+ };
+}
diff --git a/lib/core/luna_ui.dart b/lib/core/luna_ui.dart
index bef7af10..3e24186c 100644
--- a/lib/core/luna_ui.dart
+++ b/lib/core/luna_ui.dart
@@ -1,6 +1,7 @@
export 'luna_ui/appbar.dart';
export 'luna_ui/decoration.dart';
export 'luna_ui/snackbar.dart';
+export 'luna_ui/text.dart';
class LunaUI {
static const double FONT_SIZE_APPBAR = 18.0;
diff --git a/lib/core/luna_ui/appbar/appbar.dart b/lib/core/luna_ui/appbar/appbar.dart
index 0cbba0ac..a5c75ef1 100644
--- a/lib/core/luna_ui/appbar/appbar.dart
+++ b/lib/core/luna_ui/appbar/appbar.dart
@@ -2,6 +2,7 @@ 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,
diff --git a/lib/core/luna_ui/text.dart b/lib/core/luna_ui/text.dart
new file mode 100644
index 00000000..121fc8a7
--- /dev/null
+++ b/lib/core/luna_ui/text.dart
@@ -0,0 +1,46 @@
+import 'package:flutter/material.dart';
+import 'package:lunasea/core.dart';
+
+class LunaText extends Text {
+ /// Create a new [Text] widget.
+ LunaText({
+ @required String text,
+ Key key,
+ int maxLines,
+ TextOverflow overflow,
+ bool softWrap,
+ TextStyle style,
+ TextAlign textAlign,
+ }) : super(
+ text,
+ key: key,
+ maxLines: maxLines,
+ overflow: overflow,
+ softWrap: softWrap,
+ style: style,
+ textAlign: textAlign,
+ );
+
+ /// Create a [Text] widget with the styling pre-assigned to be a LunaSea title.
+ factory LunaText.title({
+ @required String text,
+ Key key,
+ int maxLines = 1,
+ bool softWrap = false,
+ TextAlign textAlign = TextAlign.start,
+ Color color = Colors.white,
+ bool darken = false,
+ }) => LunaText(
+ text: text,
+ key: key,
+ maxLines: maxLines,
+ overflow: TextOverflow.fade,
+ softWrap: softWrap,
+ textAlign: textAlign,
+ style: TextStyle(
+ color: darken ? color.withOpacity(0.3) : color,
+ fontWeight: FontWeight.bold,
+ fontSize: Constants.UI_FONT_SIZE_TITLE,
+ ),
+ );
+}
diff --git a/lib/core/ui/button/button.dart b/lib/core/ui/button/button.dart
index 0568e200..dfc93e77 100644
--- a/lib/core/ui/button/button.dart
+++ b/lib/core/ui/button/button.dart
@@ -7,6 +7,7 @@ class LSButton extends StatelessWidget {
final Color backgroundColor;
final Color textColor;
final bool reducedMargin;
+ final bool isLoading;
LSButton({
@required this.text,
@@ -14,6 +15,7 @@ class LSButton extends StatelessWidget {
this.backgroundColor = const Color(LunaColours.ACCENT_COLOR),
this.textColor = Colors.white,
this.reducedMargin = false,
+ this.isLoading = false,
});
@override
@@ -23,18 +25,23 @@ class LSButton extends StatelessWidget {
child: Card(
child: InkWell(
child: ListTile(
- title: Text(
- text,
- style: TextStyle(
- color: textColor,
- fontWeight: FontWeight.bold,
- fontSize: Constants.UI_FONT_SIZE_STICKYHEADER,
+ title: isLoading
+ ? LSLoader(
+ color: Colors.white,
+ size: 20.0,
+ )
+ : Text(
+ text,
+ style: TextStyle(
+ color: textColor,
+ fontWeight: FontWeight.bold,
+ fontSize: Constants.UI_FONT_SIZE_STICKYHEADER,
+ ),
+ textAlign: TextAlign.center,
),
- textAlign: TextAlign.center,
- ),
),
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
- onTap: onTap,
+ onTap: isLoading ? null : onTap,
),
color: backgroundColor,
margin: reducedMargin
diff --git a/lib/core/ui/drawer/drawer_header.dart b/lib/core/ui/drawer/drawer_header.dart
index f7003061..4e14b748 100644
--- a/lib/core/ui/drawer/drawer_header.dart
+++ b/lib/core/ui/drawer/drawer_header.dart
@@ -3,7 +3,10 @@ import 'package:lunasea/core.dart';
// ignore: non_constant_identifier_names
Widget LSDrawerHeader() => UserAccountsDrawerHeader(
- accountName: LSTitle(text: Constants.APPLICATION_NAME),
+ accountName: StreamBuilder(
+ stream: LunaFirebaseAuth.instance.authStateChanges(),
+ builder: (context, snapshot) => LSTitle(text: LunaFirebaseAuth().email ?? Constants.APPLICATION_NAME),
+ ),
accountEmail: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, lunaBox, widget) => ValueListenableBuilder(
diff --git a/lib/core/ui/text/text_subtitle.dart b/lib/core/ui/text/text_subtitle.dart
index 32ce3378..91cf3baa 100644
--- a/lib/core/ui/text/text_subtitle.dart
+++ b/lib/core/ui/text/text_subtitle.dart
@@ -6,20 +6,24 @@ class LSSubtitle extends StatelessWidget {
final int maxLines;
final bool darken;
final bool centerText;
+ final bool softWrap;
+ final TextOverflow overflow;
LSSubtitle({
@required this.text,
this.maxLines = 1,
this.darken = false,
this.centerText = false,
+ this.softWrap = false,
+ this.overflow = TextOverflow.fade,
});
@override
Widget build(BuildContext context) => Text(
text,
- overflow: TextOverflow.fade,
- softWrap: false,
+ overflow: overflow,
+ softWrap: softWrap,
maxLines: maxLines,
textAlign: centerText
? TextAlign.center
diff --git a/lib/main.dart b/lib/main.dart
index def5c412..bea6344f 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,5 +1,7 @@
import 'dart:async';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
@@ -55,6 +57,25 @@ class LunaBIOS extends StatefulWidget {
}
class _State extends State {
+ @override
+ void initState() {
+ super.initState();
+ SchedulerBinding.instance.addPostFrameCallback((_) => _boot());
+ }
+
+ @override
+ void dispose() {
+ Database.deinitialize()
+ .whenComplete(() => LunaInAppPurchases.deinitialize())
+ .whenComplete(() => super.dispose());
+ }
+
+ /// Runs the first-step boot sequence that is required for widgets
+ Future _boot() async {
+ LunaFirebaseMessaging().requestNotificationPermissions();
+ if(kDebugMode) print(await LunaFirebaseMessaging.instance.getToken());
+ }
+
@override
Widget build(BuildContext context) => LunaState.providers(
child: ValueListenableBuilder(
@@ -74,11 +95,4 @@ class _State extends State {
}
),
);
-
- @override
- void dispose() {
- Database.deinitialize()
- .whenComplete(() => LunaInAppPurchases.deinitialize())
- .whenComplete(() => super.dispose());
- }
}
diff --git a/lib/modules/home/routes/home.dart b/lib/modules/home/routes/home.dart
index 748a0807..36453eff 100644
--- a/lib/modules/home/routes/home.dart
+++ b/lib/modules/home/routes/home.dart
@@ -1,6 +1,7 @@
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 {
@@ -20,6 +21,12 @@ class _State extends State {
void initState() {
super.initState();
LunaQuickActions.initialize(context);
+ // Check and show changelog if needed
+ PackageInfo.fromPlatform()
+ .then((package) {
+ if(LunaDatabaseValue.LATEST_CHANGELOG_BUILD_SHOWN.data != package.buildNumber) LunaBottomModalSheet().showChangelog(context);
+ })
+ .catchError((error, stack) => LunaLogger().error('Failed to fetch package info', error, stack));
}
@override
diff --git a/lib/modules/search/core/constants.dart b/lib/modules/search/core/constants.dart
index 19191c01..9bed9c21 100644
--- a/lib/modules/search/core/constants.dart
+++ b/lib/modules/search/core/constants.dart
@@ -10,7 +10,7 @@ class SearchConstants {
name: 'Search',
description: 'Search Newznab Indexers',
settingsDescription: 'Configure Search',
- helpMessage: 'LunaSea currently supports all indexers that support the newznab protocol, including NZBHydra2.',
+ helpMessage: '${Constants.APPLICATION_NAME} currently supports all indexers that support the newznab protocol, including NZBHydra2.',
icon: Icons.search,
route: '/search',
color: Color(LunaColours.ACCENT_COLOR),
diff --git a/lib/modules/settings/core/dialogs.dart b/lib/modules/settings/core/dialogs.dart
index 3021408f..8f1ef9a7 100644
--- a/lib/modules/settings/core/dialogs.dart
+++ b/lib/modules/settings/core/dialogs.dart
@@ -6,6 +6,63 @@ import 'package:wake_on_lan/wake_on_lan.dart';
class SettingsDialogs {
SettingsDialogs._();
+ static Future> confirmSignOut(BuildContext context) async {
+ bool _flag = false;
+
+ void _setValues(bool flag) {
+ _flag = flag;
+ Navigator.of(context, rootNavigator: true).pop();
+ }
+
+ await LSDialog.dialog(
+ context: context,
+ title: 'Sign Out',
+ buttons: [
+ LSDialog.button(
+ text: 'Sign Out',
+ textColor: Colors.red,
+ onPressed: () => _setValues(true),
+ ),
+ ],
+ content: [
+ LSDialog.textContent(
+ text: 'Are you sure you want to sign out of your ${Constants.APPLICATION_NAME} account?'
+ ),
+ ],
+ contentPadding: LSDialog.textDialogContentPadding(),
+ );
+ return [_flag];
+ }
+
+ static Future> getBackupFromCloud(BuildContext context, List backups) async {
+ bool _flag = false;
+ LunaFirebaseBackupDocument _document;
+
+ void _setValues(bool flag, LunaFirebaseBackupDocument document) {
+ _flag = flag;
+ _document = document;
+ Navigator.of(context, rootNavigator: true).pop();
+ }
+
+ await LSDialog.dialog(
+ context: context,
+ title: 'Backup List',
+ content: backups.length > 0
+ ? List.generate(
+ backups.length,
+ (index) => LSDialog.tile(
+ icon: Icons.file_copy,
+ iconColor: LunaColours.list(index),
+ text: backups[index].title.toString(),
+ onTap: () => _setValues(true, backups[index]),
+ ),
+ )
+ : [LSDialog.textContent(text: 'No Backups Found')],
+ contentPadding: backups.length > 0 ? LSDialog.listDialogContentPadding() : LSDialog.textDialogContentPadding(),
+ );
+ return [_flag, _document];
+ }
+
static Future> editHost(BuildContext context, String title, { String prefill = '' }) async {
bool _flag = false;
final _formKey = GlobalKey();
@@ -665,7 +722,8 @@ class SettingsDialogs {
],
content: [
LSDialog.textContent(text: 'Are you sure you want to clear your configuration?\n'),
- LSDialog.textContent(text: 'You will be starting from a clean slate, please ensure you backup your current configuration first!'),
+ LSDialog.textContent(text: 'You will be starting from a clean slate, please ensure you backup your current configuration first!\n'),
+ LSDialog.textContent(text: 'If you have signed into a LunaSea account, you will need to log back in after clearing the configuration.'),
],
contentPadding: LSDialog.textDialogContentPadding(),
);
@@ -738,7 +796,7 @@ class SettingsDialogs {
content: [
LSDialog.richText(
children: [
- LSDialog.textSpanContent(text: '•\tAll backups are encrypted before being exported to the filesystem\n'),
+ LSDialog.textSpanContent(text: '•\tAll backups are encrypted before being exported\n'),
LSDialog.textSpanContent(text: '•\tThe encryption key must be at least 8 characters'),
],
),
diff --git a/lib/modules/settings/core/router.dart b/lib/modules/settings/core/router.dart
index 573a05ed..a6f1af4a 100644
--- a/lib/modules/settings/core/router.dart
+++ b/lib/modules/settings/core/router.dart
@@ -6,6 +6,8 @@ class SettingsRouter extends LunaModuleRouter {
@override
void defineAllRoutes(FluroRouter router) {
SettingsHomeRouter().defineRoute(router);
+ // Account
+ SettingsAccountRouter().defineRoute(router);
// Configuration
SettingsConfigurationRouter().defineRoute(router);
SettingsConfigurationAppearanceRouter().defineRoute(router);
diff --git a/lib/modules/settings/modules.dart b/lib/modules/settings/modules.dart
index 7e5377a0..d49510b3 100644
--- a/lib/modules/settings/modules.dart
+++ b/lib/modules/settings/modules.dart
@@ -1,3 +1,4 @@
+export 'modules/account.dart';
export 'modules/donations.dart';
export 'modules/donations_thankyou.dart';
export 'modules/configuration.dart';
diff --git a/lib/modules/settings/modules/account.dart b/lib/modules/settings/modules/account.dart
new file mode 100644
index 00000000..29d4ed3d
--- /dev/null
+++ b/lib/modules/settings/modules/account.dart
@@ -0,0 +1,2 @@
+export 'account/route.dart';
+export 'account/widgets.dart';
diff --git a/lib/modules/settings/modules/account/route.dart b/lib/modules/settings/modules/account/route.dart
new file mode 100644
index 00000000..50f5a513
--- /dev/null
+++ b/lib/modules/settings/modules/account/route.dart
@@ -0,0 +1,50 @@
+import 'package:fluro/fluro.dart';
+import 'package:flutter/material.dart';
+import 'package:lunasea/core.dart';
+import 'package:lunasea/modules/settings.dart';
+
+class SettingsAccountRouter extends LunaPageRouter {
+ SettingsAccountRouter() : super('/settings/account');
+
+ @override
+ void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _SettingsAccountRoute());
+}
+
+class _SettingsAccountRoute extends StatefulWidget {
+ @override
+ State<_SettingsAccountRoute> createState() => _State();
+}
+
+class _State extends State<_SettingsAccountRoute> {
+ final GlobalKey _scaffoldKey = GlobalKey();
+
+ @override
+ Widget build(BuildContext context) => Scaffold(
+ key: _scaffoldKey,
+ appBar: _appBar,
+ body: _body,
+ );
+
+ Widget get _appBar => LunaAppBar(
+ context: context,
+ title: 'Account',
+ actions: [_helpMessageButton],
+ );
+
+ Widget get _helpMessageButton => LSIconButton(
+ icon: Icons.help_outline,
+ onPressed: () async => SettingsDialogs.helpMessage(
+ context,
+ title: 'Account',
+ message: '${Constants.APPLICATION_NAME} offers a free account to backup your configuration to the cloud, with additional features coming in the future!',
+ ),
+ );
+
+ Widget get _body => StreamBuilder(
+ stream: LunaFirebaseAuth.instance.authStateChanges(),
+ builder: (context, snapshot) {
+ if(snapshot.data != null) return SettingsAccountSignedInBody();
+ return SettingsAccountSignedOutBody();
+ },
+ );
+}
diff --git a/lib/modules/settings/modules/account/widgets.dart b/lib/modules/settings/modules/account/widgets.dart
new file mode 100644
index 00000000..5052bbd2
--- /dev/null
+++ b/lib/modules/settings/modules/account/widgets.dart
@@ -0,0 +1,5 @@
+export 'widgets/backup_tile.dart';
+export 'widgets/delete_tile.dart';
+export 'widgets/restore_tile.dart';
+export 'widgets/signed_in_body.dart';
+export 'widgets/signed_out_body.dart';
diff --git a/lib/modules/settings/modules/account/widgets/backup_tile.dart b/lib/modules/settings/modules/account/widgets/backup_tile.dart
new file mode 100644
index 00000000..c0ec3ccb
--- /dev/null
+++ b/lib/modules/settings/modules/account/widgets/backup_tile.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:lunasea/core.dart';
+import 'package:lunasea/modules/settings.dart';
+
+class SettingsAccountBackupConfigurationTile extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) => LSCardTile(
+ title: LSTitle(text: 'Backup to Cloud'),
+ subtitle: LSSubtitle(text: 'Backup Configuration Data'),
+ trailing: LSIconButton(icon: Icons.cloud_upload_rounded),
+ onTap: () async => _backup(context),
+ );
+
+ Future _backup(BuildContext context) async {
+ try {
+ List _values = await SettingsDialogs.backupConfiguration(context);
+ if(_values[0]) {
+ String decrypted = LunaConfiguration().export();
+ String encrypted = LunaEncryption().encrypt(_values[1], decrypted);
+ int timestamp = DateTime.now().millisecondsSinceEpoch;
+ String title = DateFormat('MMMM dd, yyyy\nhh:mm:ss a').format(DateTime.now());
+ String id = LunaUUID().uuid;
+ if(encrypted != LunaEncryption.ENCRYPTION_FAILURE) LunaFirebaseFirestore().addBackupEntry(id, timestamp, title: title)
+ .then((_) => LunaFirebaseStorage().uploadBackup(encrypted, id))
+ .then((_) => showLunaSuccessSnackBar(context: context, title: 'Successfully Backed Up', message: title.replaceAll('\n', ' ${Constants.TEXT_EMDASH} ')))
+ .catchError((error, stack) {
+ LunaLogger().error('Failed to backup configuration to the cloud', error, stack);
+ showLunaErrorSnackBar(context: context, title: 'Failed to Backup', error: error);
+ });
+ }
+ } catch (error, stack) {
+ LunaLogger().error('Backup Failed', error, stack);
+ showLunaErrorSnackBar(
+ context: context,
+ title: 'Failed to Backup',
+ error: error,
+ );
+ }
+ }
+}
diff --git a/lib/modules/settings/modules/account/widgets/delete_tile.dart b/lib/modules/settings/modules/account/widgets/delete_tile.dart
new file mode 100644
index 00000000..992c35ba
--- /dev/null
+++ b/lib/modules/settings/modules/account/widgets/delete_tile.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+import 'package:lunasea/core.dart';
+import 'package:lunasea/modules/settings.dart';
+//import 'package:lunasea/modules/settings.dart';
+
+class SettingsAccountDeleteConfigurationTile extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) => LSCardTile(
+ title: LSTitle(text: 'Delete Cloud Backup'),
+ subtitle: LSSubtitle(text: 'Delete a Configuration File'),
+ trailing: LSIconButton(icon: Icons.cloud_off_rounded),
+ onTap: () async => _delete(context),
+ );
+
+ Future _delete(BuildContext context) async {
+ try {
+ List documents = await LunaFirebaseFirestore().getBackupEntries();
+ List values = await SettingsDialogs.getBackupFromCloud(context, documents);
+ if(values[0]) {
+ LunaFirebaseBackupDocument selected = values[1];
+ LunaFirebaseFirestore().deleteBackupEntry(selected.id)
+ .then((_) => LunaFirebaseStorage().deleteBackup(selected.id))
+ .then((_) => showLunaSuccessSnackBar(
+ context: context,
+ title: 'Deleted Backup',
+ message: selected.title.replaceAll('\n', ' ${Constants.TEXT_EMDASH} '),
+ ))
+ .catchError((error, stack) {
+ LunaLogger().error('Failed to delete backup entry', error, stack);
+ showLunaErrorSnackBar(context: context, title: 'Failed to Delete Backup', error: error);
+ });
+ }
+ } catch (error, stack) {
+ LunaLogger().error('Deletion Failed', error, stack);
+ showLunaErrorSnackBar(
+ context: context,
+ title: 'Failed to Delete',
+ error: error,
+ );
+ }
+ }
+}
diff --git a/lib/modules/settings/modules/account/widgets/restore_tile.dart b/lib/modules/settings/modules/account/widgets/restore_tile.dart
new file mode 100644
index 00000000..6eb99a5a
--- /dev/null
+++ b/lib/modules/settings/modules/account/widgets/restore_tile.dart
@@ -0,0 +1,49 @@
+import 'package:flutter/material.dart';
+import 'package:lunasea/core.dart';
+import 'package:lunasea/modules/settings.dart';
+//import 'package:lunasea/modules/settings.dart';
+
+class SettingsAccountRestoreConfigurationTile extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) => LSCardTile(
+ title: LSTitle(text: 'Restore from Cloud'),
+ subtitle: LSSubtitle(text: 'Restore Configuration Data'),
+ trailing: LSIconButton(icon: Icons.cloud_download_rounded),
+ onTap: () async => _restore(context),
+ );
+
+ Future _restore(BuildContext context) async {
+ try {
+ List documents = await LunaFirebaseFirestore().getBackupEntries();
+ List values = await SettingsDialogs.getBackupFromCloud(context, documents);
+ if(values[0]) {
+ LunaFirebaseBackupDocument document = values[1];
+ String encrypted = await LunaFirebaseStorage().downloadBackup(document.id);
+ List key = await SettingsDialogs.enterEncryptionKey(context);
+ if(key[0]) {
+ String decrypted = LunaEncryption().decrypt(key[1], encrypted);
+ if(decrypted != LunaEncryption.ENCRYPTION_FAILURE) {
+ LunaConfiguration().import(context, decrypted).then((_) => showLunaSuccessSnackBar(
+ context: context,
+ title: 'Restored',
+ message: 'Your configuration has been restored',
+ ));
+ } else {
+ showLunaErrorSnackBar(
+ context: context,
+ title: 'Failed to Restore',
+ message: 'An incorrect encryption key was supplied',
+ );
+ }
+ }
+ }
+ } catch (error, stack) {
+ LunaLogger().error('Restore Failed', error, stack);
+ showLunaErrorSnackBar(
+ context: context,
+ title: 'Failed to Restore',
+ error: error,
+ );
+ }
+ }
+}
diff --git a/lib/modules/settings/modules/account/widgets/signed_in_body.dart b/lib/modules/settings/modules/account/widgets/signed_in_body.dart
new file mode 100644
index 00000000..e47fc746
--- /dev/null
+++ b/lib/modules/settings/modules/account/widgets/signed_in_body.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:lunasea/core.dart';
+import 'package:lunasea/modules/settings.dart';
+
+class SettingsAccountSignedInBody extends StatefulWidget {
+ @override
+ State createState() => _State();
+}
+
+class _State extends State {
+ @override
+ Widget build(BuildContext context) => LSListView(children: [
+ SettingsAccountBackupConfigurationTile(),
+ SettingsAccountRestoreConfigurationTile(),
+ SettingsAccountDeleteConfigurationTile(),
+ LSDivider(),
+ _signOutTile,
+ ]);
+
+ Widget get _signOutTile => LSButton(
+ text: 'Sign Out',
+ backgroundColor: LunaColours.red,
+ onTap: () async {
+ List values = await SettingsDialogs.confirmSignOut(context);
+ if(values[0]) LunaFirebaseAuth().signOut()
+ .then((_) => showLunaSuccessSnackBar(context: context, title: 'Signed Out', message: 'Signed out of your ${Constants.APPLICATION_NAME} account'))
+ .catchError((error, stack) {
+ LunaLogger().error('Failed to sign out', error, stack);
+ showLunaErrorSnackBar(context: context, title: 'Failed to Sign Out', error: error);
+ });
+ },
+ );
+}
diff --git a/lib/modules/settings/modules/account/widgets/signed_out_body.dart b/lib/modules/settings/modules/account/widgets/signed_out_body.dart
new file mode 100644
index 00000000..ccf150dc
--- /dev/null
+++ b/lib/modules/settings/modules/account/widgets/signed_out_body.dart
@@ -0,0 +1,160 @@
+import 'package:flutter/material.dart';
+import 'package:lunasea/core.dart';
+
+class SettingsAccountSignedOutBody extends StatefulWidget {
+ @override
+ State createState() => _State();
+}
+
+class _State extends State {
+ final GlobalKey _formKey = GlobalKey();
+ final TextEditingController _emailController = TextEditingController();
+ final TextEditingController _passwordController = TextEditingController();
+ LunaLoadingState _state = LunaLoadingState.INACTIVE;
+
+ @override
+ Widget build(BuildContext context) => Form(
+ key: _formKey,
+ child: SafeArea(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Padding(
+ child: Center(
+ child: Image.asset(
+ 'assets/branding/splash.png',
+ width: 200.0,
+ ),
+ ),
+ padding: EdgeInsets.symmetric(vertical: 40.0),
+ ),
+ LSCard(
+ child: Padding(
+ child: Column(
+ children: [
+ Padding(
+ child: TextFormField(
+ controller: _emailController,
+ decoration: InputDecoration(
+ hintText: 'Email',
+ labelStyle: TextStyle(
+ color: Colors.white54,
+ decoration: TextDecoration.none,
+ fontSize: Constants.UI_FONT_SIZE_STICKYHEADER,
+ ),
+ focusedBorder: UnderlineInputBorder(
+ borderSide: BorderSide(color: LunaColours.accent),
+ ),
+ enabledBorder: UnderlineInputBorder(
+ borderSide: BorderSide(color: LunaColours.accent.withOpacity(0.3)),
+ ),
+ ),
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: Constants.UI_FONT_SIZE_STICKYHEADER,
+ ),
+ autocorrect: false,
+ validator: (value) {
+ if(value.isEmpty) return 'Email Required';
+ return null;
+ },
+ keyboardType: TextInputType.emailAddress,
+ ),
+ padding: EdgeInsets.only(bottom: 16.0),
+ ),
+ Padding(
+ child: TextFormField(
+ controller: _passwordController,
+ decoration: InputDecoration(
+ hintText: 'Password',
+ labelStyle: TextStyle(
+ color: Colors.white54,
+ decoration: TextDecoration.none,
+ fontSize: Constants.UI_FONT_SIZE_STICKYHEADER,
+ ),
+ focusedBorder: UnderlineInputBorder(
+ borderSide: BorderSide(color: LunaColours.accent),
+ ),
+ enabledBorder: UnderlineInputBorder(
+ borderSide: BorderSide(color: LunaColours.accent.withOpacity(0.3)),
+ ),
+ ),
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: Constants.UI_FONT_SIZE_STICKYHEADER,
+ ),
+ validator: (value) {
+ if(value.isEmpty) return 'Password Required';
+ return null;
+ },
+ autocorrect: false,
+ obscureText: true,
+ ),
+ padding: EdgeInsets.only(bottom: 8.0),
+ ),
+ ],
+ ),
+ padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
+ ),
+ ),
+ LSContainerRow(
+ children: [
+ Expanded(
+ child: LSButton(
+ text: 'Register',
+ backgroundColor: LunaColours.blueGrey,
+ onTap: _register,
+ reducedMargin: true,
+ isLoading: _state == LunaLoadingState.ACTIVE,
+ ),
+ ),
+ Expanded(
+ child: LSButton(
+ text: 'Sign In',
+ onTap: _signIn,
+ reducedMargin: true,
+ isLoading: _state == LunaLoadingState.ACTIVE,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+
+ Future _register() async {
+ if(mounted) setState(() => _state = LunaLoadingState.ACTIVE);
+ if(_formKey.currentState.validate()) LunaFirebaseAuth().registerUser(_emailController.text, _passwordController.text)
+ .then((response) {
+ if(response.state) {
+ showLunaSuccessSnackBar(context: context, title: 'Successfully Registered', message: response.user.email);
+ } else {
+ showLunaErrorSnackBar(context: context, title: 'Failed to Register', message: response.error?.message ?? 'Unknown Error');
+ if(mounted) setState(() => _state = LunaLoadingState.INACTIVE);
+ }
+ })
+ .catchError((error, stack) {
+ showLunaErrorSnackBar(context: context, title: 'Failed to Register User', error: error);
+ if(mounted) setState(() => _state = LunaLoadingState.INACTIVE);
+ });
+ }
+
+ Future _signIn() async {
+ if(mounted) setState(() => _state = LunaLoadingState.ACTIVE);
+ if(_formKey.currentState.validate()) LunaFirebaseAuth().signInUser(_emailController.text, _passwordController.text)
+ .then((response) {
+ if(response.state) {
+ showLunaSuccessSnackBar(context: context, title: 'Successfully Signed In', message: response.user.email);
+ } else {
+ showLunaErrorSnackBar(context: context, title: 'Failed to Sign In', message: response.error?.message ?? 'Unknown Error');
+ if(mounted) setState(() => _state = LunaLoadingState.INACTIVE);
+ }
+ })
+ .catchError((error, stack) {
+ showLunaErrorSnackBar(context: context, title: 'Failed to Register User', error: error);
+ if(mounted) setState(() => _state = LunaLoadingState.INACTIVE);
+ });
+ }
+}
\ No newline at end of file
diff --git a/lib/modules/settings/modules/configuration_lidarr.dart b/lib/modules/settings/modules/configuration_lidarr.dart
index 853b4d3f..adf67ffd 100644
--- a/lib/modules/settings/modules/configuration_lidarr.dart
+++ b/lib/modules/settings/modules/configuration_lidarr.dart
@@ -130,7 +130,7 @@ class _State extends State<_SettingsConfigurationLidarrRoute> {
.then((_) => showLunaSuccessSnackBar(
context: context,
title: 'Connected Successfully',
- message: 'Lidarr is ready to use with LunaSea',
+ message: 'Lidarr is ready to use with ${Constants.APPLICATION_NAME}',
))
.catchError((error, stack) {
LunaLogger().error('Connection Test Failed', error, stack);
diff --git a/lib/modules/settings/modules/configuration_nzbget.dart b/lib/modules/settings/modules/configuration_nzbget.dart
index c64b4e7b..2586e6d9 100644
--- a/lib/modules/settings/modules/configuration_nzbget.dart
+++ b/lib/modules/settings/modules/configuration_nzbget.dart
@@ -154,7 +154,7 @@ class _State extends State<_SettingsConfigurationNZBGetRoute> {
.then((_) => showLunaSuccessSnackBar(
context: context,
title: 'Connected Successfully',
- message: 'NZBGet is ready to use with LunaSea',
+ message: 'NZBGet is ready to use with ${Constants.APPLICATION_NAME}',
))
.catchError((error, stack) {
LunaLogger().error('Connection Test Failed', error, stack);
diff --git a/lib/modules/settings/modules/configuration_radarr.dart b/lib/modules/settings/modules/configuration_radarr.dart
index 0f860b0c..18b36f56 100644
--- a/lib/modules/settings/modules/configuration_radarr.dart
+++ b/lib/modules/settings/modules/configuration_radarr.dart
@@ -130,7 +130,7 @@ class _State extends State<_SettingsConfigurationRadarrRoute> {
.then((_) => showLunaSuccessSnackBar(
context: context,
title: 'Connected Successfully',
- message: 'Radarr is ready to use with LunaSea',
+ message: 'Radarr is ready to use with ${Constants.APPLICATION_NAME}',
))
.catchError((error, stack) {
LunaLogger().error('Connection Test Failed', error, stack);
diff --git a/lib/modules/settings/modules/configuration_sabnzbd.dart b/lib/modules/settings/modules/configuration_sabnzbd.dart
index 2650802c..d4c62b92 100644
--- a/lib/modules/settings/modules/configuration_sabnzbd.dart
+++ b/lib/modules/settings/modules/configuration_sabnzbd.dart
@@ -127,7 +127,7 @@ class _State extends State<_SettingsConfigurationSABnzbdRoute> {
showLunaSuccessSnackBar(
context: context,
title: 'Connected Successfully',
- message: 'SABnzbd is ready to use with LunaSea',
+ message: 'SABnzbd is ready to use with ${Constants.APPLICATION_NAME}',
);
} else {
showLunaErrorSnackBar(
diff --git a/lib/modules/settings/modules/configuration_sonarr.dart b/lib/modules/settings/modules/configuration_sonarr.dart
index 5b437e38..506b45f2 100644
--- a/lib/modules/settings/modules/configuration_sonarr.dart
+++ b/lib/modules/settings/modules/configuration_sonarr.dart
@@ -145,7 +145,7 @@ class _State extends State<_SettingsConfigurationSonarrRoute> {
.then((_) => showLunaSuccessSnackBar(
context: context,
title: 'Connected Successfully',
- message: 'Sonarr is ready to use with LunaSea',
+ message: 'Sonarr is ready to use with ${Constants.APPLICATION_NAME}',
)).catchError((error, trace) {
LunaLogger().error('Connection Test Failed', error, trace);
showLunaErrorSnackBar(
diff --git a/lib/modules/settings/modules/configuration_tautulli.dart b/lib/modules/settings/modules/configuration_tautulli.dart
index 8b9fe1ea..950b522e 100644
--- a/lib/modules/settings/modules/configuration_tautulli.dart
+++ b/lib/modules/settings/modules/configuration_tautulli.dart
@@ -146,7 +146,7 @@ class _State extends State<_SettingsConfigurationTautulliRoute> {
showLunaSuccessSnackBar(
context: context,
title: 'Connected Successfully',
- message: 'Tautulli is ready to use with LunaSea',
+ message: 'Tautulli is ready to use with ${Constants.APPLICATION_NAME}',
);
}).catchError((error, trace) {
LunaLogger().error('Connection Test Failed', error, trace);
diff --git a/lib/modules/settings/modules/donations_thankyou.dart b/lib/modules/settings/modules/donations_thankyou.dart
index f1ee380f..7e85f805 100644
--- a/lib/modules/settings/modules/donations_thankyou.dart
+++ b/lib/modules/settings/modules/donations_thankyou.dart
@@ -35,7 +35,7 @@ class _State extends State<_SettingsDonationsThankYouRoute> {
text: 'Thank You',
subtitle: [
"Thank you for supporting the open-source community!",
- "Donations are never expected or assumed, so thank you for giving back and helping LunaSea remain free, forever!",
+ "Donations are never expected or assumed, so thank you for giving back and helping ${Constants.APPLICATION_NAME} remain free, forever!",
].join("\n\n"),
),
],
diff --git a/lib/modules/settings/modules/resources.dart b/lib/modules/settings/modules/resources.dart
index 31d24b85..14fcc77c 100644
--- a/lib/modules/settings/modules/resources.dart
+++ b/lib/modules/settings/modules/resources.dart
@@ -70,7 +70,7 @@ class _State extends State<_SettingsResourcesRoute> {
),
LSCardTile(
title: LSTitle(text: 'Website'),
- subtitle: LSSubtitle(text: 'Visit LunaSea\'s Website'),
+ subtitle: LSSubtitle(text: 'Visit ${Constants.APPLICATION_NAME}\'s Website'),
trailing: LSIconButton(icon: CustomIcons.home),
onTap: () async => await Constants.URL_WEBSITE.lunaOpenGenericLink(),
),
diff --git a/lib/modules/settings/modules/settings.dart b/lib/modules/settings/modules/settings.dart
index e6d30899..7bcc6f88 100644
--- a/lib/modules/settings/modules/settings.dart
+++ b/lib/modules/settings/modules/settings.dart
@@ -48,15 +48,15 @@ class _State extends State<_SettingsHomeRoute> {
Widget get _body => LSListView(
children: [
- // LSCardTile(
- // title: LSTitle(text: 'Account'),
- // subtitle: LSSubtitle(text: 'Your LunaSea Account'),
- // trailing: LSIconButton(icon: Icons.account_circle),
- // //onTap: () async => SettingsConfigurationRouter().navigateTo(context),
- // ),
+ LSCardTile(
+ title: LSTitle(text: 'Account'),
+ subtitle: LSSubtitle(text: 'Your ${Constants.APPLICATION_NAME} Account'),
+ trailing: LSIconButton(icon: Icons.account_circle),
+ onTap: () async => SettingsAccountRouter().navigateTo(context),
+ ),
LSCardTile(
title: LSTitle(text: 'Configuration'),
- subtitle: LSSubtitle(text: 'Configure & Setup LunaSea'),
+ subtitle: LSSubtitle(text: 'Configure & Setup ${Constants.APPLICATION_NAME}'),
trailing: LSIconButton(icon: Icons.device_hub),
onTap: () async => SettingsConfigurationRouter().navigateTo(context),
),
diff --git a/lib/modules/settings/modules/system/route.dart b/lib/modules/settings/modules/system/route.dart
index 1a872168..9f20b6ca 100644
--- a/lib/modules/settings/modules/system/route.dart
+++ b/lib/modules/settings/modules/system/route.dart
@@ -67,7 +67,7 @@ class _State extends State<_SettingsSystemRoute> with AutomaticKeepAliveClientMi
),
subtitle: LSSubtitle(text: 'View Recent Changes'),
trailing: LSIconButton(icon: Icons.system_update),
- onTap: () async => await Constants.URL_CHANGELOG.lunaOpenGenericLink(),
+ onTap: () async => LunaBottomModalSheet().showChangelog(context),
),
);
@@ -93,6 +93,7 @@ class _State extends State<_SettingsSystemRoute> with AutomaticKeepAliveClientMi
List values = await SettingsDialogs.clearConfiguration(context);
if(values[0]) {
Database.setDefaults();
+ LunaFirebaseAuth().signOut();
LunaState.reset(context);
showLunaSuccessSnackBar(
context: context,
diff --git a/lib/modules/settings/modules/system/widgets/backup_tile.dart b/lib/modules/settings/modules/system/widgets/backup_tile.dart
index 96a280d7..b0a34002 100644
--- a/lib/modules/settings/modules/system/widgets/backup_tile.dart
+++ b/lib/modules/settings/modules/system/widgets/backup_tile.dart
@@ -6,7 +6,7 @@ import 'package:lunasea/modules/settings.dart';
class SettingsSystemBackupRestoreBackupTile extends StatelessWidget {
@override
Widget build(BuildContext context) => LSCardTile(
- title: LSTitle(text: 'Backup'),
+ title: LSTitle(text: 'Backup to Device'),
subtitle: LSSubtitle(text: 'Backup Configuration Data'),
trailing: LSIconButton(icon: Icons.upload_rounded),
onTap: () async => _backup(context),
@@ -25,7 +25,7 @@ class SettingsSystemBackupRestoreBackupTile extends StatelessWidget {
LunaLogger().error('Backup Failed', error, stack);
showLunaErrorSnackBar(
context: context,
- title: 'Back Up Failed',
+ title: 'Failed to Backup',
error: error,
);
}
diff --git a/lib/modules/settings/modules/system/widgets/restore_tile.dart b/lib/modules/settings/modules/system/widgets/restore_tile.dart
index 86e69c89..b0795e89 100644
--- a/lib/modules/settings/modules/system/widgets/restore_tile.dart
+++ b/lib/modules/settings/modules/system/widgets/restore_tile.dart
@@ -6,7 +6,7 @@ import 'package:lunasea/modules/settings.dart';
class SettingsSystemBackupRestoreRestoreTile extends StatelessWidget {
@override
Widget build(BuildContext context) => LSCardTile(
- title: LSTitle(text: 'Restore'),
+ title: LSTitle(text: 'Restore from Device'),
subtitle: LSSubtitle(text: 'Restore Configuration Data'),
trailing: LSIconButton(icon: Icons.download_rounded),
onTap: () async => _restore(context),
diff --git a/lib/modules/settings/modules/system_logs.dart b/lib/modules/settings/modules/system_logs.dart
index 29a7b046..b3f3fd0e 100644
--- a/lib/modules/settings/modules/system_logs.dart
+++ b/lib/modules/settings/modules/system_logs.dart
@@ -3,7 +3,7 @@ import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/settings.dart';
-import 'package:share/share.dart';
+import 'package:share_plus/share_plus.dart';
class SettingsSystemLogsRouter extends LunaPageRouter {
SettingsSystemLogsRouter() : super('/settings/logs');
diff --git a/pubspec.lock b/pubspec.lock
index d757a686..515d1c29 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -316,6 +316,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1+1"
+ firebase_messaging:
+ dependency: "direct main"
+ description:
+ name: firebase_messaging
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "8.0.0-dev.11"
+ firebase_messaging_platform_interface:
+ dependency: transitive
+ description:
+ name: firebase_messaging_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0-dev.7"
+ firebase_messaging_web:
+ dependency: transitive
+ description:
+ name: firebase_messaging_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.0-dev.2"
firebase_storage:
dependency: "direct main"
description:
@@ -350,7 +371,7 @@ packages:
name: fl_chart
url: "https://pub.dartlang.org"
source: hosted
- version: "0.12.1"
+ version: "0.12.2"
flash:
dependency: "direct main"
description:
@@ -550,6 +571,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7"
+ modal_bottom_sheet:
+ dependency: "direct main"
+ description:
+ name: modal_bottom_sheet
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0+1"
nested:
dependency: transitive
description:
@@ -774,13 +802,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
- share:
- dependency: "direct main"
+ service_worker:
+ dependency: transitive
description:
- name: share
+ name: service_worker
url: "https://pub.dartlang.org"
source: hosted
- version: "0.6.5+4"
+ version: "0.2.4"
+ share_plus:
+ dependency: "direct main"
+ description:
+ name: share_plus
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ share_plus_linux:
+ dependency: transitive
+ description:
+ name: share_plus_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ share_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: share_plus_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ share_plus_web:
+ dependency: transitive
+ description:
+ name: share_plus_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.0"
+ share_plus_windows:
+ dependency: transitive
+ description:
+ name: share_plus_windows
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.0"
shelf:
dependency: transitive
description:
@@ -801,7 +864,7 @@ packages:
name: simple_gesture_detector
url: "https://pub.dartlang.org"
source: hosted
- version: "0.1.5"
+ version: "0.1.6"
sky_engine:
dependency: transitive
description: flutter
@@ -813,7 +876,7 @@ packages:
name: sonarr
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.1"
+ version: "1.1.0+1"
source_gen:
dependency: transitive
description:
@@ -876,7 +939,7 @@ packages:
name: tautulli
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0+2"
+ version: "1.2.0"
term_glyph:
dependency: transitive
description:
@@ -981,7 +1044,7 @@ packages:
name: wake_on_lan
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.1+4"
+ version: "1.1.1+5"
watcher:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index d4dcf659..481376cd 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: lunasea
description: Self-Hosted Controller
-version: 4.2.0+40200003
+version: 4.2.0+40200004
publish_to: 'none'
environment:
sdk: ">=2.7.0 <3.0.0"
@@ -15,6 +15,7 @@ dependencies:
file_picker: ^2.1.4
firebase_core: ^0.5.3
firebase_auth: ^0.18.4+1
+ firebase_messaging: ^8.0.0-dev.11
firebase_storage: ^5.2.0
flash: ^1.3.1
fluro: ^1.7.8
@@ -22,13 +23,14 @@ dependencies:
sdk: flutter
flutter_spinkit: ^4.1.2+1
flutter_sticky_header: ^0.5.0
- fl_chart: ^0.12.1
+ fl_chart: ^0.12.2
f_logs: ^1.3.0-alpha-02
google_nav_bar: ^3.1.0
hive: ^1.4.4+1
hive_flutter: ^0.3.1
in_app_purchase: ^0.3.4+16
intl: ^0.16.1
+ modal_bottom_sheet: ^1.0.0+1
package_info: ^0.4.3+2
path_provider: ^1.6.24
percent_indicator: ^2.1.9
@@ -36,7 +38,7 @@ dependencies:
provider: ^4.3.2+3
quick_actions: ^0.4.0+10
sentry_flutter: ^4.0.1
- share: ^0.6.5+4
+ share_plus: ^1.2.0
stack_trace: ^1.9.6
table_calendar: ^2.3.3
tuple: ^1.0.3
@@ -44,9 +46,9 @@ dependencies:
uuid: ^2.2.2
xml_parser: ^0.1.2
# Comet.Tools Packages
- sonarr: ^1.0.1
- tautulli: ^1.1.0+2
- wake_on_lan: ^1.1.1+4
+ sonarr: ^1.1.0+1
+ tautulli: ^1.2.0
+ wake_on_lan: ^1.1.1+5
dev_dependencies:
analyzer: ^0.41.1
@@ -59,6 +61,8 @@ dev_dependencies:
flutter:
uses-material-design: true
assets:
+ # Changelog
+ - assets/changelog.json
# Colors
- assets/images/colors/
# Automation
diff --git a/scripts/build_aab b/scripts/build_aab
index 611d51d7..0d7bf421 100755
--- a/scripts/build_aab
+++ b/scripts/build_aab
@@ -8,6 +8,6 @@ flutter clean
flutter build appbundle
# Copy APKs to root of project
mkdir -p output
-cp build/app/outputs/bundle/release/app-release.aab ./LunaSea-release.aab
+cp build/app/outputs/bundle/release/app-release.aab output/LunaSea-release.aab
# Remove build files
rm -rf build
diff --git a/scripts/build_ipa b/scripts/build_ipa
index 868d5efa..e189009d 100755
--- a/scripts/build_ipa
+++ b/scripts/build_ipa
@@ -13,7 +13,7 @@ xcodebuild -exportArchive -archivePath build/LunaSea.xcarchive -exportOptionsPli
# Copy IPA to root of project
cd ..
mkdir -p output
-cp ios/build/LunaSea.app/LunaSea.ipa output/LunaSea.ipa
+cp ios/build/LunaSea.app/LunaSea.ipa output/LunaSea-arm64-release.ipa
# Remove build files
rm -rf build
rm -rf build ios/build