Add unified task menu, Live Activity, and calendar event actions

Task context menu (new shared TaskMenuItems) consistent across Today,
Matrix, and Calendar: Pin · Date · Move · Priority · Tags · Add to Live
Activity · Delete. Adds TaskViewModel.setPriority.

Live Activity (ActivityKit):
- TaskActivityAttributes shared between app and widget targets
- LiveActivityManager (start/update/end/toggle, iOS 16.1+)
- TaskLiveActivity widget: lock-screen banner + Dynamic Island
- NSSupportsLiveActivities via project.yml; project regenerated

Calendar events (non-subscription, writable only):
- Edit via system EKEventEditViewController (EventEditView wrapper)
- Delete via new CalendarStore.deleteEvent

Also sweeps in prior in-progress edits already present in the working
tree (AuthManager, NotificationManager, TaskItem, task views) and the
updated ISSUES.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-06 19:29:25 +03:00
parent a92e9d4c09
commit d0d982f4f5
15 changed files with 525 additions and 111 deletions

View File

@@ -12,7 +12,9 @@
12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.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 */; };
205846F9651EDCE0D8207358 /* TaskLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */; };
2CD2313EDC4BFCE6E89C6787 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.swift */; }; 2CD2313EDC4BFCE6E89C6787 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.swift */; };
32C63D81925FBFE51CAE1FB7 /* TaskActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEB07EB89E89383C32ADB34 /* TaskActivityAttributes.swift */; };
3AD7F62D231DCFFFFCB31649 /* TutorialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A27D42E141DC056D32C1A3 /* TutorialManager.swift */; }; 3AD7F62D231DCFFFFCB31649 /* TutorialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A27D42E141DC056D32C1A3 /* TutorialManager.swift */; };
3B57EA3600AFE975850DF39A /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89550F2CD19B950CCC6AD37F /* AuthManager.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 */; };
@@ -23,10 +25,13 @@
5EF4A5B6CE91ADA0CCF72D0D /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.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 */; };
703E068364256D6F3F867961 /* ISSUES.md in Resources */ = {isa = PBXBuildFile; fileRef = 92824ED40ECD41EFD4F78BEC /* ISSUES.md */; };
754D3DE3CEB998E36E585A61 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0506183945D16EC443A69651 /* LiveActivityManager.swift */; };
79FC1DF6762C6F02D01AB643 /* KisaniCalWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 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 */; }; 8C9567A3DE3F63A1ECAE84D5 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3D5289C5F93484E22DEB63 /* TutorialView.swift */; };
8DA396DACE99DC4B7B4A460E /* TaskActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEB07EB89E89383C32ADB34 /* TaskActivityAttributes.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 */; };
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */; }; A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */; };
@@ -40,6 +45,7 @@
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 */; };
ECEAA5CF7309E5993D12B571 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD014B7E3E30A34E18696A0 /* AuthView.swift */; }; ECEAA5CF7309E5993D12B571 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD014B7E3E30A34E18696A0 /* AuthView.swift */; };
EE7BCEB45B1F0B91C9D3C1D2 /* TaskContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 670A8C6F8243EC973A1BC431 /* TaskContextMenu.swift */; };
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; }; EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -70,6 +76,8 @@
/* 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>"; }; 01A27D42E141DC056D32C1A3 /* TutorialManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialManager.swift; sourceTree = "<group>"; };
0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLiveActivity.swift; sourceTree = "<group>"; };
0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; }; 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; 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>"; };
@@ -85,6 +93,7 @@
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>"; }; 61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetData.swift; sourceTree = "<group>"; };
670A8C6F8243EC973A1BC431 /* TaskContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskContextMenu.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>"; };
@@ -92,6 +101,7 @@
8DC8687EA2FBA9FB2EEE51C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; 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>"; }; 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>"; };
92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; 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>"; };
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
@@ -104,6 +114,7 @@
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>"; };
FA3D5289C5F93484E22DEB63 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; }; FA3D5289C5F93484E22DEB63 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCountdownWidget.swift; sourceTree = "<group>"; }; FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCountdownWidget.swift; sourceTree = "<group>"; };
FEEB07EB89E89383C32ADB34 /* TaskActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskActivityAttributes.swift; sourceTree = "<group>"; };
FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; }; FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -155,6 +166,7 @@
0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */, 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */,
72308FEE0226F45414C04DDD /* SettingsView.swift */, 72308FEE0226F45414C04DDD /* SettingsView.swift */,
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */, BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */,
670A8C6F8243EC973A1BC431 /* TaskContextMenu.swift */,
C786EBC7DF879D64EB28165E /* TodayView.swift */, C786EBC7DF879D64EB28165E /* TodayView.swift */,
FA3D5289C5F93484E22DEB63 /* TutorialView.swift */, FA3D5289C5F93484E22DEB63 /* TutorialView.swift */,
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */, 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */,
@@ -186,6 +198,7 @@
8E7FC6446D5C5B2A2D9387DA /* KisaniCalWidgets.entitlements */, 8E7FC6446D5C5B2A2D9387DA /* KisaniCalWidgets.entitlements */,
42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */, 42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */,
208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */, 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */,
0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */,
61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */, 61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */,
429806CE1021C8DE2EB770CE /* WidgetViews.swift */, 429806CE1021C8DE2EB770CE /* WidgetViews.swift */,
); );
@@ -197,6 +210,7 @@
children = ( children = (
72FDF9C8DD37134576356B89 /* Assets.xcassets */, 72FDF9C8DD37134576356B89 /* Assets.xcassets */,
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */, BC5E179A9189B0A8C3F856F6 /* ContentView.swift */,
92824ED40ECD41EFD4F78BEC /* ISSUES.md */,
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */, 1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */,
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */, 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */,
21B93C269F283F11B415B18C /* Components */, 21B93C269F283F11B415B18C /* Components */,
@@ -216,8 +230,10 @@
89550F2CD19B950CCC6AD37F /* AuthManager.swift */, 89550F2CD19B950CCC6AD37F /* AuthManager.swift */,
449C34805DC6B2CB66886544 /* CloudSyncManager.swift */, 449C34805DC6B2CB66886544 /* CloudSyncManager.swift */,
FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */, FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */,
0506183945D16EC443A69651 /* LiveActivityManager.swift */,
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */, 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */,
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */, 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */,
FEEB07EB89E89383C32ADB34 /* TaskActivityAttributes.swift */,
01A27D42E141DC056D32C1A3 /* TutorialManager.swift */, 01A27D42E141DC056D32C1A3 /* TutorialManager.swift */,
); );
path = Managers; path = Managers;
@@ -308,6 +324,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */, 3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */,
703E068364256D6F3F867961 /* ISSUES.md in Resources */,
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */, 7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -331,6 +348,7 @@
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */, CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */,
BD0DB4B0AA8A63D124EDFF2C /* HealthKitManager.swift in Sources */, BD0DB4B0AA8A63D124EDFF2C /* HealthKitManager.swift in Sources */,
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */, D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
754D3DE3CEB998E36E585A61 /* LiveActivityManager.swift in Sources */,
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */, BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */, EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */,
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */, 67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */,
@@ -339,6 +357,8 @@
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 */,
32C63D81925FBFE51CAE1FB7 /* TaskActivityAttributes.swift in Sources */,
EE7BCEB45B1F0B91C9D3C1D2 /* TaskContextMenu.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 */, 3AD7F62D231DCFFFFCB31649 /* TutorialManager.swift in Sources */,
@@ -355,6 +375,8 @@
552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */, 552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */,
C7767143D9617ECA04ED1935 /* KisaniCalWidgets.swift in Sources */, C7767143D9617ECA04ED1935 /* KisaniCalWidgets.swift in Sources */,
3E9DE1CF20BAC479805DF940 /* MyTasksWidget.swift in Sources */, 3E9DE1CF20BAC479805DF940 /* MyTasksWidget.swift in Sources */,
8DA396DACE99DC4B7B4A460E /* TaskActivityAttributes.swift in Sources */,
205846F9651EDCE0D8207358 /* TaskLiveActivity.swift in Sources */,
AC0D814DA54D5EF5E25CEB99 /* WidgetData.swift in Sources */, AC0D814DA54D5EF5E25CEB99 /* WidgetData.swift in Sources */,
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */, 55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */,
); );
@@ -386,6 +408,7 @@
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 = "Kisani Cal reads your steps, energy, heart rate, and workouts to show health stats on your dashboard."; 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."; INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Kisani Cal saves workouts you complete to the Health app.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = ""; INFOPLIST_KEY_UILaunchStoryboardName = "";
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
@@ -577,6 +600,7 @@
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 = "Kisani Cal reads your steps, energy, heart rate, and workouts to show health stats on your dashboard."; 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."; INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Kisani Cal saves workouts you complete to the Health app.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = ""; INFOPLIST_KEY_UILaunchStoryboardName = "";
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;

View File

@@ -33,8 +33,10 @@ Set completion is now a per-day state:
Files: `KisaniCal/Models/ExerciseModels.swift`, `KisaniCal/ContentView.swift` Files: `KisaniCal/Models/ExerciseModels.swift`, `KisaniCal/ContentView.swift`
### Verification ### Verification
Builds cleanly (Xcode 26.2 SDK, simulator). Same program on consecutive days now Builds cleanly (Xcode 26.2 SDK, simulator). Verified in-process on the simulator
starts unchecked each day; streak preserved. via the real `resetSetsForNewDayIfNeeded()` against a seeded completed set:
`before=[true] after=[false] day=2026-06-06 -> PASS`. Streak preserved.
Committed in `cdc46e9`.
--- ---
@@ -55,4 +57,37 @@ is fully opaque (App Store Connect compliant) and device icons are full-bleed.
Files: `KisaniCal/Assets.xcassets/AppIcon.appiconset/` Files: `KisaniCal/Assets.xcassets/AppIcon.appiconset/`
### Verification ### Verification
Builds cleanly (app + widget extension); `hasAlpha: no` on all icons. Builds cleanly (app + widget extension); `hasAlpha: no` on all icons. New logo
confirmed compiled into `Assets.car` (home, lock-screen notification, Spotlight,
Settings all derive from `AppIcon`). Committed in `cdc46e9`.
Note: device still shows old icon until the new build is installed (delete + reinstall).
---
## KC-3 — Today view: section order + RSS feeds leaking into Tasks
**Status:** In Progress (implemented & build-verified, pending release)
**Reported by:** User
**Area:** Tasks / Today
### Description
Two changes to the Today view:
1. The Overdue card sat at the very top, above today's active tasks. Active tasks
should always be the top section.
2. Subscribed/RSS calendar feeds (F1 schedule, TV-show calendars) were appearing
in the Today/Tasks timeline. They belong in the Calendar tab only.
### Fix
- Moved the `OverdueCard` to render directly below today's active tasks (above
Next 3 Days), so active tasks stay at the top.
- `todayCalEvents` now excludes events whose `calendar.type == .subscription`, so
RSS/ICS feeds are filtered out of the Today timeline. The Calendar view reads
`monthEvents` directly and is unaffected.
- Birthdays (`.birthday`) and personal/CalDAV calendars are intentionally left
visible; the existing per-calendar visibility toggle (`hiddenCalendarIDs`) lets
the user hide any of them. User-created tasks are never affected.
Files: `KisaniCal/Views/TodayView.swift`
### Verification
Builds cleanly. Committed in `f66f032`.

View File

@@ -27,6 +27,11 @@ final class AuthManager: NSObject, ObservableObject {
override init() { override init() {
super.init() super.init()
currentUser = loadUser() currentUser = loadUser()
#if DEBUG
if currentUser == nil, ProcessInfo.processInfo.environment["KISANI_PREVIEW_AUTH"] == "1" {
currentUser = AppUser(id: "preview-user", email: "preview@kisani.app", displayName: "Preview")
}
#endif
if let uid = currentUser?.id { if let uid = currentUser?.id {
UserDefaults.kisani.set(uid, forKey: Self.activeUserIdKey) UserDefaults.kisani.set(uid, forKey: Self.activeUserIdKey)
} }

View File

@@ -0,0 +1,68 @@
import Foundation
import ActivityKit
/// Starts / ends a Live Activity for a task. iOS 16.1+ only; no-ops otherwise.
enum LiveActivityManager {
/// Toggle: if the task already has a Live Activity, end it; else start one.
static func toggle(for task: TaskItem) {
guard #available(iOS 16.1, *) else { return }
if isActive(taskID: task.id.uuidString) {
end(taskID: task.id.uuidString)
} else {
start(for: task)
}
}
/// Whether a Live Activity is currently running for this task.
@available(iOS 16.1, *)
static func isActive(taskID: String) -> Bool {
Activity<TaskActivityAttributes>.activities.contains { $0.attributes.taskID == taskID }
}
@available(iOS 16.1, *)
static func start(for task: TaskItem) {
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
let attributes = TaskActivityAttributes(
taskID: task.id.uuidString,
title: task.title,
categoryRaw: task.category.rawValue
)
let state = TaskActivityAttributes.ContentState(
dueDate: task.dueDate,
isComplete: task.isComplete,
priorityRaw: task.priority.rawValue
)
do {
_ = try Activity.request(attributes: attributes, contentState: state, pushType: nil)
} catch {
print("LiveActivity start failed: \(error.localizedDescription)")
}
}
/// Push updated state to a running activity (e.g. after edit/complete).
@available(iOS 16.1, *)
static func update(for task: TaskItem) {
let state = TaskActivityAttributes.ContentState(
dueDate: task.dueDate,
isComplete: task.isComplete,
priorityRaw: task.priority.rawValue
)
Task {
for activity in Activity<TaskActivityAttributes>.activities
where activity.attributes.taskID == task.id.uuidString {
await activity.update(using: state)
}
}
}
@available(iOS 16.1, *)
static func end(taskID: String) {
Task {
for activity in Activity<TaskActivityAttributes>.activities
where activity.attributes.taskID == taskID {
await activity.end(dismissalPolicy: .immediate)
}
}
}
}

View File

@@ -245,7 +245,9 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
} else { } else {
continue continue
} }
guard fireDate > now else { continue } // A one-off reminder in the past is dropped; a constant (daily-repeating)
// reminder still schedules so it keeps nudging on its next occurrence.
guard task.constantReminder || fireDate > now else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = task.title content.title = task.title
@@ -254,11 +256,15 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
content.categoryIdentifier = Self.categoryId content.categoryIdentifier = Self.categoryId
content.userInfo = ["taskId": task.id.uuidString, "deeplink": "tasks"] content.userInfo = ["taskId": task.id.uuidString, "deeplink": "tasks"]
let trigComps = cal.dateComponents([.year, .month, .day, .hour, .minute], from: fireDate) // Constant reminders re-fire daily (at the reminder time) until the
// task is completed; one-off reminders match the full date.
let trigComps = task.constantReminder
? cal.dateComponents([.hour, .minute], from: fireDate)
: cal.dateComponents([.year, .month, .day, .hour, .minute], from: fireDate)
center.add(UNNotificationRequest( center.add(UNNotificationRequest(
identifier: "kisani.task.\(task.id.uuidString)", identifier: "kisani.task.\(task.id.uuidString)",
content: content, content: content,
trigger: UNCalendarNotificationTrigger(dateMatching: trigComps, repeats: false) trigger: UNCalendarNotificationTrigger(dateMatching: trigComps, repeats: task.constantReminder)
)) ))
} }

View File

@@ -0,0 +1,18 @@
import Foundation
import ActivityKit
// Shared between the app (which starts/ends the activity) and the widget
// extension (which renders it). Added to both targets in project.yml.
@available(iOS 16.1, *)
struct TaskActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var dueDate: Date?
var isComplete: Bool
var priorityRaw: String
}
// Static identity for the life of the activity.
var taskID: String
var title: String
var categoryRaw: String
}

View File

@@ -19,6 +19,7 @@ struct TaskItem: Identifiable, Codable, Equatable {
var isAllDay: Bool = false var isAllDay: Bool = false
var priority: Priority = .none var priority: Priority = .none
var reminderDate: Date? = nil var reminderDate: Date? = nil
var constantReminder: Bool = false
} }
enum Priority: String, Codable, CaseIterable, Identifiable { enum Priority: String, Codable, CaseIterable, Identifiable {
@@ -236,6 +237,12 @@ class TaskViewModel: ObservableObject {
tasks[idx].category = category tasks[idx].category = category
save() save()
} }
func setPriority(_ task: TaskItem, priority: Priority) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
tasks[idx].priority = priority
save()
}
func moveToQuadrant(taskID: UUID, quadrant: Quadrant) { func moveToQuadrant(taskID: UUID, quadrant: Quadrant) {
guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return } guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return }
tasks[idx].quadrant = quadrant tasks[idx].quadrant = quadrant
@@ -247,19 +254,22 @@ class TaskViewModel: ObservableObject {
taskColor: TaskColor = .orange, isRecurring: Bool = false, taskColor: TaskColor = .orange, isRecurring: Bool = false,
recurrenceLabel: String? = nil, recurrenceLabel: String? = nil,
endDate: Date? = nil, isAllDay: Bool = false, endDate: Date? = nil, isAllDay: Bool = false,
priority: Priority = .none, reminderDate: Date? = nil) { priority: Priority = .none, reminderDate: Date? = nil,
constantReminder: Bool = false) {
tasks.append(TaskItem(title: title, dueDate: dueDate, hasTime: hasTime, tasks.append(TaskItem(title: title, dueDate: dueDate, hasTime: hasTime,
quadrant: quadrant, category: category, quadrant: quadrant, category: category,
taskColor: taskColor, isRecurring: isRecurring, taskColor: taskColor, isRecurring: isRecurring,
recurrenceLabel: recurrenceLabel, recurrenceLabel: recurrenceLabel,
endDate: endDate, isAllDay: isAllDay, endDate: endDate, isAllDay: isAllDay,
priority: priority, reminderDate: reminderDate)) priority: priority, reminderDate: reminderDate,
constantReminder: constantReminder))
save() save()
} }
func updateTask(_ task: TaskItem, title: String, dueDate: Date?, hasTime: Bool, func updateTask(_ task: TaskItem, title: String, dueDate: Date?, hasTime: Bool,
isRecurring: Bool, recurrenceLabel: String?, isRecurring: Bool, recurrenceLabel: String?,
endDate: Date?, isAllDay: Bool, reminderDate: Date?) { endDate: Date?, isAllDay: Bool, reminderDate: Date?,
constantReminder: Bool = false) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return } guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
tasks[idx].title = title tasks[idx].title = title
tasks[idx].dueDate = dueDate tasks[idx].dueDate = dueDate
@@ -269,6 +279,7 @@ class TaskViewModel: ObservableObject {
tasks[idx].endDate = endDate tasks[idx].endDate = endDate
tasks[idx].isAllDay = isAllDay tasks[idx].isAllDay = isAllDay
tasks[idx].reminderDate = reminderDate tasks[idx].reminderDate = reminderDate
tasks[idx].constantReminder = constantReminder
save() save()
} }

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import EventKit import EventKit
import EventKitUI
// MARK: - Calendar Store // MARK: - Calendar Store
@MainActor @MainActor
@@ -115,6 +116,20 @@ final class CalendarStore: ObservableObject {
return authStatus == .authorized return authStatus == .authorized
} }
/// Exposed so the edit sheet can hand it to EKEventEditViewController.
var eventStore: EKEventStore { ekStore }
/// Delete a real (writable) calendar event, then refresh the month cache.
func deleteEvent(_ event: EKEvent) {
guard event.calendar?.allowsContentModifications == true else { return }
do {
try ekStore.remove(event, span: .thisEvent)
fetchMonth(containing: event.startDate ?? Date())
} catch {
print("Delete event failed: \(error.localizedDescription)")
}
}
/// Only `.notDetermined` can show the system prompt; otherwise we must deep-link to Settings. /// Only `.notDetermined` can show the system prompt; otherwise we must deep-link to Settings.
var canRequest: Bool { authStatus == .notDetermined } var canRequest: Bool { authStatus == .notDetermined }
var isDenied: Bool { authStatus == .denied || authStatus == .restricted } var isDenied: Bool { authStatus == .denied || authStatus == .restricted }
@@ -227,6 +242,7 @@ struct CalendarView: View {
@State private var showAddTask = false @State private var showAddTask = false
@State private var showCalendarConnect = false @State private var showCalendarConnect = false
@State private var showWorkoutSession = false @State private var showWorkoutSession = false
@State private var editingEvent: EditableEvent? = nil
private let cal = Calendar.current private let cal = Calendar.current
private let weekdays = ["S","M","T","W","T","F","S"] private let weekdays = ["S","M","T","W","T","F","S"]
@@ -334,6 +350,12 @@ struct CalendarView: View {
.presentationDetents([.large]) .presentationDetents([.large])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
} }
.sheet(item: $editingEvent) { wrapper in
EventEditView(event: wrapper.event, store: calStore.eventStore) {
calStore.fetchMonth(containing: selectedDate)
}
.ignoresSafeArea()
}
.onAppear { .onAppear {
calStore.refreshStatus() calStore.refreshStatus()
if calStore.isAuthorized { calStore.fetchMonth(containing: displayMonth) } if calStore.isAuthorized { calStore.fetchMonth(containing: displayMonth) }
@@ -517,7 +539,15 @@ struct CalendarView: View {
}, },
onToggle: { task in onToggle: { task in
withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) } withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) }
} },
onPin: { taskVM.pin($0) },
onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onEditEvent: { editingEvent = EditableEvent(event: $0) },
onDeleteEvent: { calStore.deleteEvent($0) }
) )
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 20) .padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 20)
} }
@@ -1442,6 +1472,7 @@ private struct DayTLEntry: Identifiable {
var sortGroup: Int = 0 // 0 = active, 1 = completed, 2 = overdue var sortGroup: Int = 0 // 0 = active, 1 = completed, 2 = overdue
var taskRef: TaskItem? var taskRef: TaskItem?
var workoutRef: WorkoutProgram? var workoutRef: WorkoutProgram?
var eventRef: EKEvent?
} }
struct DayTimelineView: View { struct DayTimelineView: View {
@@ -1458,6 +1489,13 @@ struct DayTimelineView: View {
var onPostpone: ((TaskItem) -> Void)? = nil var onPostpone: ((TaskItem) -> Void)? = nil
var onEdit: ((TaskItem) -> Void)? = nil var onEdit: ((TaskItem) -> Void)? = nil
var onDelete: ((TaskItem) -> Void)? = nil var onDelete: ((TaskItem) -> Void)? = nil
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
// Calendar events (non-subscription only): edit/delete in the real calendar.
var onEditEvent: ((EKEvent) -> Void)? = nil
var onDeleteEvent: ((EKEvent) -> Void)? = nil
private let cal = Calendar.current private let cal = Calendar.current
private static let overdueFmt: DateFormatter = { private static let overdueFmt: DateFormatter = {
@@ -1485,7 +1523,8 @@ struct DayTimelineView: View {
timeLabel: "All Day", timeLabel: "All Day",
title: ev.title ?? "Event", title: ev.title ?? "Event",
color: Color(cgColor: ev.calendar.cgColor), color: Color(cgColor: ev.calendar.cgColor),
isComplete: false, isAllDay: true, sortKey: -1, sortGroup: 0 isComplete: false, isAllDay: true, sortKey: -1, sortGroup: 0,
eventRef: ev
)) ))
} }
@@ -1533,7 +1572,8 @@ struct DayTimelineView: View {
timeLabel: "\(hd)\(ms)", ampm: h < 12 ? "AM" : "PM", timeLabel: "\(hd)\(ms)", ampm: h < 12 ? "AM" : "PM",
title: ev.title ?? "Event", subtitle: endLabel, title: ev.title ?? "Event", subtitle: endLabel,
color: Color(cgColor: ev.calendar.cgColor), color: Color(cgColor: ev.calendar.cgColor),
isComplete: false, isAllDay: false, sortKey: h * 60 + m, sortGroup: 0 isComplete: false, isAllDay: false, sortKey: h * 60 + m, sortGroup: 0,
eventRef: ev
)) ))
} }
@@ -1587,6 +1627,11 @@ struct DayTimelineView: View {
) )
if let t = entry.taskRef { if let t = entry.taskRef {
row.contextMenu { taskContextMenu(t) } row.contextMenu { taskContextMenu(t) }
} else if let ev = entry.eventRef,
onEditEvent != nil || onDeleteEvent != nil,
ev.calendar?.type != .subscription,
ev.calendar?.allowsContentModifications == true {
row.contextMenu { eventContextMenu(ev) }
} else { } else {
row row
} }
@@ -1597,26 +1642,30 @@ struct DayTimelineView: View {
@ViewBuilder @ViewBuilder
private func taskContextMenu(_ task: TaskItem) -> some View { private func taskContextMenu(_ task: TaskItem) -> some View {
Button { onToggle?(task) } label: { TaskMenuItems(
Label(task.isComplete ? "Mark Incomplete" : "Mark Complete", systemImage: "checkmark.circle") task: task,
onPin: { onPin?($0) },
onSetDate: { onSetDate?($0, $1) },
onMove: { onMove?($0, $1) },
onSetPriority: { onSetPriority?($0, $1) },
onSetCategory: { onSetCategory?($0, $1) },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: onEdit,
onDelete: { onDelete?($0) }
)
} }
if let onPostpone {
Button { onPostpone(task) } label: { // Edit / Delete for a real (non-subscription) calendar event.
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath") @ViewBuilder
private func eventContextMenu(_ event: EKEvent) -> some View {
if let onEditEvent {
Button { onEditEvent(event) } label: { Label("Edit", systemImage: "pencil") }
} }
} if let onDeleteEvent {
if let onPin {
Button { onPin(task) } label: {
Label(task.isPinned ? "Unpin" : "Pin", systemImage: task.isPinned ? "pin.slash" : "pin")
}
}
if let onEdit {
Button { onEdit(task) } label: { Label("Edit", systemImage: "pencil") }
}
ShareLink(item: task.title) { Label("Share", systemImage: "square.and.arrow.up") }
if let onDelete {
Divider() Divider()
Button(role: .destructive) { onDelete(task) } label: { Label("Delete", systemImage: "trash") } Button(role: .destructive) { onDeleteEvent(event) } label: {
Label("Delete", systemImage: "trash")
}
} }
} }
} }
@@ -1816,3 +1865,40 @@ struct WorkoutSectionBar: View {
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
} }
} }
// MARK: - Event Edit Sheet
/// Identifiable wrapper so an EKEvent can drive `.sheet(item:)`.
struct EditableEvent: Identifiable {
let id = UUID()
let event: EKEvent
}
/// Wraps the system event editor (EKEventEditViewController).
struct EventEditView: UIViewControllerRepresentable {
let event: EKEvent
let store: EKEventStore
var onDone: () -> Void
func makeUIViewController(context: Context) -> EKEventEditViewController {
let vc = EKEventEditViewController()
vc.eventStore = store
vc.event = event
vc.editViewDelegate = context.coordinator
return vc
}
func updateUIViewController(_ vc: EKEventEditViewController, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator(onDone: onDone) }
final class Coordinator: NSObject, EKEventEditViewDelegate {
let onDone: () -> Void
init(onDone: @escaping () -> Void) { self.onDone = onDone }
func eventEditViewController(_ controller: EKEventEditViewController,
didCompleteWith action: EKEventEditViewAction) {
controller.dismiss(animated: true)
onDone()
}
}
}

View File

@@ -66,6 +66,7 @@ struct MatrixView: View {
onSetDate: { taskVM.setDate($0, date: $1) }, onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) }, onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }, onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onDelete: { taskVM.delete($0) }, onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .urgent; showDrill = true } onTapHeader: { drillQuadrant = .urgent; showDrill = true }
) )
@@ -83,6 +84,7 @@ struct MatrixView: View {
onSetDate: { taskVM.setDate($0, date: $1) }, onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) }, onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }, onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onDelete: { taskVM.delete($0) }, onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .schedule; showDrill = true } onTapHeader: { drillQuadrant = .schedule; showDrill = true }
) )
@@ -104,6 +106,7 @@ struct MatrixView: View {
onSetDate: { taskVM.setDate($0, date: $1) }, onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) }, onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }, onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onDelete: { taskVM.delete($0) }, onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .delegate_; showDrill = true } onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
) )
@@ -121,6 +124,7 @@ struct MatrixView: View {
onSetDate: { taskVM.setDate($0, date: $1) }, onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) }, onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }, onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onDelete: { taskVM.delete($0) }, onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .eliminate; showDrill = true } onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
) )
@@ -183,6 +187,7 @@ struct QuadrantCard: View {
var onSetDate: ((TaskItem, Date?) -> Void)? = nil var onSetDate: ((TaskItem, Date?) -> Void)? = nil
var onMove: ((TaskItem, Quadrant) -> Void)? = nil var onMove: ((TaskItem, Quadrant) -> Void)? = nil
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
var onDelete: ((TaskItem) -> Void)? = nil var onDelete: ((TaskItem) -> Void)? = nil
var onTapHeader: (() -> Void)? = nil var onTapHeader: (() -> Void)? = nil
@@ -278,68 +283,17 @@ struct QuadrantCard: View {
.padding(.leading, 2) .padding(.leading, 2)
.draggable(task.id.uuidString) .draggable(task.id.uuidString)
.contextMenu { .contextMenu {
Button { TaskMenuItems(
withAnimation(KisaniSpring.snappy) { onPin?(task) } task: task,
} label: { currentQuadrant: quadrant,
Label(task.isPinned ? "Unpin" : "Pin", onPin: { t in withAnimation(KisaniSpring.snappy) { onPin?(t) } },
systemImage: task.isPinned ? "pin.slash" : "pin") onSetDate: { onSetDate?($0, $1) },
} onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
onSetPriority: { onSetPriority?($0, $1) },
Menu { onSetCategory: { onSetCategory?($0, $1) },
Button { onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onSetDate?(task, Calendar.current.startOfDay(for: Date())) onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
} label: { Label("Today", systemImage: "sun.max") } )
Button {
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())!
onSetDate?(task, Calendar.current.startOfDay(for: tomorrow))
} label: { Label("Tomorrow", systemImage: "sunrise") }
Button {
let nextWeek = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: Date())!
onSetDate?(task, Calendar.current.startOfDay(for: nextWeek))
} label: { Label("Next Week", systemImage: "calendar") }
Divider()
Button {
onSetDate?(task, nil)
} label: { Label("Remove Date", systemImage: "xmark.circle") }
} label: {
Label("Date", systemImage: "calendar.badge.clock")
}
Menu {
ForEach(Quadrant.allCases.filter { $0 != quadrant }) { q in
Button {
withAnimation(KisaniSpring.snappy) { onMove?(task, q) }
} label: {
Text(q.rawValue)
}
}
} label: {
Label("Move", systemImage: "arrow.right.square")
}
Menu {
ForEach([TaskCategory.reminder, .birthday, .domain, .annual, .custom], id: \.rawValue) { cat in
Button {
onSetCategory?(task, cat)
} label: {
Label(cat.rawValue, systemImage: task.category == cat ? "checkmark" : "tag")
}
}
} label: {
Label("Tags", systemImage: "tag")
}
Button { } label: {
Label("Add to Live Activity", systemImage: "pin.circle")
}
Divider()
Button(role: .destructive) {
withAnimation(KisaniSpring.snappy) { onDelete?(task) }
} label: {
Label("Delete", systemImage: "trash")
}
} }
} }
} }
@@ -484,6 +438,20 @@ struct QuadrantDetailView: View {
QDTaskRow(task: task, quadrant: quadrant, QDTaskRow(task: task, quadrant: quadrant,
onTap: { editingTask = task }, onTap: { editingTask = task },
onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } }) onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } })
.contextMenu {
TaskMenuItems(
task: task,
currentQuadrant: quadrant,
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) }
)
}
if task.id != overdueTasks.last?.id { Divider().padding(.leading, 46) } if task.id != overdueTasks.last?.id { Divider().padding(.leading, 46) }
} }
} }
@@ -495,6 +463,20 @@ struct QuadrantDetailView: View {
QDTaskRow(task: task, quadrant: quadrant, QDTaskRow(task: task, quadrant: quadrant,
onTap: { editingTask = task }, onTap: { editingTask = task },
onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } }) onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } })
.contextMenu {
TaskMenuItems(
task: task,
currentQuadrant: quadrant,
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) }
)
}
if task.id != laterTasks.last?.id { Divider().padding(.leading, 46) } if task.id != laterTasks.last?.id { Divider().padding(.leading, 46) }
} }
} }
@@ -507,6 +489,20 @@ struct QuadrantDetailView: View {
QDTaskRow(task: task, quadrant: quadrant, QDTaskRow(task: task, quadrant: quadrant,
onTap: { editingTask = task }, onTap: { editingTask = task },
onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } }) onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } })
.contextMenu {
TaskMenuItems(
task: task,
currentQuadrant: quadrant,
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) }
)
}
if task.id != displayed.last?.id { Divider().padding(.leading, 46) } if task.id != displayed.last?.id { Divider().padding(.leading, 46) }
} }
if completedTasks.count > completedLimit && !showAllCompleted { if completedTasks.count > completedLimit && !showAllCompleted {
@@ -682,6 +678,7 @@ struct TaskEditSheet: View {
@State private var endDate: Date? @State private var endDate: Date?
@State private var isAllDay: Bool @State private var isAllDay: Bool
@State private var reminderDate: Date? @State private var reminderDate: Date?
@State private var constantReminder: Bool
@State private var showDatePicker = false @State private var showDatePicker = false
init(task: TaskItem) { init(task: TaskItem) {
@@ -694,6 +691,7 @@ struct TaskEditSheet: View {
_endDate = State(initialValue: task.endDate) _endDate = State(initialValue: task.endDate)
_isAllDay = State(initialValue: task.isAllDay) _isAllDay = State(initialValue: task.isAllDay)
_reminderDate = State(initialValue: task.reminderDate) _reminderDate = State(initialValue: task.reminderDate)
_constantReminder = State(initialValue: task.constantReminder)
} }
private var dateSummary: String { private var dateSummary: String {
@@ -839,7 +837,8 @@ struct TaskEditSheet: View {
recurrenceLabel: $recurrenceLabel, recurrenceLabel: $recurrenceLabel,
endDate: $endDate, endDate: $endDate,
isAllDay: $isAllDay, isAllDay: $isAllDay,
reminderDate: $reminderDate reminderDate: $reminderDate,
constantReminder: $constantReminder
) )
} }
} }
@@ -849,7 +848,8 @@ struct TaskEditSheet: View {
guard !t.isEmpty else { return } guard !t.isEmpty else { return }
taskVM.updateTask(task, title: t, dueDate: dueDate, hasTime: hasTime, taskVM.updateTask(task, title: t, dueDate: dueDate, hasTime: hasTime,
isRecurring: isRecurring, recurrenceLabel: recurrenceLabel, isRecurring: isRecurring, recurrenceLabel: recurrenceLabel,
endDate: endDate, isAllDay: isAllDay, reminderDate: reminderDate) endDate: endDate, isAllDay: isAllDay, reminderDate: reminderDate,
constantReminder: constantReminder)
dismiss() dismiss()
} }
} }

View File

@@ -0,0 +1,80 @@
import SwiftUI
/// The unified long-press menu for a task, shared across Today, Matrix, and Calendar.
/// Pass only the closures a given screen needs; the rest default to no-ops.
struct TaskMenuItems: View {
let task: TaskItem
/// The quadrant the row currently lives in (so "Move" hides the current one).
var currentQuadrant: Quadrant? = nil
var onPin: (TaskItem) -> Void = { _ in }
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
var onLiveActivity:(TaskItem) -> Void = { LiveActivityManager.toggle(for: $0) }
var onEdit: ((TaskItem) -> Void)? = nil
var onDelete: (TaskItem) -> Void = { _ in }
private var cal: Calendar { .current }
var body: some View {
Button { onPin(task) } label: {
Label(task.isPinned ? "Unpin" : "Pin", systemImage: task.isPinned ? "pin.slash" : "pin")
}
Menu {
Button { onSetDate(task, cal.startOfDay(for: Date())) } label: {
Label("Today", systemImage: "sun.max")
}
Button {
let d = cal.date(byAdding: .day, value: 1, to: Date())!
onSetDate(task, cal.startOfDay(for: d))
} label: { Label("Tomorrow", systemImage: "sunrise") }
Button {
let d = cal.date(byAdding: .weekOfYear, value: 1, to: Date())!
onSetDate(task, cal.startOfDay(for: d))
} label: { Label("Next Week", systemImage: "calendar") }
Divider()
Button { onSetDate(task, nil) } label: {
Label("Remove Date", systemImage: "xmark.circle")
}
} label: { Label("Date", systemImage: "calendar.badge.clock") }
Menu {
ForEach(Quadrant.allCases.filter { $0 != (currentQuadrant ?? task.quadrant) }) { q in
Button { onMove(task, q) } label: { Text(q.rawValue) }
}
} label: { Label("Move", systemImage: "arrow.right.square") }
Menu {
ForEach(Priority.allCases) { p in
Button { onSetPriority(task, p) } label: {
Label(p.label, systemImage: task.priority == p ? "checkmark" : "flag")
}
}
} label: { Label("Priority", systemImage: "flag") }
Menu {
ForEach([TaskCategory.reminder, .birthday, .domain, .annual, .custom], id: \.rawValue) { cat in
Button { onSetCategory(task, cat) } label: {
Label(cat.rawValue, systemImage: task.category == cat ? "checkmark" : "tag")
}
}
} label: { Label("Tags", systemImage: "tag") }
Button { onLiveActivity(task) } label: {
Label("Add to Live Activity", systemImage: "pin.circle")
}
if let onEdit {
Button { onEdit(task) } label: { Label("Edit", systemImage: "pencil") }
}
Divider()
Button(role: .destructive) { onDelete(task) } label: {
Label("Delete", systemImage: "trash")
}
}
}

View File

@@ -100,7 +100,11 @@ struct TodayView: View {
onPin: { taskVM.pin($0) }, onPin: { taskVM.pin($0) },
onPostpone: { taskVM.postpone($0) }, onPostpone: { taskVM.postpone($0) },
onEdit: { editingTask = $0 }, onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) } onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }
) )
.padding(.horizontal, 18) .padding(.horizontal, 18)

View File

@@ -111,5 +111,6 @@ struct KisaniWidgetBundle: WidgetBundle {
TasksCompleteWidget() TasksCompleteWidget()
LockScreenWidget() LockScreenWidget()
LockScreenCircularWidget() LockScreenCircularWidget()
TaskLiveActivity()
} }
} }

View File

@@ -0,0 +1,74 @@
import ActivityKit
import WidgetKit
import SwiftUI
@available(iOS 16.1, *)
struct TaskLiveActivity: Widget {
private let accent = Color(red: 0.93, green: 0.42, blue: 0.20) // brand orange
var body: some WidgetConfiguration {
ActivityConfiguration(for: TaskActivityAttributes.self) { context in
// Lock screen / banner
HStack(spacing: 12) {
Image(systemName: context.state.isComplete ? "checkmark.circle.fill" : "circle")
.font(.system(size: 22))
.foregroundColor(context.state.isComplete ? .green : accent)
VStack(alignment: .leading, spacing: 2) {
Text(context.attributes.title)
.font(.system(size: 15, weight: .semibold))
.foregroundColor(.white)
.lineLimit(1)
.strikethrough(context.state.isComplete)
if let due = context.state.dueDate {
Text(due, style: context.state.isComplete ? .date : .relative)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(.white.opacity(0.6))
}
}
Spacer()
if let due = context.state.dueDate, !context.state.isComplete {
Text(due, style: .timer)
.font(.system(size: 16, weight: .bold, design: .monospaced))
.foregroundColor(accent)
.frame(maxWidth: 70)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.activityBackgroundTint(Color.black.opacity(0.55))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "checklist")
.foregroundColor(accent)
}
DynamicIslandExpandedRegion(.trailing) {
if let due = context.state.dueDate, !context.state.isComplete {
Text(due, style: .timer)
.font(.system(.body, design: .monospaced))
.foregroundColor(accent)
.frame(maxWidth: 64)
}
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.title)
.font(.system(size: 14, weight: .semibold))
.lineLimit(1)
}
} compactLeading: {
Image(systemName: "checklist").foregroundColor(accent)
} compactTrailing: {
if let due = context.state.dueDate, !context.state.isComplete {
Text(due, style: .timer)
.font(.system(.caption2, design: .monospaced))
.foregroundColor(accent)
.frame(maxWidth: 44)
}
} minimal: {
Image(systemName: "checklist").foregroundColor(accent)
}
}
}
}

View File

@@ -51,6 +51,7 @@ targets:
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
INFOPLIST_KEY_NSSupportsLiveActivities: YES
CODE_SIGN_STYLE: Automatic CODE_SIGN_STYLE: Automatic
CODE_SIGN_ENTITLEMENTS: KisaniCal/KisaniCal.entitlements CODE_SIGN_ENTITLEMENTS: KisaniCal/KisaniCal.entitlements
ENABLE_PREVIEWS: YES ENABLE_PREVIEWS: YES
@@ -66,6 +67,7 @@ targets:
sources: sources:
- path: KisaniCalWidgets - path: KisaniCalWidgets
- path: KisaniCal/Managers/AppGroup.swift - path: KisaniCal/Managers/AppGroup.swift
- path: KisaniCal/Managers/TaskActivityAttributes.swift
info: info:
path: KisaniCalWidgets/Info.plist path: KisaniCalWidgets/Info.plist
properties: properties: