[share_plus] iOS 12 and lower crashes at launch due to missing LinkPresentation.framework (#442)

* chore(packages): Added pedantic package to enforce linting and styling
* feat(lunasea): Enforce effective dart styling
* Comment and format colours class
* Localize additional classes in Dashboard, cleanup additional code
* chore(lunasea): Major refactor by running dartfmt on the entire codebase, added widget return for page routers
* Additional work on conforming to effective dart style
* fix(share_plus): Roll back share_plus package to 2.0.0 to support iOS 12 or lower
* chore(lunasea): Fix remaining enabled analysis warnings
This commit is contained in:
Jagandeep Brar
2021-04-24 04:26:39 -05:00
committed by GitHub
parent 0424f54430
commit a1d68cd234
696 changed files with 51603 additions and 44176 deletions

16
analysis_options.yaml Normal file
View File

@@ -0,0 +1,16 @@
include: package:effective_dart/analysis_options.yaml
linter:
rules:
avoid_function_literals_in_foreach_calls: false
avoid_positional_boolean_parameters: false
avoid_types_on_closure_parameters: false
constant_identifier_names: false
lines_longer_than_80_chars: false
omit_local_variable_types: false
one_member_abstracts: false
# Will be disabled and required once other linting rules have been fixed
prefer_relative_imports: false
public_member_api_docs: false
curly_braces_in_flow_control_structures: false
unnecessary_this: false
avoid_catches_without_on_clauses: false

View File

@@ -9,9 +9,11 @@
"lunasea.GoToSettings": "Go to Settings",
"lunasea.ModuleIsNotEnabled": "{} Is Not Enabled",
"lunasea.NoModulesEnabled": "No Modules Enabled",
"lunasea.Page": "Page",
"lunasea.Refresh": "Refresh",
"lunasea.ReturnToDashboard": "Return to Dashboard",
"lunasea.SearchTextBar": "Search…",
"lunasea.Set": "Set",
"lunasea.Settings": "Settings",
"lunasea.TryAgain": "Try Again",
"lunasea.Unknown": "Unknown",
@@ -134,6 +136,24 @@
"search.SentTo": "Sent to {}",
"search.Size": "Size",
"search.Subcategories": "Subcategories",
"dashboard.Calendar": "Calendar",
"dashboard.Friday": "Friday",
"dashboard.FutureDays": "Future Days",
"dashboard.FutureDaysDescription": "Set the number of days in the future to fetch calendar entries for.",
"dashboard.MinimumOfOneDay": "Minimum of 1 Day",
"dashboard.Monday": "Monday",
"dashboard.NoNewContent": "No New Content",
"dashboard.OneMonth": "One Month",
"dashboard.OneWeek": "One Week",
"dashboard.PastDays": "Past Days",
"dashboard.PastDaysDescription": "Set the number of days in the past to fetch calendar entries for.",
"dashboard.Saturday": "Saturday",
"dashboard.Schedule": "Schedule",
"dashboard.Sunday": "Sunday",
"dashboard.Thursday": "Thursday",
"dashboard.Tuesday": "Tuesday",
"dashboard.TwoWeeks": "Two Weeks",
"dashboard.Wednesday": "Wednesday",
"tautulli.Activity": "Activity",
"tautulli.ActivityDetails": "Activity Details",
"tautulli.Audio": "Audio",

View File

@@ -3,9 +3,6 @@ PODS:
- Firebase/Firestore (= 7.4.0)
- firebase_core
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- Reachability
- DKImagePickerController/Core (4.3.2):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
@@ -187,8 +184,6 @@ 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_plus (0.4.5):
- Flutter
- path_provider (0.0.1):
@@ -196,7 +191,6 @@ PODS:
- PromisesObjC (1.2.12)
- quick_actions (0.0.1):
- Flutter
- Reachability (3.2)
- SDWebImage (5.11.0):
- SDWebImage/Core (= 5.11.0)
- SDWebImage/Core (5.11.0)
@@ -210,7 +204,6 @@ PODS:
DEPENDENCIES:
- cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Firebase/Messaging
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
@@ -223,7 +216,6 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- GoogleUtilities
- in_app_purchase (from `.symlinks/plugins/in_app_purchase/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- quick_actions (from `.symlinks/plugins/quick_actions/ios`)
@@ -251,15 +243,12 @@ SPEC REPOS:
- GTMSessionFetcher
- nanopb
- PromisesObjC
- Reachability
- SDWebImage
- SwiftyGif
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_analytics:
@@ -281,8 +270,6 @@ 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_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider:
@@ -303,7 +290,6 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
cloud_firestore: aed5075be38f024e58c5ce57f17f047b4668eaea
connectivity_plus: f5e5e0b894ec1187bfab9816d6c2f034da8d46da
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
@@ -331,14 +317,12 @@ SPEC CHECKSUMS:
GTMSessionFetcher: b3503b20a988c4e20cc189aa798fd18220133f52
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
nanopb: 59221d7f958fb711001e6a449489542d92ae113e
network_info_plus: b78876159360f5580608c2cea620d6ceffabd0ad
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97
quick_actions: 6cb2390c4dab0e737c94573c27e18d9666710720
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SDWebImage: 7acbb57630ac7db4a495547fb73916ff3e432f6b
share_plus: 1210d16d59b24d9fe73c2cbec481758952348b81
share_plus: 2e6276f752c279a84cd68a9fc27dfbbe2a74b072
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
SwiftyGif: 5d4af95df24caf1c570dbbcb32a3b8a0763bc6d7
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef

View File

@@ -8,9 +8,10 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
196951923D925D6A6F5C2A20 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A16350510A2E575392903ACD /* Pods_Runner.framework */; };
2B459739A964984F348F3731 /* Pods_ImageNotification.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0546E93FC4041C243CF8BB9C /* Pods_ImageNotification.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7A7972099246A5B492349195 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFA6080C320C00539C8CABAC /* Pods_Runner.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -18,7 +19,6 @@
9C5DBBF2257EB50400836D8D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9C5DBBF1257EB50400836D8D /* GoogleService-Info.plist */; };
9C7092B325E77E470056AE0B /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 9C7092B225E77E470056AE0B /* NotificationService.m */; };
9C7092B725E77E470056AE0B /* ImageNotification.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 9C7092AF25E77E470056AE0B /* ImageNotification.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
AA16AC97D489C54994D590FB /* Pods_ImageNotification.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 122B41E3781B6CAD85602DC1 /* Pods_ImageNotification.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -56,17 +56,16 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
050183A13FDEAAEC33600FBB /* Pods-ImageNotification.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.release.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.release.xcconfig"; sourceTree = "<group>"; };
122B41E3781B6CAD85602DC1 /* Pods_ImageNotification.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImageNotification.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0546E93FC4041C243CF8BB9C /* Pods_ImageNotification.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImageNotification.framework; sourceTree = BUILT_PRODUCTS_DIR; };
136F0380FFE663A7FF458282 /* Pods-ImageNotification.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.debug.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
32D42D42F9870A13C12BDA3A /* Pods-ImageNotification.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.debug.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.debug.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
482EE6D4EA146B54B20CB63C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
5AD69099DEA56F21470CCECC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
56C69F7B708ED113CDD2719E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7DDB2B949F17E3468D9ADD9C /* Pods-ImageNotification.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.release.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -83,9 +82,10 @@
9C7092B125E77E470056AE0B /* NotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationService.h; sourceTree = "<group>"; };
9C7092B225E77E470056AE0B /* NotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationService.m; sourceTree = "<group>"; };
9C7092B425E77E470056AE0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A16350510A2E575392903ACD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EDA4787A07E08E808303EABD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
FF7233BB1EEE96DC5F4BE495 /* Pods-ImageNotification.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.profile.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.profile.xcconfig"; sourceTree = "<group>"; };
AFA6080C320C00539C8CABAC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C7722DD560755C5A2C7CEF27 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
CABB913576E0CCFF93AF88B1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D6723E8BA54F949F70EDB89B /* Pods-ImageNotification.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageNotification.profile.xcconfig"; path = "Target Support Files/Pods-ImageNotification/Pods-ImageNotification.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -94,7 +94,7 @@
buildActionMask = 2147483647;
files = (
9C2991242453FE3100B93AB2 /* StoreKit.framework in Frameworks */,
196951923D925D6A6F5C2A20 /* Pods_Runner.framework in Frameworks */,
7A7972099246A5B492349195 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -102,7 +102,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AA16AC97D489C54994D590FB /* Pods_ImageNotification.framework in Frameworks */,
2B459739A964984F348F3731 /* Pods_ImageNotification.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -182,8 +182,8 @@
isa = PBXGroup;
children = (
9C2991232453FE3100B93AB2 /* StoreKit.framework */,
122B41E3781B6CAD85602DC1 /* Pods_ImageNotification.framework */,
A16350510A2E575392903ACD /* Pods_Runner.framework */,
0546E93FC4041C243CF8BB9C /* Pods_ImageNotification.framework */,
AFA6080C320C00539C8CABAC /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -191,12 +191,12 @@
F5D05D221249C73674C5DB0D /* Pods */ = {
isa = PBXGroup;
children = (
32D42D42F9870A13C12BDA3A /* Pods-ImageNotification.debug.xcconfig */,
050183A13FDEAAEC33600FBB /* Pods-ImageNotification.release.xcconfig */,
FF7233BB1EEE96DC5F4BE495 /* Pods-ImageNotification.profile.xcconfig */,
EDA4787A07E08E808303EABD /* Pods-Runner.debug.xcconfig */,
482EE6D4EA146B54B20CB63C /* Pods-Runner.release.xcconfig */,
5AD69099DEA56F21470CCECC /* Pods-Runner.profile.xcconfig */,
136F0380FFE663A7FF458282 /* Pods-ImageNotification.debug.xcconfig */,
7DDB2B949F17E3468D9ADD9C /* Pods-ImageNotification.release.xcconfig */,
D6723E8BA54F949F70EDB89B /* Pods-ImageNotification.profile.xcconfig */,
C7722DD560755C5A2C7CEF27 /* Pods-Runner.debug.xcconfig */,
CABB913576E0CCFF93AF88B1 /* Pods-Runner.release.xcconfig */,
56C69F7B708ED113CDD2719E /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -208,7 +208,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
CD860C5112864ACD549EE475 /* [CP] Check Pods Manifest.lock */,
401DBDF2E3C6C15BD1D59A9D /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
@@ -217,8 +217,8 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
9C7092B825E77E470056AE0B /* Embed App Extensions */,
9CF1F8FD25F2A02100D93ED1 /* Firebase Crashlytics */,
0C573E9546C3B55D92E84C84 /* [CP] Embed Pods Frameworks */,
DC42D81E9E2BA9408535A47D /* [CP] Copy Pods Resources */,
6B2FE6A3E51E24C6C31E3021 /* [CP] Embed Pods Frameworks */,
79C480EBAC36173E632B4F90 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -234,7 +234,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 9C7092BC25E77E470056AE0B /* Build configuration list for PBXNativeTarget "ImageNotification" */;
buildPhases = (
5CC95EE6151AEC6B5D5F0A9D /* [CP] Check Pods Manifest.lock */,
588DF2A615F0453A18143E7A /* [CP] Check Pods Manifest.lock */,
9C7092AB25E77E470056AE0B /* Sources */,
9C7092AC25E77E470056AE0B /* Frameworks */,
9C7092AD25E77E470056AE0B /* Resources */,
@@ -313,7 +313,65 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0C573E9546C3B55D92E84C84 /* [CP] Embed Pods Frameworks */ = {
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
401DBDF2E3C6C15BD1D59A9D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
588DF2A615F0453A18143E7A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ImageNotification-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
6B2FE6A3E51E24C6C31E3021 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -334,14 +392,11 @@
"${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}/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_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
"${BUILT_PRODUCTS_DIR}/quick_actions/quick_actions.framework",
@@ -365,14 +420,11 @@
"${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}/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_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/quick_actions.framework",
@@ -385,40 +437,22 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
79C480EBAC36173E632B4F90 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
"${PODS_ROOT}/FirebaseFirestore/FirebaseFirestore/Resources/gRPCCertificates-Cpp.bundle",
);
name = "Thin Binary";
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates-Cpp.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
5CC95EE6151AEC6B5D5F0A9D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ImageNotification-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
@@ -455,46 +489,6 @@
shellPath = /bin/sh;
shellScript = "${PODS_ROOT}/FirebaseCrashlytics/run\n";
};
CD860C5112864ACD549EE475 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DC42D81E9E2BA9408535A47D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
"${PODS_ROOT}/FirebaseFirestore/FirebaseFirestore/Resources/gRPCCertificates-Cpp.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates-Cpp.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

@@ -1,3 +1,15 @@
// Packages
export 'package:easy_localization/easy_localization.dart';
export 'package:flash/flash.dart';
export 'package:fluro/fluro.dart';
export 'package:hive/hive.dart';
export 'package:hive_flutter/hive_flutter.dart';
export 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
export 'package:pedantic/pedantic.dart';
export 'package:provider/provider.dart';
export 'package:supercharged/supercharged.dart';
export 'package:tuple/tuple.dart';
// Local
export 'core/database.dart';
export 'core/dialogs.dart';
export 'core/extensions.dart';
@@ -11,13 +23,4 @@ export 'core/state.dart';
export 'core/system.dart';
export 'core/ui.dart';
export 'core/utilities.dart';
export 'core/widgets.dart';
export 'package:easy_localization/easy_localization.dart';
export 'package:flash/flash.dart';
export 'package:fluro/fluro.dart';
export 'package:hive/hive.dart';
export 'package:hive_flutter/hive_flutter.dart';
export 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
export 'package:provider/provider.dart';
export 'package:supercharged/supercharged.dart';
export 'package:tuple/tuple.dart';
export 'core/widgets.dart';

View File

@@ -1,36 +1,48 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/core/database/database.dart';
import 'package:lunasea/core/utilities/logger.dart';
enum AlertsDatabaseValue {
CHANGELOG,
CHANGELOG,
}
extension AlertsDatabaseValueExtension on AlertsDatabaseValue {
String get key {
switch(this) {
case AlertsDatabaseValue.CHANGELOG: return 'ALERTS_CHANGELOG';
}
throw Exception('key not found');
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');
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'}');
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(
ValueListenableBuilder listen({
@required Widget Function(BuildContext, dynamic, Widget) builder,
}) =>
ValueListenableBuilder(
valueListenable: Database.alertsBox.listenable(keys: [this.key]),
builder: builder,
);
}
);
}

View File

@@ -1,77 +1,84 @@
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:lunasea/core.dart';
export 'package:hive_flutter/hive_flutter.dart';
class Database {
static const String _DATABASE_PATH = 'database';
static const String _DATABASE_PATH = 'database';
/// Initialize the database.
///
/// - Initializes Hive database path
/// - Register all adapters
/// - Open all boxes
/// - Set default database state of necessary
Future<void> initialize() async {
await Hive.initFlutter(_DATABASE_PATH);
_registerAdapters();
await _openBoxes();
if(profilesBox.keys.length == 0) setDefaults(clearEverything: true);
/// Initialize the database.
///
/// - Initializes Hive database path
/// - Register all adapters
/// - Open all boxes
/// - Set default database state of necessary
Future<void> initialize() async {
await Hive.initFlutter(_DATABASE_PATH);
_registerAdapters();
await _openBoxes();
if (profilesBox.keys?.isEmpty ?? true) setDefaults(clearEverything: true);
}
/// Deinitialize the database by closing all open hive boxes.
Future<void> deinitialize() async => await Hive.close();
/// Registers all necessary object adapters for Hive.
void _registerAdapters() {
LunaDatabase().registerAdapters();
LunaModule.values.forEach((module) {
if (module.database != null) module.database.registerAdapters();
});
}
/// Open all Hive boxes for reading.
Future<void> _openBoxes() async {
await Hive.openBox('alerts');
await Hive.openBox<IndexerHiveObject>('indexers');
await Hive.openBox<LunaLogHiveObject>('logs');
await Hive.openBox('lunasea');
await Hive.openBox<ProfileHiveObject>('profiles');
}
/// Set the default state for all hive boxes.
void setDefaults({bool clearEverything = false}) {
//Clear all the boxes
clearAllBoxes(clearEverything: clearEverything);
//Set default profile & enabled profile
profilesBox.put('default', ProfileHiveObject.empty());
lunaSeaBox.put(LunaDatabaseValue.ENABLED_PROFILE.key, 'default');
}
//Get boxes
static Box get alertsBox => Hive.box('alerts');
static Box<IndexerHiveObject> get indexersBox =>
Hive.box<IndexerHiveObject>('indexers');
static Box<LunaLogHiveObject> get logsBox =>
Hive.box<LunaLogHiveObject>('logs');
static Box get lunaSeaBox => Hive.box('lunasea');
static Box<ProfileHiveObject> get profilesBox =>
Hive.box<ProfileHiveObject>('profiles');
//Clear boxes
void clearAlertsBox() => alertsBox.deleteAll(alertsBox.keys);
void clearIndexersBox() => indexersBox.deleteAll(indexersBox.keys);
void clearLogsBox() => logsBox.deleteAll(logsBox.keys);
void clearLunaSeaBox() => lunaSeaBox.deleteAll(lunaSeaBox.keys);
void clearProfilesBox() => profilesBox.deleteAll(profilesBox.keys);
void clearAllBoxes({
bool clearEverything = false,
}) {
clearLunaSeaBox();
clearProfilesBox();
clearIndexersBox();
if (clearEverything) {
clearAlertsBox();
clearLogsBox();
}
}
/// Deinitialize the database by closing all open hive boxes.
Future<void> deinitialize() async => await Hive.close();
/// Registers all necessary object adapters for Hive.
void _registerAdapters() {
LunaDatabase().registerAdapters();
LunaModule.values.forEach((module) {
if(module.database != null) module.database.registerAdapters();
});
}
/// Open all Hive boxes for reading.
Future<void> _openBoxes() async {
await Hive.openBox('alerts');
await Hive.openBox<IndexerHiveObject>('indexers');
await Hive.openBox<LunaLogHiveObject>('logs');
await Hive.openBox('lunasea');
await Hive.openBox<ProfileHiveObject>('profiles');
}
/// Set the default state for all hive boxes.
void setDefaults({ bool clearEverything = false }) {
//Clear all the boxes
clearAllBoxes(clearEverything: clearEverything);
//Set default profile & enabled profile
profilesBox.put('default', ProfileHiveObject.empty());
lunaSeaBox.put(LunaDatabaseValue.ENABLED_PROFILE.key, 'default');
}
//Get boxes
static Box get alertsBox => Hive.box('alerts');
static Box<IndexerHiveObject> get indexersBox => Hive.box<IndexerHiveObject>('indexers');
static Box<LunaLogHiveObject> get logsBox => Hive.box<LunaLogHiveObject>('logs');
static Box get lunaSeaBox => Hive.box('lunasea');
static Box<ProfileHiveObject> get profilesBox => Hive.box<ProfileHiveObject>('profiles');
//Clear boxes
void clearAlertsBox() => alertsBox.deleteAll(alertsBox.keys);
void clearIndexersBox() => indexersBox.deleteAll(indexersBox.keys);
void clearLogsBox() => logsBox.deleteAll(logsBox.keys);
void clearLunaSeaBox() => lunaSeaBox.deleteAll(lunaSeaBox.keys);
void clearProfilesBox() => profilesBox.deleteAll(profilesBox.keys);
void clearAllBoxes({ bool clearEverything = false }) {
clearLunaSeaBox();
clearProfilesBox();
clearIndexersBox();
if(clearEverything) {
clearAlertsBox();
clearLogsBox();
}
}
//Profile values
static String get currentProfile => lunaSeaBox.get('profile');
static ProfileHiveObject get currentProfileObject => profilesBox.get(currentProfile);
//Profile values
static String get currentProfile => lunaSeaBox.get('profile');
static ProfileHiveObject get currentProfileObject =>
profilesBox.get(currentProfile);
}

View File

@@ -2,191 +2,290 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaDatabase extends LunaModuleDatabase {
@override
void registerAdapters() {
Hive.registerAdapter(IndexerHiveObjectAdapter());
Hive.registerAdapter(ProfileHiveObjectAdapter());
Hive.registerAdapter(LunaLogHiveObjectAdapter());
Hive.registerAdapter(LunaLogTypeAdapter());
Hive.registerAdapter(LunaBrowserAdapter());
Hive.registerAdapter(LunaIndexerIconAdapter());
}
@override
void registerAdapters() {
Hive.registerAdapter(IndexerHiveObjectAdapter());
Hive.registerAdapter(ProfileHiveObjectAdapter());
Hive.registerAdapter(LunaLogHiveObjectAdapter());
Hive.registerAdapter(LunaLogTypeAdapter());
Hive.registerAdapter(LunaBrowserAdapter());
Hive.registerAdapter(LunaIndexerIconAdapter());
}
@override
Map<String, dynamic> export() {
Map<String, dynamic> data = {};
for(LunaDatabaseValue value in LunaDatabaseValue.values) {
switch(value) {
// Non-primitive values
case LunaDatabaseValue.SELECTED_BROWSER: data[value.key] = (LunaDatabaseValue.SELECTED_BROWSER.data as LunaBrowser).key; break;
// Primitive values
case LunaDatabaseValue.ENABLED_PROFILE:
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS:
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS:
case LunaDatabaseValue.THEME_AMOLED:
case LunaDatabaseValue.THEME_AMOLED_BORDER:
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY:
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR:
case LunaDatabaseValue.QUICK_ACTIONS_RADARR:
case LunaDatabaseValue.QUICK_ACTIONS_SONARR:
case LunaDatabaseValue.QUICK_ACTIONS_NZBGET:
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD:
case LunaDatabaseValue.QUICK_ACTIONS_OMBI:
case LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR:
case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI:
case LunaDatabaseValue.QUICK_ACTIONS_SEARCH:
case LunaDatabaseValue.USE_24_HOUR_TIME: data[value.key] = value.data; break;
}
}
return data;
@override
Map<String, dynamic> export() {
Map<String, dynamic> data = {};
for (LunaDatabaseValue value in LunaDatabaseValue.values) {
switch (value) {
// Non-primitive values
case LunaDatabaseValue.SELECTED_BROWSER:
data[value.key] =
(LunaDatabaseValue.SELECTED_BROWSER.data as LunaBrowser).key;
break;
// Primitive values
case LunaDatabaseValue.ENABLED_PROFILE:
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS:
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS:
case LunaDatabaseValue.THEME_AMOLED:
case LunaDatabaseValue.THEME_AMOLED_BORDER:
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY:
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR:
case LunaDatabaseValue.QUICK_ACTIONS_RADARR:
case LunaDatabaseValue.QUICK_ACTIONS_SONARR:
case LunaDatabaseValue.QUICK_ACTIONS_NZBGET:
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD:
case LunaDatabaseValue.QUICK_ACTIONS_OMBI:
case LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR:
case LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI:
case LunaDatabaseValue.QUICK_ACTIONS_SEARCH:
case LunaDatabaseValue.USE_24_HOUR_TIME:
data[value.key] = value.data;
break;
}
}
return data;
}
@override
void import(Map<String, dynamic> config) {
for(String key in config.keys) {
LunaDatabaseValue value = valueFromKey(key);
if(value != null) switch(value) {
// Non-primitive values
case LunaDatabaseValue.SELECTED_BROWSER: value.put(LunaBrowser.APPLE_SAFARI.fromKey(config[key])); break;
// Primitive values
case LunaDatabaseValue.ENABLED_PROFILE:
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS:
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS:
case LunaDatabaseValue.THEME_AMOLED:
case LunaDatabaseValue.THEME_AMOLED_BORDER:
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY:
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR:
case LunaDatabaseValue.QUICK_ACTIONS_RADARR:
case LunaDatabaseValue.QUICK_ACTIONS_SONARR:
case LunaDatabaseValue.QUICK_ACTIONS_NZBGET:
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD:
case LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR:
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;
}
@override
void import(Map<String, dynamic> config) {
for (String key in config.keys) {
LunaDatabaseValue value = valueFromKey(key);
if (value != null)
switch (value) {
// Non-primitive values
case LunaDatabaseValue.SELECTED_BROWSER:
value.put(LunaBrowser.APPLE_SAFARI.fromKey(config[key]));
break;
// Primitive values
case LunaDatabaseValue.ENABLED_PROFILE:
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS:
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS:
case LunaDatabaseValue.THEME_AMOLED:
case LunaDatabaseValue.THEME_AMOLED_BORDER:
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY:
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR:
case LunaDatabaseValue.QUICK_ACTIONS_RADARR:
case LunaDatabaseValue.QUICK_ACTIONS_SONARR:
case LunaDatabaseValue.QUICK_ACTIONS_NZBGET:
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD:
case LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR:
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;
}
}
}
LunaDatabaseValue valueFromKey(String key) {
switch(key) {
case 'profile': return LunaDatabaseValue.ENABLED_PROFILE;
case 'LUNASEA_ENABLE_FIREBASE_ANALYTICS': return LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS;
case 'LUNASEA_ENABLE_FIREBASE_CRASHLYTICS': return LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS;
case 'LUNASEA_THEME_AMOLED': return LunaDatabaseValue.THEME_AMOLED;
case 'LUNASEA_THEME_AMOLED_BORDER': return LunaDatabaseValue.THEME_AMOLED_BORDER;
case 'LUNASEA_THEME_IMAGE_BACKGROUND_OPACITY': return LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY;
case 'LUNASEA_SELECTED_BROWSER': return LunaDatabaseValue.SELECTED_BROWSER;
case 'LUNASEA_QUICK_ACTIONS_LIDARR': return LunaDatabaseValue.QUICK_ACTIONS_LIDARR;
case 'LUNASEA_QUICK_ACTIONS_RADARR': return LunaDatabaseValue.QUICK_ACTIONS_RADARR;
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_OVERSEERR': return LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR;
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;
default: return null;
}
LunaDatabaseValue valueFromKey(String key) {
switch (key) {
case 'profile':
return LunaDatabaseValue.ENABLED_PROFILE;
case 'LUNASEA_ENABLE_FIREBASE_ANALYTICS':
return LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS;
case 'LUNASEA_ENABLE_FIREBASE_CRASHLYTICS':
return LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS;
case 'LUNASEA_THEME_AMOLED':
return LunaDatabaseValue.THEME_AMOLED;
case 'LUNASEA_THEME_AMOLED_BORDER':
return LunaDatabaseValue.THEME_AMOLED_BORDER;
case 'LUNASEA_THEME_IMAGE_BACKGROUND_OPACITY':
return LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY;
case 'LUNASEA_SELECTED_BROWSER':
return LunaDatabaseValue.SELECTED_BROWSER;
case 'LUNASEA_QUICK_ACTIONS_LIDARR':
return LunaDatabaseValue.QUICK_ACTIONS_LIDARR;
case 'LUNASEA_QUICK_ACTIONS_RADARR':
return LunaDatabaseValue.QUICK_ACTIONS_RADARR;
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_OVERSEERR':
return LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR;
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;
default:
return null;
}
}
}
enum LunaDatabaseValue {
ENABLED_PROFILE,
ENABLE_FIREBASE_ANALYTICS,
ENABLE_FIREBASE_CRASHLYTICS,
THEME_AMOLED,
THEME_AMOLED_BORDER,
THEME_IMAGE_BACKGROUND_OPACITY,
SELECTED_BROWSER,
QUICK_ACTIONS_LIDARR,
QUICK_ACTIONS_RADARR,
QUICK_ACTIONS_SONARR,
QUICK_ACTIONS_NZBGET,
QUICK_ACTIONS_SABNZBD,
QUICK_ACTIONS_OMBI,
QUICK_ACTIONS_OVERSEERR,
QUICK_ACTIONS_TAUTULLI,
QUICK_ACTIONS_SEARCH,
USE_24_HOUR_TIME,
ENABLED_PROFILE,
ENABLE_FIREBASE_ANALYTICS,
ENABLE_FIREBASE_CRASHLYTICS,
THEME_AMOLED,
THEME_AMOLED_BORDER,
THEME_IMAGE_BACKGROUND_OPACITY,
SELECTED_BROWSER,
QUICK_ACTIONS_LIDARR,
QUICK_ACTIONS_RADARR,
QUICK_ACTIONS_SONARR,
QUICK_ACTIONS_NZBGET,
QUICK_ACTIONS_SABNZBD,
QUICK_ACTIONS_OMBI,
QUICK_ACTIONS_OVERSEERR,
QUICK_ACTIONS_TAUTULLI,
QUICK_ACTIONS_SEARCH,
USE_24_HOUR_TIME,
}
extension LunaDatabaseValueExtension on LunaDatabaseValue {
String get key {
switch(this) {
case LunaDatabaseValue.ENABLED_PROFILE: return 'profile';
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS: return 'LUNASEA_ENABLE_FIREBASE_ANALYTICS';
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS: return 'LUNASEA_ENABLE_FIREBASE_CRASHLYTICS';
case LunaDatabaseValue.THEME_AMOLED: return 'LUNASEA_THEME_AMOLED';
case LunaDatabaseValue.THEME_AMOLED_BORDER: return 'LUNASEA_THEME_AMOLED_BORDER';
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY: return 'LUNASEA_THEME_IMAGE_BACKGROUND_OPACITY';
case LunaDatabaseValue.SELECTED_BROWSER: return 'LUNASEA_SELECTED_BROWSER';
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR: return 'LUNASEA_QUICK_ACTIONS_LIDARR';
case LunaDatabaseValue.QUICK_ACTIONS_RADARR: return 'LUNASEA_QUICK_ACTIONS_RADARR';
case LunaDatabaseValue.QUICK_ACTIONS_SONARR: return 'LUNASEA_QUICK_ACTIONS_SONARR';
case LunaDatabaseValue.QUICK_ACTIONS_NZBGET: return 'LUNASEA_QUICK_ACTIONS_NZBGET';
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD: return 'LUNASEA_QUICK_ACTIONS_SABNZBD';
case LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR: return 'LUNASEA_QUICK_ACTIONS_OVERSEERR';
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';
}
throw Exception('key not found');
String get key {
switch (this) {
case LunaDatabaseValue.ENABLED_PROFILE:
return 'profile';
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS:
return 'LUNASEA_ENABLE_FIREBASE_ANALYTICS';
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS:
return 'LUNASEA_ENABLE_FIREBASE_CRASHLYTICS';
case LunaDatabaseValue.THEME_AMOLED:
return 'LUNASEA_THEME_AMOLED';
case LunaDatabaseValue.THEME_AMOLED_BORDER:
return 'LUNASEA_THEME_AMOLED_BORDER';
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY:
return 'LUNASEA_THEME_IMAGE_BACKGROUND_OPACITY';
case LunaDatabaseValue.SELECTED_BROWSER:
return 'LUNASEA_SELECTED_BROWSER';
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR:
return 'LUNASEA_QUICK_ACTIONS_LIDARR';
case LunaDatabaseValue.QUICK_ACTIONS_RADARR:
return 'LUNASEA_QUICK_ACTIONS_RADARR';
case LunaDatabaseValue.QUICK_ACTIONS_SONARR:
return 'LUNASEA_QUICK_ACTIONS_SONARR';
case LunaDatabaseValue.QUICK_ACTIONS_NZBGET:
return 'LUNASEA_QUICK_ACTIONS_NZBGET';
case LunaDatabaseValue.QUICK_ACTIONS_SABNZBD:
return 'LUNASEA_QUICK_ACTIONS_SABNZBD';
case LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR:
return 'LUNASEA_QUICK_ACTIONS_OVERSEERR';
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';
}
throw Exception('key not found');
}
dynamic get data {
final box = Database.lunaSeaBox;
switch(this) {
case LunaDatabaseValue.ENABLED_PROFILE: return box.get(this.key, defaultValue: 'default');
case LunaDatabaseValue.SELECTED_BROWSER: return box.get(this.key, defaultValue: LunaBrowser.APPLE_SAFARI);
case LunaDatabaseValue.THEME_AMOLED: return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.THEME_AMOLED_BORDER: return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY: return box.get(this.key, defaultValue: 10);
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS: return box.get(this.key, defaultValue: true);
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS: return box.get(this.key, defaultValue: true);
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR: return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.QUICK_ACTIONS_RADARR: return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.QUICK_ACTIONS_SONARR: return box.get(this.key, defaultValue: false);
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_OVERSEERR: 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);
}
throw Exception('data not found');
dynamic get data {
final box = Database.lunaSeaBox;
switch (this) {
case LunaDatabaseValue.ENABLED_PROFILE:
return box.get(this.key, defaultValue: 'default');
case LunaDatabaseValue.SELECTED_BROWSER:
return box.get(this.key, defaultValue: LunaBrowser.APPLE_SAFARI);
case LunaDatabaseValue.THEME_AMOLED:
return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.THEME_AMOLED_BORDER:
return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY:
return box.get(this.key, defaultValue: 10);
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS:
return box.get(this.key, defaultValue: true);
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS:
return box.get(this.key, defaultValue: true);
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR:
return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.QUICK_ACTIONS_RADARR:
return box.get(this.key, defaultValue: false);
case LunaDatabaseValue.QUICK_ACTIONS_SONARR:
return box.get(this.key, defaultValue: false);
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_OVERSEERR:
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);
}
throw Exception('data not found');
}
void put(dynamic value) {
final box = Database.lunaSeaBox;
switch(this) {
case LunaDatabaseValue.ENABLED_PROFILE: if(value.runtimeType == String) box.put(this.key, value); return;
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS: if(value.runtimeType == bool) box.put(this.key, value); return;
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS: if(value.runtimeType == bool) box.put(this.key, value); return;
case LunaDatabaseValue.THEME_AMOLED: if(value.runtimeType == bool) box.put(this.key, value); return;
case LunaDatabaseValue.THEME_AMOLED_BORDER: if(value.runtimeType == bool) box.put(this.key, value); return;
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY: if(value.runtimeType == int) box.put(this.key, value); return;
case LunaDatabaseValue.SELECTED_BROWSER: if(value.runtimeType == LunaBrowser) box.put(this.key, value); return;
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR: if(value.runtimeType == bool) box.put(this.key, value); return;
case LunaDatabaseValue.QUICK_ACTIONS_RADARR: if(value.runtimeType == bool) box.put(this.key, value); return;
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_OVERSEERR: 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;
}
LunaLogger().warning('LunaDatabaseValueExtension', 'put', 'Attempted to enter data for invalid LunaDatabaseValue: ${this?.toString() ?? 'null'}');
void put(dynamic value) {
final box = Database.lunaSeaBox;
switch (this) {
case LunaDatabaseValue.ENABLED_PROFILE:
if (value.runtimeType == String) box.put(this.key, value);
return;
case LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS:
if (value.runtimeType == bool) box.put(this.key, value);
return;
case LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS:
if (value.runtimeType == bool) box.put(this.key, value);
return;
case LunaDatabaseValue.THEME_AMOLED:
if (value.runtimeType == bool) box.put(this.key, value);
return;
case LunaDatabaseValue.THEME_AMOLED_BORDER:
if (value.runtimeType == bool) box.put(this.key, value);
return;
case LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY:
if (value.runtimeType == int) box.put(this.key, value);
return;
case LunaDatabaseValue.SELECTED_BROWSER:
if (value.runtimeType == LunaBrowser) box.put(this.key, value);
return;
case LunaDatabaseValue.QUICK_ACTIONS_LIDARR:
if (value.runtimeType == bool) box.put(this.key, value);
return;
case LunaDatabaseValue.QUICK_ACTIONS_RADARR:
if (value.runtimeType == bool) box.put(this.key, value);
return;
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_OVERSEERR:
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;
}
LunaLogger().warning('LunaDatabaseValueExtension', 'put',
'Attempted to enter data for invalid LunaDatabaseValue: ${this?.toString() ?? 'null'}');
}
ValueListenableBuilder listen({ @required Widget Function(BuildContext, dynamic, Widget) builder }) => ValueListenableBuilder(
ValueListenableBuilder listen(
{@required Widget Function(BuildContext, dynamic, Widget) builder}) =>
ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [this.key]),
builder: builder,
);
);
}

View File

@@ -1,13 +1,13 @@
abstract class LunaModuleDatabase {
/// Register all adapters necessary for this module
void registerAdapters();
/// Register all adapters necessary for this module
void registerAdapters();
/// Export the current module's database into a [Map] object
Map<String, dynamic> export();
/// Receiving a map object, import a configuration
void import(Map<String, dynamic> config);
/// Export the current module's database into a [Map] object
Map<String, dynamic> export();
/// Return the Database enumerator value given the key
dynamic valueFromKey(String value);
/// Receiving a map object, import a configuration
void import(Map<String, dynamic> config);
/// Return the Database enumerator value given the key
dynamic valueFromKey(String value);
}

View File

@@ -3,139 +3,152 @@ import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
class LunaDialogs {
/// Show an an edit text prompt.
///
/// Can pass in [prefill] String to prefill the [TextFormField]. Can also pass in a list of [TextSpan] tp show text above the field.
///
/// Returns list containing:
/// - 0: Flag (true if they hit save, false if they cancelled the prompt)
/// - 1: Value from the [TextEditingController].
Future<Tuple2<bool, String>> editText(BuildContext context, String dialogTitle, { String prefill = '', List<TextSpan> extraText }) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController()..text = prefill;
/// Show an an edit text prompt.
///
/// Can pass in [prefill] String to prefill the [TextFormField]. Can also pass in a list of [TextSpan] tp show text above the field.
///
/// Returns list containing:
/// - 0: Flag (true if they hit save, false if they cancelled the prompt)
/// - 1: Value from the [TextEditingController].
Future<Tuple2<bool, String>> editText(
BuildContext context, String dialogTitle,
{String prefill = '', List<TextSpan> extraText}) async {
bool _flag = false;
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController()..text = prefill;
void _setValues(bool flag) {
if(_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context, rootNavigator: true).pop();
}
}
void _setValues(bool flag) {
if (_formKey.currentState.validate()) {
_flag = flag;
Navigator.of(context, rootNavigator: true).pop();
}
}
await LunaDialog.dialog(
context: context,
await LunaDialog.dialog(
context: context,
title: dialogTitle,
buttons: [
LunaDialog.button(
text: 'Save',
onPressed: () => _setValues(true),
),
],
content: [
if ((extraText?.length ?? 0) != 0)
LunaDialog.richText(children: extraText),
Form(
key: _formKey,
child: LunaDialog.textFormInput(
controller: _textController,
title: dialogTitle,
buttons: [
LunaDialog.button(
text: 'Save',
onPressed: () => _setValues(true),
),
],
content: [
if((extraText?.length ?? 0) != 0) LunaDialog.richText(children: extraText),
Form(
key: _formKey,
child: LunaDialog.textFormInput(
controller: _textController,
title: dialogTitle,
onSubmitted: (_) => _setValues(true),
validator: (_) => null,
),
),
],
contentPadding: (extraText?.length ?? 0) == 0 ? LunaDialog.inputDialogContentPadding() : LunaDialog.inputTextDialogContentPadding(),
);
return Tuple2(_flag, _textController.text);
onSubmitted: (_) => _setValues(true),
validator: (_) => null,
),
),
],
contentPadding: (extraText?.length ?? 0) == 0
? LunaDialog.inputDialogContentPadding()
: LunaDialog.inputTextDialogContentPadding(),
);
return Tuple2(_flag, _textController.text);
}
/// Show a text preview dialog.
///
/// Can pass in boolean [alignLeft] to left align the text in the dialog (useful for bulleted lists)
Future<void> textPreview(
BuildContext context, String dialogTitle, String text,
{bool alignLeft = false}) async {
await LunaDialog.dialog(
context: context,
title: dialogTitle,
cancelButtonText: 'Close',
buttons: [
LunaDialog.button(
text: 'Copy',
onPressed: () async {
await Clipboard.setData(ClipboardData(text: text));
showLunaSuccessSnackBar(
title: 'Copied Content',
message: 'Copied text to the clipboard');
Navigator.of(context, rootNavigator: true).pop();
}),
],
content: [
LunaDialog.textContent(text: text),
],
contentPadding: LunaDialog.textDialogContentPadding(),
);
}
Future<void> showRejections(
BuildContext context, List<String> rejections) async {
if ((rejections ?? []).isEmpty)
return textPreview(context, 'Rejection Reasons', 'No rejections found');
await LunaDialog.dialog(
context: context,
title: 'Rejection Reasons',
cancelButtonText: 'Close',
content: List.generate(
rejections.length,
(index) => LunaDialog.tile(
text: rejections[index],
icon: Icons.report_outlined,
iconColor: LunaColours.red,
),
),
contentPadding: LunaDialog.listDialogContentPadding(),
);
}
Future<void> showMessages(BuildContext context, List<String> messages) async {
if ((messages ?? []).isEmpty)
return textPreview(context, 'Messages', 'No messages found');
await LunaDialog.dialog(
context: context,
title: 'Messages',
cancelButtonText: 'Close',
content: List.generate(
messages.length,
(index) => LunaDialog.tile(
text: messages[index],
icon: Icons.info_outline_rounded,
iconColor: LunaColours.accent,
),
),
contentPadding: LunaDialog.listDialogContentPadding(),
);
}
/// **Will be removed in future**
///
/// Show a delete catalogue with all files warning dialog.
Future<List<dynamic>> deleteCatalogueWithFiles(
BuildContext context, String moduleTitle) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context, rootNavigator: true).pop();
}
/// Show a text preview dialog.
///
/// Can pass in boolean [alignLeft] to left align the text in the dialog (useful for bulleted lists)
Future<void> textPreview(BuildContext context, String dialogTitle, String text, { bool alignLeft = false }) async {
await LunaDialog.dialog(
context: context,
title: dialogTitle,
cancelButtonText: 'Close',
buttons: [
LunaDialog.button(
text: 'Copy',
onPressed: () async {
await Clipboard.setData(ClipboardData(text: text));
showLunaSuccessSnackBar(title: 'Copied Content', message: 'Copied text to the clipboard');
Navigator.of(context, rootNavigator: true).pop();
}
),
],
content: [
LunaDialog.textContent(text: text),
],
contentPadding: LunaDialog.textDialogContentPadding(),
);
}
Future<void> showRejections(BuildContext context, List<String> rejections) async {
if((rejections ?? []).isEmpty) return textPreview(context, 'Rejection Reasons', 'No rejections found');
await LunaDialog.dialog(
context: context,
title: 'Rejection Reasons',
cancelButtonText: 'Close',
content: List.generate(
rejections.length,
(index) => LunaDialog.tile(
text: rejections[index],
icon: Icons.report_outlined,
iconColor: LunaColours.red,
),
),
contentPadding: LunaDialog.listDialogContentPadding(),
);
}
Future<void> showMessages(BuildContext context, List<String> messages) async {
if((messages ?? []).isEmpty) return textPreview(context, 'Messages', 'No messages found');
await LunaDialog.dialog(
context: context,
title: 'Messages',
cancelButtonText: 'Close',
content: List.generate(
messages.length,
(index) => LunaDialog.tile(
text: messages[index],
icon: Icons.info_outline_rounded,
iconColor: LunaColours.accent,
),
),
contentPadding: LunaDialog.listDialogContentPadding(),
);
}
/// **Will be removed in future**
///
/// Show a delete catalogue with all files warning dialog.
Future<List<dynamic>> deleteCatalogueWithFiles(BuildContext context, String moduleTitle) async {
bool _flag = false;
void _setValues(bool flag) {
_flag = flag;
Navigator.of(context, rootNavigator: true).pop();
}
await LunaDialog.dialog(
context: context,
title: 'Delete All Files',
buttons: [
LunaDialog.button(
text: 'Delete',
textColor: LunaColours.red,
onPressed: () => _setValues(true),
),
],
content: [
LunaDialog.textContent(text: 'Are you sure you want to delete all the files and folders for $moduleTitle?'),
],
contentPadding: LunaDialog.textDialogContentPadding(),
);
return [_flag];
}
await LunaDialog.dialog(
context: context,
title: 'Delete All Files',
buttons: [
LunaDialog.button(
text: 'Delete',
textColor: LunaColours.red,
onPressed: () => _setValues(true),
),
],
content: [
LunaDialog.textContent(
text:
'Are you sure you want to delete all the files and folders for $moduleTitle?'),
],
contentPadding: LunaDialog.textDialogContentPadding(),
);
return [_flag];
}
}

View File

@@ -2,62 +2,73 @@ import 'package:intl/intl.dart';
import 'package:lunasea/core.dart';
extension DateTimeExtension on DateTime {
/// Get the floor of a date.
///
/// The floor is a new [DateTime] object with only the year, month, and day copied from the original source.
DateTime get lunaFloor => DateTime(this.year, this.month, this.day);
/// Get the floor of a date.
///
/// The floor is a new [DateTime] object with only the year, month, and day copied from the original source.
DateTime get lunaFloor => DateTime(this.year, this.month, this.day);
/// Returns a string representation of the "age" of the [DateTime] object.
///
/// Compares to [DateTime.now().toLocal()] to calculate the age.
String get lunaAge {
if(this == null) return 'Unknown';
Duration diff = DateTime.now().toLocal().difference(this);
if(diff.inSeconds < 10) return 'Just Now';
if(diff.inDays >= 1) return '${diff.inDays} ${diff.inDays == 1 ? 'Day' : 'Days'} Ago';
if(diff.inHours >= 1) return '${diff.inHours} ${diff.inHours == 1 ? 'Hour' : 'Hours'} Ago';
if(diff.inMinutes >= 1) return '${diff.inMinutes} ${diff.inMinutes == 1 ? 'Minute' : 'Minutes'} Ago';
return '${diff.inSeconds} ${diff.inSeconds == 1 ? 'Second' : 'Seconds'} Ago';
}
/// Returns a string representation of the "age" of the [DateTime] object.
///
/// Compares to [DateTime.now().toLocal()] to calculate the age.
String get lunaAge {
if (this == null) return 'Unknown';
Duration diff = DateTime.now().toLocal().difference(this);
if (diff.inSeconds < 10) return 'Just Now';
if (diff.inDays >= 1)
return '${diff.inDays} ${diff.inDays == 1 ? 'Day' : 'Days'} Ago';
if (diff.inHours >= 1)
return '${diff.inHours} ${diff.inHours == 1 ? 'Hour' : 'Hours'} Ago';
if (diff.inMinutes >= 1)
return '${diff.inMinutes} ${diff.inMinutes == 1 ? 'Minute' : 'Minutes'} Ago';
return '${diff.inSeconds} ${diff.inSeconds == 1 ? 'Second' : 'Seconds'} Ago';
}
/// Returns a string representation of the difference in days/months/years.
///
/// 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 a string representation of the difference in days/months/years.
///
/// 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.
///
/// 3 PM will return either 15:00 (24 hour style) or 3:00 PM depending on the configured database option.
String get lunaTime => LunaDatabaseValue.USE_24_HOUR_TIME.data ? DateFormat.Hm().format(this) : DateFormat.jm().format(this);
/// Returns just the time as a string.
///
/// 3 PM will return either 15:00 (24 hour style) or 3:00 PM depending on the configured database option.
String get lunaTime => LunaDatabaseValue.USE_24_HOUR_TIME.data
? DateFormat.Hm().format(this)
: DateFormat.jm().format(this);
/// Returns just the date as a string.
///
/// 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 just the date as a string.
///
/// 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 "<month name> <day>, <year>".
String get lunaDateReadable => DateFormat('MMMM dd, y').format(this.toLocal());
/// Returns the date as a string.
///
/// Formatted as "<month name> <day>, <year>".
String get lunaDateReadable =>
DateFormat('MMMM dd, y').format(this.toLocal());
/// Returns the date and time as a string.
///
/// Formatted as `<month name> <day>, <year> • <hour>:<minute>:<second>`.
///
/// 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());
/// Returns the date and time as a string.
///
/// Formatted as `<month name> <day>, <year> • <hour>:<minute>:<second>`.
///
/// 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());
}

View File

@@ -1,13 +1,13 @@
import 'package:lunasea/core.dart';
extension DoubleExtension on double {
/// Given a double (of hours), return a string signifying how long ago it occurred.
///
/// Time greater than 48 hours => "x Days Ago" else "x Hours Ago"
String lunaHoursToAge() {
if(this == null) return LunaUI.TEXT_EMDASH;
int days = (this/24).floor();
if(this > 48) return '$days Days Ago';
return this == 1 ? '1 Hour Ago' : '${this.toStringAsFixed(1)} Hours Ago';
}
/// Given a double (of hours), return a string signifying how long ago it occurred.
///
/// Time greater than 48 hours => "x Days Ago" else "x Hours Ago"
String lunaHoursToAge() {
if (this == null) return LunaUI.TEXT_EMDASH;
int days = (this / 24).floor();
if (this > 48) return '$days Days Ago';
return this == 1 ? '1 Hour Ago' : '${this.toStringAsFixed(1)} Hours Ago';
}
}

View File

@@ -1,31 +1,32 @@
import 'package:lunasea/core.dart';
extension DurationExtension on Duration {
/// Return a string representation of the timestamp.
///
/// Format example: HH:MM:SS or MM:SS if the duration is less than one hour
String get lunaTimestamp => [
if(this.inHours != 0) this.inHours.toString().padLeft(2, '0'),
(this.inMinutes%60).toString().padLeft(2, '0'),
(this.inSeconds%60).toString().padLeft(2, '0'),
].join(':');
/// Return a string representation of the timestamp.
///
/// Format example: HH:MM:SS or MM:SS if the duration is less than one hour
String get lunaTimestamp => [
if (this.inHours != 0) this.inHours.toString().padLeft(2, '0'),
(this.inMinutes % 60).toString().padLeft(2, '0'),
(this.inSeconds % 60).toString().padLeft(2, '0'),
].join(':');
/// Returns a string representation of the timestamp as words.
///
/// Format example: 1 Day, 23 Hours, 10 Minutes
String get lunaTimestampWords {
String days, hours, minutes;
if(this.inDays == 1) days = '1 Day';
if(this.inDays > 1) days = '${this.inDays.toString()} Days';
if(this.inHours == 1) hours = '1 Hour';
if(this.inHours > 1) hours = '${(this.inHours%24).toString()} Hours';
if(this.inMinutes == 1) minutes = '1 Minute';
if(this.inMinutes > 1) minutes = '${(this.inMinutes%60).toString()} Minutes';
return [
if(days != null) days,
if(hours != null) hours,
if(minutes != null) minutes,
if(minutes == null) LunaUI.TEXT_EMDASH,
].join(' ');
}
/// Returns a string representation of the timestamp as words.
///
/// Format example: 1 Day, 23 Hours, 10 Minutes
String get lunaTimestampWords {
String days, hours, minutes;
if (this.inDays == 1) days = '1 Day';
if (this.inDays > 1) days = '${this.inDays.toString()} Days';
if (this.inHours == 1) hours = '1 Hour';
if (this.inHours > 1) hours = '${(this.inHours % 24).toString()} Hours';
if (this.inMinutes == 1) minutes = '1 Minute';
if (this.inMinutes > 1)
minutes = '${(this.inMinutes % 60).toString()} Minutes';
return [
if (days != null) days,
if (hours != null) hours,
if (minutes != null) minutes,
if (minutes == null) LunaUI.TEXT_EMDASH,
].join(' ');
}
}

View File

@@ -2,25 +2,35 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
extension ProductDetailsExtension on ProductDetails {
/// Returns [IconData] icon corresponding to the IAP
IconData get lunaIcon {
switch(this.id) {
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;
}
/// Returns [IconData] icon corresponding to the IAP
IconData get lunaIcon {
switch (this.id) {
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;
}
}
/// Returns a readable name for the corresponding IAP
String get lunaName {
switch(this.id) {
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';
}
/// Returns a readable name for the corresponding IAP
String get lunaName {
switch (this.id) {
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';
}
}
}

View File

@@ -1,76 +1,80 @@
extension IntegerBytesExtension on int {
static const _BIT_SIZES = ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb'];
static const _BYTE_SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
static const _BIT_SIZES = ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb'];
static const _BYTE_SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
String _bytesToString({ int decimals = 2, position = 0, bool bytes = true }) {
if(this == null || this <= 0) return '${0.toStringAsFixed(decimals)} B';
int chunk = bytes ? 1024 : 1000;
double size = this.toDouble();
while(size > chunk) {
size /= chunk;
position++;
}
return '${size.toStringAsFixed(decimals)} ${bytes ? _BYTE_SIZES[position] : _BIT_SIZES[position]}';
String _bytesToString({int decimals = 2, position = 0, bool bytes = true}) {
if (this == null || this <= 0) return '${0.toStringAsFixed(decimals)} B';
int chunk = bytes ? 1024 : 1000;
double size = this.toDouble();
while (size > chunk) {
size /= chunk;
position++;
}
return '${size.toStringAsFixed(decimals)} ${bytes ? _BYTE_SIZES[position] : _BIT_SIZES[position]}';
}
/// Given an integer of the number of bytes/bits, return a string representation.
String lunaBytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 0, bytes: bytes);
/// Given an integer of the number of bytes/bits, return a string representation.
String lunaBytesToString({int decimals = 2, bool bytes = true}) =>
_bytesToString(decimals: decimals, position: 0, bytes: bytes);
/// Given an integer of the number of kilobytes/kilobits, return a string representation.
String lunaKilobytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 1, bytes: bytes);
/// Given an integer of the number of kilobytes/kilobits, return a string representation.
String lunaKilobytesToString({int decimals = 2, bool bytes = true}) =>
_bytesToString(decimals: decimals, position: 1, bytes: bytes);
/// Given an integer of the number of megabytes/megabits, return a string representation.
String lunaMegabytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 2, bytes: bytes);
/// Given an integer of the number of megabytes/megabits, return a string representation.
String lunaMegabytesToString({int decimals = 2, bool bytes = true}) =>
_bytesToString(decimals: decimals, position: 2, bytes: bytes);
/// Given an integer of the number of gigabytes/gigabits, return a string representation.
String lunaGigabytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 3, bytes: bytes);
/// Given an integer of the number of gigabytes/gigabits, return a string representation.
String lunaGigabytesToString({int decimals = 2, bool bytes = true}) =>
_bytesToString(decimals: decimals, position: 3, bytes: bytes);
}
extension IntegerTimeExtension on int {
static const int _MINUTE_IN_SECONDS = 60;
static const int _HOUR_IN_SECONDS = 60*60;
static const int _DAY_IN_SECONDS = 60*60*24;
static const int _MINUTE_IN_SECONDS = 60;
static const int _HOUR_IN_SECONDS = 60 * 60;
static const int _DAY_IN_SECONDS = 60 * 60 * 24;
/// Given an integer duration, converts it to a readable string. Assumes the duration is in seconds, use the `divisor` and `multipler` variables to easily modify the input.
///
/// Only contains the highest factor (2.5 Days would return 2 Days).
String lunaDuration({ int divisor = 1, int multiplier = 1 }) {
if(this == null) return '';
int duration = ((this*multiplier)/divisor).floor();
int days = (duration/_DAY_IN_SECONDS).floor();
if(days > 0) return '$days ${days == 1 ? 'Day' : 'Days'}';
int hours = (duration/_HOUR_IN_SECONDS).floor();
if(hours > 0) return '$hours ${hours == 1 ? 'Hour' : 'Hours'}';
int minutes = (duration/_MINUTE_IN_SECONDS).floor();
if(minutes > 0) return '$minutes ${minutes == 1 ? 'Minute' : 'Minutes'}';
return '$duration ${duration == 1 ? 'Second' : 'Seconds'}';
}
/// Given an integer duration, converts it to a readable string. Assumes the duration is in seconds, use the `divisor` and `multipler` variables to easily modify the input.
///
/// Only contains the highest factor (2.5 Days would return 2 Days).
String lunaDuration({int divisor = 1, int multiplier = 1}) {
if (this == null) return '';
int duration = ((this * multiplier) / divisor).floor();
int days = (duration / _DAY_IN_SECONDS).floor();
if (days > 0) return '$days ${days == 1 ? 'Day' : 'Days'}';
int hours = (duration / _HOUR_IN_SECONDS).floor();
if (hours > 0) return '$hours ${hours == 1 ? 'Hour' : 'Hours'}';
int minutes = (duration / _MINUTE_IN_SECONDS).floor();
if (minutes > 0) return '$minutes ${minutes == 1 ? 'Minute' : 'Minutes'}';
return '$duration ${duration == 1 ? 'Second' : 'Seconds'}';
}
/// Given an integer duration, converts it to a readable runtime string. Assumes the duration is in seconds, use the `divisor` and `multipler` variables to easily modify the input.
///
/// Format example: 1h 23m
String lunaRuntime({ int divisor = 1, int multiplier = 1 }) {
if(this == null || this == 0) return '';
double runtime = ((this*multiplier)/divisor);
if(runtime < 60) return '${runtime.floor()}m';
return '${(runtime/60).floor()}h ${(runtime-((runtime/60).floor()*60)).floor()}m';
}
/// Given an integer duration, converts it to a readable runtime string. Assumes the duration is in seconds, use the `divisor` and `multipler` variables to easily modify the input.
///
/// Format example: 1h 23m
String lunaRuntime({int divisor = 1, int multiplier = 1}) {
if (this == null || this == 0) return '';
double runtime = ((this * multiplier) / divisor);
if (runtime < 60) return '${runtime.floor()}m';
return '${(runtime / 60).floor()}h ${(runtime - ((runtime / 60).floor() * 60)).floor()}m';
}
/// Given an integer duration, converts it to a readable timestamp string. Assumes the duration is in seconds, use the `divisor` and `multiplier` variables to easily modify the input.
///
/// Format example: 1:23:45 (1 hour, 23 minutes, and 45 seconds)
String lunaTimestamp({ int divisor = 1, int multiplier = 1 }) {
if(this == null) return '';
int duration = ((this*multiplier)/divisor).floor();
int hours = 0, minutes = 0;
while(duration >= _HOUR_IN_SECONDS) {
duration -= _HOUR_IN_SECONDS;
hours++;
}
while(duration >= _MINUTE_IN_SECONDS) {
duration -= _MINUTE_IN_SECONDS;
minutes++;
}
return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${duration.toString().padLeft(2, '0')}';
/// Given an integer duration, converts it to a readable timestamp string. Assumes the duration is in seconds, use the `divisor` and `multiplier` variables to easily modify the input.
///
/// Format example: 1:23:45 (1 hour, 23 minutes, and 45 seconds)
String lunaTimestamp({int divisor = 1, int multiplier = 1}) {
if (this == null) return '';
int duration = ((this * multiplier) / divisor).floor();
int hours = 0, minutes = 0;
while (duration >= _HOUR_IN_SECONDS) {
duration -= _HOUR_IN_SECONDS;
hours++;
}
while (duration >= _MINUTE_IN_SECONDS) {
duration -= _MINUTE_IN_SECONDS;
minutes++;
}
return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${duration.toString().padLeft(2, '0')}';
}
}

View File

@@ -1,15 +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();
}
/// 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);
}
/// Pops every route until the route is the first/lowest route in the navigator.
void lunaPopToFirst() {
this.popUntil((route) => route.isFirst);
}
}

View File

@@ -2,17 +2,18 @@ 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<void> lunaAnimateToPage(int index) async {
if(this.hasClients) this.animateToPage(
index,
duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED),
curve: Curves.decelerate,
);
}
/// Animate a [PageView] back to 1.00, with a predefined duration and curve.
Future<void> lunaAnimateToPage(int index) async {
if (this.hasClients)
this.animateToPage(
index,
duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED),
curve: Curves.decelerate,
);
}
/// Jump a [PageView] to the given index.
Future<void> lunaJumpToPage(int index) async {
if(this.hasClients) this.jumpToPage(index);
}
/// Jump a [PageView] to the given index.
Future<void> lunaJumpToPage(int index) async {
if (this.hasClients) this.jumpToPage(index);
}
}

View File

@@ -2,12 +2,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<void> lunaAnimateToStart() async {
if(this.hasClients) this.animateTo(
0.00,
duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED*2),
curve: Curves.decelerate,
);
}
/// Animate a [Scrollable] back to 1.00, with a predefined duration and curve.
Future<void> lunaAnimateToStart() async {
if (this.hasClients)
this.animateTo(
0.00,
duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED * 2),
curve: Curves.decelerate,
);
}
}

View File

@@ -2,79 +2,99 @@ import 'package:lunasea/core.dart';
import 'package:url_launcher/url_launcher.dart';
extension StringExtension on String {
/// Returns a string with all first letters of each word capitalized
///
/// Default word splitting pattern is by a space.
String lunaCapitalizeFirstLetters({ Pattern pattern = ' ' }) {
List<String> split = this.split(pattern);
for(var i=0; i<split.length; i++) {
if(split[i].length == 1) {
split[i] = split[i].toUpperCase();
} else {
split[i] = split[i].substring(0, 1).toUpperCase()+split[i].substring(1);
}
}
return split.join(pattern);
/// Returns a string with all first letters of each word capitalized
///
/// Default word splitting pattern is by a space.
String lunaCapitalizeFirstLetters({Pattern pattern = ' '}) {
List<String> split = this.split(pattern);
for (var i = 0; i < split.length; i++) {
if (split[i].length == 1) {
split[i] = split[i].toUpperCase();
} else {
split[i] =
split[i].substring(0, 1).toUpperCase() + split[i].substring(1);
}
}
return split.join(pattern);
}
/// Convert a string into a slug format.
///
/// Example "LunaSea Flutter" => 'lunasea-flutter';
String lunaConvertToSlug() => this.toLowerCase().replaceAll(RegExp(r'[\ \.]'), '-').replaceAll(RegExp(r'[^a-zA-Z0-9\-]'), '').trim();
/// Convert a string into a slug format.
///
/// 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;
}
/// 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 {
Future<void> _openLink(String url, { Map<String, String> headers }) async {
try {
//Attempt to launch the link forced as a universal link
if(await _launchUniversalLink(url)) return;
//If that fails and we're using a custom browser, format the link for the correct browser and attempt to launch
LunaBrowser _browser = LunaDatabaseValue.SELECTED_BROWSER.data;
if(_browser != LunaBrowser.APPLE_SAFARI && await _launchCustomBrowser(_browser.formatted(url))) return;
//If all else fails, just launch it in Safari/stock browser
await _launch(url, headers: headers);
} catch (error, stack) {
LunaLogger().error('Unable to open link: $url', error, stack);
}
Future<void> _openLink(String url, {Map<String, String> headers}) async {
try {
//Attempt to launch the link forced as a universal link
if (await _launchUniversalLink(url)) return;
//If that fails and we're using a custom browser, format the link for the correct browser and attempt to launch
LunaBrowser _browser = LunaDatabaseValue.SELECTED_BROWSER.data;
if (_browser != LunaBrowser.APPLE_SAFARI &&
await _launchCustomBrowser(_browser.formatted(url))) return;
//If all else fails, just launch it in Safari/stock browser
await _launch(url, headers: headers);
} catch (error, stack) {
LunaLogger().error('Unable to open link: $url', error, stack);
}
}
Future<bool> _launchUniversalLink(String url) async => await launch(url, forceSafariVC: false, universalLinksOnly: true);
Future<bool> _launchCustomBrowser(String url) async => await launch(url, forceSafariVC: false);
Future<bool> _launch(String url, { Map<String, String> headers }) async => await launch(url, headers: headers);
Future<bool> _launchUniversalLink(String url) async =>
await launch(url, forceSafariVC: false, universalLinksOnly: true);
Future<bool> _launchCustomBrowser(String url) async =>
await launch(url, forceSafariVC: false);
Future<bool> _launch(String url, {Map<String, String> headers}) async =>
await launch(url, headers: headers);
/// Attempt to open this string as a URL in a browser.
Future<void> lunaOpenGenericLink({ Map<dynamic, dynamic> headers }) async => await _openLink(this, headers: headers?.cast<String, String>());
/// Attempt to open this string as a URL in a browser.
Future<void> lunaOpenGenericLink({Map<dynamic, dynamic> headers}) async =>
await _openLink(this, headers: headers?.cast<String, String>());
/// Attach this string as an ID/title to IMDB and attempt to launch it as a URL.
Future<void> lunaOpenIMDB() async => await _openLink('https://www.imdb.com/title/$this');
/// Attach this string as an ID/title to IMDB and attempt to launch it as a URL.
Future<void> lunaOpenIMDB() async =>
await _openLink('https://www.imdb.com/title/$this');
/// Attach this string as a video ID to YouTube and attempt to launch it as a URL.
Future<void> lunaOpenYouTube() async => await _openLink('https://www.youtube.com/watch?v=$this');
/// Attach this string as a video ID to YouTube and attempt to launch it as a URL.
Future<void> 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<void> lunaOpenTheMovieDBMovie() async => await _openLink('https://www.themoviedb.org/movie/$this');
/// Attach this string as a movie ID to TheMovieDB and attempt to launch it as a URL.
Future<void> 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<void> lunaOpenTheMovieDBCredits() async => await _openLink('https://www.themoviedb.org/person/$this');
/// Attach this string as a person ID to TheMovieDB and attempt to launch it as a URL.
Future<void> 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<void> lunaOpenTheTVDB() async => await _openLink('https://www.thetvdb.com/?id=$this&tab=series');
/// Attach this string as a series ID to TheTVDB and attempt to launch it as a URL.
Future<void> lunaOpenTheTVDB() async =>
await _openLink('https://www.thetvdb.com/?id=$this&tab=series');
/// Attach this string as a TVDB ID to Trakt and attempt to launch it as a URL.
Future<void> lunaOpenTraktSeries() async => await _openLink('http://trakt.tv/search/tvdb/$this?id_type=show');
/// Attach this string as a TVDB ID to Trakt and attempt to launch it as a URL.
Future<void> lunaOpenTraktSeries() async =>
await _openLink('http://trakt.tv/search/tvdb/$this?id_type=show');
/// Attach this string as a TMDB ID to Trakt and attempt to launch it as a URL.
Future<void> lunaOpenTraktMovie() async => await _openLink('https://trakt.tv/search/tmdb/$this?id_type=movie');
/// Attach this string as a TMDB ID to Trakt and attempt to launch it as a URL.
Future<void> lunaOpenTraktMovie() async =>
await _openLink('https://trakt.tv/search/tmdb/$this?id_type=movie');
/// Attach this string as a series ID to TVMaze and attempt to launch it as a URL.
Future<void> lunaOpenTVMaze() async => await _openLink('https://www.tvmaze.com/shows/$this');
/// Attach this string as a series ID to TVMaze and attempt to launch it as a URL.
Future<void> lunaOpenTVMaze() async =>
await _openLink('https://www.tvmaze.com/shows/$this');
}

View File

@@ -5,33 +5,37 @@ import 'package:firebase_analytics/observer.dart';
import 'package:lunasea/core.dart';
class LunaFirebaseAnalytics {
/// Returns true if Firebase Analytics is compatible with this build system/platform.
static bool get isPlatformCompatible {
return LunaFirebase.isPlatformCompatible && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows;
/// Returns true if Firebase Analytics is compatible with this build system/platform.
static bool get isPlatformCompatible {
return LunaFirebase.isPlatformCompatible &&
!Platform.isLinux &&
!Platform.isMacOS &&
!Platform.isWindows;
}
/// Returns an instance of [FirebaseAnalytics].
///
/// Throws an error if [LunaFirebase.initialize] has not been called.
static FirebaseAnalytics get instance => FirebaseAnalytics();
/// Returns an instance of [FirebaseAnalyticsObserver].
///
/// Throws an error if [LunaFirebase.initialize] has not been called.
static FirebaseAnalyticsObserver get observer =>
FirebaseAnalyticsObserver(analytics: instance);
/// Set the enabled state of Firebase Analytics.
///
/// If `enabled` is set to false, force-disables Analytics.
void setEnabledState() {
if (isPlatformCompatible) {
bool state = LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS.data;
instance.setAnalyticsCollectionEnabled(state);
}
}
/// Returns an instance of [FirebaseAnalytics].
///
/// Throws an error if [LunaFirebase.initialize] has not been called.
static FirebaseAnalytics get instance => FirebaseAnalytics();
/// Returns an instance of [FirebaseAnalyticsObserver].
///
/// Throws an error if [LunaFirebase.initialize] has not been called.
static FirebaseAnalyticsObserver get observer => FirebaseAnalyticsObserver(analytics: instance);
/// Set the enabled state of Firebase Analytics.
///
/// If `enabled` is set to false, force-disables Analytics.
void setEnabledState() {
if(isPlatformCompatible) {
bool state = LunaDatabaseValue.ENABLE_FIREBASE_ANALYTICS.data;
instance.setAnalyticsCollectionEnabled(state);
}
}
/// Log an "app_open" event.
Future<void> appOpened() async {
if(isPlatformCompatible) instance.logAppOpen();
}
/// Log an "app_open" event.
Future<void> appOpened() async {
if (isPlatformCompatible) instance.logAppOpen();
}
}

View File

@@ -2,71 +2,77 @@ 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;
/// 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 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 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;
/// 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;
/// 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<void> signOut() async => instance.signOut();
/// Sign out a logged in user.
///
/// If the user is not signed in, this is a non-op.
Future<void> 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<LunaFirebaseAuthResponse> registerUser(String email, String password) async {
try {
assert(email != null && password != null);
UserCredential _user = await instance.createUserWithEmailAndPassword(email: email, password: password);
LunaFirebaseFirestore().addDeviceToken();
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);
}
/// 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<LunaFirebaseAuthResponse> registerUser(
String email, String password) async {
try {
assert(email != null && password != null);
UserCredential _user = await instance.createUserWithEmailAndPassword(
email: email, password: password);
LunaFirebaseFirestore().addDeviceToken();
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<LunaFirebaseAuthResponse> signInUser(String email, String password) async {
try {
assert(email != null && password != null);
UserCredential _user = await instance.signInWithEmailAndPassword(email: email, password: password);
LunaFirebaseFirestore().addDeviceToken();
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);
}
/// 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<LunaFirebaseAuthResponse> signInUser(
String email, String password) async {
try {
assert(email != null && password != null);
UserCredential _user = await instance.signInWithEmailAndPassword(
email: email, password: password);
LunaFirebaseFirestore().addDeviceToken();
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);
}
}
/// Reset a user's password by sending them a password reset email.
Future<void> resetPassword(String email) async {
assert(email != null);
instance.sendPasswordResetEmail(email: email);
}
/// Reset a user's password by sending them a password reset email.
Future<void> resetPassword(String email) async {
assert(email != null);
instance.sendPasswordResetEmail(email: email);
}
}

View File

@@ -3,16 +3,18 @@ import 'dart:io';
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();
/// Return an instance of [FirebaseApp].
///
/// Throws an error if [LunaFirebase.initialize] has not been called.
static FirebaseApp get instance => Firebase.app();
/// Returns true if Firebase in its entirety is compatible with this build system/platform.
static bool get isPlatformCompatible => Platform.isIOS || Platform.isAndroid || Platform.isMacOS;
/// Returns true if Firebase in its entirety is compatible with this build system/platform.
static bool get isPlatformCompatible =>
Platform.isIOS || Platform.isAndroid || Platform.isMacOS;
/// Initialize Firebase and configuration.
///
/// This must be called before anything accesses Firebase services, or an exception will be thrown.
Future<void> initialize({ String name }) async => await Firebase.initializeApp();
/// Initialize Firebase and configuration.
///
/// This must be called before anything accesses Firebase services, or an exception will be thrown.
Future<void> initialize({String name}) async =>
await Firebase.initializeApp();
}

View File

@@ -4,23 +4,27 @@ import 'package:flutter/foundation.dart';
import 'package:lunasea/core.dart';
class LunaFirebaseCrashlytics {
/// Returns true if Firebase Analytics is compatible with this build system/platform.
static bool get isPlatformCompatible {
return LunaFirebase.isPlatformCompatible && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows;
}
/// Returns true if Firebase Analytics is compatible with this build system/platform.
static bool get isPlatformCompatible {
return LunaFirebase.isPlatformCompatible &&
!Platform.isLinux &&
!Platform.isMacOS &&
!Platform.isWindows;
}
/// Returns an instance of [FirebaseCrashlytics].
///
/// Throws an error if [LunaFirebase.initialize] has not been called.
static FirebaseCrashlytics get instance => FirebaseCrashlytics.instance;
/// Returns an instance of [FirebaseCrashlytics].
///
/// Throws an error if [LunaFirebase.initialize] has not been called.
static FirebaseCrashlytics get instance => FirebaseCrashlytics.instance;
/// Set the enabled state of Firebase Crashlytics.
///
/// If `enabled` is set to false, force-disables Analytics.
void setEnabledState() {
if(isPlatformCompatible) {
bool state = kReleaseMode && LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS.data;
instance.setCrashlyticsCollectionEnabled(state);
}
/// Set the enabled state of Firebase Crashlytics.
///
/// If `enabled` is set to false, force-disables Analytics.
void setEnabledState() {
if (isPlatformCompatible) {
bool state =
kReleaseMode && LunaDatabaseValue.ENABLE_FIREBASE_CRASHLYTICS.data;
instance.setCrashlyticsCollectionEnabled(state);
}
}
}

View File

@@ -2,66 +2,78 @@ 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;
/// 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<bool> addBackupEntry(String id, int timestamp, { String title = '', String description = '' }) async {
if(!LunaFirebaseAuth().isSignedIn) 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;
}
/// 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<bool> addBackupEntry(String id, int timestamp,
{String title = '', String description = ''}) async {
if (!LunaFirebaseAuth().isSignedIn) 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<bool> deleteBackupEntry(String id) async {
if(!LunaFirebaseAuth().isSignedIn) 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;
}
/// 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<bool> deleteBackupEntry(String id) async {
if (!LunaFirebaseAuth().isSignedIn) 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<List<LunaFirebaseBackupDocument>> 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<LunaFirebaseBackupDocument>((document) => LunaFirebaseBackupDocument.fromQueryDocumentSnapshot(document)).toList();
} catch (error, stack) {
LunaLogger().error('Failed to get backup list', error, stack);
return [];
}
/// Returns a list of all backups available for this account.
///
/// If the user is not signed in, returns an empty list.
Future<List<LunaFirebaseBackupDocument>> 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<LunaFirebaseBackupDocument>((document) =>
LunaFirebaseBackupDocument.fromQueryDocumentSnapshot(document))
.toList();
} catch (error, stack) {
LunaLogger().error('Failed to get backup list', error, stack);
return [];
}
}
/// Add the current device token to Firestore. Returns true if successful, and false on any error.
Future<bool> addDeviceToken() async {
if(!LunaFirebaseAuth().isSignedIn) return false;
try {
String token = await LunaFirebaseMessaging.instance.getToken();
instance.doc('users/${LunaFirebaseAuth().uid}').set({
'devices': FieldValue.arrayUnion([token]),
}, SetOptions(merge: true));
return true;
} catch (error, stack) {
LunaLogger().error('Failed to add device token', error, stack);
return false;
}
/// Add the current device token to Firestore. Returns true if successful, and false on any error.
Future<bool> addDeviceToken() async {
if (!LunaFirebaseAuth().isSignedIn) return false;
try {
String token = await LunaFirebaseMessaging.instance.getToken();
instance.doc('users/${LunaFirebaseAuth().uid}').set({
'devices': FieldValue.arrayUnion([token]),
}, SetOptions(merge: true));
return true;
} catch (error, stack) {
LunaLogger().error('Failed to add device token', error, stack);
return false;
}
}
}

View File

@@ -3,119 +3,132 @@ 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;
/// Returns an instance of [FirebaseMessaging].
///
/// 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<RemoteMessage> get onMessage => FirebaseMessaging.onMessage;
/// Returns a [Stream] to handle any new messages that are received while the application is in the open and in foreground.
Stream<RemoteMessage> get onMessage => FirebaseMessaging.onMessage;
/// Returns a [Stream] to handle any notifications that are tapped while the application is in the background (not terminated).
Stream<RemoteMessage> get onMessageOpenedApp => FirebaseMessaging.onMessageOpenedApp;
/// Returns a [Stream] to handle any notifications that are tapped while the application is in the background (not terminated).
Stream<RemoteMessage> get onMessageOpenedApp =>
FirebaseMessaging.onMessageOpenedApp;
/// Returns the Firebase Cloud Messaging device token for this device.
Future<String> get token async => instance.getToken();
/// Returns the Firebase Cloud Messaging device token for this device.
Future<String> get token async => instance.getToken();
/// Request for permission to send a user notifications.
///
/// Returns true if permissions are allowed at either a full or provisional level.
/// Returns false if permissions are denied or not determined.
Future<bool> requestNotificationPermissions() async {
try {
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;
}
} catch (error, stack) {
LunaLogger().error('Failed to request notification permission', error, stack);
return false;
}
/// 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<bool> requestNotificationPermissions() async {
try {
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;
}
} catch (error, stack) {
LunaLogger()
.error('Failed to request notification permission', error, stack);
return false;
}
}
/// Return the current notification authorization status.
Future<AuthorizationStatus> getAuthorizationStatus() async {
return instance.getNotificationSettings().then((settings) => settings.authorizationStatus);
/// Return the current notification authorization status.
Future<AuthorizationStatus> getAuthorizationStatus() async {
return instance
.getNotificationSettings()
.then((settings) => settings.authorizationStatus);
}
/// Returns true if permissions are allowed at either a full or provisional level.
/// Returns false on any other status (denied, not determined, null, etc.).
Future<bool> areNotificationsAllowed() async {
return instance?.getNotificationSettings()?.then((settings) {
switch (settings.authorizationStatus) {
case AuthorizationStatus.authorized:
case AuthorizationStatus.provisional:
return true;
case AuthorizationStatus.denied:
case AuthorizationStatus.notDetermined:
default:
return false;
}
});
}
/// Return a [StreamSubscription] that will show a notification banner on a newly received notification.
///
/// This listens on [FirebaseMessaging.onMessage], where the application must be open and in the foreground.
StreamSubscription<RemoteMessage> onMessageListener() {
return onMessage.listen((message) {
if (message == null) return;
LunaModule module = (message.data ?? {}).isNotEmpty
? LunaModule.DASHBOARD.fromKey(message.data['module'])
: null;
showLunaSnackBar(
title: message.notification?.title ?? 'Unknown Content',
message: message.notification?.body ?? LunaUI.TEXT_EMDASH,
type: LunaSnackbarType.INFO,
position: FlashPosition.top,
duration: Duration(seconds: 6, milliseconds: 750),
showButton: module != null,
buttonOnPressed: () async => _handleWebhook(message),
);
});
}
/// Returns a [StreamSubscription] that will handle messages/notifications that are opened while LunaSea is running in the background.
///
/// This listens on [FirebaseMessaging.onMessageOpenedApp], where the application must be open but in the background.
StreamSubscription<RemoteMessage> onMessageOpenedAppListener() =>
onMessageOpenedApp.listen(_handleWebhook);
/// Check to see if there was an initial [RemoteMessage] available to be accessed.
///
/// If so, handles the notification webhook.
Future<void> checkAndHandleInitialMessage() async {
RemoteMessage message =
await FirebaseMessaging.instance.getInitialMessage();
_handleWebhook(message);
}
/// Shared webhook handler.
Future<void> _handleWebhook(RemoteMessage message) async {
if (message == null || (message.data ?? {}).isEmpty) return;
// Extract module
LunaModule module = LunaModule.DASHBOARD.fromKey(message.data['module']);
if (module == null) {
LunaLogger().warning(
'LunaFirebaseMessaging',
'_handleWebhook',
'Unknown module found inside of RemoteMessage: ${message.data['module'] ?? 'null'}',
);
return;
}
/// Returns true if permissions are allowed at either a full or provisional level.
/// Returns false on any other status (denied, not determined, null, etc.).
Future<bool> areNotificationsAllowed() async {
return instance?.getNotificationSettings()
?.then((settings) {
switch(settings.authorizationStatus) {
case AuthorizationStatus.authorized:
case AuthorizationStatus.provisional: return true;
case AuthorizationStatus.denied:
case AuthorizationStatus.notDetermined:
default: return false;
}
});
String profile = message.data['profile'];
if (profile?.isEmpty ?? true) {
LunaLogger().warning(
'LunaFirebaseMessaging',
'_handleWebhook',
'Invalid profile received in webhook: ${message.data['profile'] ?? 'null'}',
);
return;
}
/// Return a [StreamSubscription] that will show a notification banner on a newly received notification.
///
/// This listens on [FirebaseMessaging.onMessage], where the application must be open and in the foreground.
StreamSubscription<RemoteMessage> onMessageListener() {
return onMessage.listen((message) {
if(message == null) return;
LunaModule module = (message.data ?? {}).isNotEmpty ? LunaModule.DASHBOARD.fromKey(message.data['module']) : null;
showLunaSnackBar(
title: message.notification?.title ?? 'Unknown Content',
message: message.notification?.body ?? LunaUI.TEXT_EMDASH,
type: LunaSnackbarType.INFO,
position: FlashPosition.top,
duration: Duration(seconds: 6, milliseconds: 750),
showButton: module != null,
buttonOnPressed: () async => _handleWebhook(message),
);
});
}
/// Returns a [StreamSubscription] that will handle messages/notifications that are opened while LunaSea is running in the background.
///
/// This listens on [FirebaseMessaging.onMessageOpenedApp], where the application must be open but in the background.
StreamSubscription<RemoteMessage> onMessageOpenedAppListener() => onMessageOpenedApp.listen(_handleWebhook);
/// Check to see if there was an initial [RemoteMessage] available to be accessed.
///
/// If so, handles the notification webhook.
Future<void> checkAndHandleInitialMessage() async {
RemoteMessage message = await FirebaseMessaging.instance.getInitialMessage();
_handleWebhook(message);
}
/// Shared webhook handler.
Future<void> _handleWebhook(RemoteMessage message) async {
if(message == null || (message.data ?? {}).isEmpty) return;
// Extract module
LunaModule module = LunaModule.DASHBOARD.fromKey(message.data['module']);
if(module == null) {
LunaLogger().warning(
'LunaFirebaseMessaging',
'_handleWebhook',
'Unknown module found inside of RemoteMessage: ${message.data['module'] ?? 'null'}',
);
return;
}
String profile = message.data['profile'];
if(profile?.isEmpty ?? true) {
LunaLogger().warning(
'LunaFirebaseMessaging',
'_handleWebhook',
'Invalid profile received in webhook: ${message.data['profile'] ?? 'null'}',
);
return;
}
bool result = await LunaProfile().safelyChangeProfiles(profile);
if(result) {
module?.handleWebhook(message.data);
} else {
showLunaErrorSnackBar(title: 'Unknown Profile', message: '"$profile" does not exist in LunaSea');
}
bool result = await LunaProfile().safelyChangeProfiles(profile);
if (result) {
module?.handleWebhook(message.data);
} else {
showLunaErrorSnackBar(
title: 'Unknown Profile',
message: '"$profile" does not exist in LunaSea');
}
}
}

View File

@@ -3,57 +3,65 @@ import 'package:firebase_storage/firebase_storage.dart';
import 'package:lunasea/core.dart';
class LunaFirebaseStorage {
static const String _BACKUP_BUCKET = 'backup.lunasea.app';
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 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);
/// 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<bool> 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;
}
/// Upload a backup configuration to Firebase storage.
///
/// If the user is not signed in, returns null.
Future<bool> 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<bool> 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;
}
/// Delete a backup configuration from Firebase storage.
///
/// If the user is not signed in, returns null.
Future<bool> 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<String> 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;
}
/// Download a backup configuration from Firebase storage.
///
/// If the user is not signed in, returns null.
Future<String> 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;
}
}
}

View File

@@ -2,13 +2,13 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
class LunaFirebaseAuthResponse {
final bool state;
final User user;
final FirebaseAuthException error;
final bool state;
final User user;
final FirebaseAuthException error;
LunaFirebaseAuthResponse({
@required this.state,
@required this.user,
@required this.error,
});
LunaFirebaseAuthResponse({
@required this.state,
@required this.user,
@required this.error,
});
}

View File

@@ -2,30 +2,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;
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,
});
LunaFirebaseBackupDocument({
@required this.id,
@required this.timestamp,
@required this.title,
@required this.description,
});
factory LunaFirebaseBackupDocument.fromQueryDocumentSnapshot(QueryDocumentSnapshot document) => LunaFirebaseBackupDocument(
factory LunaFirebaseBackupDocument.fromQueryDocumentSnapshot(
QueryDocumentSnapshot document) =>
LunaFirebaseBackupDocument(
id: document.data()['id'],
timestamp: document.data()['timestamp'],
title: document.data()['title'],
description: document.data()['description'],
);
);
Map<String, dynamic> toJSON() => {
Map<String, dynamic> toJSON() => {
'id': id,
'title': title,
'timestamp': timestamp,
'description': description,
};
};
}

View File

@@ -2,12 +2,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<T extends StatefulWidget> on State<T> {
@mustCallSuper
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => loadCallback());
}
@mustCallSuper
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => loadCallback());
}
Future<void> loadCallback();
Future<void> loadCallback();
}

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
mixin LunaScrollControllerMixin<T extends StatefulWidget> on State<T> {
final ScrollController scrollController = ScrollController();
@mustCallSuper
@override
void dispose() {
scrollController?.dispose();
super.dispose();
}
final ScrollController scrollController = ScrollController();
@mustCallSuper
@override
void dispose() {
scrollController?.dispose();
super.dispose();
}
}

View File

@@ -6,63 +6,64 @@ part 'indexer.g.dart';
/// Hive database object containing all information on an indexer
@HiveType(typeId: 1, adapterName: 'IndexerHiveObjectAdapter')
class IndexerHiveObject extends HiveObject {
/// Create a new [IndexerHiveObject] object with all fields set to empty values('', false, 0, {}, etc.)
factory IndexerHiveObject.empty() => IndexerHiveObject(
/// Create a new [IndexerHiveObject] object with all fields set to empty values('', false, 0, {}, etc.)
factory IndexerHiveObject.empty() => IndexerHiveObject(
displayName: '',
host: '',
apiKey: '',
headers: {},
icon: null,
);
);
/// Create a new [IndexerHiveObject] from another [IndexerHiveObject] (deep-copy).
factory IndexerHiveObject.fromIndexerHiveObject(IndexerHiveObject indexer) => IndexerHiveObject(
/// Create a new [IndexerHiveObject] from another [IndexerHiveObject] (deep-copy).
factory IndexerHiveObject.fromIndexerHiveObject(IndexerHiveObject indexer) =>
IndexerHiveObject(
displayName: indexer.displayName,
host: indexer.host,
apiKey: indexer.apiKey,
headers: indexer.headers,
icon: indexer.icon,
);
);
/// Create a new [IndexerHiveObject] from a map where the keys map 1-to-1 except for "key", which was the old key for apiKey.
///
/// - Does _not_ do type checking, and will throw an error if the type is invalid.
/// - If the key is null, sets to the "empty" value
factory IndexerHiveObject.fromMap(Map indexer) => IndexerHiveObject(
/// Create a new [IndexerHiveObject] from a map where the keys map 1-to-1 except for "key", which was the old key for apiKey.
///
/// - Does _not_ do type checking, and will throw an error if the type is invalid.
/// - If the key is null, sets to the "empty" value
factory IndexerHiveObject.fromMap(Map indexer) => IndexerHiveObject(
displayName: indexer['displayName'] ?? '',
host: indexer['host'] ?? '',
apiKey: indexer['key'] ?? '',
headers: indexer['headers'] ?? {},
icon: LunaIndexerIcon.GENERIC.fromKey(indexer['icon'] ?? ''),
);
);
IndexerHiveObject({
@required this.displayName,
@required this.host,
@required this.apiKey,
@required this.headers,
@required this.icon,
});
IndexerHiveObject({
@required this.displayName,
@required this.host,
@required this.apiKey,
@required this.headers,
@required this.icon,
});
@override
String toString() => toMap().toString();
@override
String toString() => toMap().toString();
Map<String, dynamic> toMap() => {
Map<String, dynamic> toMap() => {
"displayName": displayName ?? '',
"host": host ?? '',
"key": apiKey ?? '',
"headers": headers ?? {},
"icon": icon?.key ?? '',
};
};
@HiveField(0)
String displayName;
@HiveField(1)
String host;
@HiveField(2)
String apiKey;
@HiveField(3)
Map headers;
@HiveField(4)
LunaIndexerIcon icon;
@HiveField(0)
String displayName;
@HiveField(1)
String host;
@HiveField(2)
String apiKey;
@HiveField(3)
Map headers;
@HiveField(4)
LunaIndexerIcon icon;
}

View File

@@ -7,8 +7,8 @@ part 'profile.g.dart';
/// Hive database object containing all profile fields
@HiveType(typeId: 0, adapterName: 'ProfileHiveObjectAdapter')
class ProfileHiveObject extends HiveObject {
/// Create a new [ProfileHiveObject] object with all fields set to empty values('', false, 0, {}, etc.)
factory ProfileHiveObject.empty() => ProfileHiveObject(
/// Create a new [ProfileHiveObject] object with all fields set to empty values('', false, 0, {}, etc.)
factory ProfileHiveObject.empty() => ProfileHiveObject(
//Lidarr
lidarrEnabled: false,
lidarrHost: '',
@@ -56,10 +56,11 @@ class ProfileHiveObject extends HiveObject {
overseerrHost: '',
overseerrKey: '',
overseerrHeaders: {},
);
);
/// Create a new [ProfileHiveObject] from another [ProfileHiveObject] (deep-copy).
factory ProfileHiveObject.fromProfileHiveObject(ProfileHiveObject profile) => ProfileHiveObject(
/// Create a new [ProfileHiveObject] from another [ProfileHiveObject] (deep-copy).
factory ProfileHiveObject.fromProfileHiveObject(ProfileHiveObject profile) =>
ProfileHiveObject(
//Lidarr
lidarrEnabled: profile.lidarrEnabled,
lidarrHost: profile.lidarrHost,
@@ -107,13 +108,13 @@ class ProfileHiveObject extends HiveObject {
overseerrHost: profile.overseerrHost,
overseerrKey: profile.overseerrKey,
overseerrHeaders: profile.overseerrHeaders,
);
);
/// Create a new [ProfileHiveObject] from a map where the keys map 1-to-1.
///
/// - Does _not_ do type checking, and will throw an error if the type is invalid.
/// - If the key is null, sets to the "empty" value
factory ProfileHiveObject.fromMap(Map profile) => ProfileHiveObject(
/// Create a new [ProfileHiveObject] from a map where the keys map 1-to-1.
///
/// - Does _not_ do type checking, and will throw an error if the type is invalid.
/// - If the key is null, sets to the "empty" value
factory ProfileHiveObject.fromMap(Map profile) => ProfileHiveObject(
//Sonarr
sonarrEnabled: profile['sonarrEnabled'] ?? false,
sonarrHost: profile['sonarrHost'] ?? '',
@@ -161,63 +162,62 @@ class ProfileHiveObject extends HiveObject {
overseerrHost: profile['overseerrHost'] ?? '',
overseerrKey: profile['overseerrKey'] ?? '',
overseerrHeaders: profile['overseerrHeaders'] ?? {},
);
);
ProfileHiveObject({
//Lidarr
@required this.lidarrEnabled,
@required this.lidarrHost,
@required this.lidarrKey,
@required this.lidarrHeaders,
//Radarr
@required this.radarrEnabled,
@required this.radarrHost,
@required this.radarrKey,
@required this.radarrHeaders,
//Sonarr
@required this.sonarrEnabled,
@required this.sonarrHost,
@required this.sonarrKey,
@required this.sonarrVersion3,
@required this.sonarrHeaders,
//SABnzbd
@required this.sabnzbdEnabled,
@required this.sabnzbdHost,
@required this.sabnzbdKey,
@required this.sabnzbdHeaders,
//NZBGet
@required this.nzbgetEnabled,
@required this.nzbgetHost,
@required this.nzbgetUser,
@required this.nzbgetPass,
@required this.nzbgetBasicAuth,
@required this.nzbgetHeaders,
//Wake On LAN
@required this.wakeOnLANEnabled,
@required this.wakeOnLANBroadcastAddress,
@required this.wakeOnLANMACAddress,
//Tautulli
@required this.tautulliEnabled,
@required this.tautulliHost,
@required this.tautulliKey,
@required this.tautulliHeaders,
//Ombi
@required this.ombiEnabled,
@required this.ombiHost,
@required this.ombiKey,
@required this.ombiHeaders,
//Overseerr
@required this.overseerrEnabled,
@required this.overseerrHost,
@required this.overseerrKey,
@required this.overseerrHeaders,
});
ProfileHiveObject({
//Lidarr
@required this.lidarrEnabled,
@required this.lidarrHost,
@required this.lidarrKey,
@required this.lidarrHeaders,
//Radarr
@required this.radarrEnabled,
@required this.radarrHost,
@required this.radarrKey,
@required this.radarrHeaders,
//Sonarr
@required this.sonarrEnabled,
@required this.sonarrHost,
@required this.sonarrKey,
@required this.sonarrVersion3,
@required this.sonarrHeaders,
//SABnzbd
@required this.sabnzbdEnabled,
@required this.sabnzbdHost,
@required this.sabnzbdKey,
@required this.sabnzbdHeaders,
//NZBGet
@required this.nzbgetEnabled,
@required this.nzbgetHost,
@required this.nzbgetUser,
@required this.nzbgetPass,
@required this.nzbgetBasicAuth,
@required this.nzbgetHeaders,
//Wake On LAN
@required this.wakeOnLANEnabled,
@required this.wakeOnLANBroadcastAddress,
@required this.wakeOnLANMACAddress,
//Tautulli
@required this.tautulliEnabled,
@required this.tautulliHost,
@required this.tautulliKey,
@required this.tautulliHeaders,
//Ombi
@required this.ombiEnabled,
@required this.ombiHost,
@required this.ombiKey,
@required this.ombiHeaders,
//Overseerr
@required this.overseerrEnabled,
@required this.overseerrHost,
@required this.overseerrKey,
@required this.overseerrHeaders,
});
@override
String toString() => toMap().toString();
@override
String toString() => toMap().toString();
Map<String, dynamic> toMap() => {
Map<String, dynamic> toMap() => {
"key": key,
//Sonarr
"sonarrEnabled": sonarrEnabled,
@@ -266,191 +266,199 @@ class ProfileHiveObject extends HiveObject {
"overseerrHost": overseerrHost,
"overseerrKey": overseerrKey,
"overseerrHeaders": overseerrHeaders,
};
};
//Lidarr
@HiveField(0)
bool lidarrEnabled;
@HiveField(1)
String lidarrHost;
@HiveField(2)
String lidarrKey;
@HiveField(26)
Map lidarrHeaders;
//Lidarr
@HiveField(0)
bool lidarrEnabled;
@HiveField(1)
String lidarrHost;
@HiveField(2)
String lidarrKey;
@HiveField(26)
Map lidarrHeaders;
Map<String, dynamic> getLidarr() => {
Map<String, dynamic> getLidarr() => {
'enabled': lidarrEnabled ?? false,
'host': lidarrHost ?? '',
'key': lidarrKey ?? '',
'headers': lidarrHeaders ?? {},
};
};
//Radarr
@HiveField(3)
bool radarrEnabled;
@HiveField(4)
String radarrHost;
@HiveField(5)
String radarrKey;
@HiveField(27)
Map radarrHeaders;
//Radarr
@HiveField(3)
bool radarrEnabled;
@HiveField(4)
String radarrHost;
@HiveField(5)
String radarrKey;
@HiveField(27)
Map radarrHeaders;
Map<String, dynamic> getRadarr() => {
Map<String, dynamic> getRadarr() => {
'enabled': radarrEnabled ?? false,
'host': radarrHost ?? '',
'key': radarrKey ?? '',
'headers': radarrHeaders ?? {},
};
};
//Sonarr
@HiveField(6)
bool sonarrEnabled;
@HiveField(7)
String sonarrHost;
@HiveField(8)
String sonarrKey;
@HiveField(21)
bool sonarrVersion3;
@HiveField(28)
Map sonarrHeaders;
//Sonarr
@HiveField(6)
bool sonarrEnabled;
@HiveField(7)
String sonarrHost;
@HiveField(8)
String sonarrKey;
@HiveField(21)
bool sonarrVersion3;
@HiveField(28)
Map sonarrHeaders;
Map<String, dynamic> getSonarr() => {
Map<String, dynamic> getSonarr() => {
'enabled': sonarrEnabled ?? false,
'host': sonarrHost ?? '',
'key': sonarrKey ?? '',
'v3': sonarrVersion3 ?? false,
'headers': sonarrHeaders ?? {},
};
};
//SABnzbd
@HiveField(9)
bool sabnzbdEnabled;
@HiveField(10)
String sabnzbdHost;
@HiveField(11)
String sabnzbdKey;
@HiveField(29)
Map sabnzbdHeaders;
Map<String, dynamic> getSABnzbd() => {
//SABnzbd
@HiveField(9)
bool sabnzbdEnabled;
@HiveField(10)
String sabnzbdHost;
@HiveField(11)
String sabnzbdKey;
@HiveField(29)
Map sabnzbdHeaders;
Map<String, dynamic> getSABnzbd() => {
'enabled': sabnzbdEnabled ?? false,
'host': sabnzbdHost ?? '',
'key': sabnzbdKey ?? '',
'headers': sabnzbdHeaders ?? {},
};
};
//NZBGet
@HiveField(12)
bool nzbgetEnabled;
@HiveField(13)
String nzbgetHost;
@HiveField(14)
String nzbgetUser;
@HiveField(15)
String nzbgetPass;
@HiveField(22)
bool nzbgetBasicAuth;
@HiveField(30)
Map nzbgetHeaders;
//NZBGet
@HiveField(12)
bool nzbgetEnabled;
@HiveField(13)
String nzbgetHost;
@HiveField(14)
String nzbgetUser;
@HiveField(15)
String nzbgetPass;
@HiveField(22)
bool nzbgetBasicAuth;
@HiveField(30)
Map nzbgetHeaders;
Map<String, dynamic> getNZBGet() => {
Map<String, dynamic> getNZBGet() => {
'enabled': nzbgetEnabled ?? false,
'host': nzbgetHost ?? '',
'user': nzbgetUser ?? '',
'pass': nzbgetPass ?? '',
'basic_auth': nzbgetBasicAuth ?? false,
'headers': nzbgetHeaders ?? {},
};
};
//Wake On LAN
@HiveField(23)
bool wakeOnLANEnabled;
@HiveField(24)
String wakeOnLANBroadcastAddress;
@HiveField(25)
String wakeOnLANMACAddress;
//Wake On LAN
@HiveField(23)
bool wakeOnLANEnabled;
@HiveField(24)
String wakeOnLANBroadcastAddress;
@HiveField(25)
String wakeOnLANMACAddress;
Map<String, dynamic> getWakeOnLAN() => {
Map<String, dynamic> getWakeOnLAN() => {
'enabled': wakeOnLANEnabled ?? false,
'broadcastAddress': wakeOnLANBroadcastAddress ?? '',
'MACAddress': wakeOnLANMACAddress ?? '',
};
};
//Tautulli
@HiveField(31)
bool tautulliEnabled;
@HiveField(32)
String tautulliHost;
@HiveField(33)
String tautulliKey;
@HiveField(35)
Map tautulliHeaders;
//Tautulli
@HiveField(31)
bool tautulliEnabled;
@HiveField(32)
String tautulliHost;
@HiveField(33)
String tautulliKey;
@HiveField(35)
Map tautulliHeaders;
Map<String, dynamic> getTautulli() => {
Map<String, dynamic> getTautulli() => {
'enabled': tautulliEnabled ?? false,
'host': tautulliHost ?? '',
'key': tautulliKey ?? '',
'headers': tautulliHeaders ?? {},
};
};
//Ombi
@HiveField(36)
bool ombiEnabled;
@HiveField(37)
String ombiHost;
@HiveField(38)
String ombiKey;
@HiveField(39)
Map ombiHeaders;
//Ombi
@HiveField(36)
bool ombiEnabled;
@HiveField(37)
String ombiHost;
@HiveField(38)
String ombiKey;
@HiveField(39)
Map ombiHeaders;
Map<String, dynamic> getOmbi() => {
Map<String, dynamic> getOmbi() => {
'enabled': ombiEnabled ?? false,
'host': ombiHost ?? '',
'key': ombiKey ?? '',
'headers': ombiHeaders ?? {},
};
};
//Overseerr
@HiveField(40)
bool overseerrEnabled;
@HiveField(41)
String overseerrHost;
@HiveField(42)
String overseerrKey;
@HiveField(43)
Map overseerrHeaders;
//Overseerr
@HiveField(40)
bool overseerrEnabled;
@HiveField(41)
String overseerrHost;
@HiveField(42)
String overseerrKey;
@HiveField(43)
Map overseerrHeaders;
Map<String, dynamic> getOverseerr() => {
Map<String, dynamic> getOverseerr() => {
'enabled': overseerrEnabled ?? false,
'host': overseerrHost ?? '',
'key': overseerrKey ?? '',
'headers': overseerrHeaders ?? {},
};
};
List<LunaModule> get enabledModules => [
List<LunaModule> get enabledModules => [
...enabledAutomationModules,
...enabledClientModules,
...enabledMonitoringModules,
];
];
List<LunaModule> get enabledAutomationModules => [
if(LunaModule.LIDARR.enabled && (lidarrEnabled ?? false)) LunaModule.LIDARR,
if(LunaModule.RADARR.enabled && (radarrEnabled ?? false)) LunaModule.RADARR,
if(LunaModule.SONARR.enabled && (sonarrEnabled ?? false)) LunaModule.SONARR,
];
List<LunaModule> get enabledAutomationModules => [
if (LunaModule.LIDARR.enabled && (lidarrEnabled ?? false))
LunaModule.LIDARR,
if (LunaModule.RADARR.enabled && (radarrEnabled ?? false))
LunaModule.RADARR,
if (LunaModule.SONARR.enabled && (sonarrEnabled ?? false))
LunaModule.SONARR,
];
List<LunaModule> get enabledClientModules => [
if(LunaModule.NZBGET.enabled && (nzbgetEnabled ?? false)) LunaModule.NZBGET,
if(LunaModule.SABNZBD.enabled && (sabnzbdEnabled ?? false)) LunaModule.SABNZBD,
];
List<LunaModule> get enabledClientModules => [
if (LunaModule.NZBGET.enabled && (nzbgetEnabled ?? false))
LunaModule.NZBGET,
if (LunaModule.SABNZBD.enabled && (sabnzbdEnabled ?? false))
LunaModule.SABNZBD,
];
List<LunaModule> get enabledMonitoringModules => [
if(LunaModule.OVERSEERR.enabled && (overseerrEnabled ?? false)) LunaModule.OVERSEERR,
if(LunaModule.TAUTULLI.enabled && (tautulliEnabled ?? false)) LunaModule.TAUTULLI,
];
List<LunaModule> get enabledMonitoringModules => [
if (LunaModule.OVERSEERR.enabled && (overseerrEnabled ?? false))
LunaModule.OVERSEERR,
if (LunaModule.TAUTULLI.enabled && (tautulliEnabled ?? false))
LunaModule.TAUTULLI,
];
bool get anyAutomationEnabled => enabledAutomationModules.isNotEmpty;
bool get anyClientsEnabled => enabledClientModules.isNotEmpty;
bool get anyMonitoringEnabled => enabledMonitoringModules.isNotEmpty;
bool get anythingEnabled => anyAutomationEnabled || anyClientsEnabled || anyMonitoringEnabled;
bool get anyAutomationEnabled => enabledAutomationModules.isNotEmpty;
bool get anyClientsEnabled => enabledClientModules.isNotEmpty;
bool get anyMonitoringEnabled => enabledMonitoringModules.isNotEmpty;
bool get anythingEnabled =>
anyAutomationEnabled || anyClientsEnabled || anyMonitoringEnabled;
}

View File

@@ -7,82 +7,85 @@ part 'log.g.dart';
/// Hive database object containing all information on a log
@HiveType(typeId: 23, adapterName: 'LunaLogHiveObjectAdapter')
class LunaLogHiveObject extends HiveObject {
LunaLogHiveObject({
@required this.timestamp,
@required this.type,
this.className,
this.methodName,
@required this.message,
this.error,
this.stackTrace,
});
LunaLogHiveObject({
@required this.timestamp,
@required this.type,
this.className,
this.methodName,
@required this.message,
this.error,
this.stackTrace,
});
factory LunaLogHiveObject.withMessage({
@required LunaLogType type,
@required String message,
String className,
String methodName,
}) {
assert(type != null);
int timestamp = DateTime.now().millisecondsSinceEpoch;
return LunaLogHiveObject(
timestamp: timestamp,
type: type,
message: message,
);
}
factory LunaLogHiveObject.withMessage({
@required LunaLogType type,
@required String message,
String className,
String methodName,
}) {
assert(type != null);
int timestamp = DateTime.now().millisecondsSinceEpoch;
return LunaLogHiveObject(
timestamp: timestamp,
type: type,
message: message,
);
}
factory LunaLogHiveObject.withError({
@required LunaLogType type,
@required String message,
@required dynamic error,
@required StackTrace stackTrace,
String className,
String methodName,
}) {
assert(type != null);
int timestamp = DateTime.now().millisecondsSinceEpoch;
Trace trace = stackTrace == null ? null : Trace.from(stackTrace);
String _className, _methodName;
if((trace?.frames?.length ?? 0) >= 1) {
_className = trace.frames[0]?.uri?.toString();
_methodName = trace.frames[0]?.member?.toString();
}
return LunaLogHiveObject(
timestamp: timestamp,
type: type,
className: className ?? _className,
methodName: methodName ?? _methodName,
message: message,
error: error?.toString(),
stackTrace: trace?.toString(),
);
factory LunaLogHiveObject.withError({
@required LunaLogType type,
@required String message,
@required dynamic error,
@required StackTrace stackTrace,
String className,
String methodName,
}) {
assert(type != null);
int timestamp = DateTime.now().millisecondsSinceEpoch;
Trace trace = stackTrace == null ? null : Trace.from(stackTrace);
String _className, _methodName;
if ((trace?.frames?.length ?? 0) >= 1) {
_className = trace.frames[0]?.uri?.toString();
_methodName = trace.frames[0]?.member?.toString();
}
return LunaLogHiveObject(
timestamp: timestamp,
type: type,
className: className ?? _className,
methodName: methodName ?? _methodName,
message: message,
error: error?.toString(),
stackTrace: trace?.toString(),
);
}
Map<String, dynamic> toMap() {
return {
"timestamp": DateTime.fromMillisecondsSinceEpoch(timestamp).toIso8601String(),
"type": type.name,
if(className != null && className.isNotEmpty) "class_name": className,
if(methodName != null && methodName.isNotEmpty) "method_name": methodName,
"message": message,
if(error != null && (error?.isNotEmpty ?? false)) "error": error,
if(stackTrace != null && (stackTrace?.isNotEmpty ?? false)) "stack_trace": stackTrace?.trim()?.split('\n') ?? [],
};
}
@HiveField(0)
final int timestamp;
@HiveField(1)
final LunaLogType type;
@HiveField(2)
final String className;
@HiveField(3)
final String methodName;
@HiveField(4)
final String message;
@HiveField(5)
final String error;
@HiveField(6)
final String stackTrace;
Map<String, dynamic> toMap() {
return {
"timestamp":
DateTime.fromMillisecondsSinceEpoch(timestamp).toIso8601String(),
"type": type.name,
if (className != null && className.isNotEmpty) "class_name": className,
if (methodName != null && methodName.isNotEmpty)
"method_name": methodName,
"message": message,
if (error != null && (error?.isNotEmpty ?? false)) "error": error,
if (stackTrace != null && (stackTrace?.isNotEmpty ?? false))
"stack_trace": stackTrace?.trim()?.split('\n') ?? [],
};
}
@HiveField(0)
final int timestamp;
@HiveField(1)
final LunaLogType type;
@HiveField(2)
final String className;
@HiveField(3)
final String methodName;
@HiveField(4)
final String message;
@HiveField(5)
final String error;
@HiveField(6)
final String stackTrace;
}

View File

@@ -6,76 +6,100 @@ part 'log_type.g.dart';
@HiveType(typeId: 24, adapterName: 'LunaLogTypeAdapter')
enum LunaLogType {
@HiveField(0)
WARNING,
@HiveField(1)
ERROR,
@HiveField(2)
CRITICAL,
@HiveField(3)
DEBUG,
@HiveField(0)
WARNING,
@HiveField(1)
ERROR,
@HiveField(2)
CRITICAL,
@HiveField(3)
DEBUG,
}
extension LunaLogTypeExtension on LunaLogType {
LunaLogType fromKey(String key) {
switch(key) {
case 'warning': return LunaLogType.WARNING;
case 'error': return LunaLogType.ERROR;
case 'critical': return LunaLogType.CRITICAL;
case 'debug': return LunaLogType.DEBUG;
}
return null;
LunaLogType fromKey(String key) {
switch (key) {
case 'warning':
return LunaLogType.WARNING;
case 'error':
return LunaLogType.ERROR;
case 'critical':
return LunaLogType.CRITICAL;
case 'debug':
return LunaLogType.DEBUG;
}
return null;
}
String get key {
switch(this) {
case LunaLogType.WARNING: return 'warning';
case LunaLogType.ERROR: return 'error';
case LunaLogType.CRITICAL: return 'critical';
case LunaLogType.DEBUG: return 'debug';
}
throw Exception('Invalid LunaLogType');
String get key {
switch (this) {
case LunaLogType.WARNING:
return 'warning';
case LunaLogType.ERROR:
return 'error';
case LunaLogType.CRITICAL:
return 'critical';
case LunaLogType.DEBUG:
return 'debug';
}
throw Exception('Invalid LunaLogType');
}
bool get enabled {
switch(this) {
case LunaLogType.WARNING: return true;
case LunaLogType.ERROR: return true;
case LunaLogType.CRITICAL: return true;
case LunaLogType.DEBUG: return kDebugMode;
}
throw Exception('Invalid LunaLogType');
bool get enabled {
switch (this) {
case LunaLogType.WARNING:
return true;
case LunaLogType.ERROR:
return true;
case LunaLogType.CRITICAL:
return true;
case LunaLogType.DEBUG:
return kDebugMode;
}
throw Exception('Invalid LunaLogType');
}
String get name {
switch(this) {
case LunaLogType.WARNING: return 'Warning';
case LunaLogType.ERROR: return 'Error';
case LunaLogType.CRITICAL: return 'Critical';
case LunaLogType.DEBUG: return 'Debug';
}
throw Exception('Invalid LunaLogType');
String get name {
switch (this) {
case LunaLogType.WARNING:
return 'Warning';
case LunaLogType.ERROR:
return 'Error';
case LunaLogType.CRITICAL:
return 'Critical';
case LunaLogType.DEBUG:
return 'Debug';
}
throw Exception('Invalid LunaLogType');
}
String get description => 'View $name Logs';
String get description => 'View $name Logs';
IconData get icon {
switch(this) {
case LunaLogType.WARNING: return Icons.warning_rounded;
case LunaLogType.ERROR: return Icons.report_rounded;
case LunaLogType.CRITICAL: return Icons.new_releases_rounded;
case LunaLogType.DEBUG: return Icons.bug_report_rounded;
}
throw Exception('Invalid LunaLogType');
IconData get icon {
switch (this) {
case LunaLogType.WARNING:
return Icons.warning_rounded;
case LunaLogType.ERROR:
return Icons.report_rounded;
case LunaLogType.CRITICAL:
return Icons.new_releases_rounded;
case LunaLogType.DEBUG:
return Icons.bug_report_rounded;
}
throw Exception('Invalid LunaLogType');
}
Color get color {
switch(this) {
case LunaLogType.WARNING: return LunaColours.orange;
case LunaLogType.ERROR: return LunaColours.red;
case LunaLogType.CRITICAL: return LunaColours.accent;
case LunaLogType.DEBUG: return LunaColours.blueGrey;
}
throw Exception('Invalid LunaLogType');
Color get color {
switch (this) {
case LunaLogType.WARNING:
return LunaColours.orange;
case LunaLogType.ERROR:
return LunaColours.red;
case LunaLogType.CRITICAL:
return LunaColours.accent;
case LunaLogType.DEBUG:
return LunaColours.blueGrey;
}
throw Exception('Invalid LunaLogType');
}
}

View File

@@ -7,76 +7,105 @@ part 'browser.g.dart';
@HiveType(typeId: 11, adapterName: 'LunaBrowserAdapter')
enum LunaBrowser {
@HiveField(0)
APPLE_SAFARI,
@HiveField(1)
BRAVE_BROWSER,
@HiveField(2)
GOOGLE_CHROME,
@HiveField(3)
MICROSOFT_EDGE,
@HiveField(4)
MOZILLA_FIREFOX,
@HiveField(0)
APPLE_SAFARI,
@HiveField(1)
BRAVE_BROWSER,
@HiveField(2)
GOOGLE_CHROME,
@HiveField(3)
MICROSOFT_EDGE,
@HiveField(4)
MOZILLA_FIREFOX,
}
extension LunaBrowserExtension on LunaBrowser {
String get name {
switch(this) {
case LunaBrowser.APPLE_SAFARI: return 'Apple Safari';
case LunaBrowser.BRAVE_BROWSER: return 'Brave Browser';
case LunaBrowser.GOOGLE_CHROME: return 'Google Chrome';
case LunaBrowser.MICROSOFT_EDGE: return 'Microsoft Edge';
case LunaBrowser.MOZILLA_FIREFOX: return 'Mozilla Firefox';
default: return null;
}
String get name {
switch (this) {
case LunaBrowser.APPLE_SAFARI:
return 'Apple Safari';
case LunaBrowser.BRAVE_BROWSER:
return 'Brave Browser';
case LunaBrowser.GOOGLE_CHROME:
return 'Google Chrome';
case LunaBrowser.MICROSOFT_EDGE:
return 'Microsoft Edge';
case LunaBrowser.MOZILLA_FIREFOX:
return 'Mozilla Firefox';
default:
return null;
}
}
String get key {
switch(this) {
case LunaBrowser.APPLE_SAFARI: return 'applesafari';
case LunaBrowser.BRAVE_BROWSER: return 'bravebrowser';
case LunaBrowser.GOOGLE_CHROME: return 'googlechrome';
case LunaBrowser.MICROSOFT_EDGE: return 'microsoftedge';
case LunaBrowser.MOZILLA_FIREFOX: return 'mozillafirefox';
default: return null;
}
String get key {
switch (this) {
case LunaBrowser.APPLE_SAFARI:
return 'applesafari';
case LunaBrowser.BRAVE_BROWSER:
return 'bravebrowser';
case LunaBrowser.GOOGLE_CHROME:
return 'googlechrome';
case LunaBrowser.MICROSOFT_EDGE:
return 'microsoftedge';
case LunaBrowser.MOZILLA_FIREFOX:
return 'mozillafirefox';
default:
return null;
}
}
LunaBrowser fromKey(String key) {
switch(key) {
case 'applesafari': return LunaBrowser.APPLE_SAFARI;
case 'bravebrowser': return LunaBrowser.BRAVE_BROWSER;
case 'googlechrome': return LunaBrowser.GOOGLE_CHROME;
case 'microsoftedge': return LunaBrowser.MICROSOFT_EDGE;
case 'mozillafirefox': return LunaBrowser.MOZILLA_FIREFOX;
default: return null;
}
LunaBrowser fromKey(String key) {
switch (key) {
case 'applesafari':
return LunaBrowser.APPLE_SAFARI;
case 'bravebrowser':
return LunaBrowser.BRAVE_BROWSER;
case 'googlechrome':
return LunaBrowser.GOOGLE_CHROME;
case 'microsoftedge':
return LunaBrowser.MICROSOFT_EDGE;
case 'mozillafirefox':
return LunaBrowser.MOZILLA_FIREFOX;
default:
return null;
}
}
IconData get icon {
switch(this) {
case LunaBrowser.APPLE_SAFARI: return LunaIcons.safari;
case LunaBrowser.BRAVE_BROWSER: return LunaIcons.bravebrowser;
case LunaBrowser.GOOGLE_CHROME: return LunaIcons.chrome;
case LunaBrowser.MICROSOFT_EDGE: return LunaIcons.microsoftedge;
case LunaBrowser.MOZILLA_FIREFOX: return LunaIcons.firefox;
default: return null;
}
IconData get icon {
switch (this) {
case LunaBrowser.APPLE_SAFARI:
return LunaIcons.safari;
case LunaBrowser.BRAVE_BROWSER:
return LunaIcons.bravebrowser;
case LunaBrowser.GOOGLE_CHROME:
return LunaIcons.chrome;
case LunaBrowser.MICROSOFT_EDGE:
return LunaIcons.microsoftedge;
case LunaBrowser.MOZILLA_FIREFOX:
return LunaIcons.firefox;
default:
return null;
}
}
String formatted(String url) {
bool isHttps = url.substring(0, min(8, url.length)) == 'https://';
switch(this) {
case LunaBrowser.BRAVE_BROWSER: return 'brave://open-url?url=$url';
case LunaBrowser.GOOGLE_CHROME: return isHttps
? url.replaceFirst('https://', 'googlechromes://')
: url.replaceFirst('http://', 'googlechrome://');
case LunaBrowser.MICROSOFT_EDGE: return isHttps
? url.replaceFirst('https://', 'microsoft-edge-https://')
: url.replaceFirst('http://', 'microsoft-edge-http://');
case LunaBrowser.MOZILLA_FIREFOX: return 'firefox://open-url?url=$url';
case LunaBrowser.APPLE_SAFARI:
default: return url;
}
String formatted(String url) {
bool isHttps = url.substring(0, min(8, url.length)) == 'https://';
switch (this) {
case LunaBrowser.BRAVE_BROWSER:
return 'brave://open-url?url=$url';
case LunaBrowser.GOOGLE_CHROME:
return isHttps
? url.replaceFirst('https://', 'googlechromes://')
: url.replaceFirst('http://', 'googlechrome://');
case LunaBrowser.MICROSOFT_EDGE:
return isHttps
? url.replaceFirst('https://', 'microsoft-edge-https://')
: url.replaceFirst('http://', 'microsoft-edge-http://');
case LunaBrowser.MOZILLA_FIREFOX:
return 'firefox://open-url?url=$url';
case LunaBrowser.APPLE_SAFARI:
default:
return url;
}
}
}
}

View File

@@ -5,72 +5,101 @@ part 'indexer_icon.g.dart';
@HiveType(typeId: 22, adapterName: 'LunaIndexerIconAdapter')
enum LunaIndexerIcon {
@HiveField(0)
GENERIC,
@HiveField(1)
DOGNZB,
@HiveField(2)
DRUNKENSLUG,
@HiveField(3)
NZBFINDER,
@HiveField(4)
NZBGEEK,
@HiveField(5)
NZBHYDRA,
@HiveField(6)
NZBSU,
@HiveField(0)
GENERIC,
@HiveField(1)
DOGNZB,
@HiveField(2)
DRUNKENSLUG,
@HiveField(3)
NZBFINDER,
@HiveField(4)
NZBGEEK,
@HiveField(5)
NZBHYDRA,
@HiveField(6)
NZBSU,
}
extension LunaIndexerIconExtension on LunaIndexerIcon {
LunaIndexerIcon fromKey(String key) {
switch(key) {
case 'generic': return LunaIndexerIcon.GENERIC;
case 'dognzb': return LunaIndexerIcon.DOGNZB;
case 'drunkenslug': return LunaIndexerIcon.DRUNKENSLUG;
case 'nzbfinder': return LunaIndexerIcon.NZBFINDER;
case 'nzbgeek': return LunaIndexerIcon.NZBGEEK;
case 'nzbhydra': return LunaIndexerIcon.NZBHYDRA;
case 'nzbsu': return LunaIndexerIcon.NZBSU;
default: return LunaIndexerIcon.GENERIC;
}
LunaIndexerIcon fromKey(String key) {
switch (key) {
case 'generic':
return LunaIndexerIcon.GENERIC;
case 'dognzb':
return LunaIndexerIcon.DOGNZB;
case 'drunkenslug':
return LunaIndexerIcon.DRUNKENSLUG;
case 'nzbfinder':
return LunaIndexerIcon.NZBFINDER;
case 'nzbgeek':
return LunaIndexerIcon.NZBGEEK;
case 'nzbhydra':
return LunaIndexerIcon.NZBHYDRA;
case 'nzbsu':
return LunaIndexerIcon.NZBSU;
default:
return LunaIndexerIcon.GENERIC;
}
}
String get key {
switch(this) {
case LunaIndexerIcon.GENERIC: return 'generic';
case LunaIndexerIcon.DOGNZB: return 'dognzb';
case LunaIndexerIcon.DRUNKENSLUG: return 'drunkenslug';
case LunaIndexerIcon.NZBFINDER: return 'nzbfinder';
case LunaIndexerIcon.NZBGEEK: return 'nzbgeek';
case LunaIndexerIcon.NZBHYDRA: return 'nzbhydra';
case LunaIndexerIcon.NZBSU: return 'nzbsu';
}
throw Exception('Unknown LunaIndexerIcon');
String get key {
switch (this) {
case LunaIndexerIcon.GENERIC:
return 'generic';
case LunaIndexerIcon.DOGNZB:
return 'dognzb';
case LunaIndexerIcon.DRUNKENSLUG:
return 'drunkenslug';
case LunaIndexerIcon.NZBFINDER:
return 'nzbfinder';
case LunaIndexerIcon.NZBGEEK:
return 'nzbgeek';
case LunaIndexerIcon.NZBHYDRA:
return 'nzbhydra';
case LunaIndexerIcon.NZBSU:
return 'nzbsu';
}
throw Exception('Unknown LunaIndexerIcon');
}
String get name {
switch(this) {
case LunaIndexerIcon.GENERIC: return 'Generic';
case LunaIndexerIcon.DOGNZB: return 'DOGnzb';
case LunaIndexerIcon.DRUNKENSLUG: return 'DrunkenSlug';
case LunaIndexerIcon.NZBFINDER: return 'NZBFinder';
case LunaIndexerIcon.NZBGEEK: return 'NZBGeek';
case LunaIndexerIcon.NZBHYDRA: return 'NZBHydra2';
case LunaIndexerIcon.NZBSU: return 'NZB.su';
}
throw Exception('Unknown LunaIndexerIcon');
String get name {
switch (this) {
case LunaIndexerIcon.GENERIC:
return 'Generic';
case LunaIndexerIcon.DOGNZB:
return 'DOGnzb';
case LunaIndexerIcon.DRUNKENSLUG:
return 'DrunkenSlug';
case LunaIndexerIcon.NZBFINDER:
return 'NZBFinder';
case LunaIndexerIcon.NZBGEEK:
return 'NZBGeek';
case LunaIndexerIcon.NZBHYDRA:
return 'NZBHydra2';
case LunaIndexerIcon.NZBSU:
return 'NZB.su';
}
throw Exception('Unknown LunaIndexerIcon');
}
IconData get icon {
switch(this) {
case LunaIndexerIcon.GENERIC: return Icons.rss_feed_rounded;
case LunaIndexerIcon.DOGNZB: return Icons.rss_feed_rounded;
case LunaIndexerIcon.DRUNKENSLUG: return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBFINDER: return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBGEEK: return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBHYDRA: return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBSU: return Icons.rss_feed_rounded;
}
throw Exception('Unknown LunaIndexerIcon');
IconData get icon {
switch (this) {
case LunaIndexerIcon.GENERIC:
return Icons.rss_feed_rounded;
case LunaIndexerIcon.DOGNZB:
return Icons.rss_feed_rounded;
case LunaIndexerIcon.DRUNKENSLUG:
return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBFINDER:
return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBGEEK:
return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBHYDRA:
return Icons.rss_feed_rounded;
case LunaIndexerIcon.NZBSU:
return Icons.rss_feed_rounded;
}
}
throw Exception('Unknown LunaIndexerIcon');
}
}

View File

@@ -1,38 +1,50 @@
import 'package:lunasea/core.dart';
enum LunaLinks {
CHANGELOG,
CRASHLYTICS,
DISCORD,
DOCUMENTATION,
FEEDBACK_BOARD,
GITHUB,
NOTIFICATIONS_GETTING_STARTED,
REDDIT,
SYSTEM_STATUS,
TESTFLIGHT,
WEBLATE,
WEBSITE,
CHANGELOG,
CRASHLYTICS,
DISCORD,
DOCUMENTATION,
FEEDBACK_BOARD,
GITHUB,
NOTIFICATIONS_GETTING_STARTED,
REDDIT,
SYSTEM_STATUS,
TESTFLIGHT,
WEBLATE,
WEBSITE,
}
extension LunaLinksExtension on LunaLinks {
String get url {
switch(this) {
case LunaLinks.CHANGELOG: return 'https://www.lunasea.app/changelog';
case LunaLinks.CRASHLYTICS: return 'https://firebase.google.com/products/crashlytics';
case LunaLinks.DISCORD: return 'https://www.lunasea.app/discord';
case LunaLinks.DOCUMENTATION: return 'https://www.lunasea.app/docs';
case LunaLinks.FEEDBACK_BOARD: return 'https://www.lunasea.app/feedback';
case LunaLinks.GITHUB: return 'https://www.lunasea.app/github';
case LunaLinks.REDDIT: return 'https://www.lunasea.app/reddit';
case LunaLinks.SYSTEM_STATUS: return 'https://www.lunasea.app/status';
case LunaLinks.TESTFLIGHT: return 'https://www.lunasea.app/testflight';
case LunaLinks.WEBLATE: return 'https:/www.lunasea.app/translate';
case LunaLinks.WEBSITE: return 'https://www.lunasea.app';
case LunaLinks.NOTIFICATIONS_GETTING_STARTED: return 'https://docs.lunasea.app/lunasea/notifications';
}
throw Exception('Invalid LunaLinks');
String get url {
switch (this) {
case LunaLinks.CHANGELOG:
return 'https://www.lunasea.app/changelog';
case LunaLinks.CRASHLYTICS:
return 'https://firebase.google.com/products/crashlytics';
case LunaLinks.DISCORD:
return 'https://www.lunasea.app/discord';
case LunaLinks.DOCUMENTATION:
return 'https://www.lunasea.app/docs';
case LunaLinks.FEEDBACK_BOARD:
return 'https://www.lunasea.app/feedback';
case LunaLinks.GITHUB:
return 'https://www.lunasea.app/github';
case LunaLinks.REDDIT:
return 'https://www.lunasea.app/reddit';
case LunaLinks.SYSTEM_STATUS:
return 'https://www.lunasea.app/status';
case LunaLinks.TESTFLIGHT:
return 'https://www.lunasea.app/testflight';
case LunaLinks.WEBLATE:
return 'https:/www.lunasea.app/translate';
case LunaLinks.WEBSITE:
return 'https://www.lunasea.app';
case LunaLinks.NOTIFICATIONS_GETTING_STARTED:
return 'https://docs.lunasea.app/lunasea/notifications';
}
throw Exception('Invalid LunaLinks');
}
Future<void> launch() async => this?.url?.lunaOpenGenericLink();
Future<void> launch() async => this?.url?.lunaOpenGenericLink();
}

View File

@@ -1,5 +1,5 @@
enum LunaLoadingState {
ACTIVE,
INACTIVE,
ERROR,
ACTIVE,
INACTIVE,
ERROR,
}

View File

@@ -1,12 +1,14 @@
import 'package:lunasea/core.dart';
abstract class LunaWebhooks {
/// Handle the incoming webhook for the module.
Future<void> handle(Map<dynamic, dynamic> data);
/// Handle the incoming webhook for the module.
Future<void> handle(Map<dynamic, dynamic> data);
/// Given a [LunaModule] and Firebase UID, return the module's webhook endpoint for the user.
static String buildUserTokenURL(String token, LunaModule module) => 'https://notify.lunasea.app/v1/${module.key}/user/$token';
/// Given a [LunaModule] and Firebase UID, return the module's webhook endpoint for the user.
static String buildUserTokenURL(String token, LunaModule module) =>
'https://notify.lunasea.app/v1/${module.key}/user/$token';
/// Given a [LunaModule] and Firebase device token, return the module's webhook endpoint for the device.
static String buildDeviceTokenURL(String token, LunaModule module) => 'https://notify.lunasea.app/v1/${module.key}/device/$token';
/// Given a [LunaModule] and Firebase device token, return the module's webhook endpoint for the device.
static String buildDeviceTokenURL(String token, LunaModule module) =>
'https://notify.lunasea.app/v1/${module.key}/device/$token';
}

View File

@@ -5,312 +5,501 @@ import 'package:lunasea/modules.dart';
import 'package:quick_actions/quick_actions.dart';
enum LunaModule {
DASHBOARD,
LIDARR,
NZBGET,
OVERSEERR,
RADARR,
SABNZBD,
SEARCH,
SETTINGS,
SONARR,
TAUTULLI,
WAKE_ON_LAN,
DASHBOARD,
LIDARR,
NZBGET,
OVERSEERR,
RADARR,
SABNZBD,
SEARCH,
SETTINGS,
SONARR,
TAUTULLI,
WAKE_ON_LAN,
}
extension LunaModuleExtension on LunaModule {
LunaModule fromKey(String key) {
switch(key) {
case 'dashboard': return LunaModule.DASHBOARD;
case 'lidarr': return LunaModule.LIDARR;
case 'nzbget': return LunaModule.NZBGET;
case 'radarr': return LunaModule.RADARR;
case 'sabnzbd': return LunaModule.SABNZBD;
case 'search': return LunaModule.SEARCH;
case 'settings': return LunaModule.SETTINGS;
case 'sonarr': return LunaModule.SONARR;
case 'overseerr': return LunaModule.OVERSEERR;
case 'tautulli': return LunaModule.TAUTULLI;
case 'wake_on_lan': return LunaModule.WAKE_ON_LAN;
}
LunaModule fromKey(String key) {
switch (key) {
case 'dashboard':
return LunaModule.DASHBOARD;
case 'lidarr':
return LunaModule.LIDARR;
case 'nzbget':
return LunaModule.NZBGET;
case 'radarr':
return LunaModule.RADARR;
case 'sabnzbd':
return LunaModule.SABNZBD;
case 'search':
return LunaModule.SEARCH;
case 'settings':
return LunaModule.SETTINGS;
case 'sonarr':
return LunaModule.SONARR;
case 'overseerr':
return LunaModule.OVERSEERR;
case 'tautulli':
return LunaModule.TAUTULLI;
case 'wake_on_lan':
return LunaModule.WAKE_ON_LAN;
}
return null;
}
String get key {
switch (this) {
case LunaModule.DASHBOARD:
return 'dashboard';
case LunaModule.LIDARR:
return 'lidarr';
case LunaModule.NZBGET:
return 'nzbget';
case LunaModule.RADARR:
return 'radarr';
case LunaModule.SABNZBD:
return 'sabnzbd';
case LunaModule.SEARCH:
return 'search';
case LunaModule.SETTINGS:
return 'settings';
case LunaModule.SONARR:
return 'sonarr';
case LunaModule.OVERSEERR:
return 'overseerr';
case LunaModule.TAUTULLI:
return 'tautulli';
case LunaModule.WAKE_ON_LAN:
return 'wake_on_lan';
}
throw Exception('Invalid LunaModule');
}
bool get enabled {
switch (this) {
// Always enabled
case LunaModule.DASHBOARD:
return true;
case LunaModule.SETTINGS:
return true;
// Modules
case LunaModule.SEARCH:
return true;
case LunaModule.WAKE_ON_LAN:
return true;
case LunaModule.LIDARR:
return true;
case LunaModule.RADARR:
return true;
case LunaModule.SONARR:
return true;
case LunaModule.NZBGET:
return true;
case LunaModule.SABNZBD:
return true;
case LunaModule.OVERSEERR:
return kDebugMode;
case LunaModule.TAUTULLI:
return true;
}
throw Exception('Invalid LunaModule');
}
String get name {
switch (this) {
case LunaModule.DASHBOARD:
return 'lunasea.Dashboard'.tr();
case LunaModule.LIDARR:
return 'Lidarr';
case LunaModule.NZBGET:
return 'NZBGet';
case LunaModule.RADARR:
return 'Radarr';
case LunaModule.SABNZBD:
return 'SABnzbd';
case LunaModule.SEARCH:
return 'search.Search'.tr();
case LunaModule.SETTINGS:
return 'lunasea.Settings'.tr();
case LunaModule.SONARR:
return 'Sonarr';
case LunaModule.TAUTULLI:
return 'Tautulli';
case LunaModule.OVERSEERR:
return 'Overseerr';
case LunaModule.WAKE_ON_LAN:
return 'Wake on LAN';
}
throw Exception('Invalid LunaModule');
}
String get route {
switch (this) {
case LunaModule.DASHBOARD:
return DashboardHomeRouter().route();
case LunaModule.LIDARR:
return Lidarr.ROUTE_NAME;
case LunaModule.NZBGET:
return NZBGet.ROUTE_NAME;
case LunaModule.RADARR:
return RadarrHomeRouter().route();
case LunaModule.SABNZBD:
return SABnzbd.ROUTE_NAME;
case LunaModule.SEARCH:
return SearchHomeRouter().route();
case LunaModule.SETTINGS:
return SettingsHomeRouter().route();
case LunaModule.SONARR:
return SonarrHomeRouter().route();
case LunaModule.TAUTULLI:
return TautulliHomeRouter().route();
case LunaModule.OVERSEERR:
return OverseerrHomeRouter().route();
case LunaModule.WAKE_ON_LAN:
return null;
}
throw Exception('Invalid LunaModule');
}
String get key {
switch(this) {
case LunaModule.DASHBOARD: return 'dashboard';
case LunaModule.LIDARR: return 'lidarr';
case LunaModule.NZBGET: return 'nzbget';
case LunaModule.RADARR: return 'radarr';
case LunaModule.SABNZBD: return 'sabnzbd';
case LunaModule.SEARCH: return 'search';
case LunaModule.SETTINGS: return 'settings';
case LunaModule.SONARR: return 'sonarr';
case LunaModule.OVERSEERR: return 'overseerr';
case LunaModule.TAUTULLI: return 'tautulli';
case LunaModule.WAKE_ON_LAN: return 'wake_on_lan';
}
throw Exception('Invalid LunaModule');
IconData get icon {
switch (this) {
case LunaModule.DASHBOARD:
return LunaIcons.home;
case LunaModule.LIDARR:
return LunaIcons.music;
case LunaModule.NZBGET:
return LunaIcons.nzbget;
case LunaModule.RADARR:
return LunaIcons.radarr;
case LunaModule.SABNZBD:
return LunaIcons.sabnzbd;
case LunaModule.SEARCH:
return Icons.search_rounded;
case LunaModule.SETTINGS:
return LunaIcons.settings;
case LunaModule.SONARR:
return LunaIcons.television;
case LunaModule.TAUTULLI:
return LunaIcons.tautulli;
case LunaModule.OVERSEERR:
return LunaIcons.overseerr;
case LunaModule.WAKE_ON_LAN:
return Icons.settings_remote;
}
throw Exception('Invalid LunaModule');
}
bool get enabled {
switch(this) {
// Always enabled
case LunaModule.DASHBOARD: return true;
case LunaModule.SETTINGS: return true;
// Modules
case LunaModule.SEARCH: return true;
case LunaModule.WAKE_ON_LAN: return true;
case LunaModule.LIDARR: return true;
case LunaModule.RADARR: return true;
case LunaModule.SONARR: return true;
case LunaModule.NZBGET: return true;
case LunaModule.SABNZBD: return true;
case LunaModule.OVERSEERR: return kDebugMode;
case LunaModule.TAUTULLI: return true;
}
throw Exception('Invalid LunaModule');
LunaModuleDatabase get database {
switch (this) {
case LunaModule.SETTINGS:
return null;
case LunaModule.WAKE_ON_LAN:
return null;
case LunaModule.DASHBOARD:
return DashboardDatabase();
case LunaModule.SEARCH:
return SearchDatabase();
case LunaModule.LIDARR:
return LidarrDatabase();
case LunaModule.RADARR:
return RadarrDatabase();
case LunaModule.SONARR:
return SonarrDatabase();
case LunaModule.NZBGET:
return NZBGetDatabase();
case LunaModule.SABNZBD:
return SABnzbdDatabase();
case LunaModule.OVERSEERR:
return OverseerrDatabase();
case LunaModule.TAUTULLI:
return TautulliDatabase();
}
throw Exception('Invalid LunaModule');
}
String get name {
switch(this) {
case LunaModule.DASHBOARD: return 'lunasea.Dashboard'.tr();
case LunaModule.LIDARR: return 'Lidarr';
case LunaModule.NZBGET: return 'NZBGet';
case LunaModule.RADARR: return 'Radarr';
case LunaModule.SABNZBD: return 'SABnzbd';
case LunaModule.SEARCH: return 'search.Search'.tr();
case LunaModule.SETTINGS: return 'lunasea.Settings'.tr();
case LunaModule.SONARR: return 'Sonarr';
case LunaModule.TAUTULLI: return 'Tautulli';
case LunaModule.OVERSEERR: return 'Overseerr';
case LunaModule.WAKE_ON_LAN: return 'Wake on LAN';
}
throw Exception('Invalid LunaModule');
LunaModuleState state(BuildContext context) {
switch (this) {
case LunaModule.WAKE_ON_LAN:
return null;
case LunaModule.DASHBOARD:
return context.read<DashboardState>();
case LunaModule.SETTINGS:
return context.read<SettingsState>();
case LunaModule.SEARCH:
return context.read<SearchState>();
case LunaModule.LIDARR:
return context.read<LidarrState>();
case LunaModule.RADARR:
return context.read<RadarrState>();
case LunaModule.SONARR:
return context.read<SonarrState>();
case LunaModule.NZBGET:
return context.read<NZBGetState>();
case LunaModule.SABNZBD:
return context.read<SABnzbdState>();
case LunaModule.OVERSEERR:
return context.read<OverseerrState>();
case LunaModule.TAUTULLI:
return context.read<TautulliState>();
}
throw Exception('Invalid LunaModule');
}
String get route {
switch(this) {
case LunaModule.DASHBOARD: return DashboardHomeRouter().route();
case LunaModule.LIDARR: return Lidarr.ROUTE_NAME;
case LunaModule.NZBGET: return NZBGet.ROUTE_NAME;
case LunaModule.RADARR: return RadarrHomeRouter().route();
case LunaModule.SABNZBD: return SABnzbd.ROUTE_NAME;
case LunaModule.SEARCH: return SearchHomeRouter().route();
case LunaModule.SETTINGS: return SettingsHomeRouter().route();
case LunaModule.SONARR: return SonarrHomeRouter().route();
case LunaModule.TAUTULLI: return TautulliHomeRouter().route();
case LunaModule.OVERSEERR: return OverseerrHomeRouter().route();
case LunaModule.WAKE_ON_LAN: return null;
}
throw Exception('Invalid LunaModule');
Color get color {
switch (this) {
case LunaModule.DASHBOARD:
return LunaColours.accent;
case LunaModule.LIDARR:
return Color(0xFF159552);
case LunaModule.NZBGET:
return Color(0xFF42D535);
case LunaModule.RADARR:
return Color(0xFFFEC333);
case LunaModule.SABNZBD:
return Color(0xFFFECC2B);
case LunaModule.SEARCH:
return LunaColours.accent;
case LunaModule.SETTINGS:
return LunaColours.accent;
case LunaModule.SONARR:
return Color(0xFF3FC6F4);
case LunaModule.TAUTULLI:
return Color(0xFFDBA23A);
case LunaModule.OVERSEERR:
return Color(0xFF6366F1);
case LunaModule.WAKE_ON_LAN:
return LunaColours.accent;
}
throw Exception('Invalid LunaModule');
}
IconData get icon {
switch(this) {
case LunaModule.DASHBOARD: return LunaIcons.home;
case LunaModule.LIDARR: return LunaIcons.music;
case LunaModule.NZBGET: return LunaIcons.nzbget;
case LunaModule.RADARR: return LunaIcons.radarr;
case LunaModule.SABNZBD: return LunaIcons.sabnzbd;
case LunaModule.SEARCH: return Icons.search_rounded;
case LunaModule.SETTINGS: return LunaIcons.settings;
case LunaModule.SONARR: return LunaIcons.television;
case LunaModule.TAUTULLI: return LunaIcons.tautulli;
case LunaModule.OVERSEERR: return LunaIcons.overseerr;
case LunaModule.WAKE_ON_LAN: return Icons.settings_remote;
}
throw Exception('Invalid LunaModule');
String get website {
switch (this) {
case LunaModule.DASHBOARD:
return null;
case LunaModule.LIDARR:
return 'https://lidarr.audio';
case LunaModule.NZBGET:
return 'https://nzbget.net';
case LunaModule.RADARR:
return 'https://radarr.video';
case LunaModule.SABNZBD:
return 'https://sabnzbd.org';
case LunaModule.SEARCH:
return null;
case LunaModule.SETTINGS:
return null;
case LunaModule.SONARR:
return 'https://sonarr.tv';
case LunaModule.TAUTULLI:
return 'https://tautulli.com';
case LunaModule.OVERSEERR:
return 'https://overseerr.dev';
case LunaModule.WAKE_ON_LAN:
return null;
}
throw Exception('Invalid LunaModule');
}
LunaModuleDatabase get database {
switch(this) {
case LunaModule.SETTINGS: return null;
case LunaModule.WAKE_ON_LAN: return null;
case LunaModule.DASHBOARD: return DashboardDatabase();
case LunaModule.SEARCH: return SearchDatabase();
case LunaModule.LIDARR: return LidarrDatabase();
case LunaModule.RADARR: return RadarrDatabase();
case LunaModule.SONARR: return SonarrDatabase();
case LunaModule.NZBGET: return NZBGetDatabase();
case LunaModule.SABNZBD: return SABnzbdDatabase();
case LunaModule.OVERSEERR: return OverseerrDatabase();
case LunaModule.TAUTULLI: return TautulliDatabase();
}
throw Exception('Invalid LunaModule');
String get github {
switch (this) {
case LunaModule.DASHBOARD:
return null;
case LunaModule.LIDARR:
return 'https://github.com/Lidarr/Lidarr';
case LunaModule.NZBGET:
return 'https://github.com/nzbget/nzbget';
case LunaModule.RADARR:
return 'https://github.com/Radarr/Radarr';
case LunaModule.SABNZBD:
return 'https://github.com/sabnzbd/sabnzbd';
case LunaModule.SEARCH:
return 'https://github.com/theotherp/nzbhydra2';
case LunaModule.SETTINGS:
return null;
case LunaModule.SONARR:
return 'https://github.com/Sonarr/Sonarr';
case LunaModule.TAUTULLI:
return 'https://github.com/Tautulli/Tautulli';
case LunaModule.OVERSEERR:
return 'https://github.com/sct/overseerr';
case LunaModule.WAKE_ON_LAN:
return null;
}
throw Exception('Invalid LunaModule');
}
LunaModuleState state(BuildContext context) {
switch(this) {
case LunaModule.WAKE_ON_LAN: return null;
case LunaModule.DASHBOARD: return context.read<DashboardState>();
case LunaModule.SETTINGS: return context.read<SettingsState>();
case LunaModule.SEARCH: return context.read<SearchState>();
case LunaModule.LIDARR: return context.read<LidarrState>();
case LunaModule.RADARR: return context.read<RadarrState>();
case LunaModule.SONARR: return context.read<SonarrState>();
case LunaModule.NZBGET: return context.read<NZBGetState>();
case LunaModule.SABNZBD: return context.read<SABnzbdState>();
case LunaModule.OVERSEERR: return context.read<OverseerrState>();
case LunaModule.TAUTULLI: return context.read<TautulliState>();
}
throw Exception('Invalid LunaModule');
String get description {
switch (this) {
case LunaModule.DASHBOARD:
return 'lunasea.Dashboard'.tr();
case LunaModule.LIDARR:
return 'Manage Music';
case LunaModule.NZBGET:
return 'Manage Usenet Downloads';
case LunaModule.RADARR:
return 'Manage Movies';
case LunaModule.SABNZBD:
return 'Manage Usenet Downloads';
case LunaModule.SEARCH:
return 'Search Newznab Indexers';
case LunaModule.SETTINGS:
return 'Configure LunaSea';
case LunaModule.SONARR:
return 'Manage Television Series';
case LunaModule.TAUTULLI:
return 'View Plex Activity';
case LunaModule.OVERSEERR:
return 'Manage Requests for New Content';
case LunaModule.WAKE_ON_LAN:
return 'Wake Your Machine';
}
throw Exception('Invalid LunaModule');
}
Color get color {
switch(this) {
case LunaModule.DASHBOARD: return LunaColours.accent;
case LunaModule.LIDARR: return Color(0xFF159552);
case LunaModule.NZBGET: return Color(0xFF42D535);
case LunaModule.RADARR: return Color(0xFFFEC333);
case LunaModule.SABNZBD: return Color(0xFFFECC2B);
case LunaModule.SEARCH: return LunaColours.accent;
case LunaModule.SETTINGS: return LunaColours.accent;
case LunaModule.SONARR: return Color(0xFF3FC6F4);
case LunaModule.TAUTULLI: return Color(0xFFDBA23A);
case LunaModule.OVERSEERR: return Color(0xFF6366F1);
case LunaModule.WAKE_ON_LAN: return LunaColours.accent;
}
throw Exception('Invalid LunaModule');
String get information {
switch (this) {
case LunaModule.DASHBOARD:
return null;
case LunaModule.LIDARR:
return '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.';
case LunaModule.NZBGET:
return 'NZBGet is a binary downloader, which downloads files from Usenet based on information given in nzb-files.';
case LunaModule.RADARR:
return 'Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.';
case LunaModule.SABNZBD:
return 'SABnzbd is a multi-platform binary newsgroup downloader. The program works in the background and simplifies the downloading verifying and extracting of files from Usenet.';
case LunaModule.SEARCH:
return 'LunaSea currently supports all indexers that support the newznab protocol, including NZBHydra2.';
case LunaModule.SETTINGS:
return null;
case LunaModule.SONARR:
return 'Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.';
case LunaModule.TAUTULLI:
return 'Tautulli is an application that you can run alongside your Plex Media Server to monitor activity and track various statistics. Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched.';
case LunaModule.OVERSEERR:
return 'Overseerr is a free and open source software application for managing requests for your media library. It integrates with your existing services, such as Sonarr, Radarr, and Plex!';
case LunaModule.WAKE_ON_LAN:
return 'Wake on LAN is an industry standard protocol for waking computers up from a very low power mode remotely by sending a specially constructed packet to the machine.';
}
throw Exception('Invalid LunaModule');
}
String get website {
switch(this) {
case LunaModule.DASHBOARD: return null;
case LunaModule.LIDARR: return 'https://lidarr.audio';
case LunaModule.NZBGET: return 'https://nzbget.net';
case LunaModule.RADARR: return 'https://radarr.video';
case LunaModule.SABNZBD: return 'https://sabnzbd.org';
case LunaModule.SEARCH: return null;
case LunaModule.SETTINGS: return null;
case LunaModule.SONARR: return 'https://sonarr.tv';
case LunaModule.TAUTULLI: return 'https://tautulli.com';
case LunaModule.OVERSEERR: return 'https://overseerr.dev';
case LunaModule.WAKE_ON_LAN: return null;
}
throw Exception('Invalid LunaModule');
ShortcutItem get shortcutItem {
switch (this) {
case LunaModule.DASHBOARD:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.LIDARR:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.NZBGET:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.RADARR:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SABNZBD:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SEARCH:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SETTINGS:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SONARR:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.TAUTULLI:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.OVERSEERR:
return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.WAKE_ON_LAN:
return null;
}
throw Exception('Invalid LunaModule');
}
String get github {
switch(this) {
case LunaModule.DASHBOARD: return null;
case LunaModule.LIDARR: return 'https://github.com/Lidarr/Lidarr';
case LunaModule.NZBGET: return 'https://github.com/nzbget/nzbget';
case LunaModule.RADARR: return 'https://github.com/Radarr/Radarr';
case LunaModule.SABNZBD: return 'https://github.com/sabnzbd/sabnzbd';
case LunaModule.SEARCH: return 'https://github.com/theotherp/nzbhydra2';
case LunaModule.SETTINGS: return null;
case LunaModule.SONARR: return 'https://github.com/Sonarr/Sonarr';
case LunaModule.TAUTULLI: return 'https://github.com/Tautulli/Tautulli';
case LunaModule.OVERSEERR: return 'https://github.com/sct/overseerr';
case LunaModule.WAKE_ON_LAN: return null;
}
throw Exception('Invalid LunaModule');
bool get hasWebhooks {
switch (this) {
case LunaModule.DASHBOARD:
return false;
case LunaModule.LIDARR:
return true;
case LunaModule.NZBGET:
return false;
case LunaModule.RADARR:
return true;
case LunaModule.SABNZBD:
return false;
case LunaModule.SEARCH:
return false;
case LunaModule.SETTINGS:
return false;
case LunaModule.SONARR:
return true;
case LunaModule.OVERSEERR:
return true;
case LunaModule.TAUTULLI:
return true;
case LunaModule.WAKE_ON_LAN:
return false;
}
throw Exception('Invalid LunaModule');
}
String get description {
switch(this) {
case LunaModule.DASHBOARD: return 'lunasea.Dashboard'.tr();
case LunaModule.LIDARR: return 'Manage Music';
case LunaModule.NZBGET: return 'Manage Usenet Downloads';
case LunaModule.RADARR: return 'Manage Movies';
case LunaModule.SABNZBD: return 'Manage Usenet Downloads';
case LunaModule.SEARCH: return 'Search Newznab Indexers';
case LunaModule.SETTINGS: return 'Configure LunaSea';
case LunaModule.SONARR: return 'Manage Television Series';
case LunaModule.TAUTULLI: return 'View Plex Activity';
case LunaModule.OVERSEERR: return 'Manage Requests for New Content';
case LunaModule.WAKE_ON_LAN: return 'Wake Your Machine';
}
throw Exception('Invalid LunaModule');
void hideAllBanners() {
switch (this) {
case LunaModule.DASHBOARD:
return;
case LunaModule.LIDARR:
return;
case LunaModule.NZBGET:
return;
case LunaModule.RADARR:
return;
case LunaModule.SABNZBD:
return;
case LunaModule.SEARCH:
return;
case LunaModule.SETTINGS:
return SettingsBanners.values.forEach((banner) => banner.markSeen());
case LunaModule.SONARR:
return;
case LunaModule.TAUTULLI:
return;
case LunaModule.OVERSEERR:
return;
case LunaModule.WAKE_ON_LAN:
return;
}
throw Exception('Invalid LunaModule');
}
String get information {
switch(this) {
case LunaModule.DASHBOARD: return null;
case LunaModule.LIDARR: return '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.';
case LunaModule.NZBGET: return 'NZBGet is a binary downloader, which downloads files from Usenet based on information given in nzb-files.';
case LunaModule.RADARR: return 'Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.';
case LunaModule.SABNZBD: return 'SABnzbd is a multi-platform binary newsgroup downloader. The program works in the background and simplifies the downloading verifying and extracting of files from Usenet.';
case LunaModule.SEARCH: return 'LunaSea currently supports all indexers that support the newznab protocol, including NZBHydra2.';
case LunaModule.SETTINGS: return null;
case LunaModule.SONARR: return 'Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.';
case LunaModule.TAUTULLI: return 'Tautulli is an application that you can run alongside your Plex Media Server to monitor activity and track various statistics. Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched.';
case LunaModule.OVERSEERR: return 'Overseerr is a free and open source software application for managing requests for your media library. It integrates with your existing services, such as Sonarr, Radarr, and Plex!';
case LunaModule.WAKE_ON_LAN: return 'Wake on LAN is an industry standard protocol for waking computers up from a very low power mode remotely by sending a specially constructed packet to the machine.';
}
throw Exception('Invalid LunaModule');
Future<void> handleWebhook(Map<String, dynamic> data) async {
switch (this) {
case LunaModule.DASHBOARD:
return;
case LunaModule.LIDARR:
return LidarrWebhooks().handle(data);
case LunaModule.NZBGET:
return;
case LunaModule.RADARR:
return RadarrWebhooks().handle(data);
case LunaModule.SABNZBD:
return;
case LunaModule.SEARCH:
return;
case LunaModule.SETTINGS:
return;
case LunaModule.SONARR:
return SonarrWebhooks().handle(data);
case LunaModule.OVERSEERR:
return OverseerrWebhooks().handle(data);
case LunaModule.TAUTULLI:
return TautulliWebhooks().handle(data);
case LunaModule.WAKE_ON_LAN:
return;
}
throw Exception('Invalid LunaModule');
}
ShortcutItem get shortcutItem {
switch(this) {
case LunaModule.DASHBOARD: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.LIDARR: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.NZBGET: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.RADARR: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SABNZBD: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SEARCH: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SETTINGS: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.SONARR: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.TAUTULLI: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.OVERSEERR: return ShortcutItem(type: key, localizedTitle: name);
case LunaModule.WAKE_ON_LAN: return null;
}
throw Exception('Invalid LunaModule');
}
bool get hasWebhooks {
switch(this) {
case LunaModule.DASHBOARD: return false;
case LunaModule.LIDARR: return true;
case LunaModule.NZBGET: return false;
case LunaModule.RADARR: return true;
case LunaModule.SABNZBD: return false;
case LunaModule.SEARCH: return false;
case LunaModule.SETTINGS: return false;
case LunaModule.SONARR: return true;
case LunaModule.OVERSEERR: return true;
case LunaModule.TAUTULLI: return true;
case LunaModule.WAKE_ON_LAN: return false;
}
throw Exception('Invalid LunaModule');
}
void hideAllBanners() {
switch(this) {
case LunaModule.DASHBOARD: return;
case LunaModule.LIDARR: return;
case LunaModule.NZBGET: return;
case LunaModule.RADARR: return;
case LunaModule.SABNZBD: return;
case LunaModule.SEARCH: return;
case LunaModule.SETTINGS: return SettingsBanners.values.forEach((banner) => banner.markSeen());
case LunaModule.SONARR: return;
case LunaModule.TAUTULLI: return;
case LunaModule.OVERSEERR: return;
case LunaModule.WAKE_ON_LAN: return;
}
throw Exception('Invalid LunaModule');
}
Future<void> handleWebhook(Map<String, dynamic> data) async {
switch(this) {
case LunaModule.DASHBOARD: return;
case LunaModule.LIDARR: return LidarrWebhooks().handle(data);
case LunaModule.NZBGET: return;
case LunaModule.RADARR: return RadarrWebhooks().handle(data);
case LunaModule.SABNZBD: return;
case LunaModule.SEARCH: return;
case LunaModule.SETTINGS: return;
case LunaModule.SONARR: return SonarrWebhooks().handle(data);
case LunaModule.OVERSEERR: return OverseerrWebhooks().handle(data);
case LunaModule.TAUTULLI: return TautulliWebhooks().handle(data);
case LunaModule.WAKE_ON_LAN: return;
}
throw Exception('Invalid LunaModule');
}
Future<void> launch() async {
if(route != null) LunaState.navigatorKey.currentState.pushNamedAndRemoveUntil(route, (Route<dynamic> route) => false);
}
Future<void> launch() async {
if (route != null)
LunaState.navigatorKey.currentState
.pushNamedAndRemoveUntil(route, (Route<dynamic> route) => false);
}
}

View File

@@ -2,28 +2,28 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaInvalidRoute extends StatelessWidget {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final String title;
final String message;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final String title;
final String message;
LunaInvalidRoute({
Key key,
this.title = 'LunaSea',
this.message = '404: Not Found',
}) : super(key: key);
LunaInvalidRoute({
Key key,
this.title = 'LunaSea',
this.message = '404: Not Found',
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LunaScaffold(
scaffoldKey: _scaffoldKey,
appBar: LunaAppBar(
title: title,
scrollControllers: [],
),
body: LunaMessage.goBack(
context: context,
text: message,
),
);
}
@override
Widget build(BuildContext context) {
return LunaScaffold(
scaffoldKey: _scaffoldKey,
appBar: LunaAppBar(
title: title,
scrollControllers: [],
),
body: LunaMessage.goBack(
context: context,
text: message,
),
);
}
}

View File

@@ -2,26 +2,26 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaNotEnabledRoute extends StatelessWidget {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final String module;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final String module;
LunaNotEnabledRoute({
Key key,
@required this.module,
}) : super(key: key);
LunaNotEnabledRoute({
Key key,
@required this.module,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LunaScaffold(
scaffoldKey: _scaffoldKey,
appBar: LunaAppBar(
title: module,
scrollControllers: [],
),
body: LunaMessage.moduleNotEnabled(
context: context,
module: module,
),
);
}
@override
Widget build(BuildContext context) {
return LunaScaffold(
scaffoldKey: _scaffoldKey,
appBar: LunaAppBar(
title: module,
scrollControllers: [],
),
body: LunaMessage.moduleNotEnabled(
context: context,
module: module,
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:fluro/fluro.dart';
abstract class LunaModuleRouter {
/// Given the [FluroRouter], define all routes and handlers required for the module.
void defineAllRoutes(FluroRouter router);
/// Given the [FluroRouter], define all routes and handlers required for the module.
void defineAllRoutes(FluroRouter router);
}

View File

@@ -2,47 +2,51 @@ import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
abstract class LunaPageRouter {
/// The full string representation of the route without the named parameters being swapped out
final String fullRoute;
abstract class LunaPageRouter {
/// The full string representation of the route without the named parameters being swapped out
final String fullRoute;
/// Use [FluroRouter] to navigate to the page
Future<void> navigateTo(BuildContext context) => LunaRouter.router.navigateTo(context, fullRoute);
/// Use [FluroRouter] to navigate to the page
Future<dynamic> navigateTo(BuildContext context) {
return LunaRouter.router.navigateTo(context, fullRoute);
}
/// Get a string representation of the route with any named parameters inserted
String route() => fullRoute;
/// Get a string representation of the route with any named parameters inserted
String route() => fullRoute;
/// Used to define the route in the passed in router.
///
/// If the route requires no parameters, call [noParameterRouteDefinition] with the route as the [Widget] parameter.
void defineRoute(FluroRouter router);
/// Used to define the route in the passed in router.
///
/// If the route requires no parameters, call [noParameterRouteDefinition] with the route as the [Widget] parameter.
void defineRoute(FluroRouter router);
/// Define the route in the passed in router without any parameters required.
///
/// Already defines:
/// - Calls [FluroRouter.define] with the [fullRoute]
/// - Creates a basic [Handler]
/// - Sets the transition type to [LunaRouter.transitionType]
/// - Checks that the module is enabled, and if not returns a `[LunaNotEnabledRoute] instead of the intended route
void noParameterRouteDefinition(
FluroRouter router,
Widget widget,
{ bool homeRoute = false }
);
/// Return an instance of the actual route widget.
Widget widget();
/// Define the route in the passed in router with some parameters required.
///
/// Already defines:
/// - Calls [FluroRouter.define] with the [fullRoute]
/// - Creates a basic [Handler]
/// - Sets the transition type to [LunaRouter.transitionType]
/// - Checks that the module is enabled, and if not returns a `[LunaNotEnabledRoute] instead of the intended route
/// - Parses and injects the required parameters into the route for the handler
void withParameterRouteDefinition(
FluroRouter router,
Widget Function(BuildContext, Map<String, List<String>>) handlerFunc,
{ bool homeRoute = false }
);
/// Define the route in the passed in router without any parameters required.
///
/// Already defines:
/// - Calls [FluroRouter.define] with the [fullRoute]
/// - Creates a basic [Handler]
/// - Sets the transition type to [LunaRouter.transitionType]
/// - Checks that the module is enabled, and if not returns a `[LunaNotEnabledRoute] instead of the intended route
void noParameterRouteDefinition(
FluroRouter router, {
bool homeRoute = false,
});
LunaPageRouter(this.fullRoute);
/// Define the route in the passed in router with some parameters required.
///
/// Already defines:
/// - Calls [FluroRouter.define] with the [fullRoute]
/// - Creates a basic [Handler]
/// - Sets the transition type to [LunaRouter.transitionType]
/// - Checks that the module is enabled, and if not returns a `[LunaNotEnabledRoute] instead of the intended route
/// - Parses and injects the required parameters into the route for the handler
void withParameterRouteDefinition(
FluroRouter router,
Widget Function(BuildContext, Map<String, List<String>>) handlerFunc, {
bool homeRoute = false,
});
LunaPageRouter(this.fullRoute);
}

View File

@@ -4,31 +4,32 @@ import 'package:lunasea/core.dart';
import 'package:lunasea/modules.dart';
class LunaRouter {
static FluroRouter router = FluroRouter();
static TransitionType get transitionType => TransitionType.native;
static FluroRouter router = FluroRouter();
static TransitionType get transitionType => TransitionType.native;
/// Calls `defineAllRoutes()` on all module routers that implement [LunaModuleRouter].
void initialize() {
router.notFoundHandler = Handler(handlerFunc: (context, params) => LunaInvalidRoute());
DashboardRouter().defineAllRoutes(router);
SettingsRouter().defineAllRoutes(router);
if(LunaModule.SEARCH.enabled) SearchRouter().defineAllRoutes(router);
if(LunaModule.RADARR.enabled) RadarrRouter().defineAllRoutes(router);
if(LunaModule.SONARR.enabled) SonarrRouter().defineAllRoutes(router);
if(LunaModule.OVERSEERR.enabled) OverseerrRouter().defineAllRoutes(router);
if(LunaModule.TAUTULLI.enabled) TautulliRouter().defineAllRoutes(router);
}
/// Calls `defineAllRoutes()` on all module routers that implement [LunaModuleRouter].
void initialize() {
router.notFoundHandler =
Handler(handlerFunc: (context, params) => LunaInvalidRoute());
DashboardRouter().defineAllRoutes(router);
SettingsRouter().defineAllRoutes(router);
if (LunaModule.SEARCH.enabled) SearchRouter().defineAllRoutes(router);
if (LunaModule.RADARR.enabled) RadarrRouter().defineAllRoutes(router);
if (LunaModule.SONARR.enabled) SonarrRouter().defineAllRoutes(router);
if (LunaModule.OVERSEERR.enabled) OverseerrRouter().defineAllRoutes(router);
if (LunaModule.TAUTULLI.enabled) TautulliRouter().defineAllRoutes(router);
}
/// **Will be removed when all module routers are integrated.**
///
/// Returns a map of all module routes.
Map<String, WidgetBuilder> get routes => <String, WidgetBuilder> {
if(LunaModule.LIDARR.enabled) ..._lidarr,
if(LunaModule.SABNZBD.enabled) ..._sabnzbd,
if(LunaModule.NZBGET.enabled) ..._nzbget,
};
/// **Will be removed when all module routers are integrated.**
///
/// Returns a map of all module routes.
Map<String, WidgetBuilder> get routes => <String, WidgetBuilder>{
if (LunaModule.LIDARR.enabled) ..._lidarr,
if (LunaModule.SABNZBD.enabled) ..._sabnzbd,
if (LunaModule.NZBGET.enabled) ..._nzbget,
};
Map<String, WidgetBuilder> get _lidarr => <String, WidgetBuilder> {
Map<String, WidgetBuilder> get _lidarr => <String, WidgetBuilder>{
Lidarr.ROUTE_NAME: (context) => Lidarr(),
LidarrAddSearch.ROUTE_NAME: (context) => LidarrAddSearch(),
LidarrAddDetails.ROUTE_NAME: (context) => LidarrAddDetails(),
@@ -36,16 +37,16 @@ class LunaRouter {
LidarrDetailsAlbum.ROUTE_NAME: (context) => LidarrDetailsAlbum(),
LidarrDetailsArtist.ROUTE_NAME: (context) => LidarrDetailsArtist(),
LidarrSearchResults.ROUTE_NAME: (context) => LidarrSearchResults(),
};
};
Map<String, WidgetBuilder> get _nzbget => <String, WidgetBuilder> {
Map<String, WidgetBuilder> get _nzbget => <String, WidgetBuilder>{
NZBGet.ROUTE_NAME: (context) => NZBGet(),
NZBGetStatistics.ROUTE_NAME: (context) => NZBGetStatistics(),
};
};
Map<String, WidgetBuilder> get _sabnzbd => <String, WidgetBuilder> {
Map<String, WidgetBuilder> get _sabnzbd => <String, WidgetBuilder>{
SABnzbd.ROUTE_NAME: (context) => SABnzbd(),
SABnzbdStatistics.ROUTE_NAME: (context) => SABnzbdStatistics(),
SABnzbdHistoryStages.ROUTE_NAME: (context) => SABnzbdHistoryStages(),
};
};
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
abstract class LunaModuleState extends ChangeNotifier {
/// Reset the state back to the default
void reset();
/// Reset the state back to the default
void reset();
}

View File

@@ -4,34 +4,43 @@ import 'package:lunasea/core.dart';
import 'package:lunasea/modules.dart';
class LunaState {
LunaState._();
LunaState._();
/// Key for the [NavigatorState] to globally access context and other usages.
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
/// Calls `.reset()` on all states which extend [LunaModuleState].
static void reset(BuildContext context) {
LunaModule.values.forEach((module) {
if(module.enabled) module.state(context)?.reset();
});
}
/// Returns a [MultiProvider] with the provided child.
///
/// The [MultiProvider] has a [ChangeNotifierProvider] provider added for each module global state object.
static MultiProvider providers({ @required Widget child }) => MultiProvider(
/// Key for the [NavigatorState] to globally access context and other usages.
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
/// Calls `.reset()` on all states which extend [LunaModuleState].
static void reset(BuildContext context) {
LunaModule.values.forEach((module) {
if (module.enabled) module.state(context)?.reset();
});
}
/// Returns a [MultiProvider] with the provided child.
///
/// The [MultiProvider] has a [ChangeNotifierProvider] provider added for each module global state object.
static MultiProvider providers({@required Widget child}) => MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => DashboardState()),
ChangeNotifierProvider(create: (_) => SettingsState()),
if(LunaModule.SEARCH.enabled) ChangeNotifierProvider(create: (_) => SearchState()),
if(LunaModule.LIDARR.enabled) ChangeNotifierProvider(create: (_) => LidarrState()),
if(LunaModule.RADARR.enabled) ChangeNotifierProvider(create: (_) => RadarrState()),
if(LunaModule.SONARR.enabled) ChangeNotifierProvider(create: (_) => SonarrState()),
if(LunaModule.NZBGET.enabled) ChangeNotifierProvider(create: (_) => NZBGetState()),
if(LunaModule.SABNZBD.enabled) ChangeNotifierProvider(create: (_) => SABnzbdState()),
if(LunaModule.OVERSEERR.enabled) ChangeNotifierProvider(create: (_) => OverseerrState()),
if(LunaModule.TAUTULLI.enabled) ChangeNotifierProvider(create: (_) => TautulliState()),
ChangeNotifierProvider(create: (_) => DashboardState()),
ChangeNotifierProvider(create: (_) => SettingsState()),
if (LunaModule.SEARCH.enabled)
ChangeNotifierProvider(create: (_) => SearchState()),
if (LunaModule.LIDARR.enabled)
ChangeNotifierProvider(create: (_) => LidarrState()),
if (LunaModule.RADARR.enabled)
ChangeNotifierProvider(create: (_) => RadarrState()),
if (LunaModule.SONARR.enabled)
ChangeNotifierProvider(create: (_) => SonarrState()),
if (LunaModule.NZBGET.enabled)
ChangeNotifierProvider(create: (_) => NZBGetState()),
if (LunaModule.SABNZBD.enabled)
ChangeNotifierProvider(create: (_) => SABnzbdState()),
if (LunaModule.OVERSEERR.enabled)
ChangeNotifierProvider(create: (_) => OverseerrState()),
if (LunaModule.TAUTULLI.enabled)
ChangeNotifierProvider(create: (_) => TautulliState()),
],
child: child,
);
);
}

View File

@@ -1,8 +1,8 @@
export 'system/configuration.dart';
export 'system/desktop_window.dart';
export 'system/filesystem.dart';
export 'system/image_cache.dart';
export 'system/in_app_purchases.dart';
export 'system/filesystem.dart';
export 'system/localization.dart';
export 'system/networking.dart';
export 'system/profile.dart';

View File

@@ -3,71 +3,77 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaConfiguration {
/// Returns a list of all profiles converted to a map.
List<Map<String, dynamic>> _getProfiles() {
List<Map<String, dynamic>> _data = [];
for(var key in Database.profilesBox.keys) _data.add(Database.profilesBox.get(key).toMap());
return _data;
}
/// Returns a list of all profiles converted to a map.
List<Map<String, dynamic>> _getProfiles() {
List<Map<String, dynamic>> _data = [];
for (var key in Database.profilesBox.keys)
_data.add(Database.profilesBox.get(key).toMap());
return _data;
}
/// Given a list of map objects, creates or updates profiles for each object.
void _setProfiles(List data) {
Box<dynamic> box = Database.profilesBox;
for(Map profile in data) box.put(profile['key'], ProfileHiveObject.fromMap(profile));
}
/// Given a list of map objects, creates or updates profiles for each object.
void _setProfiles(List data) {
Box<dynamic> box = Database.profilesBox;
for (Map profile in data)
box.put(profile['key'], ProfileHiveObject.fromMap(profile));
}
/// Returns a list of all indexers converted to a map.
List<Map<String, dynamic>> _getIndexers() {
List<Map<String, dynamic>> _data = [];
for(var key in Database.indexersBox.keys) _data.add(Database.indexersBox.get(key).toMap());
return _data;
}
/// Given a list of map objects, creates or updates indexers for each object.
void _setIndexers(List data) {
Box<dynamic> box = Database.indexersBox;
for(Map indexer in data) box.add(IndexerHiveObject.fromMap(indexer));
}
/// Returns a list of all indexers converted to a map.
List<Map<String, dynamic>> _getIndexers() {
List<Map<String, dynamic>> _data = [];
for (var key in Database.indexersBox.keys)
_data.add(Database.indexersBox.get(key).toMap());
return _data;
}
/// Import the entire configuration from a JSON-encoded string (typically read through a `.lunasea` backup file).
///
/// - Clears all boxes
/// - Calls `_setProfiles()` and `_setIndexers()`
/// - Calls `import()` on all module databases, which implement [LunaDatabase].
/// - Resets the application state
///
/// On a failed import, resets LunaSea back to the default/base state
Future<void> import(BuildContext context, String data) async {
Map config = json.decode(data);
Database().clearAllBoxes();
try {
if(config['profiles'] != null) _setProfiles(config['profiles']);
if(config['indexers'] != null) _setIndexers(config['indexers']);
if(config['lunasea'] != null) LunaDatabase().import(config['lunasea']);
LunaModule.values.forEach((module) {
if(config[module.key] != null) module.database?.import(config[module.key]);
});
} catch (error, stack) {
LunaLogger().error('Failed to import configuration, resetting to default', error, stack);
Database().setDefaults();
}
// Reset the entire app's state
LunaState.reset(context);
}
/// Given a list of map objects, creates or updates indexers for each object.
void _setIndexers(List data) {
Box<dynamic> box = Database.indexersBox;
for (Map indexer in data) box.add(IndexerHiveObject.fromMap(indexer));
}
/// Converts the entire application configuration/database into a JSON string.
///
/// - Calls `_getProfiles()` and `_getIndexers()`
/// - Calls `export()` on all module databases, which implement [LunaModuleDatabase].
String export() {
Map<String, dynamic> config = {
"profiles": _getProfiles(),
"indexers": _getIndexers(),
"lunasea": LunaDatabase().export(),
};
LunaModule.values.forEach((module) {
if(module.database != null) config[module.key] = module.database.export();
});
return json.encode(config);
/// Import the entire configuration from a JSON-encoded string (typically read through a `.lunasea` backup file).
///
/// - Clears all boxes
/// - Calls `_setProfiles()` and `_setIndexers()`
/// - Calls `import()` on all module databases, which implement [LunaDatabase].
/// - Resets the application state
///
/// On a failed import, resets LunaSea back to the default/base state
Future<void> import(BuildContext context, String data) async {
Map config = json.decode(data);
Database().clearAllBoxes();
try {
if (config['profiles'] != null) _setProfiles(config['profiles']);
if (config['indexers'] != null) _setIndexers(config['indexers']);
if (config['lunasea'] != null) LunaDatabase().import(config['lunasea']);
LunaModule.values.forEach((module) {
if (config[module.key] != null)
module.database?.import(config[module.key]);
});
} catch (error, stack) {
LunaLogger().error(
'Failed to import configuration, resetting to default', error, stack);
Database().setDefaults();
}
// Reset the entire app's state
LunaState.reset(context);
}
/// Converts the entire application configuration/database into a JSON string.
///
/// - Calls `_getProfiles()` and `_getIndexers()`
/// - Calls `export()` on all module databases, which implement [LunaModuleDatabase].
String export() {
Map<String, dynamic> config = {
"profiles": _getProfiles(),
"indexers": _getIndexers(),
"lunasea": LunaDatabase().export(),
};
LunaModule.values.forEach((module) {
if (module.database != null)
config[module.key] = module.database.export();
});
return json.encode(config);
}
}

View File

@@ -1,16 +1,19 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:window_size/window_size.dart' as WindowSize;
import 'package:window_size/window_size.dart' as window_size;
class LunaDesktopWindow {
static const double _MINIMUM_WINDOW_SIZE = 400;
static bool get isPlatformCompatible => Platform.isWindows || Platform.isMacOS || Platform.isLinux;
static const double _MINIMUM_WINDOW_SIZE = 400;
static bool get isPlatformCompatible =>
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
/// Initialize the Desktop window:
/// - Sets the minimum window size
void initialize() {
if(isPlatformCompatible) {
WindowSize.setWindowMinSize(Size(_MINIMUM_WINDOW_SIZE, _MINIMUM_WINDOW_SIZE));
}
/// Initialize the Desktop window:
/// - Sets the minimum window size
void initialize() {
if (isPlatformCompatible) {
window_size.setWindowMinSize(
Size(_MINIMUM_WINDOW_SIZE, _MINIMUM_WINDOW_SIZE),
);
}
}
}

View File

@@ -8,112 +8,120 @@ import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
class LunaFileSystem {
/// Export a given byte array to the filesystem.
///
/// Depending on the platform, uses different methods to export the data:
/// - iOS/Android: Utilizes system-level sharesheet
/// - macOS: Uses standard OS save dialog
Future<bool> export(BuildContext context, String name, List<int> data) async {
if(Platform.isAndroid || Platform.isIOS) {
return _exportMobile(context, name, data);
} else if(Platform.isMacOS) {
return _exportDesktop(name, data);
} else {
throw MissingPluginException('No plugin to handle exporting data is available for this platform.');
}
/// Export a given byte array to the filesystem.
///
/// Depending on the platform, uses different methods to export the data:
/// - iOS/Android: Utilizes system-level sharesheet
/// - macOS: Uses standard OS save dialog
Future<bool> export(BuildContext context, String name, List<int> data) async {
if (Platform.isAndroid || Platform.isIOS) {
return _exportMobile(context, name, data);
} else if (Platform.isMacOS) {
return _exportDesktop(name, data);
} else {
throw MissingPluginException(
'No plugin to handle exporting data is available for this platform.');
}
}
/// Import a file from the filesystem.
///
/// Depending on the platform, uses different methods to import the data:
/// - iOS/Android: Utilizes `file_picker` to use mobile-OS file picker
/// - macOS: Uses standard OS file picker
Future<File> import(BuildContext context, List<String> acceptedExtensions) async {
if(Platform.isAndroid || Platform.isIOS) {
return _importMobile(context, acceptedExtensions);
} else if(Platform.isMacOS) {
return _importDesktop(acceptedExtensions);
} else {
throw MissingPluginException('No plugin to handle importing data is available for this platform.');
}
/// Import a file from the filesystem.
///
/// Depending on the platform, uses different methods to import the data:
/// - iOS/Android: Utilizes `file_picker` to use mobile-OS file picker
/// - macOS: Uses standard OS file picker
Future<File> import(
BuildContext context, List<String> acceptedExtensions) async {
if (Platform.isAndroid || Platform.isIOS) {
return _importMobile(context, acceptedExtensions);
} else if (Platform.isMacOS) {
return _importDesktop(acceptedExtensions);
} else {
throw MissingPluginException(
'No plugin to handle importing data is available for this platform.');
}
}
/// Import a file from the filesystem (mobile).
///
/// Allows selection of any filetype, but will check the extension of the selected file against [acceptedExtensions].
/// If not found in the array, or if any error occurs, will return null.
Future<File> _importMobile(BuildContext context, List<String> acceptedExtensions) async {
try {
FilePickerResult _file = await FilePicker.platform.pickFiles(
type: FileType.any,
allowMultiple: false,
allowCompression: false,
withData: false,
);
if(_file != null && acceptedExtensions.contains(_file.files[0].extension ?? '')) {
return File(_file.files[0].path);
}
} catch (error, stack) {
LunaLogger().error('Failed to import data from filesystem', error, stack);
}
return null;
/// Import a file from the filesystem (mobile).
///
/// Allows selection of any filetype, but will check the extension of the selected file against [acceptedExtensions].
/// If not found in the array, or if any error occurs, will return null.
Future<File> _importMobile(
BuildContext context, List<String> acceptedExtensions) async {
try {
FilePickerResult _file = await FilePicker.platform.pickFiles(
type: FileType.any,
allowMultiple: false,
allowCompression: false,
withData: false,
);
if (_file != null &&
acceptedExtensions.contains(_file.files[0].extension ?? '')) {
return File(_file.files[0].path);
}
} catch (error, stack) {
LunaLogger().error('Failed to import data from filesystem', error, stack);
}
return null;
}
/// Export a given byte array to the OS-level share sheet with the given name.
///
/// Temporarily writes the byte array as a [File] to the temporary storage directory on the OS.
///
/// Temporary storage is eventually cleared by the OS, but is more than enough time to save/send via the share sheet.
Future<bool> _exportMobile(BuildContext context, String name, List<int> data) async {
try {
final RenderBox box = context.findRenderObject();
Directory tempDirectory = await getTemporaryDirectory();
String path = '${tempDirectory.path}/$name';
File file = File(path);
await file?.writeAsBytes(data);
await Share.shareFiles(
[path],
sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size,
);
return true;
} catch (error, stack) {
LunaLogger().error('Failed to export data to sharesheet', error, stack);
}
return false;
/// Export a given byte array to the OS-level share sheet with the given name.
///
/// Temporarily writes the byte array as a [File] to the temporary storage directory on the OS.
///
/// Temporary storage is eventually cleared by the OS, but is more than enough time to save/send via the share sheet.
Future<bool> _exportMobile(
BuildContext context, String name, List<int> data) async {
try {
final RenderBox box = context.findRenderObject();
Directory tempDirectory = await getTemporaryDirectory();
String path = '${tempDirectory.path}/$name';
File file = File(path);
await file?.writeAsBytes(data);
await Share.shareFiles(
[path],
sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size,
);
return true;
} catch (error, stack) {
LunaLogger().error('Failed to export data to sharesheet', error, stack);
}
return false;
}
/// Export the given data to the OS-level filesystem save dialog with the given suggested name.
///
/// Prompts the user with a save dialog prompt with the supplied name as the recommended name.
Future<bool> _exportDesktop(String name, List<int> data) async {
try {
String path = await FileSelectorPlatform.instance?.getSavePath(suggestedName: name);
if(path?.isNotEmpty ?? false) {
File file = File(path);
await file?.writeAsBytes(data);
return true;
}
} catch (error, stack) {
LunaLogger().error('Failed to export data to filesystem', error, stack);
}
return false;
/// Export the given data to the OS-level filesystem save dialog with the given suggested name.
///
/// Prompts the user with a save dialog prompt with the supplied name as the recommended name.
Future<bool> _exportDesktop(String name, List<int> data) async {
try {
String path =
await FileSelectorPlatform.instance?.getSavePath(suggestedName: name);
if (path?.isNotEmpty ?? false) {
File file = File(path);
await file?.writeAsBytes(data);
return true;
}
} catch (error, stack) {
LunaLogger().error('Failed to export data to filesystem', error, stack);
}
return false;
}
/// Import a file from the filesystem (Desktop).
///
/// Locks selection to the given [acceptedExtensions].
/// If any error occurs, will return null.
Future<File> _importDesktop(List<String> acceptedExtensions) async {
try {
final typeGroup = XTypeGroup(
label: 'types',
extensions: acceptedExtensions,
);
XFile file = await FileSelectorPlatform.instance?.openFile(acceptedTypeGroups: [typeGroup]);
return File(file.path);
} catch (error, stack) {
LunaLogger().error('Failed to import data from filesystem', error, stack);
}
return null;
/// Import a file from the filesystem (Desktop).
///
/// Locks selection to the given [acceptedExtensions].
/// If any error occurs, will return null.
Future<File> _importDesktop(List<String> acceptedExtensions) async {
try {
final typeGroup = XTypeGroup(
label: 'types',
extensions: acceptedExtensions,
);
XFile file = await FileSelectorPlatform.instance
?.openFile(acceptedTypeGroups: [typeGroup]);
return File(file.path);
} catch (error, stack) {
LunaLogger().error('Failed to import data from filesystem', error, stack);
}
return null;
}
}

View File

@@ -1,9 +1,9 @@
import 'package:flutter/painting.dart';
class LunaImageCache {
/// Initialize the image cache by setting [ImageCache]'s maximumSize and maximumSizeBytes.
void initialize() {
ImageCache().maximumSize = 1000;
ImageCache().maximumSizeBytes = 128 << 20;
}
/// Initialize the image cache by setting [ImageCache]'s maximumSize and maximumSizeBytes.
void initialize() {
ImageCache().maximumSize = 1000;
ImageCache().maximumSizeBytes = 128 << 20;
}
}

View File

@@ -4,52 +4,63 @@ import 'package:in_app_purchase/in_app_purchase.dart';
export 'package:in_app_purchase/in_app_purchase.dart';
class LunaInAppPurchases {
// Donation IAP IDs
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];
// Donation IAP IDs
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<List<PurchaseDetails>> _purchaseStream;
static List<ProductDetails> donationIAPs = [];
static bool isAvailable = false;
static InAppPurchaseConnection get connection =>
InAppPurchaseConnection.instance;
static StreamSubscription<List<PurchaseDetails>> _purchaseStream;
static List<ProductDetails> donationIAPs = [];
static bool isAvailable = false;
static bool get isPlatformCompatible => Platform.isAndroid || Platform.isIOS;
static bool get isPlatformCompatible => Platform.isAndroid || Platform.isIOS;
/// Callback function for [purchaseUpdatedStream].
static Future<void> _purchasedCallback(List<PurchaseDetails> 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
Future<void> initialize() async {
if(isPlatformCompatible) {
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(DONATION_IDS));
donationIAPs = donation?.productDetails ?? [];
donationIAPs.sort((a, b) => a?.id?.compareTo(b?.id) ?? 0);
}
}
/// Callback function for [purchaseUpdatedStream].
static Future<void> _purchasedCallback(
List<PurchaseDetails> 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);
}
}
}
/// Deinitialize the in-app purchases connection.
///
/// - Cleanly cancel/close the [InAppPurchaseConnection.instance.purchaseUpdateStream] listener.
Future<void> deinitialize() async => _purchaseStream?.cancel();
/// Initialize the in-app purchases connection.
///
/// - Enables pending purchase
/// - Attach [InAppPurchaseConnection.instance.purchaseUpdateStream] listener for automatically completing IAP purchases
/// - Fetch and store donation IAPs
Future<void> initialize() async {
if (isPlatformCompatible) {
InAppPurchaseConnection.enablePendingPurchases();
isAvailable = await connection.isAvailable();
if (isAvailable) {
// Open listener
_purchaseStream = connection.purchaseUpdatedStream.listen(
LunaInAppPurchases._purchasedCallback,
);
// Load donation IAPs
ProductDetailsResponse donation =
await connection.queryProductDetails(Set.from(DONATION_IDS));
donationIAPs = donation?.productDetails ?? [];
donationIAPs.sort((a, b) => a?.id?.compareTo(b?.id) ?? 0);
}
}
}
/// Deinitialize the in-app purchases connection.
///
/// - Cleanly cancel/close the [InAppPurchaseConnection.instance.purchaseUpdateStream] listener.
Future<void> deinitialize() async => _purchaseStream?.cancel();
}

View File

@@ -4,103 +4,145 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaLocalization {
/// List of [LunaLanguage] values with all currently supported languages
List<LunaLanguage> get supportedLanguages => LunaLanguage.values.where((lang) => lang.enabled).toList();
/// Returns an iterable list of [Locale] objects containing all supported & enabled locales.
Iterable<Locale> get supportedLocales => supportedLanguages.map<Locale>((lang) => lang.locale).toList();
/// List of [LunaLanguage] values with all currently supported languages
List<LunaLanguage> get supportedLanguages =>
LunaLanguage.values.where((lang) => lang.enabled).toList();
Locale get fallbackLocale => LunaLanguage.ENGLISH.locale;
/// Returns an iterable list of [Locale] objects containing all supported & enabled locales.
Iterable<Locale> get supportedLocales =>
supportedLanguages.map<Locale>((lang) => lang.locale).toList();
/// Directory containing all localization files
String get fileDirectory => 'assets/localization';
Locale get fallbackLocale => LunaLanguage.ENGLISH.locale;
/// Initialize the localization system.
Future<void> initialize() async {
await EasyLocalization.ensureInitialized();
}
/// Directory containing all localization files
String get fileDirectory => 'assets/localization';
/// Initialize the localization system.
Future<void> initialize() async {
await EasyLocalization.ensureInitialized();
}
}
enum LunaLanguage {
ENGLISH,
FRENCH,
GERMAN,
ITALIAN,
NORWEGIAN_BOKMAL,
PORTUGUESE,
RUSSIAN,
SWEDISH,
ENGLISH,
FRENCH,
GERMAN,
ITALIAN,
NORWEGIAN_BOKMAL,
PORTUGUESE,
RUSSIAN,
SWEDISH,
}
extension LunaLanguageExtension on LunaLanguage {
LunaLanguage fromLocale(Locale locale) {
if(locale.toLanguageTag() == LunaLanguage.ENGLISH.languageTag) return LunaLanguage.ENGLISH;
if(locale.toLanguageTag() == LunaLanguage.FRENCH.languageTag) return LunaLanguage.FRENCH;
if(locale.toLanguageTag() == LunaLanguage.GERMAN.languageTag) return LunaLanguage.GERMAN;
if(locale.toLanguageTag() == LunaLanguage.ITALIAN.languageTag) return LunaLanguage.ITALIAN;
if(locale.toLanguageTag() == LunaLanguage.NORWEGIAN_BOKMAL.languageTag) return LunaLanguage.NORWEGIAN_BOKMAL;
if(locale.toLanguageTag() == LunaLanguage.PORTUGUESE.languageTag) return LunaLanguage.PORTUGUESE;
if(locale.toLanguageTag() == LunaLanguage.RUSSIAN.languageTag) return LunaLanguage.RUSSIAN;
if(locale.toLanguageTag() == LunaLanguage.SWEDISH.languageTag) return LunaLanguage.SWEDISH;
return null;
}
LunaLanguage fromLocale(Locale locale) {
if (locale.toLanguageTag() == LunaLanguage.ENGLISH.languageTag)
return LunaLanguage.ENGLISH;
if (locale.toLanguageTag() == LunaLanguage.FRENCH.languageTag)
return LunaLanguage.FRENCH;
if (locale.toLanguageTag() == LunaLanguage.GERMAN.languageTag)
return LunaLanguage.GERMAN;
if (locale.toLanguageTag() == LunaLanguage.ITALIAN.languageTag)
return LunaLanguage.ITALIAN;
if (locale.toLanguageTag() == LunaLanguage.NORWEGIAN_BOKMAL.languageTag)
return LunaLanguage.NORWEGIAN_BOKMAL;
if (locale.toLanguageTag() == LunaLanguage.PORTUGUESE.languageTag)
return LunaLanguage.PORTUGUESE;
if (locale.toLanguageTag() == LunaLanguage.RUSSIAN.languageTag)
return LunaLanguage.RUSSIAN;
if (locale.toLanguageTag() == LunaLanguage.SWEDISH.languageTag)
return LunaLanguage.SWEDISH;
return null;
}
bool get enabled {
switch(this) {
case LunaLanguage.ENGLISH: return true;
// Keep all languages enabled in debug mode while LunaSea is being adapted for localization
case LunaLanguage.FRENCH: return kDebugMode;
case LunaLanguage.GERMAN: return kDebugMode;
case LunaLanguage.ITALIAN: return kDebugMode;
case LunaLanguage.NORWEGIAN_BOKMAL: return kDebugMode;
case LunaLanguage.PORTUGUESE: return kDebugMode;
case LunaLanguage.RUSSIAN: return kDebugMode;
case LunaLanguage.SWEDISH: return kDebugMode;
}
throw Exception('Invalid LunaLanguage');
bool get enabled {
switch (this) {
case LunaLanguage.ENGLISH:
return true;
// Keep all languages enabled in debug mode while LunaSea is being adapted for localization
case LunaLanguage.FRENCH:
return kDebugMode;
case LunaLanguage.GERMAN:
return kDebugMode;
case LunaLanguage.ITALIAN:
return kDebugMode;
case LunaLanguage.NORWEGIAN_BOKMAL:
return kDebugMode;
case LunaLanguage.PORTUGUESE:
return kDebugMode;
case LunaLanguage.RUSSIAN:
return kDebugMode;
case LunaLanguage.SWEDISH:
return kDebugMode;
}
throw Exception('Invalid LunaLanguage');
}
String get name {
switch(this) {
case LunaLanguage.ENGLISH: return 'English';
case LunaLanguage.FRENCH: return 'French';
case LunaLanguage.GERMAN: return 'German';
case LunaLanguage.ITALIAN: return 'Italian';
case LunaLanguage.NORWEGIAN_BOKMAL: return 'Norwegian (Bokmål)';
case LunaLanguage.PORTUGUESE: return 'Portuguese';
case LunaLanguage.RUSSIAN: return 'Russian';
case LunaLanguage.SWEDISH: return 'Swedish';
}
throw Exception('Invalid LunaLanguage');
String get name {
switch (this) {
case LunaLanguage.ENGLISH:
return 'English';
case LunaLanguage.FRENCH:
return 'French';
case LunaLanguage.GERMAN:
return 'German';
case LunaLanguage.ITALIAN:
return 'Italian';
case LunaLanguage.NORWEGIAN_BOKMAL:
return 'Norwegian (Bokmål)';
case LunaLanguage.PORTUGUESE:
return 'Portuguese';
case LunaLanguage.RUSSIAN:
return 'Russian';
case LunaLanguage.SWEDISH:
return 'Swedish';
}
throw Exception('Invalid LunaLanguage');
}
String get languageTag {
switch(this) {
case LunaLanguage.ENGLISH: return 'en';
case LunaLanguage.FRENCH: return 'fr';
case LunaLanguage.GERMAN: return 'de';
case LunaLanguage.ITALIAN: return 'it';
case LunaLanguage.NORWEGIAN_BOKMAL: return 'nb-NO';
case LunaLanguage.PORTUGUESE: return 'pt';
case LunaLanguage.RUSSIAN: return 'ru';
case LunaLanguage.SWEDISH: return 'sv';
}
throw Exception('Invalid LunaLanguage');
String get languageTag {
switch (this) {
case LunaLanguage.ENGLISH:
return 'en';
case LunaLanguage.FRENCH:
return 'fr';
case LunaLanguage.GERMAN:
return 'de';
case LunaLanguage.ITALIAN:
return 'it';
case LunaLanguage.NORWEGIAN_BOKMAL:
return 'nb-NO';
case LunaLanguage.PORTUGUESE:
return 'pt';
case LunaLanguage.RUSSIAN:
return 'ru';
case LunaLanguage.SWEDISH:
return 'sv';
}
throw Exception('Invalid LunaLanguage');
}
Locale get locale {
switch(this) {
case LunaLanguage.ENGLISH: return Locale('en');
case LunaLanguage.FRENCH: return Locale('fr');
case LunaLanguage.GERMAN: return Locale('de');
case LunaLanguage.ITALIAN: return Locale('it');
case LunaLanguage.NORWEGIAN_BOKMAL: return Locale('nb', 'NO');
case LunaLanguage.PORTUGUESE: return Locale('pt');
case LunaLanguage.RUSSIAN: return Locale('ru');
case LunaLanguage.SWEDISH: return Locale('sv');
}
throw Exception('Invalid LunaLanguage');
Locale get locale {
switch (this) {
case LunaLanguage.ENGLISH:
return Locale('en');
case LunaLanguage.FRENCH:
return Locale('fr');
case LunaLanguage.GERMAN:
return Locale('de');
case LunaLanguage.ITALIAN:
return Locale('it');
case LunaLanguage.NORWEGIAN_BOKMAL:
return Locale('nb', 'NO');
case LunaLanguage.PORTUGUESE:
return Locale('pt');
case LunaLanguage.RUSSIAN:
return Locale('ru');
case LunaLanguage.SWEDISH:
return Locale('sv');
}
throw Exception('Invalid LunaLanguage');
}
Future<void> use(BuildContext context) => context.setLocale(this.locale);
Future<void> use(BuildContext context) => context.setLocale(this.locale);
}

View File

@@ -1,26 +1,27 @@
import 'dart:io';
class LunaNetworking extends HttpOverrides {
/// Initialize the default HTTP client by setting [HttpOverrides]'s global HTTP client to [LunaNetworking].
void initialize() {
HttpOverrides.global = LunaNetworking();
}
/// Initialize the default HTTP client by setting [HttpOverrides]'s global HTTP client to [LunaNetworking].
void initialize() {
HttpOverrides.global = LunaNetworking();
}
/// Overrides the default [createHttpClient] function and ensures all HTTPS connections allow bad certificate connections.
///
/// Returns a new [HttpClient] using the given [context].
@override
HttpClient createHttpClient(SecurityContext context) {
final HttpClient client = super.createHttpClient(context);
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
return client;
}
/// Overrides the default [createHttpClient] function and ensures all HTTPS connections allow bad certificate connections.
///
/// Returns a new [HttpClient] using the given [context].
@override
HttpClient createHttpClient(SecurityContext context) {
final HttpClient client = super.createHttpClient(context);
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<String, String> environment) {
return super.findProxyFromEnvironment(url, environment);
}
/// Overrides the default [findProxyFromEnvironment] function.
///
/// Currently just calls the super function to use the default finding function.
@override
String findProxyFromEnvironment(Uri url, Map<String, String> environment) {
return super.findProxyFromEnvironment(url, environment);
}
}

View File

@@ -2,83 +2,111 @@ import 'package:lunasea/core.dart';
import 'package:lunasea/modules/settings.dart';
class LunaProfile {
/// Returns a list of the profiles, sorted by their lowercase key/display name.
List<String> profilesList() => Database.profilesBox.keys.map<String>((profile) => profile as String).toList()..sort((a,b) => a.toLowerCase().compareTo(b.toLowerCase()));
/// Safely change profiles.
///
/// Does this safely by:
/// - Ensures that the passed in profile isn't already enabled
/// - Ensures that the profile exists
Future<bool> safelyChangeProfiles(String profile, { bool showSnackbar = true }) async {
if(LunaDatabaseValue.ENABLED_PROFILE.data == profile) return true;
if(Database.profilesBox.containsKey(profile)) {
LunaDatabaseValue.ENABLED_PROFILE.put(profile);
LunaState.reset(LunaState.navigatorKey.currentContext);
if(showSnackbar) showLunaSuccessSnackBar(title: 'Changed Profile', message: profile);
return true;
} else {
LunaLogger().warning('LunaProfile', 'changeProfile', 'Attempted to change profile to unknown profile: $profile');
return false;
}
}
/// Returns a list of the profiles, sorted by their lowercase key/display name.
List<String> profilesList() => Database.profilesBox.keys
.map<String>((profile) => profile as String)
.toList()
..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
/// Rename a profile.
///
/// Shows a dialog with a list of profiles, on selection:
/// - Shows a input bar for a new name
/// - Updates the profile's key (which is also the display name)
Future<bool> renameProfile({ bool showSnackbar = true }) async {
List<String> profiles = profilesList();
Tuple2<bool, String> selectedProfile = await SettingsDialogs().renameProfile(LunaState.navigatorKey.currentContext, profiles);
if(selectedProfile.item1) {
Tuple2<bool, String> newProfile = await SettingsDialogs().renameProfileSelected(LunaState.navigatorKey.currentContext, profiles);
if(newProfile.item1) {
ProfileHiveObject oldProfileObject = Database.profilesBox.get(selectedProfile.item2);
Database.profilesBox.put(newProfile.item2, ProfileHiveObject.fromProfileHiveObject(oldProfileObject));
if(LunaDatabaseValue.ENABLED_PROFILE.data == selectedProfile.item2) LunaProfile().safelyChangeProfiles(newProfile.item2, showSnackbar: false);
oldProfileObject.delete();
if(showSnackbar) showLunaSuccessSnackBar(title: 'Renamed Profile', message: '"${selectedProfile.item2}" has been renamed to "${newProfile.item2}"');
}
return true;
}
return false;
/// Safely change profiles.
///
/// Does this safely by:
/// - Ensures that the passed in profile isn't already enabled
/// - Ensures that the profile exists
Future<bool> safelyChangeProfiles(String profile,
{bool showSnackbar = true}) async {
if (LunaDatabaseValue.ENABLED_PROFILE.data == profile) return true;
if (Database.profilesBox.containsKey(profile)) {
LunaDatabaseValue.ENABLED_PROFILE.put(profile);
LunaState.reset(LunaState.navigatorKey.currentContext);
if (showSnackbar)
showLunaSuccessSnackBar(title: 'Changed Profile', message: profile);
return true;
} else {
LunaLogger().warning('LunaProfile', 'changeProfile',
'Attempted to change profile to unknown profile: $profile');
return false;
}
}
/// Delete a profile.
///
/// Show a dialog with a list of profiles, on selection:
/// - Validate that the profile is not currently enabled
Future<bool> deleteProfile({ bool showSnackbar = true }) async {
List<String> profiles = profilesList();
profiles.remove(LunaDatabaseValue.ENABLED_PROFILE.data);
if(profiles.length > 0) {
Tuple2<bool, String> selectedProfile = await SettingsDialogs().deleteProfile(LunaState.navigatorKey.currentContext, profiles);
if(selectedProfile.item1) {
if(selectedProfile.item2 != LunaDatabaseValue.ENABLED_PROFILE.data) {
Database.profilesBox.delete(selectedProfile.item2);
if(showSnackbar) showLunaSuccessSnackBar(title: 'Deleted Profile', message: '"${selectedProfile.item2}" has been deleted');
return true;
}
if(showSnackbar) showLunaErrorSnackBar(title: 'Unable to Delete Profile', message: 'Cannot delete the enabled profile');
}
} else {
showLunaInfoSnackBar(title: 'No Profiles to Delete', message: 'No additional profiles have been added');
}
return false;
/// Rename a profile.
///
/// Shows a dialog with a list of profiles, on selection:
/// - Shows a input bar for a new name
/// - Updates the profile's key (which is also the display name)
Future<bool> renameProfile({bool showSnackbar = true}) async {
List<String> profiles = profilesList();
Tuple2<bool, String> selectedProfile = await SettingsDialogs()
.renameProfile(LunaState.navigatorKey.currentContext, profiles);
if (selectedProfile.item1) {
Tuple2<bool, String> newProfile = await SettingsDialogs()
.renameProfileSelected(
LunaState.navigatorKey.currentContext, profiles);
if (newProfile.item1) {
ProfileHiveObject oldProfileObject =
Database.profilesBox.get(selectedProfile.item2);
Database.profilesBox.put(newProfile.item2,
ProfileHiveObject.fromProfileHiveObject(oldProfileObject));
if (LunaDatabaseValue.ENABLED_PROFILE.data == selectedProfile.item2)
LunaProfile()
.safelyChangeProfiles(newProfile.item2, showSnackbar: false);
oldProfileObject.delete();
if (showSnackbar)
showLunaSuccessSnackBar(
title: 'Renamed Profile',
message:
'"${selectedProfile.item2}" has been renamed to "${newProfile.item2}"');
}
return true;
}
return false;
}
Future<bool> addProfile({ bool showSnackbar = true }) async {
List<String> profiles = profilesList();
Tuple2<bool, String> result = await SettingsDialogs().addProfile(LunaState.navigatorKey.currentContext, profiles);
if(result.item1) {
Database.profilesBox.put(result.item2, ProfileHiveObject.empty());
safelyChangeProfiles(result.item2);
if(showSnackbar) showLunaSuccessSnackBar(
title: 'Profile Added',
message: result.item2,
);
/// Delete a profile.
///
/// Show a dialog with a list of profiles, on selection:
/// - Validate that the profile is not currently enabled
Future<bool> deleteProfile({bool showSnackbar = true}) async {
List<String> profiles = profilesList();
profiles.remove(LunaDatabaseValue.ENABLED_PROFILE.data);
if (profiles?.isNotEmpty ?? false) {
Tuple2<bool, String> selectedProfile = await SettingsDialogs()
.deleteProfile(LunaState.navigatorKey.currentContext, profiles);
if (selectedProfile.item1) {
if (selectedProfile.item2 != LunaDatabaseValue.ENABLED_PROFILE.data) {
Database.profilesBox.delete(selectedProfile.item2);
if (showSnackbar)
showLunaSuccessSnackBar(
title: 'Deleted Profile',
message: '"${selectedProfile.item2}" has been deleted');
return true;
}
return false;
if (showSnackbar)
showLunaErrorSnackBar(
title: 'Unable to Delete Profile',
message: 'Cannot delete the enabled profile');
}
} else {
showLunaInfoSnackBar(
title: 'No Profiles to Delete',
message: 'No additional profiles have been added');
}
return false;
}
Future<bool> addProfile({bool showSnackbar = true}) async {
List<String> profiles = profilesList();
Tuple2<bool, String> result = await SettingsDialogs()
.addProfile(LunaState.navigatorKey.currentContext, profiles);
if (result.item1) {
Database.profilesBox.put(result.item2, ProfileHiveObject.empty());
safelyChangeProfiles(result.item2);
if (showSnackbar)
showLunaSuccessSnackBar(
title: 'Profile Added',
message: result.item2,
);
}
return false;
}
}

View File

@@ -2,34 +2,51 @@ import 'dart:io';
import 'package:lunasea/core.dart';
import 'package:quick_actions/quick_actions.dart';
class LunaQuickActions {
static final QuickActions _quickActions = QuickActions();
class LunaQuickActions {
static final QuickActions _quickActions = QuickActions();
static bool get isPlatformCompatible => Platform.isAndroid || Platform.isIOS;
static bool get isPlatformCompatible => Platform.isAndroid || Platform.isIOS;
/// Initialize the quick actions by setting the action handler.
///
/// Required before the handler can actually receive any intents.
void initialize() {
if(isPlatformCompatible) _quickActions.initialize((action) => _handler(action));
setShortcutItems();
}
/// Initialize the quick actions by setting the action handler.
///
/// Required before the handler can actually receive any intents.
void initialize() {
if (isPlatformCompatible) _quickActions.initialize(_handler);
setShortcutItems();
}
/// Sets the shortcut items by checking the database and enabling the respective action if enabled.
void setShortcutItems() {
if(isPlatformCompatible) _quickActions.setShortcutItems(<ShortcutItem>[
if(LunaModule.SEARCH.enabled && LunaDatabaseValue.QUICK_ACTIONS_SEARCH.data) LunaModule.SEARCH.shortcutItem,
if(LunaModule.LIDARR.enabled && LunaDatabaseValue.QUICK_ACTIONS_LIDARR.data) LunaModule.LIDARR.shortcutItem,
if(LunaModule.RADARR.enabled && LunaDatabaseValue.QUICK_ACTIONS_RADARR.data) LunaModule.RADARR.shortcutItem,
if(LunaModule.SONARR.enabled && LunaDatabaseValue.QUICK_ACTIONS_SONARR.data) LunaModule.SONARR.shortcutItem,
if(LunaModule.NZBGET.enabled && LunaDatabaseValue.QUICK_ACTIONS_NZBGET.data) LunaModule.NZBGET.shortcutItem,
if(LunaModule.SABNZBD.enabled && LunaDatabaseValue.QUICK_ACTIONS_SABNZBD.data) LunaModule.SABNZBD.shortcutItem,
if(LunaModule.OVERSEERR.enabled && LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR.data) LunaModule.OVERSEERR.shortcutItem,
if(LunaModule.TAUTULLI.enabled && LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI.data) LunaModule.TAUTULLI.shortcutItem,
LunaModule.SETTINGS.shortcutItem,
]);
}
/// Sets the shortcut items by checking the database and enabling the respective action if enabled.
void setShortcutItems() {
if (isPlatformCompatible)
_quickActions.setShortcutItems(<ShortcutItem>[
if (LunaModule.SEARCH.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_SEARCH.data)
LunaModule.SEARCH.shortcutItem,
if (LunaModule.LIDARR.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_LIDARR.data)
LunaModule.LIDARR.shortcutItem,
if (LunaModule.RADARR.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_RADARR.data)
LunaModule.RADARR.shortcutItem,
if (LunaModule.SONARR.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_SONARR.data)
LunaModule.SONARR.shortcutItem,
if (LunaModule.NZBGET.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_NZBGET.data)
LunaModule.NZBGET.shortcutItem,
if (LunaModule.SABNZBD.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_SABNZBD.data)
LunaModule.SABNZBD.shortcutItem,
if (LunaModule.OVERSEERR.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_OVERSEERR.data)
LunaModule.OVERSEERR.shortcutItem,
if (LunaModule.TAUTULLI.enabled &&
LunaDatabaseValue.QUICK_ACTIONS_TAUTULLI.data)
LunaModule.TAUTULLI.shortcutItem,
LunaModule.SETTINGS.shortcutItem,
]);
}
/// The actual shortcut command handler
void _handler(String action) => LunaModule.LIDARR.fromKey(action)?.launch();
/// The actual shortcut command handler
void _handler(String action) => LunaModule.LIDARR.fromKey(action)?.launch();
}

View File

@@ -34,34 +34,41 @@ export 'ui/text.dart';
export 'ui/theme.dart';
export 'ui/will_pop_scope.dart';
// ignore: avoid_classes_with_only_static_members
class 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 BUTTON_BACKGROUND_OPACITY = 0.80;
static const double ELEVATION = 0.0;
static const EdgeInsets MARGIN_DEFAULT = const EdgeInsets.all(12.0);
static const EdgeInsets MARGIN_CARD = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0);
// Border-Related
static bool get shouldUseBorder => LunaTheme.isAMOLEDTheme && LunaTheme.useAMOLEDBorders;
static ShapeBorder get shapeBorder => shouldUseBorder ? LunaShapeBorder.roundedWithBorder() : LunaShapeBorder.rounded();
// 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 BUTTON_BACKGROUND_OPACITY = 0.80;
static const double ELEVATION = 0.0;
static const EdgeInsets MARGIN_DEFAULT = EdgeInsets.all(12.0);
static const EdgeInsets MARGIN_CARD = EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
);
// Border-Related
static bool get shouldUseBorder =>
LunaTheme.isAMOLEDTheme && LunaTheme.useAMOLEDBorders;
static ShapeBorder get shapeBorder => shouldUseBorder
? LunaShapeBorder.roundedWithBorder()
: LunaShapeBorder.rounded();
}

View File

@@ -3,287 +3,307 @@ import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
enum _APPBAR_TYPE {
DEFAULT,
EMPTY,
DROPDOWN,
DEFAULT,
EMPTY,
DROPDOWN,
}
class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
final _APPBAR_TYPE type;
final String title;
final List<Widget> actions;
final PreferredSizeWidget bottom;
final bool hideLeading;
final bool useDrawer;
final Widget child;
final double height;
final List<String> profiles;
final PageController pageController;
final List<ScrollController> scrollControllers;
final _APPBAR_TYPE type;
final String title;
final List<Widget> actions;
final PreferredSizeWidget bottom;
final bool hideLeading;
final bool useDrawer;
final Widget child;
final double height;
final List<String> profiles;
final PageController pageController;
final List<ScrollController> scrollControllers;
@override
Size get preferredSize {
double _size = (height ?? kToolbarHeight);
if(bottom != null) _size += bottom.preferredSize.height;
return new Size.fromHeight(_size);
}
@override
Size get preferredSize {
double _size = (height ?? kToolbarHeight);
if (bottom != null) _size += bottom.preferredSize.height;
return Size.fromHeight(_size);
}
LunaAppBar._internal({
@required this.type,
@required this.useDrawer,
this.title,
this.actions,
this.bottom,
this.child,
this.height,
this.profiles,
this.pageController,
this.scrollControllers,
this.hideLeading = false,
});
LunaAppBar._internal({
@required this.type,
@required this.useDrawer,
this.title,
this.actions,
this.bottom,
this.child,
this.height,
this.profiles,
this.pageController,
this.scrollControllers,
this.hideLeading = false,
});
/// 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<Widget> actions,
PreferredSizeWidget bottom,
bool useDrawer = false,
bool hideLeading = false,
PageController pageController,
List<ScrollController> 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,
pageController: pageController,
scrollControllers: scrollControllers,
hideLeading: hideLeading,
type: _APPBAR_TYPE.DEFAULT,
);
}
/// 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<Widget> actions,
PreferredSizeWidget bottom,
bool useDrawer = false,
bool hideLeading = false,
PageController pageController,
List<ScrollController> 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,
pageController: pageController,
scrollControllers: scrollControllers,
hideLeading: hideLeading,
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.
///
/// The default padding is for a [LunaTextInputBar].
factory LunaAppBar.empty({
@required Widget child,
@required double height,
EdgeInsets padding = LunaTextInputBar.appBarMargin,
Alignment alignment = Alignment.topCenter,
}) {
assert(child != null);
assert(height != null);
return LunaAppBar._internal(
child: Container(
child: child,
height: height,
padding: padding,
alignment: alignment,
),
height: height,
useDrawer: false,
type: _APPBAR_TYPE.EMPTY,
);
}
/// 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.
///
/// The default padding is for a [LunaTextInputBar].
factory LunaAppBar.empty({
@required Widget child,
@required double height,
EdgeInsets padding = LunaTextInputBar.appBarMargin,
Alignment alignment = Alignment.topCenter,
}) {
assert(child != null);
assert(height != null);
return LunaAppBar._internal(
child: Container(
child: child,
height: height,
padding: padding,
alignment: alignment,
),
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<String> profiles,
bool useDrawer = true,
bool hideLeading = false,
List<Widget> actions,
PageController pageController,
List<ScrollController> 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,
hideLeading: hideLeading,
pageController: pageController,
scrollControllers: scrollControllers,
type: _APPBAR_TYPE.DEFAULT,
);
return LunaAppBar._internal(
title: title,
profiles: profiles,
actions: actions,
useDrawer: useDrawer,
hideLeading: hideLeading,
pageController: pageController,
scrollControllers: scrollControllers,
type: _APPBAR_TYPE.DROPDOWN,
);
}
/// 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<String> profiles,
bool useDrawer = true,
bool hideLeading = false,
List<Widget> actions,
PageController pageController,
List<ScrollController> 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,
hideLeading: hideLeading,
pageController: pageController,
scrollControllers: scrollControllers,
type: _APPBAR_TYPE.DEFAULT,
);
return LunaAppBar._internal(
title: title,
profiles: profiles,
actions: actions,
useDrawer: useDrawer,
hideLeading: hideLeading,
pageController: pageController,
scrollControllers: scrollControllers,
type: _APPBAR_TYPE.DROPDOWN,
);
}
@override
State<StatefulWidget> createState() => _State();
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LunaAppBar> {
int _index;
int _index;
@override
void initState() {
_index = widget.pageController?.initialPage ?? 0;
widget.pageController?.addListener(_pageControllerListener);
super.initState();
}
@override
void initState() {
_index = widget.pageController?.initialPage ?? 0;
widget.pageController?.addListener(_pageControllerListener);
super.initState();
}
@override
void dispose() {
widget.pageController?.removeListener(_pageControllerListener);
super.dispose();
}
@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 _pageControllerListener() {
if ((widget.pageController?.page?.round() ?? _index) == _index) return;
_index = widget.pageController.page.round();
}
void _onTap() {
try {
if(
widget.scrollControllers != null && // Ensure that the array isn't null
widget.scrollControllers.length != 0 && // Ensure that there is at least one scroll controller
(widget.scrollControllers.length-1) >= _index && // Ensure that the index isn't outside of the array bounds
widget.scrollControllers[_index] != null // Ensure that the scroll controller at the position isn't 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: _onTap,
);
void _onTap() {
try {
if ((widget.scrollControllers?.isNotEmpty ?? false) &&
((widget.scrollControllers.length - 1) >= _index) &&
(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);
}
}
Widget _sharedLeading(BuildContext context) {
if(widget.hideLeading ?? false) return null;
if(widget.useDrawer) return IconButton(
icon: Icon(Icons.menu_rounded),
onPressed: () async {
HapticFeedback.lightImpact();
if(Scaffold.of(context).hasDrawer) {
Scaffold.of(context).openDrawer();
FocusManager.instance.primaryFocus?.unfocus();
}
},
);
return InkWell(
child: Center(
child: Icon(Icons.arrow_back_ios_rounded),
@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: _onTap,
);
}
Widget _sharedLeading(BuildContext context) {
if (widget.hideLeading ?? false) return null;
if (widget.useDrawer)
return IconButton(
icon: Icon(Icons.menu_rounded),
onPressed: () async {
HapticFeedback.lightImpact();
if (Scaffold.of(context).hasDrawer) {
Scaffold.of(context).openDrawer();
FocusManager.instance.primaryFocus?.unfocus();
}
},
);
return InkWell(
child: Center(
child: Icon(Icons.arrow_back_ios_rounded),
),
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),
automaticallyImplyLeading: !(widget.hideLeading ?? false),
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<String>(
tooltip: 'Change Profiles',
child: Wrap(
direction: Axis.horizontal,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
widget.title,
style: TextStyle(fontSize: LunaUI.FONT_SIZE_HEADER),
),
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),
automaticallyImplyLeading: !(widget.hideLeading ?? false),
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<String>(
tooltip: 'Change Profiles',
child: Wrap(
direction: Axis.horizontal,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
widget.title,
style: TextStyle(fontSize: LunaUI.FONT_SIZE_HEADER),
),
Icon(Icons.arrow_drop_down),
],
Icon(Icons.arrow_drop_down),
],
),
onSelected: (result) {
HapticFeedback.selectionClick();
LunaProfile().safelyChangeProfiles(result);
},
itemBuilder: (context) {
return <PopupMenuEntry<String>>[
for (String profile in widget.profiles)
PopupMenuItem<String>(
value: profile,
child: Text(
profile,
style: TextStyle(
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
color:
(LunaDatabaseValue.ENABLED_PROFILE.data ?? 'default') ==
profile
? LunaColours.accent
: Colors.white,
),
),
onSelected: (result) {
HapticFeedback.selectionClick();
LunaProfile().safelyChangeProfiles(result);
},
itemBuilder: (context) {
return <PopupMenuEntry<String>>[for(String profile in widget.profiles) PopupMenuItem<String>(
value: profile,
child: Text(
profile,
style: TextStyle(
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
color: (LunaDatabaseValue.ENABLED_PROFILE.data ?? 'default') == profile
? LunaColours.accent
: Colors.white,
),
),
)];
},
),
leading: _sharedLeading(context),
centerTitle: false,
elevation: 0,
actions: widget.actions,
);
}
)
];
},
),
leading: _sharedLeading(context),
centerTitle: false,
elevation: 0,
actions: widget.actions,
);
}
}

View File

@@ -2,90 +2,95 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaBanner extends StatelessWidget {
// An arbitrarily large number of max lines
static const _MAX_LINES = 5000000;
final String headerText;
final String bodyText;
final IconData icon;
final Color iconColor;
final Color backgroundColor;
final Color headerColor;
final Color bodyColor;
final Function dismissCallback;
final List<LunaButton> buttons;
// An arbitrarily large number of max lines
static const _MAX_LINES = 5000000;
final String headerText;
final String bodyText;
final IconData icon;
final Color iconColor;
final Color backgroundColor;
final Color headerColor;
final Color bodyColor;
final Function dismissCallback;
final List<LunaButton> buttons;
LunaBanner({
Key key,
this.dismissCallback,
@required this.headerText,
this.bodyText,
this.icon = Icons.info_outline_rounded,
this.iconColor = LunaColours.accent,
this.backgroundColor,
this.headerColor = Colors.white,
this.bodyColor = Colors.white70,
this.buttons,
}) : super(key: key);
LunaBanner({
Key key,
this.dismissCallback,
@required this.headerText,
this.bodyText,
this.icon = Icons.info_outline_rounded,
this.iconColor = LunaColours.accent,
this.backgroundColor,
this.headerColor = Colors.white,
this.bodyColor = Colors.white70,
this.buttons,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LunaCard(
context: context,
child: Container(
padding: EdgeInsets.symmetric(vertical: LunaUI.MARGIN_CARD.top),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: LunaUI.MARGIN_CARD,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
child: Icon(
icon,
size: 20.0,
color: iconColor,
),
padding: EdgeInsets.only(right: LunaUI.MARGIN_DEFAULT.right-2.0),
),
Expanded(
child: LunaText.title(
text: headerText,
color: headerColor,
maxLines: _MAX_LINES,
softWrap: true,
),
),
if(dismissCallback != null) InkWell(
child: Icon(
Icons.close_rounded,
size: 20.0,
color: LunaColours.accent,
),
borderRadius: BorderRadius.circular(24.0),
onTap: dismissCallback,
),
],
),
),
if(bodyText?.isNotEmpty ?? false) Padding(
padding: LunaUI.MARGIN_CARD.copyWith(top: 0),
child: LunaText.subtitle(
text: bodyText.toString(),
color: bodyColor,
softWrap: true,
maxLines: _MAX_LINES,
),
),
if(buttons?.isNotEmpty ?? false) LunaButtonContainer(
padding: EdgeInsets.symmetric(horizontal: LunaUI.MARGIN_CARD.left/2),
children: buttons,
),
],
),
@override
Widget build(BuildContext context) {
return LunaCard(
context: context,
child: Container(
padding: EdgeInsets.symmetric(vertical: LunaUI.MARGIN_CARD.top),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: LunaUI.MARGIN_CARD,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
child: Icon(
icon,
size: 20.0,
color: iconColor,
),
padding: EdgeInsets.only(
right: LunaUI.MARGIN_DEFAULT.right - 2.0),
),
Expanded(
child: LunaText.title(
text: headerText,
color: headerColor,
maxLines: _MAX_LINES,
softWrap: true,
),
),
if (dismissCallback != null)
InkWell(
child: Icon(
Icons.close_rounded,
size: 20.0,
color: LunaColours.accent,
),
borderRadius: BorderRadius.circular(24.0),
onTap: dismissCallback,
),
],
),
),
color: backgroundColor,
);
}
if (bodyText?.isNotEmpty ?? false)
Padding(
padding: LunaUI.MARGIN_CARD.copyWith(top: 0),
child: LunaText.subtitle(
text: bodyText.toString(),
color: bodyColor,
softWrap: true,
maxLines: _MAX_LINES,
),
),
if (buttons?.isNotEmpty ?? false)
LunaButtonContainer(
padding: EdgeInsets.symmetric(
horizontal: LunaUI.MARGIN_CARD.left / 2),
children: buttons,
),
],
),
),
color: backgroundColor,
);
}
}

View File

@@ -1,3 +1,3 @@
export 'bottom_bar/badge.dart';
export 'bottom_bar/action_bar.dart';
export 'bottom_bar/badge.dart';
export 'bottom_bar/navigation_bar.dart';

View File

@@ -1,42 +1,41 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
/// Create a [LunaBottomActionBar] that contains button actions.
///
///
/// The children are expected to be [LunaButton]s or children of [LunaButton].
class LunaBottomActionBar extends StatelessWidget {
final EdgeInsets padding;
final List<Widget> actions;
final bool useSafeArea;
final EdgeInsets padding;
final List<Widget> actions;
final bool useSafeArea;
LunaBottomActionBar({
@required this.actions,
this.padding = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 8.0),
this.useSafeArea = true,
Key key,
}) : super(key: key) {
assert(actions != null && actions.length != 0);
}
LunaBottomActionBar({
@required this.actions,
this.padding = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 8.0),
this.useSafeArea = true,
Key key,
}) : super(key: key) {
assert(actions?.isNotEmpty ?? false);
}
@override
Widget build(BuildContext context) {
return Container(
child: SafeArea(
top: useSafeArea,
bottom: useSafeArea,
left: useSafeArea,
right: useSafeArea,
child: Padding(
child: LunaButtonContainer(
children: actions,
padding: EdgeInsets.zero,
),
padding: padding,
),
),
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
);
}
}
@override
Widget build(BuildContext context) {
return Container(
child: SafeArea(
top: useSafeArea,
bottom: useSafeArea,
left: useSafeArea,
right: useSafeArea,
child: Padding(
child: LunaButtonContainer(
children: actions,
padding: EdgeInsets.zero,
),
padding: padding,
),
),
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
);
}
}

View File

@@ -3,31 +3,35 @@ 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.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY),
elevation: 0,
animationDuration: Duration(milliseconds: LunaUI.ANIMATION_SPEED),
animationType: BadgeAnimationType.fade,
shape: BadgeShape.circle,
position: BadgePosition.topEnd(
LunaNavigationBarBadge({
Key key,
@required String text,
@required IconData icon,
@required bool showBadge,
@required bool isActive,
}) : super(
key: key,
badgeColor:
LunaColours.accent.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY),
elevation: 0,
animationDuration: Duration(milliseconds: LunaUI.ANIMATION_SPEED),
animationType: BadgeAnimationType.fade,
shape: BadgeShape.circle,
position: BadgePosition.topEnd(
top: -15,
end: -15,
),
badgeContent: Text(
),
badgeContent: Text(
text,
style: TextStyle(color: Colors.white),
),
child: Icon(
),
child: Icon(
icon,
color: isActive ? LunaColours.accent.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY) : Colors.white,
),
showBadge: showBadge,
);
}
color: isActive
? LunaColours.accent
.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY)
: Colors.white,
),
showBadge: showBadge,
);
}

View File

@@ -3,115 +3,134 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaBottomNavigationBar extends StatefulWidget {
final PageController pageController;
final List<IconData> icons;
final List<String> titles;
final List<ScrollController> scrollControllers;
final List<Widget> topActions;
final Function(int) onTabChange;
final List<Widget> leadingOnTab;
final PageController pageController;
final List<IconData> icons;
final List<String> titles;
final List<ScrollController> scrollControllers;
final List<Widget> topActions;
final Function(int) onTabChange;
final List<Widget> leadingOnTab;
LunaBottomNavigationBar({
Key key,
@required this.pageController,
@required this.icons,
@required this.titles,
this.topActions,
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.');
}
LunaBottomNavigationBar({
Key key,
@required this.pageController,
@required this.icons,
@required this.titles,
this.topActions,
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<StatefulWidget> createState() => _State();
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LunaBottomNavigationBar> {
int _index;
int _index;
@override
void initState() {
_index = widget.pageController?.initialPage ?? 0;
widget.pageController?.addListener(_pageControllerListener);
super.initState();
}
@override
void initState() {
_index = widget.pageController?.initialPage ?? 0;
widget.pageController?.addListener(_pageControllerListener);
super.initState();
}
@override
void dispose() {
widget.pageController?.removeListener(_pageControllerListener);
super.dispose();
}
@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());
}
void _pageControllerListener() {
if ((widget.pageController?.page?.round() ?? _index) == _index) return;
setState(() => _index = widget.pageController.page.round());
}
@override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if((widget.topActions?.length ?? 0) != 0) LunaBottomActionBar(
actions: widget.topActions,
useSafeArea: false,
),
SafeArea(
child: Padding(
child: GNav(
gap: 8.0,
iconSize: 24.0,
duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED),
tabBackgroundColor: Theme.of(context).canvasColor.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY),
activeColor: LunaColours.accent,
tabs: List.generate(widget.icons.length, (index) => GButton(
icon: widget.icons[index],
text: widget.titles[index],
active: _index == index,
iconSize: 22.0,
haptic: true,
padding: _index == index
? EdgeInsets.all(10.0).add(EdgeInsets.only(left: 6.0))
: EdgeInsets.all(10.0),
iconColor: Colors.white,
textStyle: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_NAVIGATION_BAR,
color: Colors.white,
),
iconActiveColor: LunaColours.accent,
leading: widget.leadingOnTab == null ? null : widget.leadingOnTab[index],
)).toList(),
tabActiveBorder: LunaUI.shouldUseBorder ? Border.all(color: Colors.white12) : null,
tabBorder: LunaUI.shouldUseBorder ? Border.all(color: Colors.transparent) : null,
selectedIndex: _index,
onTabChange: (index) {
_onTabChange(index);
if(widget.onTabChange != null) widget.onTabChange(index);
},
),
padding: widget.topActions != null && widget.topActions.length != 0
? EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0)
: EdgeInsets.all(12.0),
),
top: false,
),
],
@override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if ((widget.topActions?.length ?? 0) != 0)
LunaBottomActionBar(
actions: widget.topActions,
useSafeArea: false,
),
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
);
}
SafeArea(
child: Padding(
child: GNav(
gap: 8.0,
iconSize: 24.0,
duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED),
tabBackgroundColor: Theme.of(context)
.canvasColor
.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY),
activeColor: LunaColours.accent,
tabs: List.generate(
widget.icons.length,
(index) => GButton(
icon: widget.icons[index],
text: widget.titles[index],
active: _index == index,
iconSize: 22.0,
haptic: true,
padding: _index == index
? EdgeInsets.all(10.0)
.add(EdgeInsets.only(left: 6.0))
: EdgeInsets.all(10.0),
iconColor: Colors.white,
textStyle: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_NAVIGATION_BAR,
color: Colors.white,
),
iconActiveColor: LunaColours.accent,
leading: widget.leadingOnTab == null
? null
: widget.leadingOnTab[index],
)).toList(),
tabActiveBorder: LunaUI.shouldUseBorder
? Border.all(color: Colors.white12)
: null,
tabBorder: LunaUI.shouldUseBorder
? Border.all(color: Colors.transparent)
: null,
selectedIndex: _index,
onTabChange: (index) {
_onTabChange(index);
if (widget.onTabChange != null) widget.onTabChange(index);
},
),
padding: (widget.topActions?.isNotEmpty ?? false)
? EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0)
: 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.lunaJumpToPage(index);
}
void _onTabChange(int index) {
if (index == _index) {
if (widget.scrollControllers != null &&
widget.scrollControllers[index] != null)
widget.scrollControllers[index].lunaAnimateToStart();
} else {
widget.pageController.lunaJumpToPage(index);
}
}
}

View File

@@ -3,17 +3,19 @@ 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<dynamic> showModal({
@required BuildContext context,
@required Widget Function(BuildContext) builder,
bool expand = false,
}) async => showBarModalBottomSheet(
/// Show a bottom modal sheet modal using the supplied builder to show the content.
Future<dynamic> showModal({
@required BuildContext context,
@required Widget Function(BuildContext) builder,
bool expand = false,
}) async =>
showBarModalBottomSheet(
context: context,
expand: expand,
backgroundColor: LunaTheme.isAMOLEDTheme ? Colors.black : LunaColours.secondary,
backgroundColor:
LunaTheme.isAMOLEDTheme ? Colors.black : LunaColours.secondary,
shape: LunaUI.shapeBorder,
builder: builder,
closeProgressThreshold: 0.90,
);
);
}

View File

@@ -3,254 +3,267 @@ import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
enum LunaButtonType {
TEXT,
ICON,
LOADER,
TEXT,
ICON,
LOADER,
}
/// A Luna-styled button.
class LunaButton extends Card {
static const DEFAULT_HEIGHT = 50.0;
static const DEFAULT_HEIGHT = 50.0;
LunaButton._({
Key key,
@required Widget child,
EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
Color backgroundColor,
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
LunaLoadingState loadingState,
}) : super(
key: key,
child: InkWell(
LunaButton._({
Key key,
@required Widget child,
EdgeInsets margin =
const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
Color backgroundColor,
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
LunaLoadingState loadingState,
}) : super(
key: key,
child: InkWell(
child: Container(
child: child,
decoration: decoration,
height: height,
alignment: alignment,
child: child,
decoration: decoration,
height: height,
alignment: alignment,
),
borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS),
onTap: () async {
HapticFeedback.lightImpact();
if(onTap != null && loadingState != LunaLoadingState.ACTIVE) onTap();
HapticFeedback.lightImpact();
if (onTap != null && loadingState != LunaLoadingState.ACTIVE)
onTap();
},
onLongPress: () async {
HapticFeedback.heavyImpact();
if( onLongPress != null && loadingState != LunaLoadingState.ACTIVE) onLongPress();
HapticFeedback.heavyImpact();
if (onLongPress != null &&
loadingState != LunaLoadingState.ACTIVE) onLongPress();
},
),
margin: margin,
color: backgroundColor != null
? backgroundColor.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY)
: LunaTheme.isAMOLEDTheme
? Colors.black.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY)
: LunaColours.primary.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY),
shape: LunaUI.shapeBorder,
elevation: LunaUI.ELEVATION,
clipBehavior: Clip.antiAlias,
) {
assert(child != null);
}
),
margin: margin,
color: backgroundColor != null
? backgroundColor.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY)
: LunaTheme.isAMOLEDTheme
? Colors.black.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY)
: LunaColours.primary
.withOpacity(LunaUI.BUTTON_BACKGROUND_OPACITY),
shape: LunaUI.shapeBorder,
elevation: LunaUI.ELEVATION,
clipBehavior: Clip.antiAlias,
) {
assert(child != null);
}
/// Create a default button.
///
/// If [LunaLoadingState] is passed in, will build the correct button based on the type.
factory LunaButton({
@required LunaButtonType type,
Color color = LunaColours.accent,
Color backgroundColor,
String text,
IconData icon,
double iconSize = 22.0,
LunaLoadingState loadingState,
EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
assert(type != null);
switch(loadingState) {
case LunaLoadingState.ACTIVE: return LunaButton.loader(
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
case LunaLoadingState.ERROR: return LunaButton.icon(
icon: Icons.error,
iconSize: iconSize,
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
default: break;
}
switch(type) {
case LunaButtonType.TEXT:
assert(text != null);
return LunaButton.text(
text: text,
icon: icon,
iconSize: iconSize,
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
case LunaButtonType.ICON:
assert(icon != null);
return LunaButton.icon(
icon: icon,
iconSize: iconSize,
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
case LunaButtonType.LOADER: return LunaButton.loader(
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
throw Exception("Attempted to create an invalid LunaButton");
/// Create a default button.
///
/// If [LunaLoadingState] is passed in, will build the correct button based on the type.
factory LunaButton({
@required LunaButtonType type,
Color color = LunaColours.accent,
Color backgroundColor,
String text,
IconData icon,
double iconSize = 22.0,
LunaLoadingState loadingState,
EdgeInsets margin =
const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
assert(type != null);
switch (loadingState) {
case LunaLoadingState.ACTIVE:
return LunaButton.loader(
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
case LunaLoadingState.ERROR:
return LunaButton.icon(
icon: Icons.error,
iconSize: iconSize,
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
default:
break;
}
switch (type) {
case LunaButtonType.TEXT:
assert(text != null);
return LunaButton.text(
text: text,
icon: icon,
iconSize: iconSize,
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
case LunaButtonType.ICON:
assert(icon != null);
return LunaButton.icon(
icon: icon,
iconSize: iconSize,
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
case LunaButtonType.LOADER:
return LunaButton.loader(
color: color,
backgroundColor: backgroundColor,
margin: margin,
height: height,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
throw Exception("Attempted to create an invalid LunaButton");
}
/// Build a button that contains a centered text string.
factory LunaButton.text({
@required String text,
@required IconData icon,
double iconSize = 22.0,
Color color = LunaColours.accent,
Color backgroundColor,
EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
return LunaButton._(
child: Padding(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if(icon != null) Padding(
child: Icon(
icon,
color: color,
size: iconSize,
),
padding: EdgeInsets.only(right: 8.0),
),
Flexible(
child: Text(
text,
style: TextStyle(
color: Colors.white,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_BUTTON,
),
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
),
],
/// Build a button that contains a centered text string.
factory LunaButton.text({
@required String text,
@required IconData icon,
double iconSize = 22.0,
Color color = LunaColours.accent,
Color backgroundColor,
EdgeInsets margin =
const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
return LunaButton._(
child: Padding(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (icon != null)
Padding(
child: Icon(
icon,
color: color,
size: iconSize,
),
padding: EdgeInsets.symmetric(horizontal: 12.0),
padding: EdgeInsets.only(right: 8.0),
),
Flexible(
child: Text(
text,
style: TextStyle(
color: Colors.white,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_BUTTON,
),
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
),
margin: margin,
height: height,
backgroundColor: backgroundColor,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
],
),
padding: EdgeInsets.symmetric(horizontal: 12.0),
),
margin: margin,
height: height,
backgroundColor: backgroundColor,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
/// Build a button that contains a [LunaLoader].
factory LunaButton.loader({
EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
Color color = LunaColours.accent,
Color backgroundColor,
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
return LunaButton._(
child: LunaLoader(
useSafeArea: false,
color: color,
size: 16.0,
),
margin: margin,
height: height,
backgroundColor: backgroundColor,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
/// Build a button that contains a [LunaLoader].
factory LunaButton.loader({
EdgeInsets margin =
const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
Color color = LunaColours.accent,
Color backgroundColor,
double height = DEFAULT_HEIGHT,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
return LunaButton._(
child: LunaLoader(
useSafeArea: false,
color: color,
size: 16.0,
),
margin: margin,
height: height,
backgroundColor: backgroundColor,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
/// Build a button that contains a single, centered [Icon].
factory LunaButton.icon({
@required IconData icon,
Color color = LunaColours.accent,
Color backgroundColor,
EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
double height = DEFAULT_HEIGHT,
double iconSize = 22.0,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
return LunaButton._(
child: Icon(
icon,
color: color,
size: iconSize,
),
margin: margin,
height: height,
backgroundColor: backgroundColor,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
/// Build a button that contains a single, centered [Icon].
factory LunaButton.icon({
@required IconData icon,
Color color = LunaColours.accent,
Color backgroundColor,
EdgeInsets margin =
const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0),
double height = DEFAULT_HEIGHT,
double iconSize = 22.0,
Alignment alignment = Alignment.center,
Decoration decoration,
Function onTap,
Function onLongPress,
}) {
return LunaButton._(
child: Icon(
icon,
color: color,
size: iconSize,
),
margin: margin,
height: height,
backgroundColor: backgroundColor,
alignment: alignment,
decoration: decoration,
onTap: onTap,
onLongPress: onLongPress,
);
}
}

View File

@@ -2,30 +2,30 @@ 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,
double height,
double width,
}) : super(
key: key,
child: Container(
LunaCard({
Key key,
@required BuildContext context,
@required Widget child,
EdgeInsets margin = LunaUI.MARGIN_CARD,
Color color,
Decoration decoration,
double height,
double width,
}) : super(
key: key,
child: Container(
child: child,
decoration: decoration,
height: height,
width: width,
),
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);
}
),
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);
}
}

View File

@@ -1,38 +1,62 @@
import 'package:flutter/material.dart';
class LunaColours {
LunaColours._();
/// List of LunaSea colours in order that the should appear in a list.
///
/// Use [byListIndex] to fetch the colour at the any index
static const _LIST_COLOR_ICONS = [
blue,
accent,
red,
orange,
purple,
blueGrey,
];
static const LIST_COLOR_ICONS = [
blue,
accent,
red,
orange,
purple,
blueGrey,
];
/// Core accent colour
static const Color accent = Color(0xFF4ECCA3);
static const Color accent = const Color(0xFF4ECCA3);
static const Color splash = const Color(0xFF2EA07B);
static const Color primary = const Color(0xFF32323E);
static const Color secondary = const Color(0xFF282834);
/// Core splash colour
static const Color splash = Color(0xFF2EA07B);
static const Color blue = const Color(0xFF00A8E8);
static const Color orange = const Color(0xFFFF9000);
static const Color red = const Color(0xFFF71735);
static const Color purple = const Color(0xFF9649CB);
static const Color blueGrey = const Color(0xFF848FA5);
/// Core primary colour (background)
static const Color primary = Color(0xFF32323E);
static Color list(int i) {
return LIST_COLOR_ICONS[i%LIST_COLOR_ICONS.length];
/// Core secondary colour (appbar, bottom bar, etc.),
static const Color secondary = Color(0xFF282834);
/// Blue
static const Color blue = Color(0xFF00A8E8);
/// Orange
static const Color orange = Color(0xFFFF9000);
/// Red
static const Color red = Color(0xFFF71735);
/// Purple
static const Color purple = Color(0xFF9649CB);
/// Blue Grey
static const Color blueGrey = Color(0xFF848FA5);
/// Returns the correct colour for a graph by what layer it is on the graph canvas.
Color byGraphLayer(int i) {
switch (i) {
case 0:
return LunaColours.accent;
case 1:
return LunaColours.purple;
case 2:
return LunaColours.blue;
default:
return byListIndex(i);
}
}
static Color graph(int i) {
switch(i) {
case 0: return LunaColours.accent;
case 1: return LunaColours.purple;
case 2: return LunaColours.blue;
default: return LunaColours.list(i);
}
}
/// Return the correct colour for a list.
/// If the index is greater than the list of colour's length, uses modulus to loop list.
Color byListIndex(int i) {
return _LIST_COLOR_ICONS[i % _LIST_COLOR_ICONS.length];
}
}

View File

@@ -1,26 +1,28 @@
import 'package:flutter/material.dart';
class LunaButtonContainer extends StatelessWidget {
final List<Widget> children;
final EdgeInsets padding;
final List<Widget> 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);
}
LunaButtonContainer({
Key key,
@required this.children,
this.padding = const EdgeInsets.symmetric(horizontal: 6.0),
}) {
assert(children != null);
assert(children?.isNotEmpty ?? false);
assert(padding != null);
}
@override
Widget build(BuildContext context) {
return Padding(
child: Row(
children: children.map<Expanded>((button) => Expanded(child: button)).toList(),
),
padding: padding,
);
}
}
@override
Widget build(BuildContext context) {
return Padding(
child: Row(
children: children
.map<Expanded>((button) => Expanded(child: button))
.toList(),
),
padding: padding,
);
}
}

View File

@@ -1,24 +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 ?? 0,
) {
this?.addListener(_focusListener);
}
LunaPageController({
int initialPage,
}) : super(
initialPage: initialPage ?? 0,
) {
this?.addListener(_focusListener);
}
void _focusListener() {
FocusManager.instance.primaryFocus?.unfocus();
}
void _focusListener() {
FocusManager.instance.primaryFocus?.unfocus();
}
@override
void dispose() {
this?.removeListener(_focusListener);
super.dispose();
}
}
@override
void dispose() {
this?.removeListener(_focusListener);
super.dispose();
}
}

View File

@@ -2,23 +2,27 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
// ignore: non_constant_identifier_names
Decoration LunaCardDecoration({ @required String uri, @required Map headers }) {
if(LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY.data == 0) return null;
if(uri == null || uri.isEmpty) return BoxDecoration(borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS));
Decoration LunaCardDecoration({@required String uri, @required Map headers}) {
if (LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY.data == 0) return null;
if (uri == null || uri.isEmpty)
return BoxDecoration(
image: DecorationImage(
image: NetworkImage(
uri,
headers: Map<String, String>.from(headers),
),
// To prevent excessive logs, just handle an error loading the image with an empty callback
onError: (error, stack) {},
colorFilter: ColorFilter.mode(
Colors.black.withOpacity((LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY.data as int)/100),
BlendMode.dstATop,
),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS),
);
borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS));
return BoxDecoration(
image: DecorationImage(
image: NetworkImage(
uri,
headers: Map<String, String>.from(headers),
),
// To prevent excessive logs, just handle an error loading the image with an empty callback
onError: (error, stack) {},
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(
(LunaDatabaseValue.THEME_IMAGE_BACKGROUND_OPACITY.data as int) /
100),
BlendMode.dstATop,
),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS),
);
}

View File

@@ -0,0 +1 @@

View File

@@ -3,246 +3,271 @@ import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
abstract class LunaDialog {
static const HEADER_SIZE = LunaUI.FONT_SIZE_HEADER;
static const BODY_SIZE = LunaUI.FONT_SIZE_SUBTITLE;
static const SUBBODY_SIZE = LunaUI.FONT_SIZE_SUBHEADER;
static const BUTTON_SIZE = LunaUI.FONT_SIZE_SUBHEADER;
static const HEADER_SIZE = LunaUI.FONT_SIZE_HEADER;
static const BODY_SIZE = LunaUI.FONT_SIZE_SUBTITLE;
static const SUBBODY_SIZE = LunaUI.FONT_SIZE_SUBHEADER;
static const BUTTON_SIZE = LunaUI.FONT_SIZE_SUBHEADER;
static Widget title({ @required String text }) => Text(
static Widget title({@required String text}) => Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: LunaDialog.HEADER_SIZE,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaDialog.HEADER_SIZE,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
),
);
);
static TextSpan bolded({ @required String text, double fontSize = LunaDialog.BODY_SIZE, Color color }) => TextSpan(
static TextSpan bolded(
{@required String text,
double fontSize = LunaDialog.BODY_SIZE,
Color color}) =>
TextSpan(
text: text,
style: TextStyle(
color: color == null
? LunaColours.accent
: color,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: fontSize,
color: color == null ? LunaColours.accent : color,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: fontSize,
),
);
);
static Widget richText({ @required List<TextSpan> children, TextAlign alignment = TextAlign.start }) => RichText(
static Widget richText(
{@required List<TextSpan> children,
TextAlign alignment = TextAlign.start}) =>
RichText(
text: TextSpan(
style: TextStyle(
fontSize: LunaDialog.BODY_SIZE,
),
children: children,
style: TextStyle(
fontSize: LunaDialog.BODY_SIZE,
),
children: children,
),
textAlign: alignment,
);
);
static Widget button({ @required String text, @required void Function() onPressed, Color textColor }) => TextButton(
static Widget button(
{@required String text,
@required void Function() onPressed,
Color textColor}) =>
TextButton(
child: Text(
text,
style: TextStyle(
color: textColor == null
? LunaColours.accent
: textColor,
fontSize: LunaDialog.BUTTON_SIZE,
),
text,
style: TextStyle(
color: textColor == null ? LunaColours.accent : textColor,
fontSize: LunaDialog.BUTTON_SIZE,
),
),
onPressed: onPressed == null ? null : () async {
HapticFeedback.lightImpact();
onPressed();
},
);
onPressed: onPressed == null
? null
: () async {
HapticFeedback.lightImpact();
onPressed();
},
);
static Widget cancel(BuildContext context, { Color textColor = Colors.white, String text }) => TextButton(
static Widget cancel(BuildContext context,
{Color textColor = Colors.white, String text}) =>
TextButton(
child: Text(
text ?? 'lunasea.Cancel'.tr(),
style: TextStyle(
color: textColor,
fontSize: LunaDialog.BUTTON_SIZE,
),
text ?? 'lunasea.Cancel'.tr(),
style: TextStyle(
color: textColor,
fontSize: LunaDialog.BUTTON_SIZE,
),
),
onPressed: () {
HapticFeedback.lightImpact();
Navigator.of(context).pop();
HapticFeedback.lightImpact();
Navigator.of(context).pop();
},
);
);
static Widget content({ @required List<Widget> children }) => SingleChildScrollView(
static Widget content({@required List<Widget> children}) =>
SingleChildScrollView(
child: ListBody(
children: children,
children: children,
),
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 1.0),
);
);
static Widget textContent({ @required String text, TextAlign textAlign = TextAlign.center }) => Text(
static Widget textContent(
{@required String text, TextAlign textAlign = TextAlign.center}) =>
Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
),
textAlign: textAlign,
);
);
static TextSpan textSpanContent({ @required String text }) => TextSpan(
static TextSpan textSpanContent({@required String text}) => TextSpan(
text: text,
style: TextStyle(
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
),
);
);
static Widget textInput({
@required TextEditingController controller,
@required Function(String) onSubmitted,
@required String title,
}) => TextField(
static Widget textInput({
@required TextEditingController controller,
@required Function(String) onSubmitted,
@required String title,
}) =>
TextField(
autofocus: true,
autocorrect: false,
controller: controller,
decoration: InputDecoration(
labelText: title,
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LunaDialog.BODY_SIZE,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent.withOpacity(0.3)),
),
labelText: title,
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LunaDialog.BODY_SIZE,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent.withOpacity(0.3)),
),
),
style: TextStyle(
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
),
cursorColor: LunaColours.accent,
textInputAction: TextInputAction.done,
onSubmitted: onSubmitted,
);
);
static Widget textFormInput({
@required TextEditingController controller,
@required String title,
@required Function(String) onSubmitted,
@required Function(String) validator,
bool obscureText = false,
TextInputType keyboardType = TextInputType.text,
}) => TextFormField(
static Widget textFormInput({
@required TextEditingController controller,
@required String title,
@required Function(String) onSubmitted,
@required Function(String) validator,
bool obscureText = false,
TextInputType keyboardType = TextInputType.text,
}) =>
TextFormField(
autofocus: true,
autocorrect: false,
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: title,
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LunaDialog.BODY_SIZE,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent.withOpacity(0.3)),
),
labelText: title,
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LunaDialog.BODY_SIZE,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent.withOpacity(0.3)),
),
),
style: TextStyle(
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
color: Colors.white,
fontSize: LunaDialog.BODY_SIZE,
),
cursorColor: LunaColours.accent,
textInputAction: TextInputAction.done,
validator: validator,
onFieldSubmitted: onSubmitted,
);
);
static Widget tile({
@required IconData icon,
Color iconColor,
@required String text,
RichText subtitle,
Function onTap,
}) => ListTile(
static Widget tile({
@required IconData icon,
Color iconColor,
@required String text,
RichText subtitle,
Function onTap,
}) =>
ListTile(
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
icon ?? Icons.error_outline,
color: iconColor ?? LunaColours.accent,
),
],
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
icon ?? Icons.error_outline,
color: iconColor ?? LunaColours.accent,
),
],
),
title: Text(
text,
style: TextStyle(
fontSize: LunaDialog.BODY_SIZE,
color: Colors.white,
),
text,
style: TextStyle(
fontSize: LunaDialog.BODY_SIZE,
color: Colors.white,
),
),
subtitle: text == null
subtitle: text == null ? null : subtitle,
onTap: onTap == null
? null
: subtitle,
onTap: onTap == null ? null : () async {
HapticFeedback.selectionClick();
onTap();
},
: () async {
HapticFeedback.selectionClick();
onTap();
},
contentPadding: tileContentPadding(),
);
);
static CheckboxListTile checkbox({
@required String title,
@required bool value,
@required void Function(bool) onChanged,
}) => CheckboxListTile(
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,
),
title,
style: TextStyle(
fontSize: BODY_SIZE,
color: Colors.white,
),
),
value: value,
onChanged: onChanged,
contentPadding: tileContentPadding(),
);
);
static EdgeInsets tileContentPadding() => EdgeInsets.symmetric(horizontal: 32.0, vertical: 0.0);
static EdgeInsets textDialogContentPadding() => EdgeInsets.fromLTRB(24.0, 32.0, 24.0, 14.0);
static EdgeInsets listDialogContentPadding() => EdgeInsets.fromLTRB(0.0, 26.0, 0.0, 0.0);
static EdgeInsets inputTextDialogContentPadding() => EdgeInsets.fromLTRB(24.0, 34.0, 24.0, 22.0);
static EdgeInsets inputDialogContentPadding() => EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 22.0);
static EdgeInsets tileContentPadding() =>
EdgeInsets.symmetric(horizontal: 32.0, vertical: 0.0);
static EdgeInsets textDialogContentPadding() =>
EdgeInsets.fromLTRB(24.0, 32.0, 24.0, 14.0);
static EdgeInsets listDialogContentPadding() =>
EdgeInsets.fromLTRB(0.0, 26.0, 0.0, 0.0);
static EdgeInsets inputTextDialogContentPadding() =>
EdgeInsets.fromLTRB(24.0, 34.0, 24.0, 22.0);
static EdgeInsets inputDialogContentPadding() =>
EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 22.0);
static Future<void> dialog({
@required BuildContext context,
@required String title,
@required EdgeInsets contentPadding,
bool showCancelButton = true,
String cancelButtonText,
List<Widget> buttons,
List<Widget> content,
Widget customContent,
}) async {
if(customContent == null) assert(content != null, 'customContent and content both cannot be null');
await showDialog(
context: context,
builder: (context) => AlertDialog(
actions: <Widget>[
if(showCancelButton) LunaDialog.cancel(
context,
text: cancelButtonText,
textColor: buttons != null ? Colors.white : LunaColours.accent,
),
if(buttons != null) ...buttons,
],
title: LunaDialog.title(text: title),
content: customContent ?? LunaDialog.content(children: content),
contentPadding: contentPadding,
shape: LunaUI.shapeBorder,
static Future<void> dialog({
@required BuildContext context,
@required String title,
@required EdgeInsets contentPadding,
bool showCancelButton = true,
String cancelButtonText,
List<Widget> buttons,
List<Widget> content,
Widget customContent,
}) async {
if (customContent == null)
assert(content != null, 'customContent and content both cannot be null');
await showDialog(
context: context,
builder: (context) => AlertDialog(
actions: <Widget>[
if (showCancelButton)
LunaDialog.cancel(
context,
text: cancelButtonText,
textColor: buttons != null ? Colors.white : LunaColours.accent,
),
);
}
if (buttons != null) ...buttons,
],
title: LunaDialog.title(text: title),
content: customContent ?? LunaDialog.content(children: content),
contentPadding: contentPadding,
shape: LunaUI.shapeBorder,
),
);
}
}

View File

@@ -2,10 +2,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,
);
LunaDivider()
: super(
thickness: 1.0,
color: LunaColours.splash,
indent: 60.0,
endIndent: 60.0,
);
}

View File

@@ -3,110 +3,115 @@ import 'package:lunasea/core.dart';
import 'package:lunasea/modules/wake_on_lan.dart';
class LunaDrawer extends StatelessWidget {
final String page;
final String page;
LunaDrawer({
@required this.page,
});
LunaDrawer({
@required this.page,
});
@override
Widget build(BuildContext context) {
return LunaDatabaseValue.ENABLED_PROFILE.listen(
builder: (context, lunaBox, widget) => ValueListenableBuilder(
valueListenable: Database.indexersBox.listenable(),
builder: (context, indexerBox, widget) => Drawer(
child: ListView(
children: _getDrawerEntries(context),
padding: EdgeInsets.only(bottom: 8.0),
physics: ClampingScrollPhysics(),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return LunaDatabaseValue.ENABLED_PROFILE.listen(
builder: (context, lunaBox, widget) => ValueListenableBuilder(
valueListenable: Database.indexersBox.listenable(),
builder: (context, indexerBox, widget) => Drawer(
child: ListView(
children: _getDrawerEntries(context),
padding: EdgeInsets.only(bottom: 8.0),
physics: ClampingScrollPhysics(),
),
),
),
);
}
List<Widget> _getDrawerEntries(BuildContext context) {
List<LunaModule> automation = Database?.currentProfileObject?.enabledAutomationModules ?? [];
List<LunaModule> clients = Database?.currentProfileObject?.enabledClientModules ?? [];
List<LunaModule> monitoring = Database?.currentProfileObject?.enabledMonitoringModules ?? [];
return <Widget>[
LunaDrawerHeader(),
_buildEntry(context: context, module: LunaModule.DASHBOARD),
_buildEntry(context: context, module: LunaModule.SETTINGS),
LunaDivider(),
if(Database.indexersBox?.isNotEmpty ?? false) _buildEntry(context: context, module: LunaModule.SEARCH),
if(
(LunaModule.WAKE_ON_LAN?.enabled ?? false) &&
(Database?.currentProfileObject?.wakeOnLANEnabled ?? false)
) _buildWakeOnLAN(context),
if(automation?.isNotEmpty ?? false) ...List.generate(
automation.length,
(index) => _buildEntry(context: context, module: automation[index]),
),
if(clients?.isNotEmpty ?? false) ...List.generate(
clients.length,
(index) => _buildEntry(context: context, module: clients[index]),
),
if(monitoring?.isNotEmpty ?? false) ...List.generate(
monitoring.length,
(index) => _buildEntry(context: context, module: monitoring[index]),
),
];
}
List<Widget> _getDrawerEntries(BuildContext context) {
List<LunaModule> automation =
Database?.currentProfileObject?.enabledAutomationModules ?? [];
List<LunaModule> clients =
Database?.currentProfileObject?.enabledClientModules ?? [];
List<LunaModule> monitoring =
Database?.currentProfileObject?.enabledMonitoringModules ?? [];
return <Widget>[
LunaDrawerHeader(),
_buildEntry(context: context, module: LunaModule.DASHBOARD),
_buildEntry(context: context, module: LunaModule.SETTINGS),
LunaDivider(),
if (Database.indexersBox?.isNotEmpty ?? false)
_buildEntry(context: context, module: LunaModule.SEARCH),
if ((LunaModule.WAKE_ON_LAN?.enabled ?? false) &&
(Database?.currentProfileObject?.wakeOnLANEnabled ?? false))
_buildWakeOnLAN(context),
if (automation?.isNotEmpty ?? false)
...List.generate(
automation.length,
(index) => _buildEntry(context: context, module: automation[index]),
),
if (clients?.isNotEmpty ?? false)
...List.generate(
clients.length,
(index) => _buildEntry(context: context, module: clients[index]),
),
if (monitoring?.isNotEmpty ?? false)
...List.generate(
monitoring.length,
(index) => _buildEntry(context: context, module: monitoring[index]),
),
];
}
Widget _buildEntry({
@required BuildContext context,
@required LunaModule module,
}) {
bool currentPage = page == module.key.toLowerCase();
return ListTile(
leading: Icon(
module.icon,
color: currentPage ? LunaColours.accent : Colors.white,
),
title: Text(
module.name,
style: TextStyle(
color: currentPage
? LunaColours.accent
: Colors.white,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
onTap: () async {
Navigator.of(context).pop();
if(!currentPage) module.launch();
},
contentPadding: EdgeInsets.fromLTRB(16.0, 0.0, 0.0, 0.0),
);
}
Widget _buildEntry({
@required BuildContext context,
@required LunaModule module,
}) {
bool currentPage = page == module.key.toLowerCase();
return ListTile(
leading: Icon(
module.icon,
color: currentPage ? LunaColours.accent : Colors.white,
),
title: Text(
module.name,
style: TextStyle(
color: currentPage ? LunaColours.accent : Colors.white,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
onTap: () async {
Navigator.of(context).pop();
if (!currentPage) module.launch();
},
contentPadding: EdgeInsets.fromLTRB(16.0, 0.0, 0.0, 0.0),
);
}
Widget _buildWakeOnLAN(BuildContext context) {
return ListTile(
leading: Icon(LunaModule.WAKE_ON_LAN.icon),
title: Text(
LunaModule.WAKE_ON_LAN.name,
style: TextStyle(
color: Colors.white,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
onTap: () async {
WakeOnLANAPI api = WakeOnLANAPI.fromProfile();
await api.wake()
.then((_) => showLunaSuccessSnackBar(
title: 'Machine is Waking Up...',
message: 'Magic packet successfully sent',
Widget _buildWakeOnLAN(BuildContext context) {
return ListTile(
leading: Icon(LunaModule.WAKE_ON_LAN.icon),
title: Text(
LunaModule.WAKE_ON_LAN.name,
style: TextStyle(
color: Colors.white,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
onTap: () async {
WakeOnLANAPI api = WakeOnLANAPI.fromProfile();
await api
.wake()
.then((_) => showLunaSuccessSnackBar(
title: 'Machine is Waking Up...',
message: 'Magic packet successfully sent',
))
.catchError((error, stack) {
LunaLogger().error('Failed to wake machine', error, stack);
return showLunaErrorSnackBar(
title: 'Failed to Wake Machine',
error: error,
);
});
},
contentPadding: EdgeInsets.fromLTRB(16.0, 0.0, 0.0, 0.0),
);
}
.catchError((error, stack) {
LunaLogger().error('Failed to wake machine', error, stack);
return showLunaErrorSnackBar(
title: 'Failed to Wake Machine',
error: error,
);
});
},
contentPadding: EdgeInsets.fromLTRB(16.0, 0.0, 0.0, 0.0),
);
}
}

View File

@@ -3,60 +3,68 @@ import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
class LunaDrawerHeader extends UserAccountsDrawerHeader {
LunaDrawerHeader() : super(
accountName: StreamBuilder(
LunaDrawerHeader()
: super(
accountName: StreamBuilder(
stream: LunaFirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) => LunaText.title(text: LunaFirebaseAuth().email ?? 'LunaSea'),
),
accountEmail: LunaDatabaseValue.ENABLED_PROFILE.listen(
builder: (context, snapshot) =>
LunaText.title(text: LunaFirebaseAuth().email ?? 'LunaSea'),
),
accountEmail: LunaDatabaseValue.ENABLED_PROFILE.listen(
builder: (context, lunaBox, widget) => ValueListenableBuilder(
valueListenable: Database.profilesBox.listenable(),
builder: (context, profilesBox, widget) => Padding(
child: LunaPopupMenuButton<String>(
tooltip: 'Change Profiles',
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
LunaText.subtitle(
text: LunaDatabaseValue.ENABLED_PROFILE.data,
),
Icon(
Icons.arrow_drop_down,
color: Colors.white70,
size: LunaUI.FONT_SIZE_HEADER,
),
],
),
onSelected: (result) {
HapticFeedback.selectionClick();
LunaProfile().safelyChangeProfiles(result);
},
itemBuilder: (context) {
return <PopupMenuEntry<String>>[for(String profile in (profilesBox as Box).keys) PopupMenuItem<String>(
value: profile,
child: Text(
profile,
style: TextStyle(
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
color: (LunaDatabaseValue.ENABLED_PROFILE.data ?? 'default') == profile
? LunaColours.accent
: Colors.white,
),
),
)];
},
),
padding: EdgeInsets.only(right: 12.0),
valueListenable: Database.profilesBox.listenable(),
builder: (context, profilesBox, widget) => Padding(
child: LunaPopupMenuButton<String>(
tooltip: 'Change Profiles',
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
LunaText.subtitle(
text: LunaDatabaseValue.ENABLED_PROFILE.data,
),
Icon(
Icons.arrow_drop_down,
color: Colors.white70,
size: LunaUI.FONT_SIZE_HEADER,
),
],
),
onSelected: (result) {
HapticFeedback.selectionClick();
LunaProfile().safelyChangeProfiles(result);
},
itemBuilder: (context) {
return <PopupMenuEntry<String>>[
for (String profile in (profilesBox as Box).keys)
PopupMenuItem<String>(
value: profile,
child: Text(
profile,
style: TextStyle(
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
color: (LunaDatabaseValue.ENABLED_PROFILE.data ??
'default') ==
profile
? LunaColours.accent
: Colors.white,
),
),
)
];
},
),
padding: EdgeInsets.only(right: 12.0),
),
),
),
decoration: BoxDecoration(
),
decoration: BoxDecoration(
color: LunaColours.accent,
image: DecorationImage(
image: AssetImage('assets/images/branding/icon_drawer.png'),
colorFilter: ColorFilter.mode(LunaColours.primary.withOpacity(0.15), BlendMode.dstATop),
fit: BoxFit.cover,
image: AssetImage('assets/images/branding/icon_drawer.png'),
colorFilter: ColorFilter.mode(
LunaColours.primary.withOpacity(0.15), BlendMode.dstATop),
fit: BoxFit.cover,
),
),
);
),
);
}

View File

@@ -2,34 +2,34 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaFloatingActionButtonAnimated extends StatelessWidget {
final Object heroTag;
final AnimatedIconData icon;
final AnimationController controller;
final Color color;
final Color backgroundColor;
final Function onPressed;
final Object heroTag;
final AnimatedIconData icon;
final AnimationController controller;
final Color color;
final Color backgroundColor;
final Function onPressed;
LunaFloatingActionButtonAnimated({
Key key,
@required this.onPressed,
@required this.icon,
@required this.controller,
this.backgroundColor = LunaColours.accent,
this.color = Colors.white,
this.heroTag,
}) : super(key: key);
LunaFloatingActionButtonAnimated({
Key key,
@required this.onPressed,
@required this.icon,
@required this.controller,
this.backgroundColor = LunaColours.accent,
this.color = Colors.white,
this.heroTag,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FloatingActionButton(
child: AnimatedIcon(
icon: icon,
color: color,
progress: controller,
),
heroTag: heroTag,
onPressed: onPressed,
backgroundColor: backgroundColor,
);
}
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
child: AnimatedIcon(
icon: icon,
color: color,
progress: controller,
),
heroTag: heroTag,
onPressed: onPressed,
backgroundColor: backgroundColor,
);
}
}

View File

@@ -2,39 +2,39 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaFloatingActionButtonExtended extends StatelessWidget {
final Color color;
final Color backgroundColor;
final IconData icon;
final String label;
final void Function() onPressed;
final Object heroTag;
final Color color;
final Color backgroundColor;
final IconData icon;
final String label;
final void Function() onPressed;
final Object heroTag;
LunaFloatingActionButtonExtended({
Key key,
@required this.icon,
@required this.label,
@required this.onPressed,
this.backgroundColor = LunaColours.accent,
this.color = Colors.white,
this.heroTag,
}) : super(key: key);
LunaFloatingActionButtonExtended({
Key key,
@required this.icon,
@required this.label,
@required this.onPressed,
this.backgroundColor = LunaColours.accent,
this.color = Colors.white,
this.heroTag,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FloatingActionButton.extended(
icon: Icon(icon, color: color),
onPressed: onPressed,
label: Text(
label,
style: TextStyle(
color: color,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
letterSpacing: 0.35,
),
),
backgroundColor: backgroundColor,
heroTag: heroTag,
);
}
@override
Widget build(BuildContext context) {
return FloatingActionButton.extended(
icon: Icon(icon, color: color),
onPressed: onPressed,
label: Text(
label,
style: TextStyle(
color: color,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
letterSpacing: 0.35,
),
),
backgroundColor: backgroundColor,
heroTag: heroTag,
);
}
}

View File

@@ -2,54 +2,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,
});
final String text;
final String 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,
),
),
],
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: EdgeInsets.fromLTRB(12.0, 6.0, 12.0, 8.0),
);
}
),
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),
);
}
}

View File

@@ -2,37 +2,37 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaHighlightedNode extends StatelessWidget {
final Color backgroundColor;
final Color textColor;
final String text;
final Color backgroundColor;
final Color textColor;
final String text;
LunaHighlightedNode({
@required this.text,
this.backgroundColor = LunaColours.accent,
this.textColor = Colors.white,
}) {
assert(text != null);
}
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),
),
);
}
@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),
),
);
}
}

View File

@@ -3,83 +3,83 @@ 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;
final AlignmentGeometry alignment;
final EdgeInsetsGeometry padding;
final IconData icon;
final double iconSize;
final String text;
final double textSize;
final Color color;
final Function onPressed;
final Function onLongPress;
final LunaLoadingState loadingState;
final AlignmentGeometry alignment;
final EdgeInsetsGeometry padding;
LunaIconButton({
Key key,
this.text,
this.textSize = 10.0,
this.icon,
this.iconSize = 24.0,
this.alignment = Alignment.center,
this.padding = const EdgeInsets.all(8.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');
}
LunaIconButton({
Key key,
this.text,
this.textSize = 10.0,
this.icon,
this.iconSize = 24.0,
this.alignment = Alignment.center,
this.padding = const EdgeInsets.all(8.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,
alignment: alignment,
padding: padding,
onPressed: _onPressed(),
),
onLongPress: _onLongPress(),
);
}
@override
Widget build(BuildContext context) {
return InkWell(
child: IconButton(
icon: loadingState == LunaLoadingState.ACTIVE ? _loader() : _icon(),
iconSize: iconSize,
alignment: alignment,
padding: padding,
onPressed: _onPressed(),
),
onLongPress: _onLongPress(),
);
}
Function _onPressed() {
if(onPressed == null) return null;
if(loadingState == LunaLoadingState.ACTIVE) return null;
return () async {
HapticFeedback.lightImpact();
onPressed();
};
}
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();
};
}
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,
useSafeArea: false,
);
}
Widget _loader() {
return LunaLoader(
size: 12.0,
color: color,
useSafeArea: false,
);
}
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,
),
);
}
}
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,
),
);
}
}

View File

@@ -2,42 +2,75 @@ import 'package:flutter/widgets.dart';
// export 'package:font_awesome_flutter/font_awesome_flutter.dart';
class LunaIcons {
LunaIcons._();
LunaIcons._();
static const _kFontFam = 'LunaIcons';
static const String _kFontPkg = null;
static const _kFontFam = 'LunaIcons';
static const String _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);
static const IconData overseerr = IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg);
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);
static const IconData overseerr =
IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@@ -3,124 +3,133 @@ import 'package:flutter/rendering.dart';
import 'package:lunasea/core.dart';
class LunaTextInputBar extends StatefulWidget {
static const double appBarHeight = 62.0;
static const double appBarInnerHeight = 62.0-13.0;
static const EdgeInsets appBarMargin = const EdgeInsets.fromLTRB(12.0, 1.0, 12.0, 12.0);
final TextEditingController controller;
final ScrollController scrollController;
final TextInputAction action;
final TextInputType keyboardType;
final Iterable<String> 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;
static const double appBarHeight = 62.0;
static const double appBarInnerHeight = 62.0 - 13.0;
static const EdgeInsets appBarMargin = EdgeInsets.fromLTRB(
12.0,
1.0,
12.0,
12.0,
);
LunaTextInputBar({
@required this.controller,
this.scrollController,
this.onChanged,
this.onSubmitted,
this.validator,
this.autofillHints,
this.action = TextInputAction.search,
this.keyboardType = TextInputType.text,
this.labelText,
this.labelIcon = Icons.search,
this.margin = LunaUI.MARGIN_CARD,
this.autofocus = false,
this.obscureText = false,
this.isFormField = false,
}) {
assert(controller != null);
}
final TextEditingController controller;
final ScrollController scrollController;
final TextInputAction action;
final TextInputType keyboardType;
final Iterable<String> 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;
@override
State<StatefulWidget> createState() => _State();
LunaTextInputBar({
@required this.controller,
this.scrollController,
this.onChanged,
this.onSubmitted,
this.validator,
this.autofillHints,
this.action = TextInputAction.search,
this.keyboardType = TextInputType.text,
this.labelText,
this.labelIcon = Icons.search,
this.margin = LunaUI.MARGIN_CARD,
this.autofocus = false,
this.obscureText = false,
this.isFormField = false,
}) {
assert(controller != null);
}
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LunaTextInputBar> {
bool _isFocused = false;
bool _isFocused = false;
@override
void initState() {
super.initState();
widget.controller?.addListener(_controllerListener);
}
@override
void initState() {
super.initState();
widget.controller?.addListener(_controllerListener);
}
@override
void dispose() {
widget.controller?.removeListener(_controllerListener);
super.dispose();
}
@override
void dispose() {
widget.controller?.removeListener(_controllerListener);
super.dispose();
}
void _controllerListener() {
if(mounted) setState(() {});
}
void _controllerListener() {
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) => LunaCard(
@override
Widget build(BuildContext context) => LunaCard(
context: context,
margin: widget.margin,
child: FocusScope(
child: Focus(
onFocusChange: (value) => setState(() => _isFocused = value),
child: widget.isFormField ? _isForm : _isNotForm,
),
child: Focus(
onFocusChange: (value) => setState(() => _isFocused = value),
child: widget.isFormField ? _isForm : _isNotForm,
),
),
height: LunaTextInputBar.appBarInnerHeight,
color: Theme.of(context).canvasColor,
);
);
TextStyle get _sharedTextStyle => TextStyle(
TextStyle get _sharedTextStyle => TextStyle(
color: Colors.white,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
);
);
InputDecoration get _sharedInputDecoration => InputDecoration(
InputDecoration get _sharedInputDecoration => InputDecoration(
labelText: widget.labelText ?? 'lunasea.SearchTextBar'.tr(),
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
suffixIcon: AnimatedOpacity(
child: InkWell(
child: Icon(
Icons.close,
color: LunaColours.accent,
size: 24.0,
),
onTap: !_isFocused || widget.controller.text == '' ? null : () {
child: InkWell(
child: Icon(
Icons.close,
color: LunaColours.accent,
size: 24.0,
),
onTap: !_isFocused || widget.controller.text == ''
? null
: () {
widget.scrollController?.lunaAnimateToStart();
widget.controller.text = '';
if(widget.onChanged != null) widget.onChanged('');
},
mouseCursor: !_isFocused || widget.controller.text == '' ? SystemMouseCursors.text : SystemMouseCursors.click,
borderRadius: BorderRadius.circular(24.0),
hoverColor: Colors.transparent,
),
opacity: !_isFocused || widget.controller.text == '' ? 0.0 : 1.0,
duration: Duration(milliseconds: 200),
if (widget.onChanged != null) widget.onChanged('');
},
mouseCursor: !_isFocused || widget.controller.text == ''
? SystemMouseCursors.text
: SystemMouseCursors.click,
borderRadius: BorderRadius.circular(24.0),
hoverColor: Colors.transparent,
),
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),
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(
TextFormField get _isForm => TextFormField(
autofillHints: widget.autofillHints,
autofocus: widget.autofocus,
controller: widget.controller,
@@ -135,9 +144,9 @@ class _State extends State<LunaTextInputBar> {
onTap: widget.scrollController?.lunaAnimateToStart,
onChanged: widget.onChanged == null ? null : widget.onChanged,
onFieldSubmitted: widget?.onSubmitted,
);
);
TextField get _isNotForm => TextField(
TextField get _isNotForm => TextField(
autofillHints: widget.autofillHints,
autofocus: widget.autofocus,
controller: widget.controller,
@@ -151,5 +160,5 @@ class _State extends State<LunaTextInputBar> {
onTap: widget.scrollController?.lunaAnimateToStart,
onChanged: widget.onChanged == null ? null : widget.onChanged,
onSubmitted: widget?.onSubmitted,
);
);
}

View File

@@ -3,42 +3,47 @@ import 'package:flutter/rendering.dart';
import 'package:lunasea/core.dart';
class LunaListTile extends Card {
LunaListTile({
Key key,
EdgeInsets margin = LunaUI.MARGIN_CARD,
@required BuildContext context,
@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(
LunaListTile({
Key key,
EdgeInsets margin = LunaUI.MARGIN_CARD,
@required BuildContext context,
@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,
mouseCursor: onTap != null || onLongPress != null ? SystemMouseCursors.click : null,
contentPadding: contentPadding ? customContentPadding ?? EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0) : null,
),
borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS),
mouseCursor: SystemMouseCursors.click,
onTap: onTap,
onLongPress: onLongPress,
child: ListTile(
title: title,
subtitle: subtitle,
trailing: trailing,
leading: leading,
mouseCursor: onTap != null || onLongPress != null
? SystemMouseCursors.click
: null,
contentPadding: contentPadding
? customContentPadding ??
EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0)
: null,
),
borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS),
mouseCursor: SystemMouseCursors.click,
onTap: onTap,
onLongPress: onLongPress,
),
decoration: decoration,
),
margin: margin,
elevation: LunaUI.ELEVATION,
shape: LunaUI.shapeBorder,
color: color ?? Theme.of(context).primaryColor,
);
}
),
margin: margin,
elevation: LunaUI.ELEVATION,
shape: LunaUI.shapeBorder,
color: color ?? Theme.of(context).primaryColor,
);
}

View File

@@ -1,5 +1,5 @@
/// This is a modified version of [ReorderableListView] that was originally pulled from Flutter stable v2.0.4.
///
///
/// Change includes ensuring that the `padding` is pulled and applied to the inner padding/list padding, and not applied to the list externally.
/// This fixes the bug that caused the list to get clipped from the top padding.

View File

@@ -1,33 +1,36 @@
import 'package:flutter/material.dart';
class LunaListView extends StatelessWidget {
final List<Widget> children;
final double itemExtent;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final List<Widget> children;
final double itemExtent;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
LunaListView({
Key key,
@required this.children,
@required this.controller,
this.itemExtent,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key);
LunaListView({
Key key,
@required this.children,
@required this.controller,
this.itemExtent,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: ListView(
controller: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
itemExtent: itemExtent,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
),
);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: ListView(
controller: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
itemExtent: itemExtent,
padding: padding ??
MediaQuery.of(context)
.padding
.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
),
);
}
}

View File

@@ -1,39 +1,42 @@
import 'package:flutter/material.dart';
class LunaListViewBuilder extends StatelessWidget {
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final double itemExtent;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final double itemExtent;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
LunaListViewBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
this.itemExtent,
@required this.controller,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
LunaListViewBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
this.itemExtent,
@required this.controller,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
itemCount: itemCount,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
),
);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: padding ??
MediaQuery.of(context)
.padding
.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
itemCount: itemCount,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
),
);
}
}

View File

@@ -2,35 +2,37 @@ import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
/// [ListView] with a [Scrollbar] that is intended to be used with a modal bottom sheet.
///
///
/// The [ScrollController] is pulled from the context from the sheet, and cannot be defined.
///
/// By default, the list is shrink-wrapped.
class LunaListViewModal extends StatelessWidget {
final List<Widget> children;
final double itemExtent;
final bool shrinkWrap;
final List<Widget> children;
final double itemExtent;
final bool shrinkWrap;
LunaListViewModal({
Key key,
@required this.children,
this.itemExtent,
this.shrinkWrap = true,
}) : super(key: key);
LunaListViewModal({
Key key,
@required this.children,
this.itemExtent,
this.shrinkWrap = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: ModalScrollController.of(context),
child: ListView(
controller: ModalScrollController.of(context),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
itemExtent: itemExtent,
shrinkWrap: shrinkWrap,
padding: MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
),
);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: ModalScrollController.of(context),
child: ListView(
controller: ModalScrollController.of(context),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
itemExtent: itemExtent,
shrinkWrap: shrinkWrap,
padding: MediaQuery.of(context)
.padding
.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
),
);
}
}

View File

@@ -2,41 +2,43 @@ import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
/// [ListView] builder with a [Scrollbar] that is intended to be used with a modal bottom sheet.
///
///
/// The [ScrollController] is pulled from the context from the sheet, and cannot be defined.
///
/// By default, the list is shrink-wrapped.
class LunaListViewModalBuilder extends StatelessWidget {
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final double itemExtent;
final bool shrinkWrap;
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final double itemExtent;
final bool shrinkWrap;
LunaListViewModalBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
this.itemExtent,
this.shrinkWrap = true,
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
LunaListViewModalBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
this.itemExtent,
this.shrinkWrap = true,
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: ModalScrollController.of(context),
child: ListView.builder(
controller: ModalScrollController.of(context),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
shrinkWrap: shrinkWrap,
),
);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: ModalScrollController.of(context),
child: ListView.builder(
controller: ModalScrollController.of(context),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: MediaQuery.of(context)
.padding
.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
shrinkWrap: shrinkWrap,
),
);
}
}

View File

@@ -2,87 +2,95 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaPagedListView<T> extends StatefulWidget {
final GlobalKey<RefreshIndicatorState> refreshKey;
final PagingController<int, T> pagingController;
final ScrollController scrollController;
final void Function(int) listener;
final Widget Function(BuildContext, T, int) itemBuilder;
final double itemExtent;
final EdgeInsetsGeometry padding;
final String noItemsFoundMessage;
final GlobalKey<RefreshIndicatorState> refreshKey;
final PagingController<int, T> pagingController;
final ScrollController scrollController;
final void Function(int) listener;
final Widget Function(BuildContext, T, int) itemBuilder;
final double itemExtent;
final EdgeInsetsGeometry padding;
final String noItemsFoundMessage;
LunaPagedListView({
Key key,
@required this.refreshKey,
@required this.pagingController,
@required this.listener,
@required this.itemBuilder,
@required this.noItemsFoundMessage,
@required this.scrollController,
this.itemExtent,
this.padding,
}) : super(key: key) {
assert(refreshKey != null);
assert(pagingController != null);
assert(listener != null);
assert(itemBuilder != null);
assert(noItemsFoundMessage != null);
}
LunaPagedListView({
Key key,
@required this.refreshKey,
@required this.pagingController,
@required this.listener,
@required this.itemBuilder,
@required this.noItemsFoundMessage,
@required this.scrollController,
this.itemExtent,
this.padding,
}) : super(key: key) {
assert(refreshKey != null);
assert(pagingController != null);
assert(listener != null);
assert(itemBuilder != null);
assert(noItemsFoundMessage != null);
}
@override
State<StatefulWidget> createState() => _State<T>();
@override
State<StatefulWidget> createState() => _State<T>();
}
class _State<T> extends State<LunaPagedListView<T>> {
@override
void initState() {
super.initState();
widget.pagingController.addPageRequestListener((pageKey) => widget.listener(pageKey));
}
@override
void initState() {
super.initState();
widget.pagingController
.addPageRequestListener((pageKey) => widget.listener(pageKey));
}
@override
Widget build(BuildContext context) {
return LunaRefreshIndicator(
key: widget.refreshKey,
context: context,
onRefresh: () => Future.sync(() => widget.pagingController.refresh()),
child: Scrollbar(
controller: widget.scrollController,
child: PagedListView<int, T>(
pagingController: widget.pagingController,
scrollController: widget.scrollController,
itemExtent: widget.itemExtent,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: widget.itemBuilder,
firstPageErrorIndicatorBuilder: (context) => LunaMessage.error(onTap: () => Future.sync(() => widget.pagingController.refresh())),
firstPageProgressIndicatorBuilder: (context) => LunaLoader(),
newPageProgressIndicatorBuilder: (context) => Padding(
child: Container(
alignment: Alignment.center,
height: 40.0,
child: LunaLoader(size: 16.0, useSafeArea: false),
),
padding: EdgeInsets.only(bottom: 16.0),
),
newPageErrorIndicatorBuilder: (context) => LunaIconButton(icon: Icons.error, color: LunaColours.red),
noItemsFoundIndicatorBuilder: (context) => LunaMessage(
text: widget.noItemsFoundMessage,
buttonText: 'lunasea.Refresh'.tr(),
onTap: () => Future.sync(() => widget.pagingController.refresh()),
),
noMoreItemsIndicatorBuilder: (context) => Padding(
child: LunaIconButton(
icon: Icons.check,
color: LunaColours.accent,
),
padding: EdgeInsets.only(bottom: 16.0),
),
),
padding: widget.padding ?? MediaQuery.of(context).padding.copyWith(bottom: 8.0).add(EdgeInsets.only(top: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
),
@override
Widget build(BuildContext context) {
return LunaRefreshIndicator(
key: widget.refreshKey,
context: context,
onRefresh: () => Future.sync(() => widget.pagingController.refresh()),
child: Scrollbar(
controller: widget.scrollController,
child: PagedListView<int, T>(
pagingController: widget.pagingController,
scrollController: widget.scrollController,
itemExtent: widget.itemExtent,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: widget.itemBuilder,
firstPageErrorIndicatorBuilder: (context) => LunaMessage.error(
onTap: () =>
Future.sync(() => widget.pagingController.refresh())),
firstPageProgressIndicatorBuilder: (context) => LunaLoader(),
newPageProgressIndicatorBuilder: (context) => Padding(
child: Container(
alignment: Alignment.center,
height: 40.0,
child: LunaLoader(size: 16.0, useSafeArea: false),
),
padding: EdgeInsets.only(bottom: 16.0),
),
);
}
newPageErrorIndicatorBuilder: (context) =>
LunaIconButton(icon: Icons.error, color: LunaColours.red),
noItemsFoundIndicatorBuilder: (context) => LunaMessage(
text: widget.noItemsFoundMessage,
buttonText: 'lunasea.Refresh'.tr(),
onTap: () => Future.sync(() => widget.pagingController.refresh()),
),
noMoreItemsIndicatorBuilder: (context) => Padding(
child: LunaIconButton(
icon: Icons.check,
color: LunaColours.accent,
),
padding: EdgeInsets.only(bottom: 16.0),
),
),
padding: widget.padding ??
MediaQuery.of(context)
.padding
.copyWith(bottom: 8.0)
.add(EdgeInsets.only(top: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
),
),
);
}
}

View File

@@ -2,36 +2,39 @@ import 'package:flutter/material.dart';
import './flutter_reorderable_list_view.dart';
class LunaReorderableListView extends StatelessWidget {
final List<Widget> children;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final void Function(int, int) onReorder;
final bool buildDefaultDragHandles;
final List<Widget> children;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final void Function(int, int) onReorder;
final bool buildDefaultDragHandles;
LunaReorderableListView({
Key key,
@required this.children,
@required this.controller,
@required this.onReorder,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
this.buildDefaultDragHandles = false,
}) : super(key: key);
LunaReorderableListView({
Key key,
@required this.children,
@required this.controller,
@required this.onReorder,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
this.buildDefaultDragHandles = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: FlutterReorderableListView(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
onReorder: onReorder,
buildDefaultDragHandles: buildDefaultDragHandles,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: FlutterReorderableListView(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
padding: padding ??
MediaQuery.of(context)
.padding
.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
onReorder: onReorder,
buildDefaultDragHandles: buildDefaultDragHandles,
),
);
}
}

View File

@@ -2,42 +2,45 @@ import 'package:flutter/material.dart';
import './flutter_reorderable_list_view.dart';
class LunaReorderableListViewBuilder extends StatelessWidget {
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final void Function(int, int) onReorder;
final bool buildDefaultDragHandles;
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final void Function(int, int) onReorder;
final bool buildDefaultDragHandles;
LunaReorderableListViewBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
@required this.controller,
@required this.onReorder,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
this.buildDefaultDragHandles = false,
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
LunaReorderableListViewBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
@required this.controller,
@required this.onReorder,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
this.buildDefaultDragHandles = false,
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: FlutterReorderableListView.builder(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
itemCount: itemCount,
itemBuilder: itemBuilder,
onReorder: onReorder,
buildDefaultDragHandles: buildDefaultDragHandles,
),
);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: FlutterReorderableListView.builder(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: padding ??
MediaQuery.of(context)
.padding
.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
itemCount: itemCount,
itemBuilder: itemBuilder,
onReorder: onReorder,
buildDefaultDragHandles: buildDefaultDragHandles,
),
);
}
}

View File

@@ -3,32 +3,32 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:lunasea/core.dart';
class LunaLoader extends StatelessWidget {
final double size;
final Color color;
final bool useSafeArea;
final double size;
final Color color;
final bool useSafeArea;
LunaLoader({
Key key,
this.size = 25.0,
this.color,
this.useSafeArea = true,
}): super(key: key);
LunaLoader({
Key key,
this.size = 25.0,
this.color,
this.useSafeArea = true,
}) : super(key: key);
@override
Widget build(BuildContext context) => SafeArea(
@override
Widget build(BuildContext context) => SafeArea(
left: useSafeArea,
right: useSafeArea,
top: useSafeArea,
bottom: useSafeArea,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SpinKitThreeBounce(
color: color != null ? color : LunaColours.accent,
size: size,
),
],
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SpinKitThreeBounce(
color: color != null ? color : LunaColours.accent,
size: size,
),
],
),
);
);
}

View File

@@ -2,136 +2,139 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaMessage extends StatelessWidget {
final String text;
final Color textColor;
final String buttonText;
final Function onTap;
final bool useSafeArea;
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');
}
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,
);
}
/// 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: 'lunasea.GoBack'.tr(),
onTap: () => Navigator.of(context).pop(),
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: 'lunasea.GoBack'.tr(),
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: 'lunasea.AnErrorHasOccurred'.tr(),
buttonText: 'lunasea.TryAgain'.tr(),
onTap: onTap,
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: 'lunasea.AnErrorHasOccurred'.tr(),
buttonText: 'lunasea.TryAgain'.tr(),
onTap: onTap,
useSafeArea: useSafeArea,
);
}
/// Return a pre-structured "<module> Is Not Enabled" message, with a "Return to Dashboard" 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: 'lunasea.ModuleIsNotEnabled'.tr(args: [module]),
buttonText: 'lunasea.ReturnToDashboard'.tr(),
onTap: () async => LunaModule.DASHBOARD.launch(),
useSafeArea: useSafeArea,
);
}
/// Return a pre-structured "<module> Is Not Enabled" message, with a "Return to Dashboard" 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: 'lunasea.ModuleIsNotEnabled'.tr(args: [module]),
buttonText: 'lunasea.ReturnToDashboard'.tr(),
onTap: () async => LunaModule.DASHBOARD.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),
),
),
],
),
@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,
),
),
if(buttonText != null) LunaButtonContainer(
children: [
LunaButton.text(
text: buttonText,
icon: null,
onTap: onTap,
color: Colors.white,
backgroundColor: LunaColours.accent,
),
],
),
],
margin:
EdgeInsets.symmetric(vertical: 24.0, horizontal: 12.0),
),
),
],
),
);
}
),
if (buttonText != null)
LunaButtonContainer(
children: [
LunaButton.text(
text: buttonText,
icon: null,
onTap: onTap,
color: Colors.white,
backgroundColor: LunaColours.accent,
),
],
),
],
),
);
}
}

View File

@@ -2,45 +2,49 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaNetworkImage extends ClipRRect {
LunaNetworkImage({
double height,
double width,
String url,
@required String placeholderAsset,
Map headers,
bool roundCorners = true,
}) : super(
child: Container(
LunaNetworkImage({
double height,
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,
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<String, String>()),
imageErrorBuilder: (context, error, stack) => Container(
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<String, String>()),
imageErrorBuilder: (context, error, stack) => Container(
height: height,
width: width,
),
),
],
),
],
),
height: height,
width: width,
),
borderRadius: roundCorners ? BorderRadius.circular(LunaUI.BORDER_RADIUS) : null,
) {
// assert(height != null);
// assert(width != null);
assert(placeholderAsset != null);
}
),
borderRadius:
roundCorners ? BorderRadius.circular(LunaUI.BORDER_RADIUS) : null,
) {
// assert(height != null);
// assert(width != null);
assert(placeholderAsset != null);
}
}

View File

@@ -3,24 +3,28 @@ import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
class LunaPopupMenuButton<T> extends PopupMenuButton<T> {
LunaPopupMenuButton({
IconData icon,
Widget child,
@required void Function(T) onSelected,
@required List<PopupMenuEntry<T>> Function(BuildContext) itemBuilder,
String tooltip,
}) : super(
shape: LunaUI.shapeBorder,
tooltip: tooltip,
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');
}
LunaPopupMenuButton({
IconData icon,
Widget child,
@required void Function(T) onSelected,
@required List<PopupMenuEntry<T>> Function(BuildContext) itemBuilder,
String tooltip,
}) : super(
shape: LunaUI.shapeBorder,
tooltip: tooltip,
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');
}
}

View File

@@ -1,15 +1,15 @@
import 'package:flutter/material.dart';
class LunaRefreshIndicator extends RefreshIndicator {
LunaRefreshIndicator({
GlobalKey<RefreshIndicatorState> key,
@required BuildContext context,
@required Future<void> Function() onRefresh,
@required Widget child,
}) : super(
key: key,
backgroundColor: Theme.of(context).primaryColor,
onRefresh: onRefresh,
child: child,
);
LunaRefreshIndicator({
GlobalKey<RefreshIndicatorState> key,
@required BuildContext context,
@required Future<void> Function() onRefresh,
@required Widget child,
}) : super(
key: key,
backgroundColor: Theme.of(context).primaryColor,
onRefresh: onRefresh,
child: child,
);
}

View File

@@ -2,56 +2,59 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaScaffold extends StatelessWidget {
final GlobalKey<ScaffoldState> scaffoldKey;
final PreferredSizeWidget appBar;
final Widget body;
final Widget drawer;
final Widget bottomNavigationBar;
final Widget floatingActionButton;
final bool extendBody;
final bool extendBodyBehindAppBar;
/// Called when [LunaDatabaseValue.ENABLED_PROFILE] has changed. Triggered within the build function.
final void Function(BuildContext) onProfileChange;
final GlobalKey<ScaffoldState> scaffoldKey;
final PreferredSizeWidget appBar;
final Widget body;
final Widget drawer;
final Widget bottomNavigationBar;
final Widget floatingActionButton;
final bool extendBody;
final bool extendBodyBehindAppBar;
LunaScaffold({
Key key,
@required this.scaffoldKey,
this.appBar,
this.body,
this.drawer,
this.bottomNavigationBar,
this.floatingActionButton,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.onProfileChange,
}) : super(key: key);
/// Called when [LunaDatabaseValue.ENABLED_PROFILE] has changed. Triggered within the build function.
final void Function(BuildContext) onProfileChange;
@override
Widget build(BuildContext context) {
if(drawer != null) return LunaWillPopScope(
scaffoldKey: scaffoldKey,
child: scaffold,
LunaScaffold({
Key key,
@required this.scaffoldKey,
this.appBar,
this.body,
this.drawer,
this.bottomNavigationBar,
this.floatingActionButton,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.onProfileChange,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (drawer != null)
return LunaWillPopScope(
scaffoldKey: scaffoldKey,
child: scaffold,
);
return scaffold;
}
Widget get scaffold {
return ValueListenableBuilder(
valueListenable: Database.lunaSeaBox
.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, _, __) {
if (onProfileChange != null) onProfileChange(context);
return Scaffold(
key: scaffoldKey,
appBar: appBar,
body: body,
drawer: drawer,
bottomNavigationBar: bottomNavigationBar,
floatingActionButton: floatingActionButton,
extendBody: extendBody,
extendBodyBehindAppBar: extendBodyBehindAppBar,
onDrawerChanged: (_) => FocusManager.instance.primaryFocus?.unfocus(),
);
return scaffold;
}
Widget get scaffold {
return ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, _, __) {
if(onProfileChange != null) onProfileChange(context);
return Scaffold(
key: scaffoldKey,
appBar: appBar,
body: body,
drawer: drawer,
bottomNavigationBar: bottomNavigationBar,
floatingActionButton: floatingActionButton,
extendBody: extendBody,
extendBodyBehindAppBar: extendBodyBehindAppBar,
onDrawerChanged: (_) => FocusManager.instance.primaryFocus?.unfocus(),
);
},
);
}
},
);
}
}

View File

@@ -2,13 +2,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,
);
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);
factory LunaShapeBorder.rounded() => LunaShapeBorder._internal();
factory LunaShapeBorder.roundedWithBorder() =>
LunaShapeBorder._internal(useBorder: true);
}

View File

@@ -4,93 +4,109 @@ import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
enum LunaSnackbarType {
SUCCESS,
ERROR,
INFO,
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;
}
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;
}
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<void> 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(
@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))],
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(LunaUI.BORDER_RADIUS),
borderColor: LunaUI.shouldUseBorder ? Colors.white12 : Colors.transparent,
borderColor:
LunaUI.shouldUseBorder ? Colors.white12 : Colors.transparent,
child: FlashBar(
title: Text(
title,
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_TITLE,
),
title: Text(
title,
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_TITLE,
),
message: Text(
message,
style: TextStyle(
color: Colors.white70,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
message: Text(
message,
style: TextStyle(
color: Colors.white70,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
icon: Padding(
child: LunaIconButton(
icon: type.icon,
color: type.color,
),
padding: EdgeInsets.only(left: _PADDING),
),
icon: Padding(
child: LunaIconButton(
icon: type.icon,
color: type.color,
),
primaryAction: showButton
? TextButton(
child: Text(
buttonText.toUpperCase(),
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
color: LunaColours.accent,
),
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();
},
),
onPressed: () {
HapticFeedback.lightImpact();
controller.dismiss();
buttonOnPressed();
},
)
: null,
: null,
),
),
);
),
);

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