diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 7c56964..1dc6cf7 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 12.0
+ 13.0
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
index 592ceee..ec97fc6 100644
--- a/ios/Flutter/Debug.xcconfig
+++ b/ios/Flutter/Debug.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
index 592ceee..c4855bf 100644
--- a/ios/Flutter/Release.xcconfig
+++ b/ios/Flutter/Release.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 0000000..620e46e
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
new file mode 100644
index 0000000..9865c0b
--- /dev/null
+++ b/ios/Podfile.lock
@@ -0,0 +1,79 @@
+PODS:
+ - Flutter (1.0.0)
+ - flutter_local_notifications (0.0.1):
+ - Flutter
+ - open_mail (0.0.1):
+ - Flutter
+ - package_info_plus (0.4.5):
+ - Flutter
+ - path_provider_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - receive_sharing_intent (1.8.1):
+ - Flutter
+ - restart_app (0.0.1):
+ - Flutter
+ - share_plus (0.0.1):
+ - Flutter
+ - shared_preferences_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - sqflite_darwin (0.0.4):
+ - Flutter
+ - FlutterMacOS
+ - url_launcher_ios (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
+ - open_mail (from `.symlinks/plugins/open_mail/ios`)
+ - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
+ - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
+ - restart_app (from `.symlinks/plugins/restart_app/ios`)
+ - share_plus (from `.symlinks/plugins/share_plus/ios`)
+ - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
+ - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ flutter_local_notifications:
+ :path: ".symlinks/plugins/flutter_local_notifications/ios"
+ open_mail:
+ :path: ".symlinks/plugins/open_mail/ios"
+ package_info_plus:
+ :path: ".symlinks/plugins/package_info_plus/ios"
+ path_provider_foundation:
+ :path: ".symlinks/plugins/path_provider_foundation/darwin"
+ receive_sharing_intent:
+ :path: ".symlinks/plugins/receive_sharing_intent/ios"
+ restart_app:
+ :path: ".symlinks/plugins/restart_app/ios"
+ share_plus:
+ :path: ".symlinks/plugins/share_plus/ios"
+ shared_preferences_foundation:
+ :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
+ sqflite_darwin:
+ :path: ".symlinks/plugins/sqflite_darwin/darwin"
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
+
+SPEC CHECKSUMS:
+ Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
+ open_mail: b6b41374f5000a2ea1edc09ffe39d9e434e2747a
+ package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
+ path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
+ receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
+ restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf
+ share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
+ shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
+ sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
+ url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
+
+PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
+
+COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index f37b276..3342015 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -8,9 +8,11 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 282C99441982CA8501094594 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B330A4E7EB0D7F3F14A1C113 /* Pods_RunnerTests.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 7901162544EAEF4A1612DA25 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EF5EE3B86BFC740FF7A25E4 /* 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 */; };
@@ -40,14 +42,19 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 05FEB0F37A3ECEDFE5C8DE9C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ 0EF5EE3B86BFC740FF7A25E4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 4BC94B6C6F4DBB54399B5B2D /* 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 = ""; };
+ 5159CDFF4E027972758E5D62 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 951F0BE69A05C23BE7D9369B /* 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 = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -55,19 +62,40 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ B330A4E7EB0D7F3F14A1C113 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ B8D649AE805A0A4FC977F5C6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ D7EBFC691C8BD61A968BD6BB /* 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 = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 94D13764D2D87371C66A842E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 282C99441982CA8501094594 /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 7901162544EAEF4A1612DA25 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 119C1BA1DE158960BA4E8DFC /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 0EF5EE3B86BFC740FF7A25E4 /* Pods_Runner.framework */,
+ B330A4E7EB0D7F3F14A1C113 /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@@ -76,6 +104,20 @@
path = RunnerTests;
sourceTree = "";
};
+ 85B3F19C60BA9BAA96798E69 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 4BC94B6C6F4DBB54399B5B2D /* Pods-Runner.debug.xcconfig */,
+ 951F0BE69A05C23BE7D9369B /* Pods-Runner.release.xcconfig */,
+ D7EBFC691C8BD61A968BD6BB /* Pods-Runner.profile.xcconfig */,
+ B8D649AE805A0A4FC977F5C6 /* Pods-RunnerTests.debug.xcconfig */,
+ 05FEB0F37A3ECEDFE5C8DE9C /* Pods-RunnerTests.release.xcconfig */,
+ 5159CDFF4E027972758E5D62 /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -94,6 +136,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
+ 85B3F19C60BA9BAA96798E69 /* Pods */,
+ 119C1BA1DE158960BA4E8DFC /* Frameworks */,
);
sourceTree = "";
};
@@ -128,8 +172,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
+ D1F7CF24C81B31328E3248D7 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
+ 94D13764D2D87371C66A842E /* Frameworks */,
);
buildRules = (
);
@@ -145,12 +191,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
+ 629A2E46645A867C3067D8CA /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 83913DFF1DBDF244998F7213 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -238,6 +286,45 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
+ 629A2E46645A867C3067D8CA /* [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;
+ };
+ 83913DFF1DBDF244998F7213 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -253,6 +340,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
+ D1F7CF24C81B31328E3248D7 /* [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-RunnerTests-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;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -346,7 +455,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -378,6 +487,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = B8D649AE805A0A4FC977F5C6 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -395,6 +505,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 05FEB0F37A3ECEDFE5C8DE9C /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -410,6 +521,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 5159CDFF4E027972758E5D62 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -472,7 +584,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -523,7 +635,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
index 1d526a1..21a3cc1 100644
--- a/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -4,4 +4,7 @@
+
+
diff --git a/lib/main.dart b/lib/main.dart
index c326299..bd15208 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -28,22 +28,29 @@ import 'pages/insights_page.dart';
import 'pages/log_period_page.dart';
import 'pages/profile_page.dart';
-final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
+final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
+ FlutterLocalNotificationsPlugin();
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await NotificationService().init();
+ await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// Set system UI overlay style globally
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, // Background behind status bar (top)
- statusBarBrightness: Brightness.dark, // iOS: dark status bar content (light icons/text)
- statusBarIconBrightness: Brightness.light, // Android: light icons (dark background)
- systemNavigationBarColor: Colors.black, // Android: background color of bottom navigation bar
- systemNavigationBarDividerColor: Colors.black, // Android: divider above navbar (optional)
- systemNavigationBarIconBrightness: Brightness.light, // Android: light icons for dark navbar
+ statusBarBrightness:
+ Brightness.dark, // iOS: dark status bar content (light icons/text)
+ statusBarIconBrightness:
+ Brightness.light, // Android: light icons (dark background)
+ systemNavigationBarColor:
+ Colors.black, // Android: background color of bottom navigation bar
+ systemNavigationBarDividerColor:
+ Colors.black, // Android: divider above navbar (optional)
+ systemNavigationBarIconBrightness:
+ Brightness.light, // Android: light icons for dark navbar
systemStatusBarContrastEnforced: false, // Allow custom navbar styling
systemNavigationBarContrastEnforced: false, // Allow custom navbar styling
),
@@ -62,7 +69,10 @@ Future main() async {
ChangeNotifierProvider(create: (_) => UserProvider()),
ChangeNotifierProvider(create: (_) => SettingsProvider()),
],
- child: PeriodTrackerApp(showOnboarding: !onBoardingComplete, isAfterRestore: isAfterRestore),
+ child: PeriodTrackerApp(
+ showOnboarding: !onBoardingComplete,
+ isAfterRestore: isAfterRestore,
+ ),
),
);
}
@@ -70,7 +80,11 @@ Future main() async {
class PeriodTrackerApp extends StatefulWidget {
final bool showOnboarding;
final bool isAfterRestore;
- const PeriodTrackerApp({super.key, required this.showOnboarding, required this.isAfterRestore});
+ const PeriodTrackerApp({
+ super.key,
+ required this.showOnboarding,
+ required this.isAfterRestore,
+ });
@override
State createState() => _PeriodTrackerAppState();
@@ -78,22 +92,18 @@ class PeriodTrackerApp extends StatefulWidget {
class _PeriodTrackerAppState extends State {
late StreamSubscription _intentSub;
+ late final GoRouter _router;
final List _sharedFiles = [];
@override
void initState() {
super.initState();
+ _router = _buildRouter();
// Listen to media sharing coming from outside the app while the app is in the memory
_intentSub = ReceiveSharingIntent.instance.getMediaStream().listen(
(value) {
- setState(() {
- _sharedFiles.clear();
- _sharedFiles.addAll(value);
- if (_sharedFiles.isNotEmpty) {
- _setFileShared(true);
- }
- });
+ _handleSharedFiles(value);
},
onError: (err) {
// TODO: handle error
@@ -102,15 +112,7 @@ class _PeriodTrackerAppState extends State {
// Get the media sharing coming from outside the app while the app is closed.
ReceiveSharingIntent.instance.getInitialMedia().then((value) {
- setState(() {
- _sharedFiles.clear();
- _sharedFiles.addAll(value);
- if (_sharedFiles.isNotEmpty) {
- _setFileShared(true);
- }
-
- ReceiveSharingIntent.instance.reset();
- });
+ _handleSharedFiles(value, resetIntent: true);
});
}
@@ -124,25 +126,51 @@ class _PeriodTrackerAppState extends State {
await setFileShared(value);
}
- @override
- Widget build(BuildContext context) {
- final GoRouter router = GoRouter(
+ void _handleSharedFiles(List value, {bool resetIntent = false}) {
+ setState(() {
+ _sharedFiles.clear();
+ _sharedFiles.addAll(value);
+ });
+
+ if (_sharedFiles.isNotEmpty) {
+ _setFileShared(true);
+ _router.go('/restore');
+ }
+
+ if (resetIntent) {
+ ReceiveSharingIntent.instance.reset();
+ }
+ }
+
+ GoRouter _buildRouter() {
+ return GoRouter(
initialLocation: widget.showOnboarding ? '/onboarding' : '/',
routes: [
GoRoute(
path: '/',
- builder: (context, state) => MainNavigation(isAfterRestore: widget.isAfterRestore),
+ builder: (context, state) =>
+ MainNavigation(isAfterRestore: widget.isAfterRestore),
routes: [
GoRoute(
path: 'log',
builder: (context, state) {
- final bool isEditing = state.uri.queryParameters['isEditing'] == 'true';
+ final bool isEditing =
+ state.uri.queryParameters['isEditing'] == 'true';
final String? periodId = state.uri.queryParameters['periodId'];
- final Period? period = periodId != null ? context.read().getPeriodById(int.parse(periodId)) : null;
- final DateTime? focusedDay = state.uri.queryParameters['focusedDay'] != null
+ final Period? period = periodId != null
+ ? context.read().getPeriodById(
+ int.parse(periodId),
+ )
+ : null;
+ final DateTime? focusedDay =
+ state.uri.queryParameters['focusedDay'] != null
? DateTime.parse(state.uri.queryParameters['focusedDay']!)
: null;
- return LogPeriodPage(isEditing: isEditing, period: period, focusedDay: focusedDay);
+ return LogPeriodPage(
+ isEditing: isEditing,
+ period: period,
+ focusedDay: focusedDay,
+ );
},
),
GoRoute(
@@ -168,35 +196,43 @@ class _PeriodTrackerAppState extends State {
GoRoute(
path: '/onboarding',
builder: (context, state) => const OnboardingScreen(),
- routes: [GoRoute(path: 'restore', builder: (context, state) => const OnboardingRestoreDataPage())],
+ routes: [
+ GoRoute(
+ path: 'restore',
+ builder: (context, state) => const OnboardingRestoreDataPage(),
+ ),
+ ],
),
GoRoute(
path: '/help',
builder: (context, state) {
- final String initialTab = state.uri.queryParameters['initialPage'] ?? 'restore';
+ final String initialTab =
+ state.uri.queryParameters['initialPage'] ?? 'restore';
return RestoreHelpPage(initialTab: initialTab);
},
),
GoRoute(
path: '/restore',
- builder: (context, state) => RestoreDataPreviewPage(sharedFiles: _sharedFiles),
+ builder: (context, state) =>
+ RestoreDataPreviewPage(sharedFiles: _sharedFiles),
),
],
- redirect: (context, state) async {
- final bool fileShared = await getFileShared() == true;
- if (_sharedFiles.isNotEmpty && fileShared) {
- return '/restore';
- }
- return null; // no redirection
- },
errorBuilder: (context, state) {
- return widget.showOnboarding ? const OnboardingScreen() : MainNavigation(isAfterRestore: widget.isAfterRestore);
+ return widget.showOnboarding
+ ? const OnboardingScreen()
+ : MainNavigation(isAfterRestore: widget.isAfterRestore);
},
);
+ }
- // disable landscape mode
- SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
- return MaterialApp.router(title: 'Period Tracker', theme: appTheme, routerConfig: router, debugShowCheckedModeBanner: kDebugMode);
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp.router(
+ title: 'Period Tracker',
+ theme: appTheme,
+ routerConfig: _router,
+ debugShowCheckedModeBanner: kDebugMode,
+ );
}
}
@@ -222,7 +258,9 @@ class _MainNavigationState extends State {
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Data restored successfully 🎉'),
- content: Text('All your data has been successfully restored from the $kBackupFileName file'),
+ content: Text(
+ 'All your data has been successfully restored from the $kBackupFileName file',
+ ),
actions: [
TextButton(
onPressed: () async {
@@ -239,37 +277,56 @@ class _MainNavigationState extends State {
}
}
- final List pages = [HomePage(), InsightsPage(), ProfilePage()];
+ final List pages = const [
+ HomePage(key: PageStorageKey('home-page')),
+ InsightsPage(key: PageStorageKey('insights-page')),
+ ProfilePage(key: PageStorageKey('profile-page')),
+ ];
final List appBarTitles = ['Home', 'Insights', 'Profile'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
- title: Text(appBarTitles[_selectedIndex], style: Theme.of(context).textTheme.titleMedium),
+ title: Text(
+ appBarTitles[_selectedIndex],
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
centerTitle: true,
backgroundColor: Colors.transparent,
),
extendBodyBehindAppBar: true,
body: SafeArea(
- child: Padding(padding: EdgeInsets.all(8.0), child: pages[_selectedIndex]),
+ child: Padding(
+ padding: EdgeInsets.all(8.0),
+ child: IndexedStack(index: _selectedIndex, children: pages),
+ ),
),
bottomNavigationBar: NavigationBar(
destinations: [
NavigationDestination(
icon: Icon(Icons.home_rounded),
- selectedIcon: Icon(Icons.home_rounded, color: Theme.of(context).colorScheme.onPrimary),
+ selectedIcon: Icon(
+ Icons.home_rounded,
+ color: Theme.of(context).colorScheme.onPrimary,
+ ),
label: 'Home',
tooltip: null,
),
NavigationDestination(
icon: Icon(Icons.bar_chart_rounded),
- selectedIcon: Icon(Icons.bar_chart_rounded, color: Theme.of(context).colorScheme.onPrimary),
+ selectedIcon: Icon(
+ Icons.bar_chart_rounded,
+ color: Theme.of(context).colorScheme.onPrimary,
+ ),
label: 'Insights',
),
NavigationDestination(
icon: Icon(Icons.person_rounded),
- selectedIcon: Icon(Icons.person_rounded, color: Theme.of(context).colorScheme.onPrimary),
+ selectedIcon: Icon(
+ Icons.person_rounded,
+ color: Theme.of(context).colorScheme.onPrimary,
+ ),
label: 'Profile',
),
],
diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart
index 40e416e..827a91b 100644
--- a/lib/pages/home_page.dart
+++ b/lib/pages/home_page.dart
@@ -8,7 +8,6 @@ import 'package:period_tracker/models/user_model.dart';
import 'package:period_tracker/providers/period_provider.dart';
import 'package:period_tracker/providers/settings_provider.dart';
import 'package:period_tracker/providers/user_provider.dart';
-import 'package:period_tracker/services/period_service.dart';
import 'package:period_tracker/shared_preferences/shared_preferences.dart';
import 'package:period_tracker/utils/date_time_helper.dart';
import 'package:provider/provider.dart';
@@ -22,7 +21,7 @@ class HomePage extends StatefulWidget {
State createState() => _HomePageState();
}
-class _HomePageState extends State with TickerProviderStateMixin {
+class _HomePageState extends State {
DateTime? _rangeStart;
DateTime? _rangeEnd;
late DateTime _selectedDay;
@@ -47,24 +46,44 @@ class _HomePageState extends State with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
- final periodProvider = Provider.of(context);
- final List periods = context.watch().periods;
+ final periodProvider = context.watch();
+ final List periods = periodProvider.periods;
final Settings? settings = context.watch().settings;
final User? user = context.watch().user;
- final List next3PeriodDates = periodProvider.getNext3PeriodDates(settings?.predictionMode == 'dynamic', user?.cycleLength);
- final DateTime? nextPeriodDate = next3PeriodDates.isNotEmpty ? next3PeriodDates[0] : null;
+ final List next3PeriodDates = periodProvider.getNext3PeriodDates(
+ settings?.predictionMode == 'dynamic',
+ user?.cycleLength,
+ );
+ final DateTime? nextPeriodDate = next3PeriodDates.isNotEmpty
+ ? next3PeriodDates[0]
+ : null;
+ final int periodDuration = (user?.periodLength ?? kDefaultPeriodLength) - 1;
+ final _CalendarDayLookup calendarDayLookup = _buildCalendarDayLookup(
+ periods,
+ next3PeriodDates,
+ periodDuration,
+ );
final DateTime now = DateTime.now();
- final int currentCycleDay = periodProvider.getCurrentCycleDay(DateTime.utc(now.year, now.month, now.day));
+ final int currentCycleDay = periodProvider.getCurrentCycleDay(
+ DateTime.utc(now.year, now.month, now.day),
+ );
double? cycleLength;
if (settings?.predictionMode == 'dynamic') {
- cycleLength = periodProvider.getAverageCycleLength(userCycleLength: user?.cycleLength); // provide userCycleLength if available
+ cycleLength = periodProvider.getAverageCycleLength(
+ userCycleLength: user?.cycleLength,
+ ); // provide userCycleLength if available
} else {
cycleLength = user?.cycleLength.toDouble();
}
- final status = periodProvider.getStatusMessage(Theme.of(context).colorScheme.tertiary, nextPeriodDate);
+ final status = periodProvider.getStatusMessage(
+ Theme.of(context).colorScheme.tertiary,
+ nextPeriodDate,
+ );
+ final Period? selectedPeriod =
+ calendarDayLookup.loggedDays[_dayKey(_selectedDay)]?.period;
final bool showProgressBar = cycleLength != null;
double progress = 0;
@@ -77,8 +96,10 @@ class _HomePageState extends State with TickerProviderStateMixin {
final TextEditingController messageController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
String? userDbPhoneNumber = user?.partnerPhoneNumber;
- String userIsoCountryCode = userDbPhoneNumber?.split('|')[0] ?? kDefaultIsoCountryCode; // SI
- String userCountryCode = userDbPhoneNumber?.split('|')[1] ?? kDefaultCountryCode; // +386
+ String userIsoCountryCode =
+ userDbPhoneNumber?.split('|')[0] ?? kDefaultIsoCountryCode; // SI
+ String userCountryCode =
+ userDbPhoneNumber?.split('|')[1] ?? kDefaultCountryCode; // +386
String userPhoneNumber = userDbPhoneNumber?.split('|')[2] ?? '';
// Prefill phone number if it exists
@@ -88,11 +109,11 @@ class _HomePageState extends State with TickerProviderStateMixin {
showDialog(
context: context,
- builder: (context) {
+ builder: (dialogContext) {
return AlertDialog(
title: const Text('Order boyfriend'),
content: SizedBox(
- width: MediaQuery.of(context).size.width,
+ width: MediaQuery.of(dialogContext).size.width,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -102,7 +123,9 @@ class _HomePageState extends State with TickerProviderStateMixin {
controller: phoneController,
decoration: InputDecoration(
hintText: 'Phone number',
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
),
initialCountryCode: userIsoCountryCode,
onCountryChanged: (value) {
@@ -115,26 +138,36 @@ class _HomePageState extends State with TickerProviderStateMixin {
userPhoneNumber = phone.number;
},
),
- if (user?.partnerMessageHeading?.isEmpty ?? true) const SizedBox(height: 12),
- if (user?.partnerMessageHeading?.isNotEmpty ?? false) const SizedBox(height: 6),
+ if (user?.partnerMessageHeading?.isEmpty ?? true)
+ const SizedBox(height: 12),
+ if (user?.partnerMessageHeading?.isNotEmpty ?? false)
+ const SizedBox(height: 6),
if (user?.partnerMessageHeading?.isNotEmpty ?? false)
RichText(
text: TextSpan(
children: [
- TextSpan(text: 'Title: ', style: DefaultTextStyle.of(context).style),
+ TextSpan(
+ text: 'Title: ',
+ style: DefaultTextStyle.of(dialogContext).style,
+ ),
TextSpan(
text: user?.partnerMessageHeading ?? '',
- style: DefaultTextStyle.of(context).style.copyWith(fontStyle: FontStyle.italic),
+ style: DefaultTextStyle.of(
+ dialogContext,
+ ).style.copyWith(fontStyle: FontStyle.italic),
),
],
),
),
- if (user?.partnerMessageHeading?.isNotEmpty ?? false) const SizedBox(height: 6),
+ if (user?.partnerMessageHeading?.isNotEmpty ?? false)
+ const SizedBox(height: 6),
TextField(
controller: messageController,
decoration: InputDecoration(
hintText: 'Message',
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
),
maxLines: 3,
),
@@ -144,14 +177,20 @@ class _HomePageState extends State with TickerProviderStateMixin {
),
actions: [
TextButton(
- onPressed: () => Navigator.of(context).pop(),
- style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.tertiary),
+ onPressed: () => Navigator.of(dialogContext).pop(),
+ style: TextButton.styleFrom(
+ foregroundColor: Theme.of(dialogContext).colorScheme.tertiary,
+ ),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
if (userPhoneNumber.isEmpty) {
- ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter a phone number')));
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Please enter a phone number'),
+ ),
+ );
return;
}
@@ -160,7 +199,8 @@ class _HomePageState extends State with TickerProviderStateMixin {
name: user?.name,
cycleLength: user?.cycleLength ?? 28,
periodLength: user?.periodLength ?? 5,
- partnerPhoneNumber: '$userIsoCountryCode|$userCountryCode|$userPhoneNumber',
+ partnerPhoneNumber:
+ '$userIsoCountryCode|$userCountryCode|$userPhoneNumber',
);
// Build SMS message with optional heading
@@ -171,20 +211,26 @@ class _HomePageState extends State with TickerProviderStateMixin {
// Send SMS
final encodedBody = Uri.encodeComponent(smsBody);
- final uri = Uri.parse('sms:$userCountryCode$userPhoneNumber?body=$encodedBody');
+ final uri = Uri.parse(
+ 'sms:$userCountryCode$userPhoneNumber?body=$encodedBody',
+ );
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
- Navigator.of(context).pop();
+ if (!dialogContext.mounted) return;
+ Navigator.of(dialogContext).pop();
} else {
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Could not open SMS app')));
- }
+ if (!dialogContext.mounted) return;
+ ScaffoldMessenger.of(dialogContext).showSnackBar(
+ const SnackBar(content: Text('Could not open SMS app')),
+ );
}
},
child: const Text('Send'),
),
],
- backgroundColor: Theme.of(context).colorScheme.primaryContainer,
+ backgroundColor: Theme.of(
+ dialogContext,
+ ).colorScheme.primaryContainer,
);
},
);
@@ -193,6 +239,7 @@ class _HomePageState extends State with TickerProviderStateMixin {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
+ key: const PageStorageKey('home-scroll-view'),
child: Column(
children: [
Padding(
@@ -202,14 +249,23 @@ class _HomePageState extends State with TickerProviderStateMixin {
children: [
Row(
children: [
- Icon(Icons.calendar_month_rounded, size: 40, color: Theme.of(context).colorScheme.primary),
+ Icon(
+ Icons.calendar_month_rounded,
+ size: 40,
+ color: Theme.of(context).colorScheme.primary,
+ ),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text('Next period:', style: Theme.of(context).textTheme.bodyMedium),
Text(
- nextPeriodDate != null ? DateTimeHelper.displayDate(nextPeriodDate) : 'Not enough data',
+ 'Next period:',
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ Text(
+ nextPeriodDate != null
+ ? DateTimeHelper.displayDate(nextPeriodDate)
+ : 'Not enough data',
style: Theme.of(context).textTheme.titleMedium,
),
],
@@ -220,17 +276,32 @@ class _HomePageState extends State with TickerProviderStateMixin {
onTap: orderBoyfriend,
borderRadius: BorderRadius.circular(8),
child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 8,
+ ),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- Icon(Icons.favorite_rounded, size: 28, color: Theme.of(context).colorScheme.primary),
+ Icon(
+ Icons.favorite_rounded,
+ size: 28,
+ color: Theme.of(
+ context,
+ ).colorScheme.primary,
+ ),
const SizedBox(height: 4),
Text(
'Order boyfriend',
- style: Theme.of(
- context,
- ).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface),
+ style: Theme.of(context)
+ .textTheme
+ .labelMedium
+ ?.copyWith(
+ fontWeight: FontWeight.w600,
+ color: Theme.of(
+ context,
+ ).colorScheme.onSurface,
+ ),
),
],
),
@@ -243,61 +314,99 @@ class _HomePageState extends State with TickerProviderStateMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 26),
- Text('Current cycle day: $currentCycleDay', style: Theme.of(context).textTheme.bodyMedium),
+ Text(
+ 'Current cycle day: $currentCycleDay',
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
const SizedBox(height: 8),
LinearProgressIndicator(
value: progress,
- valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.primary),
+ valueColor: AlwaysStoppedAnimation(
+ Theme.of(context).colorScheme.primary,
+ ),
minHeight: 8,
- backgroundColor: Theme.of(context).colorScheme.secondary,
+ backgroundColor: Theme.of(
+ context,
+ ).colorScheme.secondary,
borderRadius: BorderRadius.circular(99),
),
],
),
const SizedBox(height: 16),
- Text(status.text, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: status.color)),
+ Text(
+ status.text,
+ style: Theme.of(
+ context,
+ ).textTheme.bodyMedium?.copyWith(color: status.color),
+ ),
],
),
),
// Calendar section
- SizedBox(
- // height: 420, // probably don't need fixed height
- child: TableCalendar(
- headerStyle: HeaderStyle(formatButtonVisible: false),
- startingDayOfWeek: StartingDayOfWeek.monday,
- firstDay: kFirstCalendarDay,
- lastDay: kLastCalendarDay,
- focusedDay: _focusedDay,
- availableGestures: AvailableGestures.horizontalSwipe,
- selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
- rangeStartDay: _rangeStart,
- rangeEndDay: _rangeEnd,
- calendarFormat: CalendarFormat.month,
- rangeSelectionMode: RangeSelectionMode.toggledOff,
- onDaySelected: (selectedDay, focusedDay) {
- setState(() {
- if (!isSameDay(_selectedDay, selectedDay)) {
- _selectedDay = selectedDay;
- _focusedDay = focusedDay;
- _rangeStart = null;
- }
- });
- },
- daysOfWeekHeight: kTableCalendarDaysOfTheWeekHeight,
- daysOfWeekStyle: DaysOfWeekStyle(
- weekdayStyle: TextStyle(color: Theme.of(context).colorScheme.tertiary),
- weekendStyle: TextStyle(color: Theme.of(context).colorScheme.tertiary),
- ),
- calendarStyle: CalendarStyle(outsideDaysVisible: false),
- calendarBuilders: CalendarBuilders(
- defaultBuilder: (context, day, focusedDay) => _defaultBuilder(context, day, focusedDay, periods, next3PeriodDates, user),
- todayBuilder: (context, day, focusedDay) => _todayBuilder(context, day, focusedDay, periods, next3PeriodDates, user),
- selectedBuilder: (context, day, focusedDay) => _selectedBuilder(context, day, focusedDay, periods, next3PeriodDates, user),
+ RepaintBoundary(
+ child: SizedBox(
+ // height: 420, // probably don't need fixed height
+ child: TableCalendar(
+ headerStyle: HeaderStyle(formatButtonVisible: false),
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ firstDay: kFirstCalendarDay,
+ lastDay: kLastCalendarDay,
+ focusedDay: _focusedDay,
+ availableGestures: AvailableGestures.horizontalSwipe,
+ selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
+ rangeStartDay: _rangeStart,
+ rangeEndDay: _rangeEnd,
+ calendarFormat: CalendarFormat.month,
+ rangeSelectionMode: RangeSelectionMode.toggledOff,
+ onDaySelected: (selectedDay, focusedDay) {
+ setState(() {
+ if (!isSameDay(_selectedDay, selectedDay)) {
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay;
+ _rangeStart = null;
+ }
+ });
+ },
+ daysOfWeekHeight: kTableCalendarDaysOfTheWeekHeight,
+ daysOfWeekStyle: DaysOfWeekStyle(
+ weekdayStyle: TextStyle(
+ color: Theme.of(context).colorScheme.tertiary,
+ ),
+ weekendStyle: TextStyle(
+ color: Theme.of(context).colorScheme.tertiary,
+ ),
+ ),
+ calendarStyle: CalendarStyle(outsideDaysVisible: false),
+ calendarBuilders: CalendarBuilders(
+ defaultBuilder: (context, day, focusedDay) =>
+ _defaultBuilder(
+ context,
+ day,
+ focusedDay,
+ calendarDayLookup,
+ ),
+ todayBuilder: (context, day, focusedDay) => _todayBuilder(
+ context,
+ day,
+ focusedDay,
+ calendarDayLookup,
+ ),
+ selectedBuilder: (context, day, focusedDay) =>
+ _selectedBuilder(
+ context,
+ day,
+ focusedDay,
+ calendarDayLookup,
+ ),
+ ),
),
),
),
Center(
- child: Padding(padding: const EdgeInsets.all(16.0), child: periodProvider.getDataForDate(_selectedDay, context)),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: periodProvider.getDataForDate(_selectedDay, context),
+ ),
),
SizedBox(height: 80), // to avoid FAB overlapping content
],
@@ -306,42 +415,126 @@ class _HomePageState extends State with TickerProviderStateMixin {
),
floatingActionButton: FloatingActionButton(
onPressed: () {
- final Period? selectedPeriod = PeriodService.getPeriodInDate(_selectedDay, periods);
final bool isEditing = selectedPeriod != null;
if (isEditing) {
- context.go('/log?isEditing=$isEditing&periodId=${selectedPeriod.id}&focusedDay=${Uri.encodeComponent(_selectedDay.toIso8601String())}');
+ context.go(
+ '/log?isEditing=$isEditing&periodId=${selectedPeriod.id}&focusedDay=${Uri.encodeComponent(_selectedDay.toIso8601String())}',
+ );
return;
}
- context.go('/log?isEditing=false&focusedDay=${Uri.encodeComponent(_selectedDay.toIso8601String())}');
+ context.go(
+ '/log?isEditing=false&focusedDay=${Uri.encodeComponent(_selectedDay.toIso8601String())}',
+ );
},
backgroundColor: Theme.of(context).colorScheme.primary,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(99.0)),
- child: PeriodService.getPeriodInDate(_selectedDay, periods) != null
- ? Icon(Icons.edit_rounded, color: Theme.of(context).colorScheme.onPrimary)
- : Icon(Icons.add_rounded, color: Theme.of(context).colorScheme.onPrimary),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(99.0),
+ ),
+ child: selectedPeriod != null
+ ? Icon(
+ Icons.edit_rounded,
+ color: Theme.of(context).colorScheme.onPrimary,
+ )
+ : Icon(
+ Icons.add_rounded,
+ color: Theme.of(context).colorScheme.onPrimary,
+ ),
),
);
}
- Widget _defaultBuilder(BuildContext context, DateTime day, DateTime focusedDay, periods, List next3PeriodDates, User? user) {
+ _CalendarDayLookup _buildCalendarDayLookup(
+ List periods,
+ List next3PeriodDates,
+ int periodDuration,
+ ) {
+ final Map loggedDays = {};
+ final Map upcomingDays = {};
+
+ for (final Period period in periods) {
+ final DateTime? endDate = period.endDate;
+ if (endDate == null) continue;
+
+ DateTime current = DateTimeHelper.stripTime(period.startDate);
+ final DateTime end = DateTimeHelper.stripTime(endDate);
+ while (!current.isAfter(end)) {
+ final bool isStartDay = DateTimeHelper.isSameDay(
+ current,
+ period.startDate,
+ );
+ final bool isEndDay = DateTimeHelper.isSameDay(current, endDate);
+ loggedDays[_dayKey(current)] = _LoggedPeriodDay(
+ period: period,
+ isStartDay: isStartDay,
+ isEndDay: isEndDay,
+ spansMultipleMonths:
+ !(isStartDay && isEndDay) &&
+ (DateTimeHelper.isFirstDayOfMonth(current) ||
+ DateTimeHelper.isLastDayOfMonth(current)) &&
+ period.startDate.month != endDate.month,
+ );
+ current = current.add(const Duration(days: 1));
+ }
+ }
+
+ for (final DateTime periodDate in next3PeriodDates) {
+ final DateTime periodStart = DateTimeHelper.stripTime(periodDate);
+ final DateTime periodEnd = DateTimeHelper.stripTime(
+ periodDate.add(Duration(days: periodDuration)),
+ );
+ DateTime current = periodStart;
+ while (!current.isAfter(periodEnd)) {
+ final bool isStartDay = DateTimeHelper.isSameDay(periodStart, current);
+ final bool isEndDay = DateTimeHelper.isSameDay(periodEnd, current);
+ upcomingDays[_dayKey(current)] = _UpcomingPeriodDay(
+ isStartDay: isStartDay,
+ isEndDay: isEndDay,
+ spansMultipleMonths:
+ (DateTimeHelper.isFirstDayOfMonth(current) ||
+ DateTimeHelper.isLastDayOfMonth(current)) &&
+ periodStart.month != periodEnd.month,
+ );
+ current = current.add(const Duration(days: 1));
+ }
+ }
+
+ return _CalendarDayLookup(
+ loggedDays: loggedDays,
+ upcomingDays: upcomingDays,
+ );
+ }
+
+ static int _dayKey(DateTime date) {
+ final DateTime strippedDate = DateTimeHelper.stripTime(date);
+ return strippedDate.year * 10000 +
+ strippedDate.month * 100 +
+ strippedDate.day;
+ }
+
+ Widget _defaultBuilder(
+ BuildContext context,
+ DateTime day,
+ DateTime focusedDay,
+ _CalendarDayLookup calendarDayLookup,
+ ) {
// distinguish 3 cases:
// - selected date is inside logged period
// - selected date is inside upcoming period
// - default builder for all other dates
- Period? period = PeriodService.getPeriodInDate(day, periods);
- final isInPeriod = period != null;
- final isStartDay = PeriodService.isStartDay(day, periods);
- final isEndDay = PeriodService.isEndDay(day, periods);
+ final _LoggedPeriodDay? loggedPeriodDay =
+ calendarDayLookup.loggedDays[_dayKey(day)];
+ final _UpcomingPeriodDay? upcomingPeriodDay =
+ calendarDayLookup.upcomingDays[_dayKey(day)];
+ final isInPeriod = loggedPeriodDay != null;
+ final isStartDay = loggedPeriodDay?.isStartDay ?? false;
+ final isEndDay = loggedPeriodDay?.isEndDay ?? false;
final bool isFirstDayOfMonth = DateTimeHelper.isFirstDayOfMonth(day);
final bool isLastDayOfMonth = DateTimeHelper.isLastDayOfMonth(day);
final bool spansMultipleMonths =
- isStartDay &&
- isEndDay // if this is true gradient is applied
- ? false // period lasts 1 single day - should never happen
- : (isFirstDayOfMonth || isLastDayOfMonth) && period != null && period.startDate.month != period.endDate!.month;
+ loggedPeriodDay?.spansMultipleMonths ?? false;
BoxDecoration? decoration;
Color? textColor;
@@ -367,37 +560,31 @@ class _HomePageState extends State with TickerProviderStateMixin {
),
);
}
- int periodDuration = kDefaultPeriodLength - 1;
- if (user != null) {
- periodDuration = user.periodLength - 1;
- }
// next period dates (3) styling
- DateTime current = DateTimeHelper.stripTime(day);
Color primaryColor = Theme.of(context).colorScheme.primary;
- for (int periodIndex = 0; periodIndex < next3PeriodDates.length; periodIndex++) {
- DateTime periodStart = DateTimeHelper.stripTime(next3PeriodDates[periodIndex]);
- DateTime periodEnd = DateTimeHelper.stripTime(next3PeriodDates[periodIndex].add(Duration(days: periodDuration)));
-
- if (DateTimeHelper.dayBetweenDates(current, periodStart, periodEnd)) {
- // day is inside one of the upcoming periods
- final isStartDay = DateTimeHelper.isSameDay(periodStart, day);
- final isEndDay = DateTimeHelper.isSameDay(periodEnd, day);
-
- decoration = BoxDecoration(
- border: Border(
- left: isStartDay ? BorderSide(color: primaryColor, width: 2.0) : BorderSide.none,
- right: isEndDay ? BorderSide(color: primaryColor, width: 2.0) : BorderSide.none,
- top: BorderSide(color: primaryColor, width: 2.0),
- bottom: BorderSide(color: primaryColor, width: 2.0),
- ),
- borderRadius: BorderRadius.horizontal(
- left: isStartDay ? const Radius.circular(99) : Radius.zero,
- right: isEndDay ? const Radius.circular(99) : Radius.zero,
- ),
- );
- }
+ if (upcomingPeriodDay != null) {
+ decoration = BoxDecoration(
+ border: Border(
+ left: upcomingPeriodDay.isStartDay
+ ? BorderSide(color: primaryColor, width: 2.0)
+ : BorderSide.none,
+ right: upcomingPeriodDay.isEndDay
+ ? BorderSide(color: primaryColor, width: 2.0)
+ : BorderSide.none,
+ top: BorderSide(color: primaryColor, width: 2.0),
+ bottom: BorderSide(color: primaryColor, width: 2.0),
+ ),
+ borderRadius: BorderRadius.horizontal(
+ left: upcomingPeriodDay.isStartDay
+ ? const Radius.circular(99)
+ : Radius.zero,
+ right: upcomingPeriodDay.isEndDay
+ ? const Radius.circular(99)
+ : Radius.zero,
+ ),
+ );
}
// default builder
@@ -410,42 +597,38 @@ class _HomePageState extends State with TickerProviderStateMixin {
);
}
- Widget _todayBuilder(BuildContext context, DateTime day, DateTime focusedDay, periods, List next3PeriodDates, User? user) {
+ Widget _todayBuilder(
+ BuildContext context,
+ DateTime day,
+ DateTime focusedDay,
+ _CalendarDayLookup calendarDayLookup,
+ ) {
// distinguish 3 cases:
// - selected date is inside logged period
// - selected date is inside upcoming period
// - default builder for all other dates
- final Period? period = PeriodService.getPeriodInDate(day, periods);
- final isInPeriod = period != null;
- int periodDuration = kDefaultPeriodLength - 1;
- if (user != null) {
- periodDuration = user.periodLength - 1;
- }
+ final bool isInPeriod = calendarDayLookup.loggedDays.containsKey(
+ _dayKey(day),
+ );
+ final bool isInUpcomingPeriod = calendarDayLookup.upcomingDays.containsKey(
+ _dayKey(day),
+ );
- if (isInPeriod) {
+ if (isInPeriod || isInUpcomingPeriod) {
// today is inside logged period
- return _defaultBuilder(context, day, focusedDay, periods, next3PeriodDates, user);
+ return _defaultBuilder(context, day, focusedDay, calendarDayLookup);
}
// default styling that is returned if today is just a regular day, meaning it doesn't fall into any of the logged or upcoming periods
BoxDecoration? decoration = BoxDecoration(
shape: BoxShape.circle,
- border: Border.all(color: Theme.of(context).colorScheme.secondary, width: 2),
+ border: Border.all(
+ color: Theme.of(context).colorScheme.secondary,
+ width: 2,
+ ),
);
Color? textColor = Theme.of(context).colorScheme.onSurface;
- // Check if today is in any of the upcoming periods
- DateTime current = DateTimeHelper.stripTime(day);
- for (DateTime periodDate in next3PeriodDates) {
- DateTime periodStart = DateTimeHelper.stripTime(periodDate);
- DateTime periodEnd = DateTimeHelper.stripTime(periodDate.add(Duration(days: periodDuration)));
-
- if (DateTimeHelper.dayBetweenDates(current, periodStart, periodEnd)) {
- // today is inside one of the upcoming periods
- return _defaultBuilder(context, day, focusedDay, periods, next3PeriodDates, user);
- }
- }
-
return Container(
margin: const EdgeInsets.fromLTRB(0, 4, 0, 4),
decoration: decoration,
@@ -455,50 +638,33 @@ class _HomePageState extends State with TickerProviderStateMixin {
);
}
- Widget _selectedBuilder(BuildContext context, DateTime day, DateTime focusedDay, periods, List next3PeriodDates, User? user) {
+ Widget _selectedBuilder(
+ BuildContext context,
+ DateTime day,
+ DateTime focusedDay,
+ _CalendarDayLookup calendarDayLookup,
+ ) {
// distinguish 3 cases:
// - selected date is inside logged period
// - selected date is inside upcoming period
// - default builder for all other dates
- final Period? period = PeriodService.getPeriodInDate(day, periods);
- final isInPeriod = period != null;
- final isStartDay = PeriodService.isStartDay(day, periods);
- final isEndDay = PeriodService.isEndDay(day, periods);
+ final _LoggedPeriodDay? loggedPeriodDay =
+ calendarDayLookup.loggedDays[_dayKey(day)];
+ final _UpcomingPeriodDay? upcomingPeriodDay =
+ calendarDayLookup.upcomingDays[_dayKey(day)];
+ final isInPeriod = loggedPeriodDay != null;
+ final isStartDay = loggedPeriodDay?.isStartDay ?? false;
+ final isEndDay = loggedPeriodDay?.isEndDay ?? false;
final bool isFirstDayOfMonth = DateTimeHelper.isFirstDayOfMonth(day);
final bool isLastDayOfMonth = DateTimeHelper.isLastDayOfMonth(day);
final bool spansMultipleMonths =
- isStartDay &&
- isEndDay // if this is true gradient is applied
- ? false // period lasts 1 single day - should never happen
- : (isFirstDayOfMonth || isLastDayOfMonth) && period != null && period.startDate.month != period.endDate!.month;
-
- bool insideUpcomingPeriod = false;
- bool isNextPeriodStartDay = false;
- bool isNextPeriodEndDay = false;
- bool upComingSpanMultipleMonths = false;
- int periodDuration = kDefaultPeriodLength - 1;
- if (user != null) {
- periodDuration = user.periodLength - 1;
- }
-
- // Check if selected day is in any of the upcoming periods
- DateTime current = DateTimeHelper.stripTime(day);
- for (DateTime periodDate in next3PeriodDates) {
- DateTime periodStart = DateTimeHelper.stripTime(periodDate);
- DateTime periodEnd = DateTimeHelper.stripTime(periodDate.add(Duration(days: periodDuration)));
-
- if (DateTimeHelper.dayBetweenDates(current, periodStart, periodEnd)) {
- isNextPeriodStartDay = DateTimeHelper.isSameDay(periodStart, day);
- isNextPeriodEndDay = DateTimeHelper.isSameDay(periodEnd, day);
- insideUpcomingPeriod = true;
-
- // Check if this upcoming period spans multiple months
- upComingSpanMultipleMonths = (isFirstDayOfMonth || isLastDayOfMonth) && periodStart.month != periodEnd.month;
-
- break; // Only consider the first matching period
- }
- }
+ loggedPeriodDay?.spansMultipleMonths ?? false;
+ final bool insideUpcomingPeriod = upcomingPeriodDay != null;
+ final bool isNextPeriodStartDay = upcomingPeriodDay?.isStartDay ?? false;
+ final bool isNextPeriodEndDay = upcomingPeriodDay?.isEndDay ?? false;
+ final bool upComingSpanMultipleMonths =
+ upcomingPeriodDay?.spansMultipleMonths ?? false;
BoxDecoration? decoration;
Color? textColor;
@@ -544,7 +710,10 @@ class _HomePageState extends State with TickerProviderStateMixin {
textColor = Theme.of(context).colorScheme.surface;
} else {
// default selector builder
- decoration = BoxDecoration(color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle);
+ decoration = BoxDecoration(
+ color: Theme.of(context).colorScheme.primary,
+ shape: BoxShape.circle,
+ );
textColor = Theme.of(context).colorScheme.onPrimary;
}
@@ -557,3 +726,39 @@ class _HomePageState extends State with TickerProviderStateMixin {
);
}
}
+
+class _CalendarDayLookup {
+ final Map loggedDays;
+ final Map upcomingDays;
+
+ const _CalendarDayLookup({
+ required this.loggedDays,
+ required this.upcomingDays,
+ });
+}
+
+class _LoggedPeriodDay {
+ final Period period;
+ final bool isStartDay;
+ final bool isEndDay;
+ final bool spansMultipleMonths;
+
+ const _LoggedPeriodDay({
+ required this.period,
+ required this.isStartDay,
+ required this.isEndDay,
+ required this.spansMultipleMonths,
+ });
+}
+
+class _UpcomingPeriodDay {
+ final bool isStartDay;
+ final bool isEndDay;
+ final bool spansMultipleMonths;
+
+ const _UpcomingPeriodDay({
+ required this.isStartDay,
+ required this.isEndDay,
+ required this.spansMultipleMonths,
+ });
+}
diff --git a/lib/providers/period_provider.dart b/lib/providers/period_provider.dart
index 2ba2960..5afaea0 100644
--- a/lib/providers/period_provider.dart
+++ b/lib/providers/period_provider.dart
@@ -25,6 +25,7 @@ class PeriodProvider extends ChangeNotifier {
Future insertPeriod(Period period) async {
await _db.insertPeriod(period);
_periods.add(period);
+ _periods.sort((a, b) => b.startDate.compareTo(a.startDate));
notifyListeners();
}
@@ -43,32 +44,44 @@ class PeriodProvider extends ChangeNotifier {
/// @param dynamicPeriodPrediction Whether to use dynamic prediction based on average cycle length
/// @param userCycleLength The user's set cycle length (used if dynamic prediction is false)
/// @returns The predicted next period start date, or null if not enough data
- DateTime? getNextPeriodDate(bool dynamicPeriodPrediction, int? userCycleLength) {
- if (periods.isEmpty) return null;
- if (periods.length < 2) {
+ DateTime? getNextPeriodDate(
+ bool dynamicPeriodPrediction,
+ int? userCycleLength,
+ ) {
+ if (_periods.isEmpty) return null;
+ final List sortedPeriods = List.from(_periods)
+ ..sort((a, b) => a.startDate.compareTo(b.startDate));
+ if (sortedPeriods.length < 2) {
// Not enough data to predict next period - only one period logged
// Use the provided userCycleLength
// Case when user gets not enough data to predict the next period right after onboarding
- return periods.last.startDate.add(Duration(days: userCycleLength ?? kDefaultCycleLength));
+ return sortedPeriods.last.startDate.add(
+ Duration(days: userCycleLength ?? kDefaultCycleLength),
+ );
}
- periods.sort((a, b) => a.startDate.compareTo(b.startDate));
if (dynamicPeriodPrediction) {
// dynamic prediction based on average cycle length
final avgCycle = getAverageCycleLength(useRecent6: true);
if (avgCycle == null) return null;
- return periods.last.startDate.add(Duration(days: avgCycle.round()));
+ return sortedPeriods.last.startDate.add(Duration(days: avgCycle.round()));
} else {
// static prediction based on last period and user's cycle length
if (userCycleLength == null) return null;
- return periods.last.startDate.add(Duration(days: userCycleLength));
+ return sortedPeriods.last.startDate.add(Duration(days: userCycleLength));
}
}
// Returns the next 3 expected period start dates
- List getNext3PeriodDates(bool dynamicPredictionMode, int? userCycleLength) {
+ List getNext3PeriodDates(
+ bool dynamicPredictionMode,
+ int? userCycleLength,
+ ) {
final List upcomingPeriods = [];
- final DateTime? firstPeriod = getNextPeriodDate(dynamicPredictionMode, userCycleLength);
+ final DateTime? firstPeriod = getNextPeriodDate(
+ dynamicPredictionMode,
+ userCycleLength,
+ );
if (firstPeriod == null) return upcomingPeriods;
upcomingPeriods.add(firstPeriod);
@@ -97,12 +110,19 @@ class PeriodProvider extends ChangeNotifier {
date ??= DateTime.now();
final targetDate = DateTime.utc(date.year, date.month, date.day);
- _periods.sort((a, b) => a.startDate.compareTo(b.startDate));
+ final List sortedPeriods = List.from(_periods)
+ ..sort((a, b) => a.startDate.compareTo(b.startDate));
// Normalize period dates to UTC date only for consistent comparison
- final normalizedPeriods = _periods.map((p) {
- final DateTime startDate = DateTime.utc(p.startDate.year, p.startDate.month, p.startDate.day);
- final DateTime? endDate = p.endDate != null ? DateTime.utc(p.endDate!.year, p.endDate!.month, p.endDate!.day) : null;
+ final normalizedPeriods = sortedPeriods.map((p) {
+ final DateTime startDate = DateTime.utc(
+ p.startDate.year,
+ p.startDate.month,
+ p.startDate.day,
+ );
+ final DateTime? endDate = p.endDate != null
+ ? DateTime.utc(p.endDate!.year, p.endDate!.month, p.endDate!.day)
+ : null;
return {'period': p, 'start': startDate, 'end': endDate};
}).toList();
@@ -115,7 +135,8 @@ class PeriodProvider extends ChangeNotifier {
Map? lastPeriodData;
for (var periodData in normalizedPeriods) {
final startDate = periodData['start'] as DateTime;
- if (targetDate.isAtSameMomentAs(startDate) || targetDate.isAfter(startDate)) {
+ if (targetDate.isAtSameMomentAs(startDate) ||
+ targetDate.isAfter(startDate)) {
lastPeriodData = periodData;
} else {
break;
@@ -134,28 +155,39 @@ class PeriodProvider extends ChangeNotifier {
}
// Returns a status message (e.g., late, on track)
- PeriodStatusMessage getStatusMessage(Color defaultColor, DateTime? nextPeriodDate) {
- PeriodStatusMessage status = PeriodStatusMessage(text: '', color: defaultColor);
+ PeriodStatusMessage getStatusMessage(
+ Color defaultColor,
+ DateTime? nextPeriodDate,
+ ) {
+ PeriodStatusMessage status = PeriodStatusMessage(
+ text: '',
+ color: defaultColor,
+ );
if (_periods.isEmpty || nextPeriodDate == null) {
// in this case status bar on home page is hidden
- status.text = 'Start by tapping the + button below to log your most recent period';
+ status.text =
+ 'Start by tapping the + button below to log your most recent period';
return status;
}
status.color = Colors.green;
- final List sortedPeriods = List.from(_periods)..sort((a, b) => a.startDate.compareTo(b.startDate));
+ final List sortedPeriods = List.from(_periods)
+ ..sort((a, b) => a.startDate.compareTo(b.startDate));
- final lastPeriod = sortedPeriods.last; // last period dates are normalized to UTC already
+ final lastPeriod =
+ sortedPeriods.last; // last period dates are normalized to UTC already
// ensure both nextPeriodDate and today are stripped of time and in UTC
final DateTime now = DateTime.now();
final DateTime today = DateTime.utc(now.year, now.month, now.day);
// Check if currently in period
- if ((today.isAtSameMomentAs(lastPeriod.startDate) || today.isAtSameMomentAs(lastPeriod.endDate!)) ||
- (today.isAfter(lastPeriod.startDate) && today.isBefore(lastPeriod.endDate!))) {
+ if ((today.isAtSameMomentAs(lastPeriod.startDate) ||
+ today.isAtSameMomentAs(lastPeriod.endDate!)) ||
+ (today.isAfter(lastPeriod.startDate) &&
+ today.isBefore(lastPeriod.endDate!))) {
status.text = 'Currently in period';
return status;
}
@@ -167,7 +199,8 @@ class PeriodProvider extends ChangeNotifier {
// Returns average period length in days
double? getAveragePeriodLength({bool? useRecent6}) {
// Only consider periods with both start and end dates
- final completed = _periods.where((p) => p.endDate != null).toList();
+ final completed = _periods.where((p) => p.endDate != null).toList()
+ ..sort((a, b) => a.startDate.compareTo(b.startDate));
if (completed.isEmpty) return null;
List periodsToUse = completed;
@@ -175,7 +208,10 @@ class PeriodProvider extends ChangeNotifier {
periodsToUse = completed.sublist(completed.length - 6);
}
- final lengths = periodsToUse.map((p) => p.endDate!.difference(p.startDate).inDays + 1).where((days) => days > 0).toList();
+ final lengths = periodsToUse
+ .map((p) => p.endDate!.difference(p.startDate).inDays + 1)
+ .where((days) => days > 0)
+ .toList();
if (lengths.isEmpty) return null;
return lengths.reduce((a, b) => a + b) / lengths.length;
}
@@ -188,13 +224,16 @@ class PeriodProvider extends ChangeNotifier {
// returns null if userCycleLength is null
return userCycleLength?.toDouble();
}
- final List sorted = List.from(periods)..sort((a, b) => a.startDate.compareTo(b.startDate));
+ final List sorted = List.from(periods)
+ ..sort((a, b) => a.startDate.compareTo(b.startDate));
final List cycles = [];
if (useRecent6 == true && sorted.length > 6) {
sorted.removeRange(0, sorted.length - 6);
}
for (int i = 1; i < sorted.length; i++) {
- cycles.add(sorted[i].startDate.difference(sorted[i - 1].startDate).inDays);
+ cycles.add(
+ sorted[i].startDate.difference(sorted[i - 1].startDate).inDays,
+ );
}
if (cycles.isEmpty) return null;
return cycles.reduce((a, b) => a + b) / cycles.length;
@@ -218,7 +257,10 @@ class PeriodProvider extends ChangeNotifier {
final Period? period = PeriodService.getPeriodInDate(checkDate, periods);
if (period == null) {
- return Text('Cycle Day: $cycleDay', style: Theme.of(context).textTheme.bodyMedium);
+ return Text(
+ 'Cycle Day: $cycleDay',
+ style: Theme.of(context).textTheme.bodyMedium,
+ );
}
final String? notes;
@@ -231,7 +273,10 @@ class PeriodProvider extends ChangeNotifier {
return Center(
child: Column(
children: [
- Text('Cycle Day: $cycleDay', style: Theme.of(context).textTheme.bodyMedium),
+ Text(
+ 'Cycle Day: $cycleDay',
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
SizedBox(height: 4),
Text(
'Selected period: ${DateTimeHelper.displayDate(period.startDate)} - '
diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart
index 5f6b2ac..9377137 100644
--- a/lib/services/notification_service.dart
+++ b/lib/services/notification_service.dart
@@ -54,9 +54,10 @@ class NotificationService {
tz.setLocalLocation(tz.getLocation('Europe/Ljubljana')); // TODO
const AndroidInitializationSettings androidInit = AndroidInitializationSettings('@drawable/ic_stat_notify');
+ const DarwinInitializationSettings iosInit = DarwinInitializationSettings();
await _flutterLocalNotificationsPlugin.initialize(
- InitializationSettings(android: androidInit),
+ InitializationSettings(android: androidInit, iOS: iosInit),
// onDidReceiveNotificationResponse: (NotificationResponse response) {
// if (response.actionId == 'log') {
// navigatorKey.currentState?.pushNamed('/log');
@@ -68,9 +69,15 @@ class NotificationService {
);
}
- /// Requests notification permissions (Android 13+).
+ /// Requests notification permissions.
/// @return true if permissions are granted, false otherwise.
Future requestPermissions() async {
+ final iosPlugin = NotificationService()._flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation();
+ if (iosPlugin != null) {
+ final granted = await iosPlugin.requestPermissions(alert: true, badge: true, sound: true);
+ return granted == true;
+ }
+
// Android 13+
final androidPlugin = NotificationService()._flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation();
@@ -103,7 +110,7 @@ class NotificationService {
title,
body,
tz.TZDateTime.from(scheduledDate, tz.local),
- NotificationDetails(android: _androidNotificationDetails),
+ NotificationDetails(android: _androidNotificationDetails, iOS: const DarwinNotificationDetails()),
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
payload: payload,
);
diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig
index c2efd0b..4b81f9b 100644
--- a/macos/Flutter/Flutter-Debug.xcconfig
+++ b/macos/Flutter/Flutter-Debug.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig
index c2efd0b..5caa9d1 100644
--- a/macos/Flutter/Flutter-Release.xcconfig
+++ b/macos/Flutter/Flutter-Release.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/macos/Podfile b/macos/Podfile
new file mode 100644
index 0000000..ff5ddb3
--- /dev/null
+++ b/macos/Podfile
@@ -0,0 +1,42 @@
+platform :osx, '10.15'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/pubspec.lock b/pubspec.lock
index 0675949..03f0b91 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -276,10 +276,10 @@ packages:
dependency: transitive
description:
name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.17.0"
mime:
dependency: transitive
description:
@@ -618,10 +618,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.6"
+ version: "0.7.7"
timezone:
dependency: "direct main"
description:
diff --git a/test/widget_test.dart b/test/widget_test.dart
index fe4bce0..1bc1fde 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -1,32 +1,16 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
-
import 'package:period_tracker/main.dart';
void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
+ testWidgets('shows onboarding as the initial app route', (
+ WidgetTester tester,
+ ) async {
await tester.pumpWidget(
- const PeriodTrackerApp(showOnboarding: false, isAfterRestore: false),
+ const PeriodTrackerApp(showOnboarding: true, isAfterRestore: false),
);
+ await tester.pumpAndSettle();
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
-
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add_rounded));
- await tester.pump();
-
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
+ expect(find.text('Welcome!'), findsOneWidget);
+ expect(find.text('Restore data'), findsOneWidget);
});
}