feat: home/lock widgets, Health-aware streaks, workout editor, and entitlement fixes
Widgets (new KisaniCalWidgets extension) - Event Countdown widget: configurable via AppIntent, pick from your events or set a custom name/date; dot-grid progress toward the event. - Day Progress and Tasks-Done-Today widgets; lock screen widgets. - Shared dot grid sizes circles to fill the widget evenly at any count. - Tasks/calendar prefs now persist to the App Group so widgets read live data. Health & streaks - Persist HealthKit authorization and re-establish on launch (fixes the Today dashboard and streak sync disappearing after relaunch). - Add a Health permission toggle to onboarding; unify Settings/Profile on the manager's state. - Day streak counts from a logged Health workout OR marking all exercises complete; nudge to mark exercises complete even when Health logged the workout. Workout editor - Drag to reorder exercises and move them across sections (drag-and-drop); swipe to delete. - Add-exercise sheet: global search and keep-adding-multiple with a running count. Fixes - Restore app entitlements link (Sign in with Apple, HealthKit, App Group, iCloud). - Add HealthKit usage strings; widget Info.plist NSExtension + nested bundle id. - Calendar "Connect" routes to Settings when access was denied. - Bake DEVELOPMENT_TEAM and shared versions into project.yml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,63 +7,102 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
06CA0F336E64D9F6D56F7472 /* CloudSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449C34805DC6B2CB66886544 /* CloudSyncManager.swift */; };
|
||||||
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; };
|
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; };
|
||||||
|
12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.swift */; };
|
||||||
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; };
|
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; };
|
||||||
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; };
|
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; };
|
||||||
|
2CD2313EDC4BFCE6E89C6787 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.swift */; };
|
||||||
|
3AD7F62D231DCFFFFCB31649 /* TutorialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A27D42E141DC056D32C1A3 /* TutorialManager.swift */; };
|
||||||
|
3B57EA3600AFE975850DF39A /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89550F2CD19B950CCC6AD37F /* AuthManager.swift */; };
|
||||||
3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72FDF9C8DD37134576356B89 /* Assets.xcassets */; };
|
3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72FDF9C8DD37134576356B89 /* Assets.xcassets */; };
|
||||||
497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */; };
|
497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */; };
|
||||||
|
552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */; };
|
||||||
|
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429806CE1021C8DE2EB770CE /* WidgetViews.swift */; };
|
||||||
|
5EF4A5B6CE91ADA0CCF72D0D /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */; };
|
||||||
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106EEF572C6F8990408329F0 /* OnboardingView.swift */; };
|
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106EEF572C6F8990408329F0 /* OnboardingView.swift */; };
|
||||||
6921CB73A3257502FF778381 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */; };
|
6921CB73A3257502FF778381 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */; };
|
||||||
|
79FC1DF6762C6F02D01AB643 /* KisaniCalWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; };
|
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; };
|
||||||
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A4491BFA50721082024756 /* SharedComponents.swift */; };
|
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A4491BFA50721082024756 /* SharedComponents.swift */; };
|
||||||
|
8C9567A3DE3F63A1ECAE84D5 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3D5289C5F93484E22DEB63 /* TutorialView.swift */; };
|
||||||
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF6CCD95A587E26E30F5712 /* CalendarView.swift */; };
|
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF6CCD95A587E26E30F5712 /* CalendarView.swift */; };
|
||||||
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */; };
|
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */; };
|
||||||
A1B2C3D4E5F6A7B8C9D0E1F2 /* TutorialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */; };
|
|
||||||
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */; };
|
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */; };
|
||||||
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72308FEE0226F45414C04DDD /* SettingsView.swift */; };
|
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72308FEE0226F45414C04DDD /* SettingsView.swift */; };
|
||||||
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */; };
|
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */; };
|
||||||
B2C3D4E5F6A7B8C9D0E1F2A3 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C2D3E4F5A6B7C8D9E0F1A2 /* TutorialView.swift */; };
|
AC0D814DA54D5EF5E25CEB99 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */; };
|
||||||
B38135962FCAF9CE0037DC41 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */; };
|
BD0DB4B0AA8A63D124EDFF2C /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */; };
|
||||||
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; };
|
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; };
|
||||||
|
C7767143D9617ECA04ED1935 /* KisaniCalWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */; };
|
||||||
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; };
|
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; };
|
||||||
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */; };
|
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */; };
|
||||||
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
|
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
|
||||||
CC11DD22EE33FF44AA55BB66 /* CloudSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC00DD11EE22FF33AA44BB55 /* CloudSyncManager.swift */; };
|
ECEAA5CF7309E5993D12B571 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD014B7E3E30A34E18696A0 /* AuthView.swift */; };
|
||||||
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
|
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
|
||||||
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */; };
|
|
||||||
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */; };
|
|
||||||
F9B0C1D2E3F4A5B6C7D8E9F0 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
D04FCEE0BDF30AEFD1C969B6 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = AF6DE7A812408E3742522E90 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 28B43516AD88946E21D9BFE0;
|
||||||
|
remoteInfo = KisaniCalWidgets;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
B3BE3000B362E4DDC4EE9E76 /* Embed Foundation Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
79FC1DF6762C6F02D01AB643 /* KisaniCalWidgets.appex in Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed Foundation Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalApp.swift; sourceTree = "<group>"; };
|
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalApp.swift; sourceTree = "<group>"; };
|
||||||
|
01A27D42E141DC056D32C1A3 /* TutorialManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialManager.swift; sourceTree = "<group>"; };
|
||||||
|
0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
|
||||||
|
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||||
|
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
||||||
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; };
|
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; };
|
||||||
23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; };
|
23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; };
|
||||||
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = "<group>"; };
|
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = "<group>"; };
|
||||||
|
42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalWidgets.swift; sourceTree = "<group>"; };
|
||||||
|
429806CE1021C8DE2EB770CE /* WidgetViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetViews.swift; sourceTree = "<group>"; };
|
||||||
|
449C34805DC6B2CB66886544 /* CloudSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSyncManager.swift; sourceTree = "<group>"; };
|
||||||
|
4AD014B7E3E30A34E18696A0 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
|
||||||
5722CC4B59E3939724142710 /* ExerciseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseModels.swift; sourceTree = "<group>"; };
|
5722CC4B59E3939724142710 /* ExerciseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseModels.swift; sourceTree = "<group>"; };
|
||||||
57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetData.swift; sourceTree = "<group>"; };
|
||||||
72308FEE0226F45414C04DDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
72308FEE0226F45414C04DDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
72FDF9C8DD37134576356B89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
72FDF9C8DD37134576356B89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = "<group>"; };
|
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = "<group>"; };
|
||||||
|
89550F2CD19B950CCC6AD37F /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
|
||||||
|
8DC8687EA2FBA9FB2EEE51C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
8E7FC6446D5C5B2A2D9387DA /* KisaniCalWidgets.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCalWidgets.entitlements; sourceTree = "<group>"; };
|
||||||
9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; };
|
9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; };
|
||||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||||
CC00DD11EE22FF33AA44BB55 /* CloudSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSyncManager.swift; sourceTree = "<group>"; };
|
|
||||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialManager.swift; sourceTree = "<group>"; };
|
|
||||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
||||||
B1C2D3E4F5A6B7C8D9E0F1A2 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
|
AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
|
||||||
B38135942FCAF0BD0037DC41 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
|
||||||
B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
|
|
||||||
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = "<group>"; };
|
BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = "<group>"; };
|
||||||
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
||||||
C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
|
C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
|
||||||
D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; };
|
D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; };
|
||||||
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
|
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
|
||||||
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
|
FA3D5289C5F93484E22DEB63 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
|
||||||
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
|
FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCountdownWidget.swift; sourceTree = "<group>"; };
|
||||||
F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
|
FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -89,6 +128,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F70DA4746C68CD405435DAB6 /* KisaniCal */,
|
F70DA4746C68CD405435DAB6 /* KisaniCal */,
|
||||||
|
E8D727EEA0C3A4B8006FB087 /* KisaniCalWidgets */,
|
||||||
51BD1B5DEDE9FAD9CA2FF6DA /* Products */,
|
51BD1B5DEDE9FAD9CA2FF6DA /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -97,6 +137,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */,
|
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */,
|
||||||
|
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -104,16 +145,16 @@
|
|||||||
5E9A0E064E153429180400E6 /* Views */ = {
|
5E9A0E064E153429180400E6 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */,
|
|
||||||
F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */,
|
|
||||||
B1C2D3E4F5A6B7C8D9E0F1A2 /* TutorialView.swift */,
|
|
||||||
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */,
|
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */,
|
||||||
|
4AD014B7E3E30A34E18696A0 /* AuthView.swift */,
|
||||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */,
|
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */,
|
||||||
D44530A77DF12A17E52AAF34 /* MatrixView.swift */,
|
D44530A77DF12A17E52AAF34 /* MatrixView.swift */,
|
||||||
106EEF572C6F8990408329F0 /* OnboardingView.swift */,
|
106EEF572C6F8990408329F0 /* OnboardingView.swift */,
|
||||||
|
0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */,
|
||||||
72308FEE0226F45414C04DDD /* SettingsView.swift */,
|
72308FEE0226F45414C04DDD /* SettingsView.swift */,
|
||||||
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */,
|
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */,
|
||||||
C786EBC7DF879D64EB28165E /* TodayView.swift */,
|
C786EBC7DF879D64EB28165E /* TodayView.swift */,
|
||||||
|
FA3D5289C5F93484E22DEB63 /* TutorialView.swift */,
|
||||||
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */,
|
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
@@ -135,12 +176,25 @@
|
|||||||
path = Theme;
|
path = Theme;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E8D727EEA0C3A4B8006FB087 /* KisaniCalWidgets */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */,
|
||||||
|
8DC8687EA2FBA9FB2EEE51C6 /* Info.plist */,
|
||||||
|
8E7FC6446D5C5B2A2D9387DA /* KisaniCalWidgets.entitlements */,
|
||||||
|
42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */,
|
||||||
|
61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */,
|
||||||
|
429806CE1021C8DE2EB770CE /* WidgetViews.swift */,
|
||||||
|
);
|
||||||
|
path = KisaniCalWidgets;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
F70DA4746C68CD405435DAB6 /* KisaniCal */ = {
|
F70DA4746C68CD405435DAB6 /* KisaniCal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B38135942FCAF0BD0037DC41 /* KisaniCal.entitlements */,
|
|
||||||
72FDF9C8DD37134576356B89 /* Assets.xcassets */,
|
72FDF9C8DD37134576356B89 /* Assets.xcassets */,
|
||||||
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */,
|
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */,
|
||||||
|
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */,
|
||||||
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */,
|
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */,
|
||||||
21B93C269F283F11B415B18C /* Components */,
|
21B93C269F283F11B415B18C /* Components */,
|
||||||
FB9BF734B9E493EEB09ACE21 /* Managers */,
|
FB9BF734B9E493EEB09ACE21 /* Managers */,
|
||||||
@@ -155,12 +209,13 @@
|
|||||||
FB9BF734B9E493EEB09ACE21 /* Managers */ = {
|
FB9BF734B9E493EEB09ACE21 /* Managers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */,
|
AF905C574F34B4EE51A8D21E /* AppGroup.swift */,
|
||||||
B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */,
|
89550F2CD19B950CCC6AD37F /* AuthManager.swift */,
|
||||||
|
449C34805DC6B2CB66886544 /* CloudSyncManager.swift */,
|
||||||
|
FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */,
|
||||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */,
|
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */,
|
||||||
CC00DD11EE22FF33AA44BB55 /* CloudSyncManager.swift */,
|
|
||||||
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */,
|
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */,
|
||||||
A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */,
|
01A27D42E141DC056D32C1A3 /* TutorialManager.swift */,
|
||||||
);
|
);
|
||||||
path = Managers;
|
path = Managers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -168,16 +223,35 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
28B43516AD88946E21D9BFE0 /* KisaniCalWidgets */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 63EDFF56CC5312509E567D37 /* Build configuration list for PBXNativeTarget "KisaniCalWidgets" */;
|
||||||
|
buildPhases = (
|
||||||
|
B1361CC76F252F2A0F8D64CD /* Sources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = KisaniCalWidgets;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = KisaniCalWidgets;
|
||||||
|
productReference = 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
297C0C2BFBA10AB52D5D49CE /* KisaniCal */ = {
|
297C0C2BFBA10AB52D5D49CE /* KisaniCal */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */;
|
buildConfigurationList = B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
82EA49FE155821D424C46912 /* Sources */,
|
82EA49FE155821D424C46912 /* Sources */,
|
||||||
E1F1A3EE4469B851B972E4AE /* Resources */,
|
E1F1A3EE4469B851B972E4AE /* Resources */,
|
||||||
|
B3BE3000B362E4DDC4EE9E76 /* Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
8B1A9D9F3CF53BD8088AEC59 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = KisaniCal;
|
name = KisaniCal;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -195,7 +269,12 @@
|
|||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastUpgradeCheck = 2620;
|
LastUpgradeCheck = 2620;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
28B43516AD88946E21D9BFE0 = {
|
||||||
|
DevelopmentTeam = K8BLMMR883;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
297C0C2BFBA10AB52D5D49CE = {
|
297C0C2BFBA10AB52D5D49CE = {
|
||||||
|
DevelopmentTeam = K8BLMMR883;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -215,6 +294,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
297C0C2BFBA10AB52D5D49CE /* KisaniCal */,
|
297C0C2BFBA10AB52D5D49CE /* KisaniCal */,
|
||||||
|
28B43516AD88946E21D9BFE0 /* KisaniCalWidgets */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -236,35 +316,56 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */,
|
|
||||||
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */,
|
|
||||||
F9B0C1D2E3F4A5B6C7D8E9F0 /* ProfileSetupView.swift in Sources */,
|
|
||||||
A1B2C3D4E5F6A7B8C9D0E1F2 /* TutorialManager.swift in Sources */,
|
|
||||||
B2C3D4E5F6A7B8C9D0E1F2A3 /* TutorialView.swift in Sources */,
|
|
||||||
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */,
|
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */,
|
||||||
|
2CD2313EDC4BFCE6E89C6787 /* AppGroup.swift in Sources */,
|
||||||
|
3B57EA3600AFE975850DF39A /* AuthManager.swift in Sources */,
|
||||||
|
ECEAA5CF7309E5993D12B571 /* AuthView.swift in Sources */,
|
||||||
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */,
|
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */,
|
||||||
|
06CA0F336E64D9F6D56F7472 /* CloudSyncManager.swift in Sources */,
|
||||||
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */,
|
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */,
|
||||||
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */,
|
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */,
|
||||||
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */,
|
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */,
|
||||||
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */,
|
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */,
|
||||||
|
BD0DB4B0AA8A63D124EDFF2C /* HealthKitManager.swift in Sources */,
|
||||||
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
|
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
|
||||||
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
|
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
|
||||||
B38135962FCAF9CE0037DC41 /* HealthKitManager.swift in Sources */,
|
|
||||||
CC11DD22EE33FF44AA55BB66 /* CloudSyncManager.swift in Sources */,
|
|
||||||
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */,
|
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */,
|
||||||
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */,
|
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */,
|
||||||
|
5EF4A5B6CE91ADA0CCF72D0D /* ProfileSetupView.swift in Sources */,
|
||||||
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */,
|
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */,
|
||||||
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */,
|
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */,
|
||||||
497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */,
|
497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */,
|
||||||
6921CB73A3257502FF778381 /* SplashView.swift in Sources */,
|
6921CB73A3257502FF778381 /* SplashView.swift in Sources */,
|
||||||
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */,
|
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */,
|
||||||
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */,
|
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */,
|
||||||
|
3AD7F62D231DCFFFFCB31649 /* TutorialManager.swift in Sources */,
|
||||||
|
8C9567A3DE3F63A1ECAE84D5 /* TutorialView.swift in Sources */,
|
||||||
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */,
|
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
B1361CC76F252F2A0F8D64CD /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */,
|
||||||
|
552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */,
|
||||||
|
C7767143D9617ECA04ED1935 /* KisaniCalWidgets.swift in Sources */,
|
||||||
|
AC0D814DA54D5EF5E25CEB99 /* WidgetData.swift in Sources */,
|
||||||
|
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
8B1A9D9F3CF53BD8088AEC59 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 28B43516AD88946E21D9BFE0 /* KisaniCalWidgets */;
|
||||||
|
targetProxy = D04FCEE0BDF30AEFD1C969B6 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
0CEA6637F45C32A237FBA20D /* Release */ = {
|
0CEA6637F45C32A237FBA20D /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@@ -274,27 +375,23 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "KisaniCal reads your steps, active calories, resting heart rate, and workout history to display your fitness stats.";
|
INFOPLIST_KEY_NSHealthShareUsageDescription = "Kisani Cal reads your steps, energy, heart rate, and workouts to show health stats on your dashboard.";
|
||||||
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "KisaniCal saves your completed strength training workouts to Apple Health.";
|
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Kisani Cal saves workouts you complete to the Health app.";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchStoryboardName = "";
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
NEW_SETTING = "";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
@@ -336,6 +433,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
@@ -350,6 +448,7 @@
|
|||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -361,6 +460,27 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
B60AA89AB378D4EA773989AA /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_VERSION = 5.9;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
BD527AA54887489CC665FB9D /* Debug */ = {
|
BD527AA54887489CC665FB9D /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -395,6 +515,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -415,6 +536,7 @@
|
|||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -427,6 +549,27 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
DF56CDD73E9B177D57AE17FB /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_VERSION = 5.9;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
F364512BEB70ECB7CB83FBFE /* Debug */ = {
|
F364512BEB70ECB7CB83FBFE /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -435,27 +578,23 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "KisaniCal reads your steps, active calories, resting heart rate, and workout history to display your fitness stats.";
|
INFOPLIST_KEY_NSHealthShareUsageDescription = "Kisani Cal reads your steps, energy, heart rate, and workouts to show health stats on your dashboard.";
|
||||||
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "KisaniCal saves your completed strength training workouts to Apple Health.";
|
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Kisani Cal saves workouts you complete to the Health app.";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchStoryboardName = "";
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
NEW_SETTING = "";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
@@ -466,6 +605,15 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
63EDFF56CC5312509E567D37 /* Build configuration list for PBXNativeTarget "KisaniCalWidgets" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
DF56CDD73E9B177D57AE17FB /* Debug */,
|
||||||
|
B60AA89AB378D4EA773989AA /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */ = {
|
B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ private struct TabBarScrollModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.onPreferenceChange(ScrollYKey.self) { y in
|
.onPreferenceChange(ScrollYKey.self) { y in
|
||||||
tabState.report(scrollY: y)
|
// Defer off the current view-update cycle: this callback can fire during
|
||||||
|
// layout, and mutating tabState's @Published state there triggers
|
||||||
|
// "Modifying state during view update".
|
||||||
|
Task { @MainActor in tabState.report(scrollY: y) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@StateObject private var taskVM = TaskViewModel()
|
@StateObject private var taskVM = TaskViewModel()
|
||||||
@@ -86,7 +87,13 @@ struct ContentView: View {
|
|||||||
if !hasOnboarded { showOnboarding = true }
|
if !hasOnboarded { showOnboarding = true }
|
||||||
CloudSyncManager.shared.restoreSettings()
|
CloudSyncManager.shared.restoreSettings()
|
||||||
CloudSyncManager.shared.backupSettings()
|
CloudSyncManager.shared.backupSettings()
|
||||||
|
HealthKitManager.shared.bootstrap()
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
|
// Pull workouts from Health, then reschedule so a detected workout updates reminders.
|
||||||
|
Task {
|
||||||
|
await workoutVM.syncFromHealthKit()
|
||||||
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { phase in
|
.onChange(of: scenePhase) { phase in
|
||||||
if phase == .active {
|
if phase == .active {
|
||||||
@@ -94,11 +101,15 @@ struct ContentView: View {
|
|||||||
workoutVM.checkPendingHealthConfirm()
|
workoutVM.checkPendingHealthConfirm()
|
||||||
CloudSyncManager.shared.backupSettings()
|
CloudSyncManager.shared.backupSettings()
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
Task { await workoutVM.syncFromHealthKit() }
|
Task {
|
||||||
|
await workoutVM.syncFromHealthKit()
|
||||||
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: taskVM.tasks) { _ in
|
.onChange(of: taskVM.tasks) { _ in
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
}
|
}
|
||||||
.onChange(of: workoutVM.doneSets) { _ in
|
.onChange(of: workoutVM.doneSets) { _ in
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
|
|||||||
@@ -10,5 +10,9 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.kutesir.KisaniCal</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
10
KisaniCal/Managers/AppGroup.swift
Normal file
10
KisaniCal/Managers/AppGroup.swift
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
let kisaniAppGroupID = "group.com.kutesir.KisaniCal"
|
||||||
|
|
||||||
|
extension UserDefaults {
|
||||||
|
/// Shared container readable by both the main app and the widget extension.
|
||||||
|
static var kisani: UserDefaults {
|
||||||
|
UserDefaults(suiteName: kisaniAppGroupID) ?? .standard
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ final class AuthManager: NSObject, ObservableObject {
|
|||||||
super.init()
|
super.init()
|
||||||
currentUser = loadUser()
|
currentUser = loadUser()
|
||||||
if let uid = currentUser?.id {
|
if let uid = currentUser?.id {
|
||||||
UserDefaults.standard.set(uid, forKey: Self.activeUserIdKey)
|
UserDefaults.kisani.set(uid, forKey: Self.activeUserIdKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ final class AuthManager: NSObject, ObservableObject {
|
|||||||
|
|
||||||
func signOut() {
|
func signOut() {
|
||||||
currentUser = nil
|
currentUser = nil
|
||||||
UserDefaults.standard.removeObject(forKey: Self.activeUserIdKey)
|
UserDefaults.kisani.removeObject(forKey: Self.activeUserIdKey)
|
||||||
deleteKeychain(key: keychainUser)
|
deleteKeychain(key: keychainUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ final class AuthManager: NSObject, ObservableObject {
|
|||||||
|
|
||||||
private func persist(_ user: AppUser) {
|
private func persist(_ user: AppUser) {
|
||||||
currentUser = user
|
currentUser = user
|
||||||
UserDefaults.standard.set(user.id, forKey: Self.activeUserIdKey)
|
UserDefaults.kisani.set(user.id, forKey: Self.activeUserIdKey)
|
||||||
if let data = try? JSONEncoder().encode(user) {
|
if let data = try? JSONEncoder().encode(user) {
|
||||||
saveKeychain(key: keychainUser, data: data)
|
saveKeychain(key: keychainUser, data: data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ final class CloudSyncManager {
|
|||||||
/// Copy iCloud value into UserDefaults ONLY when UserDefaults has no value yet.
|
/// Copy iCloud value into UserDefaults ONLY when UserDefaults has no value yet.
|
||||||
/// This safely restores data after a fresh install or new build without overwriting live data.
|
/// This safely restores data after a fresh install or new build without overwriting live data.
|
||||||
func restoreIfMissing(key: String) {
|
func restoreIfMissing(key: String) {
|
||||||
guard UserDefaults.standard.object(forKey: key) == nil,
|
guard UserDefaults.kisani.object(forKey: key) == nil,
|
||||||
let icloudValue = kv.object(forKey: key)
|
let icloudValue = kv.object(forKey: key)
|
||||||
else { return }
|
else { return }
|
||||||
UserDefaults.standard.set(icloudValue, forKey: key)
|
UserDefaults.kisani.set(icloudValue, forKey: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Bulk helpers
|
// MARK: - Bulk helpers
|
||||||
@@ -60,7 +60,7 @@ final class CloudSyncManager {
|
|||||||
/// Backup all small settings keys from UserDefaults → iCloud
|
/// Backup all small settings keys from UserDefaults → iCloud
|
||||||
func backupSettings() {
|
func backupSettings() {
|
||||||
for key in settingsKeys {
|
for key in settingsKeys {
|
||||||
if let val = UserDefaults.standard.object(forKey: key) {
|
if let val = UserDefaults.kisani.object(forKey: key) {
|
||||||
kv.set(val, forKey: key)
|
kv.set(val, forKey: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ final class CloudSyncManager {
|
|||||||
|
|
||||||
for key in changedKeys {
|
for key in changedKeys {
|
||||||
if let val = kv.object(forKey: key) {
|
if let val = kv.object(forKey: key) {
|
||||||
UserDefaults.standard.set(val, forKey: key)
|
UserDefaults.kisani.set(val, forKey: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ final class HealthKitManager: ObservableObject {
|
|||||||
|
|
||||||
var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() }
|
var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() }
|
||||||
|
|
||||||
|
// Persists across launches: HealthKit can't report read-authorization status,
|
||||||
|
// so we remember that the user opted in and re-establish on launch.
|
||||||
|
private let connectedKey = "kisani.health.connected"
|
||||||
|
var isConnected: Bool { UserDefaults.kisani.bool(forKey: connectedKey) }
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
// MARK: - Auth
|
// MARK: - Auth
|
||||||
@@ -23,6 +28,7 @@ final class HealthKitManager: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
try await store.requestAuthorization(toShare: shareTypes, read: readTypes)
|
try await store.requestAuthorization(toShare: shareTypes, read: readTypes)
|
||||||
authorized = true
|
authorized = true
|
||||||
|
UserDefaults.kisani.set(true, forKey: connectedKey)
|
||||||
await refresh()
|
await refresh()
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
@@ -30,6 +36,23 @@ final class HealthKitManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-establish authorization on app launch if the user previously connected Health.
|
||||||
|
func bootstrap() {
|
||||||
|
guard isAvailable, isConnected else { return }
|
||||||
|
authorized = true
|
||||||
|
Task { await refresh() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn the integration off: hides the dashboard and stops Health reads.
|
||||||
|
func disconnect() {
|
||||||
|
authorized = false
|
||||||
|
UserDefaults.kisani.set(false, forKey: connectedKey)
|
||||||
|
stepsToday = 0
|
||||||
|
activeCaloriesToday = 0
|
||||||
|
restingHeartRate = 0
|
||||||
|
workoutsThisWeek = 0
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Refresh all stats
|
// MARK: - Refresh all stats
|
||||||
|
|
||||||
func refresh() async {
|
func refresh() async {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
// MARK: - Complete task directly in UserDefaults (called from background)
|
// MARK: - Complete task directly in UserDefaults (called from background)
|
||||||
private func markTaskComplete(taskId: String, userId: String) {
|
private func markTaskComplete(taskId: String, userId: String) {
|
||||||
let key = "kisani.tasks.v2.\(userId)"
|
let key = "kisani.tasks.v2.\(userId)"
|
||||||
guard let data = UserDefaults.standard.data(forKey: key),
|
guard let data = UserDefaults.kisani.data(forKey: key),
|
||||||
var tasks = try? JSONDecoder().decode([TaskItem].self, from: data),
|
var tasks = try? JSONDecoder().decode([TaskItem].self, from: data),
|
||||||
let uuid = UUID(uuidString: taskId),
|
let uuid = UUID(uuidString: taskId),
|
||||||
let idx = tasks.firstIndex(where: { $0.id == uuid })
|
let idx = tasks.firstIndex(where: { $0.id == uuid })
|
||||||
@@ -48,7 +48,7 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
tasks[idx].isComplete = true
|
tasks[idx].isComplete = true
|
||||||
tasks[idx].completedAt = Date()
|
tasks[idx].completedAt = Date()
|
||||||
if let encoded = try? JSONEncoder().encode(tasks) {
|
if let encoded = try? JSONEncoder().encode(tasks) {
|
||||||
UserDefaults.standard.set(encoded, forKey: key)
|
UserDefaults.kisani.set(encoded, forKey: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
let schedule = workoutVM.schedule
|
let schedule = workoutVM.schedule
|
||||||
let programs = workoutVM.programs
|
let programs = workoutVM.programs
|
||||||
let progress = workoutVM.progress
|
let progress = workoutVM.progress
|
||||||
|
let recorded = workoutVM.isTodayRecorded
|
||||||
let tasks = taskVM.tasks
|
let tasks = taskVM.tasks
|
||||||
|
|
||||||
center.getNotificationSettings { [weak self] settings in
|
center.getNotificationSettings { [weak self] settings in
|
||||||
@@ -84,7 +85,7 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
let ok = settings.authorizationStatus == .authorized
|
let ok = settings.authorizationStatus == .authorized
|
||||||
|| settings.authorizationStatus == .provisional
|
|| settings.authorizationStatus == .provisional
|
||||||
guard ok else { return }
|
guard ok else { return }
|
||||||
self.scheduleWorkout(schedule: schedule, programs: programs, progress: progress)
|
self.scheduleWorkout(schedule: schedule, programs: programs, progress: progress, recordedToday: recorded)
|
||||||
self.scheduleWorkoutConfirm(schedule: schedule, programs: programs)
|
self.scheduleWorkoutConfirm(schedule: schedule, programs: programs)
|
||||||
self.scheduleTasks(snapshot: tasks)
|
self.scheduleTasks(snapshot: tasks)
|
||||||
self.updateBadge(snapshot: tasks)
|
self.updateBadge(snapshot: tasks)
|
||||||
@@ -105,9 +106,10 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
|
|
||||||
// MARK: - Workout reminders + check-in
|
// MARK: - Workout reminders + check-in
|
||||||
|
|
||||||
private func scheduleWorkout(schedule: [Int: UUID], programs: [WorkoutProgram], progress: Double) {
|
private func scheduleWorkout(schedule: [Int: UUID], programs: [WorkoutProgram], progress: Double, recordedToday: Bool) {
|
||||||
let ud = UserDefaults.standard
|
let ud = UserDefaults.kisani
|
||||||
let existingIds = (1...7).flatMap { ["\(workoutPrefix).\($0)", "\(checkInPrefix).\($0)"] }
|
let markCompleteId = "kisani.workout.markcomplete"
|
||||||
|
let existingIds = (1...7).flatMap { ["\(workoutPrefix).\($0)", "\(checkInPrefix).\($0)"] } + [markCompleteId]
|
||||||
center.removePendingNotificationRequests(withIdentifiers: existingIds)
|
center.removePendingNotificationRequests(withIdentifiers: existingIds)
|
||||||
|
|
||||||
let reminderOn = ud.object(forKey: "workoutReminderEnabled") as? Bool ?? true
|
let reminderOn = ud.object(forKey: "workoutReminderEnabled") as? Bool ?? true
|
||||||
@@ -118,9 +120,13 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
let cMinute = ud.object(forKey: "checkInMinute") as? Int ?? 30
|
let cMinute = ud.object(forKey: "checkInMinute") as? Int ?? 30
|
||||||
let todayWD = Calendar.current.component(.weekday, from: Date())
|
let todayWD = Calendar.current.component(.weekday, from: Date())
|
||||||
|
|
||||||
|
// In-app completion vs. "recorded" (in-app OR detected from Apple Health).
|
||||||
|
let inAppDoneToday = progress >= 1.0
|
||||||
|
|
||||||
for (weekday, pid) in schedule {
|
for (weekday, pid) in schedule {
|
||||||
guard let program = programs.first(where: { $0.id == pid }) else { continue }
|
guard let program = programs.first(where: { $0.id == pid }) else { continue }
|
||||||
let doneToday = weekday == todayWD && progress >= 1.0
|
// Suppress "time to work out" if today is already done by either path.
|
||||||
|
let doneToday = weekday == todayWD && (inAppDoneToday || recordedToday)
|
||||||
|
|
||||||
if reminderOn && !doneToday {
|
if reminderOn && !doneToday {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
@@ -152,12 +158,34 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark-complete nudge: Apple Health recorded a workout today (streak is safe),
|
||||||
|
// but the user hasn't marked their exercises complete in the app yet.
|
||||||
|
if recordedToday && !inAppDoneToday {
|
||||||
|
let cal = Calendar.current
|
||||||
|
var fireComps = cal.dateComponents([.year, .month, .day], from: Date())
|
||||||
|
fireComps.hour = rHour; fireComps.minute = rMinute
|
||||||
|
var fire = cal.date(from: fireComps) ?? Date()
|
||||||
|
if fire <= Date() { fire = Date().addingTimeInterval(3600) } // reminder time passed → nudge in 1h
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Mark your workout complete"
|
||||||
|
content.body = "We logged today's workout from Apple Health — mark your exercises complete to update your log."
|
||||||
|
content.sound = .default
|
||||||
|
content.userInfo = ["deeplink": "workout"]
|
||||||
|
let trigComps = cal.dateComponents([.year, .month, .day, .hour, .minute], from: fire)
|
||||||
|
center.add(UNNotificationRequest(
|
||||||
|
identifier: markCompleteId,
|
||||||
|
content: content,
|
||||||
|
trigger: UNCalendarNotificationTrigger(dateMatching: trigComps, repeats: false)
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Workout completion confirmation
|
// MARK: - Workout completion confirmation
|
||||||
|
|
||||||
private func scheduleWorkoutConfirm(schedule: [Int: UUID], programs: [WorkoutProgram]) {
|
private func scheduleWorkoutConfirm(schedule: [Int: UUID], programs: [WorkoutProgram]) {
|
||||||
let ud = UserDefaults.standard
|
let ud = UserDefaults.kisani
|
||||||
guard ud.object(forKey: "workoutConfirmEnabled") as? Bool ?? false else {
|
guard ud.object(forKey: "workoutConfirmEnabled") as? Bool ?? false else {
|
||||||
center.removePendingNotificationRequests(withIdentifiers: (1...7).map { "\(confirmPrefix).\($0)" })
|
center.removePendingNotificationRequests(withIdentifiers: (1...7).map { "\(confirmPrefix).\($0)" })
|
||||||
return
|
return
|
||||||
@@ -277,7 +305,7 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
|
|||||||
didReceive response: UNNotificationResponse,
|
didReceive response: UNNotificationResponse,
|
||||||
withCompletionHandler completionHandler: @escaping () -> Void
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
let userId = UserDefaults.standard.string(forKey: AuthManager.activeUserIdKey) ?? "shared"
|
let userId = UserDefaults.kisani.string(forKey: AuthManager.activeUserIdKey) ?? "shared"
|
||||||
let info = response.notification.request.content.userInfo
|
let info = response.notification.request.content.userInfo
|
||||||
let deeplink = info["deeplink"] as? String
|
let deeplink = info["deeplink"] as? String
|
||||||
let taskId = info["taskId"] as? String
|
let taskId = info["taskId"] as? String
|
||||||
@@ -290,7 +318,7 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
|
|||||||
case Self.logWorkoutActId:
|
case Self.logWorkoutActId:
|
||||||
// "Yes, log it" from workout confirm notification — store date for WorkoutViewModel to pick up on next foreground
|
// "Yes, log it" from workout confirm notification — store date for WorkoutViewModel to pick up on next foreground
|
||||||
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
|
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
|
||||||
UserDefaults.standard.set(fmt.string(from: Date()), forKey: "kisani.workout.pendingConfirm")
|
UserDefaults.kisani.set(fmt.string(from: Date()), forKey: "kisani.workout.pendingConfirm")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if let taskId { markTaskComplete(taskId: taskId, userId: userId) }
|
if let taskId { markTaskComplete(taskId: taskId, userId: userId) }
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.completedDates.\(uid)")
|
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.completedDates.\(uid)")
|
||||||
|
|
||||||
// ── Attempt to restore persisted state ──
|
// ── Attempt to restore persisted state ──
|
||||||
if let data = UserDefaults.standard.data(forKey: kPrograms),
|
if let data = UserDefaults.kisani.data(forKey: kPrograms),
|
||||||
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
|
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
|
||||||
!saved.isEmpty {
|
!saved.isEmpty {
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
: saved
|
: saved
|
||||||
|
|
||||||
var sched: [Int: UUID] = [:]
|
var sched: [Int: UUID] = [:]
|
||||||
if let dict = UserDefaults.standard.dictionary(forKey: kSchedule) as? [String: String] {
|
if let dict = UserDefaults.kisani.dictionary(forKey: kSchedule) as? [String: String] {
|
||||||
for (k, v) in dict {
|
for (k, v) in dict {
|
||||||
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
|
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
|
||||||
}
|
}
|
||||||
@@ -262,7 +262,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
schedule = sched
|
schedule = sched
|
||||||
|
|
||||||
var pid = saved[0].id
|
var pid = saved[0].id
|
||||||
if let str = UserDefaults.standard.string(forKey: kActivePid),
|
if let str = UserDefaults.kisani.string(forKey: kActivePid),
|
||||||
let uid = UUID(uuidString: str),
|
let uid = UUID(uuidString: str),
|
||||||
saved.contains(where: { $0.id == uid }) { pid = uid }
|
saved.contains(where: { $0.id == uid }) { pid = uid }
|
||||||
activeProgramId = pid
|
activeProgramId = pid
|
||||||
@@ -283,18 +283,18 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
activeProgramId = blank.id
|
activeProgramId = blank.id
|
||||||
activeSections = blank.sections
|
activeSections = blank.sections
|
||||||
}
|
}
|
||||||
workoutDates = UserDefaults.standard.stringArray(forKey: kWorkoutDates) ?? []
|
workoutDates = UserDefaults.kisani.stringArray(forKey: kWorkoutDates) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
if let data = try? JSONEncoder().encode(programs) {
|
if let data = try? JSONEncoder().encode(programs) {
|
||||||
UserDefaults.standard.set(data, forKey: kPrograms)
|
UserDefaults.kisani.set(data, forKey: kPrograms)
|
||||||
CloudSyncManager.shared.push(data: data, forKey: kPrograms)
|
CloudSyncManager.shared.push(data: data, forKey: kPrograms)
|
||||||
}
|
}
|
||||||
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
|
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
|
||||||
UserDefaults.standard.set(schedDict, forKey: kSchedule)
|
UserDefaults.kisani.set(schedDict, forKey: kSchedule)
|
||||||
UserDefaults.standard.set(activeProgramId.uuidString, forKey: kActivePid)
|
UserDefaults.kisani.set(activeProgramId.uuidString, forKey: kActivePid)
|
||||||
UserDefaults.standard.set(workoutDates, forKey: kWorkoutDates)
|
UserDefaults.kisani.set(workoutDates, forKey: kWorkoutDates)
|
||||||
CloudSyncManager.shared.push(value: schedDict, forKey: kSchedule)
|
CloudSyncManager.shared.push(value: schedDict, forKey: kSchedule)
|
||||||
CloudSyncManager.shared.push(value: activeProgramId.uuidString, forKey: kActivePid)
|
CloudSyncManager.shared.push(value: activeProgramId.uuidString, forKey: kActivePid)
|
||||||
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
|
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
|
||||||
@@ -326,8 +326,8 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func checkPendingHealthConfirm() {
|
func checkPendingHealthConfirm() {
|
||||||
let key = "kisani.workout.pendingConfirm"
|
let key = "kisani.workout.pendingConfirm"
|
||||||
guard let dateStr = UserDefaults.standard.string(forKey: key) else { return }
|
guard let dateStr = UserDefaults.kisani.string(forKey: key) else { return }
|
||||||
UserDefaults.standard.removeObject(forKey: key)
|
UserDefaults.kisani.removeObject(forKey: key)
|
||||||
guard !workoutDates.contains(dateStr) else { return }
|
guard !workoutDates.contains(dateStr) else { return }
|
||||||
workoutDates.append(dateStr)
|
workoutDates.append(dateStr)
|
||||||
save()
|
save()
|
||||||
@@ -363,6 +363,10 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
|
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
|
||||||
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
|
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
|
||||||
|
|
||||||
|
/// True if today already counts toward the streak — either marked complete in-app
|
||||||
|
/// or detected from Apple Health (both land in `workoutDates`).
|
||||||
|
var isTodayRecorded: Bool { workoutDates.contains(iso.string(from: Date())) }
|
||||||
|
|
||||||
// MARK: - Program management
|
// MARK: - Program management
|
||||||
func switchProgram(to id: UUID) {
|
func switchProgram(to id: UUID) {
|
||||||
syncBack()
|
syncBack()
|
||||||
@@ -480,6 +484,34 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
syncBack()
|
syncBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Relocate an exercise (within or across sections) for any program via drag-and-drop.
|
||||||
|
/// Drops the exercise just before `beforeId`; pass nil to append to the target section.
|
||||||
|
func moveExercise(programId: UUID, exerciseId: UUID, toSection targetSectionId: UUID, before beforeId: UUID?) {
|
||||||
|
guard exerciseId != beforeId else { return }
|
||||||
|
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
|
||||||
|
|
||||||
|
// Remove from its current section.
|
||||||
|
var moved: Exercise?
|
||||||
|
for si in programs[pi].sections.indices {
|
||||||
|
if let ei = programs[pi].sections[si].exercises.firstIndex(where: { $0.id == exerciseId }) {
|
||||||
|
moved = programs[pi].sections[si].exercises.remove(at: ei)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let ex = moved,
|
||||||
|
let ti = programs[pi].sections.firstIndex(where: { $0.id == targetSectionId }) else { return }
|
||||||
|
|
||||||
|
// Insert before the target row, or append if none specified.
|
||||||
|
if let beforeId, let idx = programs[pi].sections[ti].exercises.firstIndex(where: { $0.id == beforeId }) {
|
||||||
|
programs[pi].sections[ti].exercises.insert(ex, at: idx)
|
||||||
|
} else {
|
||||||
|
programs[pi].sections[ti].exercises.append(ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Universal (any program) operations
|
// MARK: - Universal (any program) operations
|
||||||
func addExercise(programId: UUID, sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
|
func addExercise(programId: UUID, sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
|
||||||
guard let pi = programs.firstIndex(where: { $0.id == programId }),
|
guard let pi = programs.firstIndex(where: { $0.id == programId }),
|
||||||
@@ -494,6 +526,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
Exercise(name: name, sfSymbol: symbol, colorIndex: colorIdx, sets: sets, isDumbbell: isDumbb)
|
Exercise(name: name, sfSymbol: symbol, colorIndex: colorIdx, sets: sets, isDumbbell: isDumbb)
|
||||||
)
|
)
|
||||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||||
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteExercise(programId: UUID, sectionId: UUID, exerciseId: UUID) {
|
func deleteExercise(programId: UUID, sectionId: UUID, exerciseId: UUID) {
|
||||||
@@ -502,18 +535,21 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
else { return }
|
else { return }
|
||||||
programs[pi].sections[si].exercises.removeAll { $0.id == exerciseId }
|
programs[pi].sections[si].exercises.removeAll { $0.id == exerciseId }
|
||||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||||
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSection(to programId: UUID, name: String) {
|
func addSection(to programId: UUID, name: String) {
|
||||||
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
|
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
|
||||||
programs[pi].sections.append(WorkoutSection(name: name, exercises: []))
|
programs[pi].sections.append(WorkoutSection(name: name, exercises: []))
|
||||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||||
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteSection(from programId: UUID, sectionId: UUID) {
|
func deleteSection(from programId: UUID, sectionId: UUID) {
|
||||||
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
|
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
|
||||||
programs[pi].sections.removeAll { $0.id == sectionId }
|
programs[pi].sections.removeAll { $0.id == sectionId }
|
||||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||||
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameSection(in programId: UUID, sectionId: UUID, name: String) {
|
func renameSection(in programId: UUID, sectionId: UUID, name: String) {
|
||||||
@@ -522,6 +558,7 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
else { return }
|
else { return }
|
||||||
programs[pi].sections[si].name = name
|
programs[pi].sections[si].name = name
|
||||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||||
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleExpanded(sectionId: UUID, exerciseId: UUID) {
|
func toggleExpanded(sectionId: UUID, exerciseId: UUID) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
// MARK: - Task Model
|
// MARK: - Task Model
|
||||||
struct TaskItem: Identifiable, Codable, Equatable {
|
struct TaskItem: Identifiable, Codable, Equatable {
|
||||||
@@ -129,8 +130,16 @@ class TaskViewModel: ObservableObject {
|
|||||||
init() {
|
init() {
|
||||||
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
||||||
self.persistKey = "kisani.tasks.v2.\(uid)"
|
self.persistKey = "kisani.tasks.v2.\(uid)"
|
||||||
|
// One-time migration: tasks used to live in UserDefaults.standard, but the
|
||||||
|
// widgets read from the shared App Group. Move legacy data over if present.
|
||||||
|
if UserDefaults.kisani.data(forKey: persistKey) == nil,
|
||||||
|
let legacy = UserDefaults.standard.data(forKey: persistKey) {
|
||||||
|
UserDefaults.kisani.set(legacy, forKey: persistKey)
|
||||||
|
UserDefaults.standard.removeObject(forKey: persistKey)
|
||||||
|
}
|
||||||
|
// Restore from iCloud only if the App Group still has nothing (fresh install / new device).
|
||||||
CloudSyncManager.shared.restoreIfMissing(key: persistKey)
|
CloudSyncManager.shared.restoreIfMissing(key: persistKey)
|
||||||
if let data = UserDefaults.standard.data(forKey: persistKey),
|
if let data = UserDefaults.kisani.data(forKey: persistKey),
|
||||||
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
|
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
|
||||||
tasks = saved
|
tasks = saved
|
||||||
} else {
|
} else {
|
||||||
@@ -139,7 +148,7 @@ class TaskViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reload() {
|
func reload() {
|
||||||
guard let data = UserDefaults.standard.data(forKey: persistKey),
|
guard let data = UserDefaults.kisani.data(forKey: persistKey),
|
||||||
let saved = try? JSONDecoder().decode([TaskItem].self, from: data)
|
let saved = try? JSONDecoder().decode([TaskItem].self, from: data)
|
||||||
else { return }
|
else { return }
|
||||||
tasks = saved
|
tasks = saved
|
||||||
@@ -147,8 +156,9 @@ class TaskViewModel: ObservableObject {
|
|||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
if let data = try? JSONEncoder().encode(tasks) {
|
if let data = try? JSONEncoder().encode(tasks) {
|
||||||
UserDefaults.standard.set(data, forKey: persistKey)
|
UserDefaults.kisani.set(data, forKey: persistKey)
|
||||||
CloudSyncManager.shared.push(data: data, forKey: persistKey)
|
CloudSyncManager.shared.push(data: data, forKey: persistKey)
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ struct ExercisePickerSheet: View {
|
|||||||
@State private var setsText = "3"
|
@State private var setsText = "3"
|
||||||
@State private var repsText = "10"
|
@State private var repsText = "10"
|
||||||
@State private var showCustom = false
|
@State private var showCustom = false
|
||||||
|
@State private var addedCount = 0
|
||||||
|
@State private var flashedId: UUID? = nil
|
||||||
@FocusState private var customFocused: Bool
|
@FocusState private var customFocused: Bool
|
||||||
|
|
||||||
private func addExercise(name: String) {
|
private func addExercise(name: String) {
|
||||||
@@ -25,27 +27,48 @@ struct ExercisePickerSheet: View {
|
|||||||
} else {
|
} else {
|
||||||
vm.addExercise(to: sectionId, name: name, setsCount: sets, repsCount: reps)
|
vm.addExercise(to: sectionId, name: name, setsCount: sets, repsCount: reps)
|
||||||
}
|
}
|
||||||
|
addedCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Briefly mark a template row as "added" so the user gets feedback while keeping the sheet open.
|
||||||
|
private func flash(_ id: UUID) {
|
||||||
|
withAnimation(KisaniSpring.micro) { flashedId = id }
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
|
||||||
|
if flashedId == id { withAnimation(KisaniSpring.micro) { flashedId = nil } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var filtered: [ExerciseTemplate] {
|
private var filtered: [ExerciseTemplate] {
|
||||||
let byMuscle = exerciseLibrary.filter { $0.muscle == selectedMuscle }
|
// When searching, look across the whole library (ignore the muscle tab).
|
||||||
guard !searchText.isEmpty else { return byMuscle }
|
guard searchText.isEmpty else {
|
||||||
return byMuscle.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
return exerciseLibrary.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||||
|
}
|
||||||
|
return exerciseLibrary.filter { $0.muscle == selectedMuscle }
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// ── Header ──
|
// ── Header ──
|
||||||
HStack {
|
HStack(spacing: 8) {
|
||||||
Text("Add Exercise")
|
Text("Add Exercise")
|
||||||
.font(AppFonts.sans(17, weight: .bold))
|
.font(AppFonts.sans(17, weight: .bold))
|
||||||
.foregroundColor(AppColors.text(cs))
|
.foregroundColor(AppColors.text(cs))
|
||||||
|
if addedCount > 0 {
|
||||||
|
Text("\(addedCount) added")
|
||||||
|
.font(AppFonts.mono(9.5, weight: .bold))
|
||||||
|
.foregroundColor(AppColors.green)
|
||||||
|
.padding(.horizontal, 7).padding(.vertical, 3)
|
||||||
|
.background(AppColors.greenSoft)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Cancel") { dismiss() }
|
Button(addedCount > 0 ? "Done" : "Cancel") { dismiss() }
|
||||||
.font(AppFonts.sans(13))
|
.font(AppFonts.sans(13, weight: addedCount > 0 ? .bold : .regular))
|
||||||
.foregroundColor(AppColors.text3(cs))
|
.foregroundColor(addedCount > 0 ? AppColors.accent : AppColors.text3(cs))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
.animation(KisaniSpring.snappy, value: addedCount)
|
||||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 12)
|
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 12)
|
||||||
|
|
||||||
Divider().background(AppColors.border(cs))
|
Divider().background(AppColors.border(cs))
|
||||||
@@ -114,7 +137,8 @@ struct ExercisePickerSheet: View {
|
|||||||
let n = customName.trimmingCharacters(in: .whitespaces)
|
let n = customName.trimmingCharacters(in: .whitespaces)
|
||||||
guard !n.isEmpty else { return }
|
guard !n.isEmpty else { return }
|
||||||
addExercise(name: n)
|
addExercise(name: n)
|
||||||
dismiss()
|
customName = ""
|
||||||
|
customFocused = true
|
||||||
} label: {
|
} label: {
|
||||||
Text("Add Custom Exercise")
|
Text("Add Custom Exercise")
|
||||||
.font(AppFonts.sans(13, weight: .bold)).foregroundColor(.white)
|
.font(AppFonts.sans(13, weight: .bold)).foregroundColor(.white)
|
||||||
@@ -134,9 +158,9 @@ struct ExercisePickerSheet: View {
|
|||||||
|
|
||||||
// Library exercises
|
// Library exercises
|
||||||
ForEach(filtered) { template in
|
ForEach(filtered) { template in
|
||||||
ExerciseTemplateRow(template: template) {
|
ExerciseTemplateRow(template: template, added: flashedId == template.id) {
|
||||||
addExercise(name: template.name)
|
addExercise(name: template.name)
|
||||||
dismiss()
|
flash(template.id)
|
||||||
}
|
}
|
||||||
Divider().background(AppColors.border(cs)).padding(.leading, 60)
|
Divider().background(AppColors.border(cs)).padding(.leading, 60)
|
||||||
}
|
}
|
||||||
@@ -188,6 +212,7 @@ struct ExercisePickerSheet: View {
|
|||||||
private struct ExerciseTemplateRow: View {
|
private struct ExerciseTemplateRow: View {
|
||||||
@Environment(\.colorScheme) private var cs
|
@Environment(\.colorScheme) private var cs
|
||||||
let template: ExerciseTemplate
|
let template: ExerciseTemplate
|
||||||
|
var added: Bool = false
|
||||||
let onAdd: () -> Void
|
let onAdd: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -206,9 +231,10 @@ private struct ExerciseTemplateRow: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: added ? "checkmark.circle.fill" : "plus.circle.fill")
|
||||||
.font(.system(size: 18))
|
.font(.system(size: 18))
|
||||||
.foregroundColor(AppColors.accent.opacity(0.7))
|
.foregroundColor(added ? AppColors.green : AppColors.accent.opacity(0.7))
|
||||||
|
.scaleEffect(added ? 1.15 : 1)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|||||||
@@ -22,9 +22,13 @@ final class CalendarStore: ObservableObject {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
authStatus = EKEventStore.authorizationStatus(for: .event)
|
authStatus = EKEventStore.authorizationStatus(for: .event)
|
||||||
hiddenCalendarIDs = Set(UserDefaults.standard.stringArray(forKey: "kisani.cal.hiddenIDs") ?? [])
|
// Migrate legacy prefs from UserDefaults.standard into the App Group (one-time).
|
||||||
localCalendarsEnabled = UserDefaults.standard.object(forKey: "kisani.cal.localEnabled") as? Bool ?? true
|
Self.migrateCalPrefIfNeeded(hiddenIDsKey)
|
||||||
doNotDisturb = UserDefaults.standard.bool(forKey: "kisani.cal.dnd")
|
Self.migrateCalPrefIfNeeded(calEnabledKey)
|
||||||
|
Self.migrateCalPrefIfNeeded(dndKey)
|
||||||
|
hiddenCalendarIDs = Set(UserDefaults.kisani.stringArray(forKey: hiddenIDsKey) ?? [])
|
||||||
|
localCalendarsEnabled = UserDefaults.kisani.object(forKey: calEnabledKey) as? Bool ?? true
|
||||||
|
doNotDisturb = UserDefaults.kisani.bool(forKey: dndKey)
|
||||||
changeToken = NotificationCenter.default.addObserver(
|
changeToken = NotificationCenter.default.addObserver(
|
||||||
forName: .EKEventStoreChanged,
|
forName: .EKEventStoreChanged,
|
||||||
object: ekStore,
|
object: ekStore,
|
||||||
@@ -45,7 +49,10 @@ final class CalendarStore: ObservableObject {
|
|||||||
let current = EKEventStore.authorizationStatus(for: .event)
|
let current = EKEventStore.authorizationStatus(for: .event)
|
||||||
guard current != authStatus else { return }
|
guard current != authStatus else { return }
|
||||||
authStatus = current
|
authStatus = current
|
||||||
if isAuthorized { fetchMonth(containing: fetchedMonth ?? Date()) }
|
if isAuthorized {
|
||||||
|
fetchMonth(containing: fetchedMonth ?? Date())
|
||||||
|
loadLocalCalendars()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -108,6 +115,10 @@ final class CalendarStore: ObservableObject {
|
|||||||
return authStatus == .authorized
|
return authStatus == .authorized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Only `.notDetermined` can show the system prompt; otherwise we must deep-link to Settings.
|
||||||
|
var canRequest: Bool { authStatus == .notDetermined }
|
||||||
|
var isDenied: Bool { authStatus == .denied || authStatus == .restricted }
|
||||||
|
|
||||||
// MARK: - Local Calendar Management
|
// MARK: - Local Calendar Management
|
||||||
|
|
||||||
func loadLocalCalendars() {
|
func loadLocalCalendars() {
|
||||||
@@ -137,12 +148,23 @@ final class CalendarStore: ObservableObject {
|
|||||||
|
|
||||||
func setDoNotDisturb(_ on: Bool) {
|
func setDoNotDisturb(_ on: Bool) {
|
||||||
doNotDisturb = on
|
doNotDisturb = on
|
||||||
UserDefaults.standard.set(on, forKey: dndKey)
|
UserDefaults.kisani.set(on, forKey: dndKey)
|
||||||
|
CloudSyncManager.shared.push(value: on, forKey: dndKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveCalPrefs() {
|
private func saveCalPrefs() {
|
||||||
UserDefaults.standard.set(Array(hiddenCalendarIDs), forKey: hiddenIDsKey)
|
UserDefaults.kisani.set(Array(hiddenCalendarIDs), forKey: hiddenIDsKey)
|
||||||
UserDefaults.standard.set(localCalendarsEnabled, forKey: calEnabledKey)
|
UserDefaults.kisani.set(localCalendarsEnabled, forKey: calEnabledKey)
|
||||||
|
CloudSyncManager.shared.push(value: Array(hiddenCalendarIDs), forKey: hiddenIDsKey)
|
||||||
|
CloudSyncManager.shared.push(value: localCalendarsEnabled, forKey: calEnabledKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func migrateCalPrefIfNeeded(_ key: String) {
|
||||||
|
if UserDefaults.kisani.object(forKey: key) == nil,
|
||||||
|
let legacy = UserDefaults.standard.object(forKey: key) {
|
||||||
|
UserDefaults.kisani.set(legacy, forKey: key)
|
||||||
|
UserDefaults.standard.removeObject(forKey: key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func groupedCalendars() -> [CalendarGroup] {
|
func groupedCalendars() -> [CalendarGroup] {
|
||||||
@@ -890,6 +912,8 @@ struct CalendarConnectSheet: View {
|
|||||||
@ObservedObject var calStore: CalendarStore
|
@ObservedObject var calStore: CalendarStore
|
||||||
@Environment(\.colorScheme) var cs
|
@Environment(\.colorScheme) var cs
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@@ -906,10 +930,19 @@ struct CalendarConnectSheet: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 9))
|
.clipShape(RoundedRectangle(cornerRadius: 9))
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("iPhone Calendar").font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
|
Text("iPhone Calendar").font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
|
||||||
Text("Tap to connect and show events").font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
Text(calStore.isDenied
|
||||||
|
? "Access denied — enable in Settings"
|
||||||
|
: "Tap to connect and show events")
|
||||||
|
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Connect") { calStore.requestAccess() }
|
Button(calStore.isDenied ? "Settings" : "Connect") {
|
||||||
|
if calStore.canRequest {
|
||||||
|
calStore.requestAccess()
|
||||||
|
} else if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||||
|
openURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
.font(AppFonts.mono(9.5, weight: .bold))
|
.font(AppFonts.mono(9.5, weight: .bold))
|
||||||
.foregroundColor(AppColors.accent)
|
.foregroundColor(AppColors.accent)
|
||||||
.padding(.horizontal, 10).padding(.vertical, 5)
|
.padding(.horizontal, 10).padding(.vertical, 5)
|
||||||
@@ -992,6 +1025,10 @@ struct CalendarConnectSheet: View {
|
|||||||
.foregroundColor(AppColors.accent)
|
.foregroundColor(AppColors.accent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear { calStore.refreshStatus() }
|
||||||
|
.onChange(of: scenePhase) { phase in
|
||||||
|
if phase == .active { calStore.refreshStatus() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct OnboardingView: View {
|
|||||||
@State private var heightStr = ""
|
@State private var heightStr = ""
|
||||||
@State private var selectedDays: Set<Int> = []
|
@State private var selectedDays: Set<Int> = []
|
||||||
@State private var calAuthStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .event)
|
@State private var calAuthStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .event)
|
||||||
|
@ObservedObject private var hk = HealthKitManager.shared
|
||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
|
|
||||||
private let ekStore = EKEventStore()
|
private let ekStore = EKEventStore()
|
||||||
@@ -284,6 +285,50 @@ struct OnboardingView: View {
|
|||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.bottom, 28)
|
.padding(.bottom, 28)
|
||||||
|
|
||||||
|
// ══════════════════════════════
|
||||||
|
// MARK: Health
|
||||||
|
// ══════════════════════════════
|
||||||
|
if hk.isAvailable {
|
||||||
|
OnboardingSectionHeader(title: "Health")
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: hk.authorized ? "heart.fill" : "heart")
|
||||||
|
.font(.system(size: 15))
|
||||||
|
.foregroundColor(hk.authorized ? AppColors.green : AppColors.accent)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.background(hk.authorized ? AppColors.greenSoft : AppColors.accentSoft)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
.animation(KisaniSpring.micro, value: hk.authorized)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Apple Health")
|
||||||
|
.font(AppFonts.sans(13, weight: .semibold))
|
||||||
|
.foregroundColor(AppColors.text(cs))
|
||||||
|
Text(hk.authorized
|
||||||
|
? "Steps, calories & workouts on your dashboard"
|
||||||
|
: "Show your activity stats above today's tasks")
|
||||||
|
.font(AppFonts.sans(11))
|
||||||
|
.foregroundColor(AppColors.text3(cs))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Toggle("", isOn: Binding(
|
||||||
|
get: { hk.authorized },
|
||||||
|
set: { on in
|
||||||
|
if on { Task { _ = await hk.requestAuthorization() } }
|
||||||
|
else { hk.disconnect() }
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.labelsHidden()
|
||||||
|
.tint(AppColors.accent)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14).padding(.vertical, 14)
|
||||||
|
.cardStyle()
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.bottom, 28)
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════
|
// ══════════════════════════════
|
||||||
// MARK: Workout
|
// MARK: Workout
|
||||||
// ══════════════════════════════
|
// ══════════════════════════════
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ struct SettingsView: View {
|
|||||||
// Persisted settings
|
// Persisted settings
|
||||||
@AppStorage("appearanceMode") private var appearanceMode = 0
|
@AppStorage("appearanceMode") private var appearanceMode = 0
|
||||||
@AppStorage("soundsOn") private var soundsOn = true
|
@AppStorage("soundsOn") private var soundsOn = true
|
||||||
@AppStorage("healthOn") private var healthOn = false
|
|
||||||
@AppStorage("showMatrixTab") private var showMatrixTab = true
|
@AppStorage("showMatrixTab") private var showMatrixTab = true
|
||||||
@AppStorage("showWorkoutTab") private var showWorkoutTab = true
|
@AppStorage("showWorkoutTab") private var showWorkoutTab = true
|
||||||
@AppStorage("dateFormat") private var dateFormat = 0
|
@AppStorage("dateFormat") private var dateFormat = 0
|
||||||
@@ -116,7 +115,7 @@ struct SettingsView: View {
|
|||||||
SttSection(label: "Fitness") {
|
SttSection(label: "Fitness") {
|
||||||
SttNavRow(icon: "dumbbell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Workout Settings", value: weightLabel) { showWorkoutSettings = true }
|
SttNavRow(icon: "dumbbell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Workout Settings", value: weightLabel) { showWorkoutSettings = true }
|
||||||
SttNavRow(icon: "figure.run", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Rest Timer", value: restLabel) { showRestTimer = true }
|
SttNavRow(icon: "figure.run", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Rest Timer", value: restLabel) { showRestTimer = true }
|
||||||
HealthToggleRow(isOn: $healthOn, isLast: true)
|
HealthToggleRow(isLast: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ACCOUNT ──
|
// ── ACCOUNT ──
|
||||||
@@ -176,7 +175,6 @@ struct SettingsView: View {
|
|||||||
// MARK: - Health Toggle Row (requests auth when turned on)
|
// MARK: - Health Toggle Row (requests auth when turned on)
|
||||||
private struct HealthToggleRow: View {
|
private struct HealthToggleRow: View {
|
||||||
@Environment(\.colorScheme) private var cs
|
@Environment(\.colorScheme) private var cs
|
||||||
@Binding var isOn: Bool
|
|
||||||
var isLast: Bool = false
|
var isLast: Bool = false
|
||||||
@ObservedObject private var hk = HealthKitManager.shared
|
@ObservedObject private var hk = HealthKitManager.shared
|
||||||
|
|
||||||
@@ -193,25 +191,26 @@ private struct HealthToggleRow: View {
|
|||||||
Text("Health Integration")
|
Text("Health Integration")
|
||||||
.font(AppFonts.sans(13))
|
.font(AppFonts.sans(13))
|
||||||
.foregroundColor(AppColors.text(cs))
|
.foregroundColor(AppColors.text(cs))
|
||||||
if isOn && hk.authorized {
|
if hk.authorized {
|
||||||
Text("Connected")
|
Text("Connected")
|
||||||
.font(AppFonts.mono(9))
|
.font(AppFonts.mono(9))
|
||||||
.foregroundColor(AppColors.green)
|
.foregroundColor(AppColors.green)
|
||||||
} else if isOn && !hk.isAvailable {
|
} else if !hk.isAvailable {
|
||||||
Text("Not available on this device")
|
Text("Not available on this device")
|
||||||
.font(AppFonts.mono(9))
|
.font(AppFonts.mono(9))
|
||||||
.foregroundColor(AppColors.text3(cs))
|
.foregroundColor(AppColors.text3(cs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Toggle("", isOn: $isOn)
|
Toggle("", isOn: Binding(
|
||||||
|
get: { hk.authorized },
|
||||||
|
set: { on in
|
||||||
|
if on { Task { _ = await hk.requestAuthorization() } }
|
||||||
|
else { hk.disconnect() }
|
||||||
|
}
|
||||||
|
))
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.tint(AppColors.green)
|
.tint(AppColors.green)
|
||||||
.onChange(of: isOn) { enabled in
|
|
||||||
if enabled {
|
|
||||||
Task { await HealthKitManager.shared.requestAuthorization() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 14).padding(.vertical, 11)
|
.padding(.horizontal, 14).padding(.vertical, 11)
|
||||||
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
|
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
|
||||||
@@ -1031,7 +1030,6 @@ private struct ProfileStatsSheet: View {
|
|||||||
@EnvironmentObject var taskVM: TaskViewModel
|
@EnvironmentObject var taskVM: TaskViewModel
|
||||||
@EnvironmentObject var workoutVM: WorkoutViewModel
|
@EnvironmentObject var workoutVM: WorkoutViewModel
|
||||||
@AppStorage("streakGoal") private var streakGoal = 5
|
@AppStorage("streakGoal") private var streakGoal = 5
|
||||||
@AppStorage("healthOn") private var healthOn = false
|
|
||||||
@ObservedObject private var hk = HealthKitManager.shared
|
@ObservedObject private var hk = HealthKitManager.shared
|
||||||
@ObservedObject private var auth = AuthManager.shared
|
@ObservedObject private var auth = AuthManager.shared
|
||||||
|
|
||||||
@@ -1225,7 +1223,7 @@ private struct ProfileStatsSheet: View {
|
|||||||
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
|
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
|
||||||
|
|
||||||
// ── Health card ──
|
// ── Health card ──
|
||||||
if healthOn && hk.authorized {
|
if hk.authorized {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text("HEALTH")
|
Text("HEALTH")
|
||||||
|
|||||||
@@ -1039,11 +1039,19 @@ struct ProgramDetailSheet: View {
|
|||||||
ForEach(prog.sections) { section in
|
ForEach(prog.sections) { section in
|
||||||
Section {
|
Section {
|
||||||
if section.exercises.isEmpty {
|
if section.exercises.isEmpty {
|
||||||
Text("No exercises — tap + to add")
|
Text("No exercises — tap + or drag one here")
|
||||||
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||||||
.frame(maxWidth: .infinity).padding(.vertical, 10)
|
.frame(maxWidth: .infinity).padding(.vertical, 10)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.dropDestination(for: String.self) { items, _ in
|
||||||
|
guard let s = items.first, let id = UUID(uuidString: s) else { return false }
|
||||||
|
withAnimation(KisaniSpring.snappy) {
|
||||||
|
vm.moveExercise(programId: programId, exerciseId: id,
|
||||||
|
toSection: section.id, before: nil)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ForEach(section.exercises) { exercise in
|
ForEach(section.exercises) { exercise in
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
@@ -1065,6 +1073,15 @@ struct ProgramDetailSheet: View {
|
|||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.listRowBackground(AppColors.surface(cs))
|
.listRowBackground(AppColors.surface(cs))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.draggable(exercise.id.uuidString)
|
||||||
|
.dropDestination(for: String.self) { items, _ in
|
||||||
|
guard let s = items.first, let id = UUID(uuidString: s) else { return false }
|
||||||
|
withAnimation(KisaniSpring.snappy) {
|
||||||
|
vm.moveExercise(programId: programId, exerciseId: id,
|
||||||
|
toSection: section.id, before: exercise.id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
.swipeActions(edge: .trailing) {
|
.swipeActions(edge: .trailing) {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
vm.deleteExercise(programId: programId, sectionId: section.id, exerciseId: exercise.id)
|
vm.deleteExercise(programId: programId, sectionId: section.id, exerciseId: exercise.id)
|
||||||
@@ -1093,6 +1110,14 @@ struct ProgramDetailSheet: View {
|
|||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
|
.dropDestination(for: String.self) { items, _ in
|
||||||
|
guard let s = items.first, let id = UUID(uuidString: s) else { return false }
|
||||||
|
withAnimation(KisaniSpring.snappy) {
|
||||||
|
vm.moveExercise(programId: programId, exerciseId: id,
|
||||||
|
toSection: section.id, before: nil)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
HStack {
|
HStack {
|
||||||
|
|||||||
153
KisaniCalWidgets/EventCountdownWidget.swift
Normal file
153
KisaniCalWidgets/EventCountdownWidget.swift
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
|
// MARK: - Configuration Intent
|
||||||
|
//
|
||||||
|
// Each widget instance stores its own event. The user sets a name, the day the
|
||||||
|
// countdown starts ("Counting from" — defaults to the day they add the widget,
|
||||||
|
// which acts as the creation anchor) and the event date. Because these values are
|
||||||
|
// persisted per-instance by WidgetKit, the widget is fully self-contained and does
|
||||||
|
// not depend on App Group data sharing.
|
||||||
|
|
||||||
|
struct EventConfigIntent: WidgetConfigurationIntent {
|
||||||
|
static var title: LocalizedStringResource = "Event Countdown"
|
||||||
|
static var description = IntentDescription("Count down to any upcoming event with a dot grid.")
|
||||||
|
|
||||||
|
// Pick one of your existing events (tasks with a due date). When set, it
|
||||||
|
// overrides the manual name/date fields below.
|
||||||
|
@Parameter(title: "Track event")
|
||||||
|
var event: EventEntity?
|
||||||
|
|
||||||
|
@Parameter(title: "Or event name", default: "My Event")
|
||||||
|
var eventName: String
|
||||||
|
|
||||||
|
@Parameter(title: "Event date", default: Date.now)
|
||||||
|
var targetDate: Date
|
||||||
|
|
||||||
|
@Parameter(title: "Counting from", default: Date.now)
|
||||||
|
var startDate: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Event picker (reads your tasks from the App Group)
|
||||||
|
|
||||||
|
struct EventEntity: AppEntity {
|
||||||
|
let id: String // task UUID string
|
||||||
|
let title: String
|
||||||
|
let dueDate: Date?
|
||||||
|
|
||||||
|
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Event"
|
||||||
|
static var defaultQuery = EventQuery()
|
||||||
|
|
||||||
|
var displayRepresentation: DisplayRepresentation {
|
||||||
|
if let d = dueDate {
|
||||||
|
let f = DateFormatter(); f.dateStyle = .medium
|
||||||
|
return DisplayRepresentation(title: "\(title)", subtitle: "\(f.string(from: d))")
|
||||||
|
}
|
||||||
|
return DisplayRepresentation(title: "\(title)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EventQuery: EntityQuery {
|
||||||
|
func entities(for identifiers: [String]) async throws -> [EventEntity] {
|
||||||
|
allEvents().filter { identifiers.contains($0.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func suggestedEntities() async throws -> [EventEntity] {
|
||||||
|
allEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func allEvents() -> [EventEntity] {
|
||||||
|
loadWidgetTasks()
|
||||||
|
.filter { !$0.isComplete && $0.dueDate != nil }
|
||||||
|
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
||||||
|
.map { EventEntity(id: $0.id.uuidString, title: $0.title, dueDate: $0.dueDate) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Entry
|
||||||
|
|
||||||
|
struct EventEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let eventName: String
|
||||||
|
let start: Date
|
||||||
|
let target: Date
|
||||||
|
|
||||||
|
/// Fraction of the countdown window that has elapsed (0…1).
|
||||||
|
var progress: Double {
|
||||||
|
let span = target.timeIntervalSince(start)
|
||||||
|
guard span > 0 else { return target <= date ? 1 : 0 }
|
||||||
|
return min(1, max(0, date.timeIntervalSince(start) / span))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whole days from start to target (the number of dots in the grid).
|
||||||
|
var totalDays: Int {
|
||||||
|
let cal = Calendar.current
|
||||||
|
let d = cal.dateComponents([.day],
|
||||||
|
from: cal.startOfDay(for: start),
|
||||||
|
to: cal.startOfDay(for: target)).day ?? 0
|
||||||
|
return max(1, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Days remaining until the event (never negative).
|
||||||
|
var daysLeft: Int {
|
||||||
|
let cal = Calendar.current
|
||||||
|
let d = cal.dateComponents([.day],
|
||||||
|
from: cal.startOfDay(for: date),
|
||||||
|
to: cal.startOfDay(for: target)).day ?? 0
|
||||||
|
return max(0, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Provider
|
||||||
|
|
||||||
|
struct EventProvider: AppIntentTimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> EventEntry {
|
||||||
|
EventEntry(date: .now,
|
||||||
|
eventName: "Mum's Birthday",
|
||||||
|
start: Date().addingTimeInterval(-86400 * 320),
|
||||||
|
target: Date().addingTimeInterval(86400 * 60))
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: EventConfigIntent, in context: Context) async -> EventEntry {
|
||||||
|
entry(for: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: EventConfigIntent, in context: Context) async -> Timeline<EventEntry> {
|
||||||
|
let entry = entry(for: configuration)
|
||||||
|
// Recompute at the next midnight so the grid advances one dot per day.
|
||||||
|
let nextMidnight = Calendar.current.startOfDay(for: Date().addingTimeInterval(86400))
|
||||||
|
return Timeline(entries: [entry], policy: .after(nextMidnight))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func entry(for config: EventConfigIntent) -> EventEntry {
|
||||||
|
let name: String
|
||||||
|
let target: Date
|
||||||
|
if let ev = config.event, let due = ev.dueDate {
|
||||||
|
// A picked event supplies its own name + date.
|
||||||
|
name = ev.title
|
||||||
|
target = due
|
||||||
|
} else {
|
||||||
|
name = config.eventName.isEmpty ? "My Event" : config.eventName
|
||||||
|
target = config.targetDate
|
||||||
|
}
|
||||||
|
// Never let the start sit after the target.
|
||||||
|
let start = min(config.startDate, target)
|
||||||
|
return EventEntry(date: .now, eventName: name, start: start, target: target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Widget
|
||||||
|
|
||||||
|
struct EventCountdownWidget: Widget {
|
||||||
|
let kind = "KisaniEventCountdown"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind, intent: EventConfigIntent.self, provider: EventProvider()) { entry in
|
||||||
|
EventCountdownView(entry: entry)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Event Countdown")
|
||||||
|
.description("Track the days until any upcoming event with a dot grid.")
|
||||||
|
.supportedFamilies([.systemMedium, .systemSmall])
|
||||||
|
}
|
||||||
|
}
|
||||||
29
KisaniCalWidgets/Info.plist
Normal file
29
KisaniCalWidgets/Info.plist
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Kisani Cal Widgets</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>XPC!</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
10
KisaniCalWidgets/KisaniCalWidgets.entitlements
Normal file
10
KisaniCalWidgets/KisaniCalWidgets.entitlements
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.kutesir.KisaniCal</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
114
KisaniCalWidgets/KisaniCalWidgets.swift
Normal file
114
KisaniCalWidgets/KisaniCalWidgets.swift
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
|
// MARK: - Shared Entry
|
||||||
|
|
||||||
|
struct KisaniEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let task: WidgetTask?
|
||||||
|
let allTasks: [WidgetTask]
|
||||||
|
let streak: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Shared Provider
|
||||||
|
|
||||||
|
struct KisaniProvider: TimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> KisaniEntry {
|
||||||
|
let sample = WidgetTask(id: UUID(), title: "Mums Birthday", dueDate: Date().addingTimeInterval(86400 * 60),
|
||||||
|
isComplete: false, quadrant: "Urgent + Important", category: "birthday")
|
||||||
|
return KisaniEntry(date: .now, task: sample, allTasks: [sample], streak: 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSnapshot(in context: Context, completion: @escaping (KisaniEntry) -> Void) {
|
||||||
|
let tasks = loadWidgetTasks()
|
||||||
|
completion(KisaniEntry(date: .now, task: nextDueTask(from: tasks), allTasks: tasks, streak: loadWorkoutStreak()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeline(in context: Context, completion: @escaping (Timeline<KisaniEntry>) -> Void) {
|
||||||
|
let tasks = loadWidgetTasks()
|
||||||
|
let entry = KisaniEntry(date: .now, task: nextDueTask(from: tasks), allTasks: tasks, streak: loadWorkoutStreak())
|
||||||
|
// Refresh every 30 minutes or when app triggers WidgetCenter.reloadAllTimelines()
|
||||||
|
let next = Calendar.current.date(byAdding: .minute, value: 30, to: .now)!
|
||||||
|
completion(Timeline(entries: [entry], policy: .after(next)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Day Progress Widget ("Day done %")
|
||||||
|
|
||||||
|
struct DayProgressWidget: Widget {
|
||||||
|
let kind = "KisaniDayProgress"
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in
|
||||||
|
ProgressDotView(period: .day, accent: Color(red: 0.48, green: 0.87, blue: 0.80))
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Day Progress")
|
||||||
|
.description("How much of today has elapsed, as a dot grid.")
|
||||||
|
.supportedFamilies([.systemSmall, .systemMedium])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tasks Done Today Widget ("Tasks complete %")
|
||||||
|
|
||||||
|
struct TasksCompleteWidget: Widget {
|
||||||
|
let kind = "KisaniTasksComplete"
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in
|
||||||
|
TasksCompleteView()
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Tasks Done Today")
|
||||||
|
.description("Percentage of today's tasks you've completed.")
|
||||||
|
.supportedFamilies([.systemSmall, .systemMedium])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lock Screen Widgets
|
||||||
|
|
||||||
|
struct LockScreenWidget: Widget {
|
||||||
|
let kind = "KisaniLockScreen"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
|
||||||
|
if let task = entry.task {
|
||||||
|
LockScreenRectView(task: task)
|
||||||
|
} else {
|
||||||
|
LockScreenRectView(task: WidgetTask(id: UUID(), title: "No tasks",
|
||||||
|
dueDate: nil, isComplete: false, quadrant: "Urgent + Important", category: "reminder"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Task (Lock Screen)")
|
||||||
|
.description("Glass countdown on your lock screen.")
|
||||||
|
.supportedFamilies([.accessoryRectangular])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LockScreenCircularWidget: Widget {
|
||||||
|
let kind = "KisaniLockCircle"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
|
||||||
|
if let task = entry.task {
|
||||||
|
LockScreenCircularView(task: task)
|
||||||
|
} else {
|
||||||
|
LockScreenCircularView(task: WidgetTask(id: UUID(), title: "–",
|
||||||
|
dueDate: nil, isComplete: false, quadrant: "Urgent + Important", category: "reminder"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Days Left (Lock Screen)")
|
||||||
|
.description("Circular countdown on your lock screen.")
|
||||||
|
.supportedFamilies([.accessoryCircular])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Widget Bundle
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct KisaniWidgetBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
EventCountdownWidget()
|
||||||
|
DayProgressWidget()
|
||||||
|
TasksCompleteWidget()
|
||||||
|
LockScreenWidget()
|
||||||
|
LockScreenCircularWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
120
KisaniCalWidgets/WidgetData.swift
Normal file
120
KisaniCalWidgets/WidgetData.swift
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// Minimal task model shared between widget and main app via App Group
|
||||||
|
struct WidgetTask: Codable, Identifiable {
|
||||||
|
let id: UUID
|
||||||
|
let title: String
|
||||||
|
let dueDate: Date?
|
||||||
|
let isComplete: Bool
|
||||||
|
let quadrant: String // raw value of Quadrant enum
|
||||||
|
let category: String
|
||||||
|
|
||||||
|
// Days remaining until due date
|
||||||
|
var daysLeft: Int? {
|
||||||
|
guard let due = dueDate else { return nil }
|
||||||
|
let d = Calendar.current.dateComponents([.day], from: Calendar.current.startOfDay(for: .now), to: due).day ?? 0
|
||||||
|
return max(0, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hours remaining (for sub-day precision)
|
||||||
|
var hoursLeft: Int? {
|
||||||
|
guard let due = dueDate else { return nil }
|
||||||
|
let h = Calendar.current.dateComponents([.hour], from: .now, to: due).hour ?? 0
|
||||||
|
return max(0, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeLeftLabel: String {
|
||||||
|
guard dueDate != nil else { return "" }
|
||||||
|
if isComplete { return "Done" }
|
||||||
|
let days = daysLeft ?? 0
|
||||||
|
if days == 0 {
|
||||||
|
let hrs = hoursLeft ?? 0
|
||||||
|
return hrs <= 0 ? "Due now" : "\(hrs)hr left"
|
||||||
|
}
|
||||||
|
if days < 30 { return "\(days)d left" }
|
||||||
|
let months = days / 30
|
||||||
|
let remDays = days % 30
|
||||||
|
if remDays == 0 { return "\(months)mth left" }
|
||||||
|
return "\(months)mth \(remDays)d..."
|
||||||
|
}
|
||||||
|
|
||||||
|
var accentColor: Color {
|
||||||
|
switch quadrant {
|
||||||
|
case "Urgent + Important": return Color(red: 0.96, green: 0.42, blue: 0.20)
|
||||||
|
case "Schedule It": return Color(red: 0.95, green: 0.78, blue: 0.10)
|
||||||
|
case "Delegate": return Color(red: 0.24, green: 0.58, blue: 0.95)
|
||||||
|
case "Eliminate": return Color(red: 0.20, green: 0.78, blue: 0.50)
|
||||||
|
default: return Color(red: 0.96, green: 0.42, blue: 0.20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load tasks from the shared App Group UserDefaults
|
||||||
|
func loadWidgetTasks() -> [WidgetTask] {
|
||||||
|
let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared"
|
||||||
|
let key = "kisani.tasks.v2.\(uid)"
|
||||||
|
guard let data = UserDefaults.kisani.data(forKey: key),
|
||||||
|
let raw = try? JSONDecoder().decode([RawTask].self, from: data)
|
||||||
|
else { return [] }
|
||||||
|
return raw.map {
|
||||||
|
WidgetTask(id: $0.id, title: $0.title, dueDate: $0.dueDate,
|
||||||
|
isComplete: $0.isComplete, quadrant: $0.quadrant, category: $0.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partial decode of TaskItem — only the fields widgets need
|
||||||
|
private struct RawTask: Codable {
|
||||||
|
let id: UUID
|
||||||
|
let title: String
|
||||||
|
let dueDate: Date?
|
||||||
|
var isComplete: Bool = false
|
||||||
|
var quadrant: String = "Urgent + Important"
|
||||||
|
var category: String = "reminder"
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, title, dueDate, isComplete, quadrant, category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next upcoming task with a due date (for default widget display)
|
||||||
|
func nextDueTask(from tasks: [WidgetTask]) -> WidgetTask? {
|
||||||
|
tasks
|
||||||
|
.filter { !$0.isComplete && $0.dueDate != nil }
|
||||||
|
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
||||||
|
.first
|
||||||
|
}
|
||||||
|
|
||||||
|
// Today's task completion: (done, total) for tasks due today.
|
||||||
|
func todayTaskCompletion() -> (done: Int, total: Int) {
|
||||||
|
let cal = Calendar.current
|
||||||
|
let start = cal.startOfDay(for: Date())
|
||||||
|
let end = cal.date(byAdding: .day, value: 1, to: start)!
|
||||||
|
let tasks = loadWidgetTasks()
|
||||||
|
func dueToday(_ t: WidgetTask) -> Bool {
|
||||||
|
guard let due = t.dueDate else { return false }
|
||||||
|
return due >= start && due < end
|
||||||
|
}
|
||||||
|
let active = tasks.filter { !$0.isComplete && dueToday($0) }
|
||||||
|
let completed = tasks.filter { $0.isComplete && dueToday($0) }
|
||||||
|
return (completed.count, active.count + completed.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workout streak count
|
||||||
|
func loadWorkoutStreak() -> Int {
|
||||||
|
let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared"
|
||||||
|
let key = "kisani.workout.completedDates.\(uid)"
|
||||||
|
let dates = UserDefaults.kisani.stringArray(forKey: key) ?? []
|
||||||
|
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
|
||||||
|
let cal = Calendar.current
|
||||||
|
let dateSet = Set(dates)
|
||||||
|
var streak = 0
|
||||||
|
var check = cal.startOfDay(for: Date())
|
||||||
|
if !dateSet.contains(fmt.string(from: check)) {
|
||||||
|
check = cal.date(byAdding: .day, value: -1, to: check)!
|
||||||
|
}
|
||||||
|
while dateSet.contains(fmt.string(from: check)) {
|
||||||
|
streak += 1
|
||||||
|
check = cal.date(byAdding: .day, value: -1, to: check)!
|
||||||
|
}
|
||||||
|
return streak
|
||||||
|
}
|
||||||
289
KisaniCalWidgets/WidgetViews.swift
Normal file
289
KisaniCalWidgets/WidgetViews.swift
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
// MARK: - Dot-grid progress (day / week / month / year)
|
||||||
|
|
||||||
|
enum TimePeriod { case day, week, month, year }
|
||||||
|
|
||||||
|
struct ProgressDotView: View {
|
||||||
|
let period: TimePeriod
|
||||||
|
let accent: Color
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
private var label: String {
|
||||||
|
switch period {
|
||||||
|
case .day: let f = DateFormatter(); f.dateFormat = "EEEE d"; return f.string(from: Date())
|
||||||
|
case .week: return "This week"
|
||||||
|
case .month: let f = DateFormatter(); f.dateFormat = "MMMM"; return f.string(from: Date())
|
||||||
|
case .year: let f = DateFormatter(); f.dateFormat = "yyyy"; return f.string(from: Date())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var progress: Double {
|
||||||
|
let cal = Calendar.current; let now = Date()
|
||||||
|
switch period {
|
||||||
|
case .day:
|
||||||
|
let sod = cal.startOfDay(for: now)
|
||||||
|
return (now.timeIntervalSince(sod)) / 86400
|
||||||
|
case .week:
|
||||||
|
let comps = cal.dateComponents([.weekday], from: now)
|
||||||
|
let wd = (comps.weekday ?? 1) - 1
|
||||||
|
let secIntoDay = now.timeIntervalSince(cal.startOfDay(for: now))
|
||||||
|
return (Double(wd) * 86400 + secIntoDay) / (7 * 86400)
|
||||||
|
case .month:
|
||||||
|
let range = cal.range(of: .day, in: .month, for: now)!
|
||||||
|
let day = cal.component(.day, from: now) - 1
|
||||||
|
let secIntoDay = now.timeIntervalSince(cal.startOfDay(for: now))
|
||||||
|
return (Double(day) * 86400 + secIntoDay) / (Double(range.count) * 86400)
|
||||||
|
case .year:
|
||||||
|
let start = cal.date(from: cal.dateComponents([.year], from: now))!
|
||||||
|
let end = cal.date(byAdding: .year, value: 1, to: start)!
|
||||||
|
return now.timeIntervalSince(start) / end.timeIntervalSince(start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subLabel: String {
|
||||||
|
let pct = Int(progress * 100)
|
||||||
|
return "\(pct)%"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let isMedium = family == .systemMedium
|
||||||
|
VStack(alignment: .leading, spacing: isMedium ? 10 : 6) {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
Text(label)
|
||||||
|
.font(.system(size: isMedium ? 22 : 14, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer()
|
||||||
|
Text(subLabel)
|
||||||
|
.font(.system(size: isMedium ? 22 : 13, weight: .bold))
|
||||||
|
.foregroundColor(Color(white: 0.5))
|
||||||
|
}
|
||||||
|
DotGrid(total: 100, filled: Int(progress * 100), color: accent)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, isMedium ? 4 : 0)
|
||||||
|
.containerBackground(.black, for: .widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tasks completed today
|
||||||
|
|
||||||
|
struct TasksCompleteView: View {
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
private let teal = Color(red: 0.48, green: 0.87, blue: 0.80)
|
||||||
|
|
||||||
|
private var stats: (done: Int, total: Int) { todayTaskCompletion() }
|
||||||
|
private var percent: Int {
|
||||||
|
let s = stats
|
||||||
|
guard s.total > 0 else { return 0 }
|
||||||
|
return Int((Double(s.done) / Double(s.total) * 100).rounded())
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let s = stats
|
||||||
|
let isMedium = family == .systemMedium
|
||||||
|
VStack(alignment: .leading, spacing: isMedium ? 10 : 6) {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
Text("Today's Tasks")
|
||||||
|
.font(.system(size: isMedium ? 22 : 14, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer()
|
||||||
|
Text("\(percent)%")
|
||||||
|
.font(.system(size: isMedium ? 22 : 13, weight: .bold))
|
||||||
|
.foregroundColor(Color(white: 0.5))
|
||||||
|
}
|
||||||
|
if s.total == 0 {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Text("No tasks due today")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(Color(white: 0.5))
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
} else {
|
||||||
|
Text("\(s.done) of \(s.total) done")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(teal)
|
||||||
|
Spacer(minLength: 2)
|
||||||
|
DotGrid(total: s.total, filled: s.done, color: teal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, isMedium ? 4 : 0)
|
||||||
|
.containerBackground(.black, for: .widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Circular dot grid (event countdown motif)
|
||||||
|
|
||||||
|
struct DotGrid: View {
|
||||||
|
let total: Int
|
||||||
|
let filled: Int
|
||||||
|
let color: Color
|
||||||
|
var gap: CGFloat = 3
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geo in
|
||||||
|
let l = layout(for: total, in: geo.size)
|
||||||
|
VStack(spacing: gap) {
|
||||||
|
ForEach(0..<l.rows, id: \.self) { row in
|
||||||
|
HStack(spacing: gap) {
|
||||||
|
ForEach(0..<l.cols, id: \.self) { col in
|
||||||
|
let idx = row * l.cols + col
|
||||||
|
if idx < total {
|
||||||
|
dot(filled: idx < filled, size: l.size)
|
||||||
|
} else {
|
||||||
|
Color.clear.frame(width: l.size, height: l.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func dot(filled: Bool, size: CGFloat) -> some View {
|
||||||
|
if filled {
|
||||||
|
Circle().fill(color).frame(width: size, height: size)
|
||||||
|
} else {
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(color.opacity(0.55), lineWidth: max(0.8, size * 0.13))
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick the column count that yields the largest uniform circle that still fits
|
||||||
|
/// every dot inside the available area — an even grid that fills the widget.
|
||||||
|
private func layout(for count: Int, in size: CGSize) -> (cols: Int, rows: Int, size: CGFloat) {
|
||||||
|
guard count > 0, size.width > 1, size.height > 1 else { return (1, 1, 6) }
|
||||||
|
var best = (cols: 1, rows: count, size: CGFloat(0))
|
||||||
|
for cols in 1...count {
|
||||||
|
let rows = Int(ceil(Double(count) / Double(cols)))
|
||||||
|
let w = (size.width - gap * CGFloat(cols - 1)) / CGFloat(cols)
|
||||||
|
let h = (size.height - gap * CGFloat(rows - 1)) / CGFloat(rows)
|
||||||
|
let s = min(w, h)
|
||||||
|
if s > best.size { best = (cols, rows, s) }
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Event Countdown Widget (small + medium)
|
||||||
|
|
||||||
|
struct EventCountdownView: View {
|
||||||
|
let entry: EventEntry
|
||||||
|
@Environment(\.widgetFamily) var family
|
||||||
|
|
||||||
|
private let teal = Color(red: 0.48, green: 0.87, blue: 0.80)
|
||||||
|
|
||||||
|
private var percent: Int { Int((entry.progress * 100).rounded()) }
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if family == .systemSmall {
|
||||||
|
small
|
||||||
|
} else {
|
||||||
|
medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.black, for: .widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wide layout that mirrors the reference: title + percent, then a dot grid.
|
||||||
|
private var medium: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
Text(LocalizedStringKey(entry.eventName))
|
||||||
|
.font(.system(size: 22, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer()
|
||||||
|
Text("\(percent)%")
|
||||||
|
.font(.system(size: 22, weight: .bold))
|
||||||
|
.foregroundColor(Color(white: 0.5))
|
||||||
|
}
|
||||||
|
DotGrid(total: entry.totalDays,
|
||||||
|
filled: Int(entry.progress * Double(entry.totalDays)),
|
||||||
|
color: teal)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var small: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text(LocalizedStringKey(entry.eventName))
|
||||||
|
.font(.system(size: 14, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(2)
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||||
|
Text("\(entry.daysLeft)")
|
||||||
|
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||||
|
.foregroundColor(teal)
|
||||||
|
Text(entry.daysLeft == 1 ? "day" : "days")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(Color(white: 0.5))
|
||||||
|
}
|
||||||
|
Spacer(minLength: 2)
|
||||||
|
DotGrid(total: entry.totalDays,
|
||||||
|
filled: Int(entry.progress * Double(entry.totalDays)),
|
||||||
|
color: teal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lock Screen Widget (accessoryRectangular)
|
||||||
|
|
||||||
|
struct LockScreenRectView: View {
|
||||||
|
let task: WidgetTask
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(task.title)
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(task.timeLeftLabel)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
if !task.isComplete, let days = task.daysLeft, days <= 365 {
|
||||||
|
ProgressView(value: Double(365 - days), total: 365)
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.frame(width: 22, height: 22)
|
||||||
|
.tint(.white)
|
||||||
|
} else if task.isComplete {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.thinMaterial, for: .widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lock Screen Circular
|
||||||
|
|
||||||
|
struct LockScreenCircularView: View {
|
||||||
|
let task: WidgetTask
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
if !task.isComplete, let days = task.daysLeft, days <= 365 {
|
||||||
|
ProgressView(value: Double(365 - days), total: 365)
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.tint(.white)
|
||||||
|
}
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if let days = task.daysLeft {
|
||||||
|
Text("\(days)").font(.system(size: 14, weight: .bold))
|
||||||
|
Text("days").font(.system(size: 7))
|
||||||
|
} else {
|
||||||
|
Image(systemName: "checkmark").font(.system(size: 14, weight: .bold))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.thinMaterial, for: .widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
37
project.yml
37
project.yml
@@ -6,6 +6,12 @@ options:
|
|||||||
xcodeVersion: "15.0"
|
xcodeVersion: "15.0"
|
||||||
createIntermediateGroups: true
|
createIntermediateGroups: true
|
||||||
|
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
DEVELOPMENT_TEAM: K8BLMMR883
|
||||||
|
MARKETING_VERSION: "1.0"
|
||||||
|
CURRENT_PROJECT_VERSION: "1"
|
||||||
|
|
||||||
schemes:
|
schemes:
|
||||||
KisaniCal:
|
KisaniCal:
|
||||||
build:
|
build:
|
||||||
@@ -29,6 +35,9 @@ targets:
|
|||||||
deploymentTarget: "16.0"
|
deploymentTarget: "16.0"
|
||||||
sources:
|
sources:
|
||||||
- path: KisaniCal
|
- path: KisaniCal
|
||||||
|
dependencies:
|
||||||
|
- target: KisaniCalWidgets
|
||||||
|
embed: true
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
SWIFT_VERSION: 5.9
|
SWIFT_VERSION: 5.9
|
||||||
@@ -43,6 +52,34 @@ targets:
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||||
CODE_SIGN_STYLE: Automatic
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
CODE_SIGN_ENTITLEMENTS: KisaniCal/KisaniCal.entitlements
|
||||||
ENABLE_PREVIEWS: YES
|
ENABLE_PREVIEWS: YES
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
|
INFOPLIST_KEY_NSCalendarsUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
|
||||||
|
INFOPLIST_KEY_NSHealthShareUsageDescription: "Kisani Cal reads your steps, energy, heart rate, and workouts to show health stats on your dashboard."
|
||||||
|
INFOPLIST_KEY_NSHealthUpdateUsageDescription: "Kisani Cal saves workouts you complete to the Health app."
|
||||||
|
|
||||||
|
KisaniCalWidgets:
|
||||||
|
type: app-extension
|
||||||
|
platform: iOS
|
||||||
|
deploymentTarget: "18.0"
|
||||||
|
sources:
|
||||||
|
- path: KisaniCalWidgets
|
||||||
|
- path: KisaniCal/Managers/AppGroup.swift
|
||||||
|
info:
|
||||||
|
path: KisaniCalWidgets/Info.plist
|
||||||
|
properties:
|
||||||
|
CFBundleDisplayName: "Kisani Cal Widgets"
|
||||||
|
CFBundleShortVersionString: "$(MARKETING_VERSION)"
|
||||||
|
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
|
||||||
|
NSExtension:
|
||||||
|
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
SWIFT_VERSION: 5.9
|
||||||
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
GENERATE_INFOPLIST_FILE: NO
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: com.kutesir.KisaniCal.KisaniCalWidgets
|
||||||
|
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
|
||||||
|
APPLICATION_EXTENSION_API_ONLY: YES
|
||||||
|
CODE_SIGN_ENTITLEMENTS: KisaniCalWidgets/KisaniCalWidgets.entitlements
|
||||||
|
|||||||
Reference in New Issue
Block a user