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:
@@ -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;
|
||||||
|
|||||||
Binary file not shown.
@@ -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`.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
68
KisaniCal/Managers/LiveActivityManager.swift
Normal file
68
KisaniCal/Managers/LiveActivityManager.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
KisaniCal/Managers/TaskActivityAttributes.swift
Normal file
18
KisaniCal/Managers/TaskActivityAttributes.swift
Normal 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
|
||||||
|
}
|
||||||
@@ -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,28 +254,32 @@ 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
|
||||||
tasks[idx].hasTime = hasTime
|
tasks[idx].hasTime = hasTime
|
||||||
tasks[idx].isRecurring = isRecurring
|
tasks[idx].isRecurring = isRecurring
|
||||||
tasks[idx].recurrenceLabel = recurrenceLabel
|
tasks[idx].recurrenceLabel = recurrenceLabel
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit / Delete for a real (non-subscription) calendar event.
|
||||||
|
@ViewBuilder
|
||||||
|
private func eventContextMenu(_ event: EKEvent) -> some View {
|
||||||
|
if let onEditEvent {
|
||||||
|
Button { onEditEvent(event) } label: { Label("Edit", systemImage: "pencil") }
|
||||||
}
|
}
|
||||||
if let onPostpone {
|
if let onDeleteEvent {
|
||||||
Button { onPostpone(task) } label: {
|
|
||||||
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,18 +678,20 @@ 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) {
|
||||||
self.task = task
|
self.task = task
|
||||||
_title = State(initialValue: task.title)
|
_title = State(initialValue: task.title)
|
||||||
_dueDate = State(initialValue: task.dueDate)
|
_dueDate = State(initialValue: task.dueDate)
|
||||||
_hasTime = State(initialValue: task.hasTime)
|
_hasTime = State(initialValue: task.hasTime)
|
||||||
_isRecurring = State(initialValue: task.isRecurring)
|
_isRecurring = State(initialValue: task.isRecurring)
|
||||||
_recurrenceLabel = State(initialValue: task.recurrenceLabel)
|
_recurrenceLabel = State(initialValue: task.recurrenceLabel)
|
||||||
_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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
KisaniCal/Views/TaskContextMenu.swift
Normal file
80
KisaniCal/Views/TaskContextMenu.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -111,5 +111,6 @@ struct KisaniWidgetBundle: WidgetBundle {
|
|||||||
TasksCompleteWidget()
|
TasksCompleteWidget()
|
||||||
LockScreenWidget()
|
LockScreenWidget()
|
||||||
LockScreenCircularWidget()
|
LockScreenCircularWidget()
|
||||||
|
TaskLiveActivity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
KisaniCalWidgets/TaskLiveActivity.swift
Normal file
74
KisaniCalWidgets/TaskLiveActivity.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user