feat: My Tasks widget, event countdown redesign, completed section
- My Tasks widget: today's tasks with interactive check-off (ToggleTaskIntent) and a "+" that opens the app's add-task sheet; writes preserve all task fields and sync via the App Group. - Event Countdown: AZURE/AWS-style header (accent name + countdown label + percent); tap the label to cycle days → weeks → time remaining. - Today: collapsible "Completed" archive section below Upcoming. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
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 */; };
|
||||||
|
3E9DE1CF20BAC479805DF940 /* MyTasksWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */; };
|
||||||
497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */; };
|
497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */; };
|
||||||
552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */; };
|
552E2F85B01C9CC437D383B5 /* EventCountdownWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */; };
|
||||||
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429806CE1021C8DE2EB770CE /* WidgetViews.swift */; };
|
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429806CE1021C8DE2EB770CE /* WidgetViews.swift */; };
|
||||||
@@ -70,9 +71,10 @@
|
|||||||
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>"; };
|
||||||
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; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; 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>"; };
|
||||||
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
||||||
|
208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = "<group>"; };
|
||||||
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; };
|
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; };
|
||||||
23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; };
|
23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; };
|
||||||
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = "<group>"; };
|
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = "<group>"; };
|
||||||
@@ -91,7 +93,7 @@
|
|||||||
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>"; };
|
||||||
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; explicitFileType = wrapper.application; includeInIndex = 0; 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>"; };
|
||||||
AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
|
AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
|
||||||
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
@@ -183,6 +185,7 @@
|
|||||||
8DC8687EA2FBA9FB2EEE51C6 /* Info.plist */,
|
8DC8687EA2FBA9FB2EEE51C6 /* Info.plist */,
|
||||||
8E7FC6446D5C5B2A2D9387DA /* KisaniCalWidgets.entitlements */,
|
8E7FC6446D5C5B2A2D9387DA /* KisaniCalWidgets.entitlements */,
|
||||||
42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */,
|
42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */,
|
||||||
|
208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */,
|
||||||
61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */,
|
61BFF3BA28331A16D0ADE9D0 /* WidgetData.swift */,
|
||||||
429806CE1021C8DE2EB770CE /* WidgetViews.swift */,
|
429806CE1021C8DE2EB770CE /* WidgetViews.swift */,
|
||||||
);
|
);
|
||||||
@@ -267,7 +270,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastUpgradeCheck = 2620;
|
LastUpgradeCheck = 1500;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
28B43516AD88946E21D9BFE0 = {
|
28B43516AD88946E21D9BFE0 = {
|
||||||
DevelopmentTeam = K8BLMMR883;
|
DevelopmentTeam = K8BLMMR883;
|
||||||
@@ -351,6 +354,7 @@
|
|||||||
12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */,
|
12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */,
|
||||||
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 */,
|
||||||
AC0D814DA54D5EF5E25CEB99 /* WidgetData.swift in Sources */,
|
AC0D814DA54D5EF5E25CEB99 /* WidgetData.swift in Sources */,
|
||||||
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */,
|
55F00C433F853F7B54F136B3 /* WidgetViews.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -438,7 +442,6 @@
|
|||||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
@@ -453,7 +456,6 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -469,11 +471,7 @@
|
|||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
@@ -520,7 +518,6 @@
|
|||||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
@@ -542,7 +539,6 @@
|
|||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -558,11 +554,7 @@
|
|||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2620"
|
LastUpgradeVersion = "1500"
|
||||||
version = "1.3">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES"
|
||||||
|
runPostActionsOnFailure = "NO">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
@@ -38,6 +40,8 @@
|
|||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
<Testables>
|
<Testables>
|
||||||
</Testables>
|
</Testables>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
@@ -59,6 +63,8 @@
|
|||||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
@@ -76,6 +82,8 @@
|
|||||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
<AnalyzeAction
|
<AnalyzeAction
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private var isIPad: Bool { UIDevice.current.userInterfaceIdiom == .pad }
|
private var isIPad: Bool { UIDevice.current.userInterfaceIdiom == .pad }
|
||||||
|
|
||||||
|
/// Picks up the "+" tapped on the My Tasks widget and opens the add-task sheet.
|
||||||
|
private func consumeWidgetAddTask() {
|
||||||
|
guard UserDefaults.kisani.bool(forKey: "kisani.widget.openAddTask") else { return }
|
||||||
|
UserDefaults.kisani.removeObject(forKey: "kisani.widget.openAddTask")
|
||||||
|
selectedTab = 0
|
||||||
|
showQuickAddTask = true
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if isIPad {
|
if isIPad {
|
||||||
@@ -89,6 +97,7 @@ struct ContentView: View {
|
|||||||
CloudSyncManager.shared.backupSettings()
|
CloudSyncManager.shared.backupSettings()
|
||||||
HealthKitManager.shared.bootstrap()
|
HealthKitManager.shared.bootstrap()
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
|
consumeWidgetAddTask()
|
||||||
// Pull workouts from Health, then reschedule so a detected workout updates reminders.
|
// Pull workouts from Health, then reschedule so a detected workout updates reminders.
|
||||||
Task {
|
Task {
|
||||||
await workoutVM.syncFromHealthKit()
|
await workoutVM.syncFromHealthKit()
|
||||||
@@ -98,6 +107,7 @@ struct ContentView: View {
|
|||||||
.onChange(of: scenePhase) { phase in
|
.onChange(of: scenePhase) { phase in
|
||||||
if phase == .active {
|
if phase == .active {
|
||||||
taskVM.reload()
|
taskVM.reload()
|
||||||
|
consumeWidgetAddTask()
|
||||||
workoutVM.checkPendingHealthConfirm()
|
workoutVM.checkPendingHealthConfirm()
|
||||||
CloudSyncManager.shared.backupSettings()
|
CloudSyncManager.shared.backupSettings()
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
|
|||||||
@@ -200,6 +200,12 @@ class TaskViewModel: ObservableObject {
|
|||||||
.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= startOfDay }
|
.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= startOfDay }
|
||||||
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
|
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
|
||||||
}
|
}
|
||||||
|
/// All completed tasks, most-recent first (for the Completed archive section).
|
||||||
|
var completedTasks: [TaskItem] {
|
||||||
|
tasks
|
||||||
|
.filter { $0.isComplete }
|
||||||
|
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
|
||||||
|
}
|
||||||
|
|
||||||
func toggle(_ task: TaskItem) {
|
func toggle(_ task: TaskItem) {
|
||||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||||
|
|||||||
@@ -123,6 +123,16 @@ struct TodayView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Completed (all, collapsible) ──
|
||||||
|
if !taskVM.completedTasks.isEmpty {
|
||||||
|
CompletedSection(
|
||||||
|
tasks: taskVM.completedTasks,
|
||||||
|
onUndo: { taskVM.toggle($0) }
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
|
||||||
let hasAnything = !taskVM.overdueTasks.isEmpty || !taskVM.todayTasks.isEmpty
|
let hasAnything = !taskVM.overdueTasks.isEmpty || !taskVM.todayTasks.isEmpty
|
||||||
|| !taskVM.upcomingTasks.isEmpty || !taskVM.completedTodayTasks.isEmpty
|
|| !taskVM.upcomingTasks.isEmpty || !taskVM.completedTodayTasks.isEmpty
|
||||||
|| !taskVM.next3DaysTasks.isEmpty || !todayCalEvents.isEmpty
|
|| !taskVM.next3DaysTasks.isEmpty || !todayCalEvents.isEmpty
|
||||||
@@ -1009,6 +1019,74 @@ struct CompletedTodaySection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Completed Section (all completed, collapsible archive)
|
||||||
|
struct CompletedSection: View {
|
||||||
|
@Environment(\.colorScheme) private var cs
|
||||||
|
let tasks: [TaskItem]
|
||||||
|
let onUndo: (TaskItem) -> Void
|
||||||
|
|
||||||
|
@State private var expanded = false
|
||||||
|
|
||||||
|
private let dateFmt: DateFormatter = {
|
||||||
|
let f = DateFormatter(); f.dateFormat = "MMM d"; return f
|
||||||
|
}()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// Header
|
||||||
|
HStack {
|
||||||
|
Text("Completed")
|
||||||
|
.font(AppFonts.sans(15, weight: .bold))
|
||||||
|
.foregroundColor(AppColors.text2(cs))
|
||||||
|
Spacer()
|
||||||
|
Text("\(tasks.count)")
|
||||||
|
.font(AppFonts.mono(11))
|
||||||
|
.foregroundColor(AppColors.text3(cs))
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.foregroundColor(AppColors.text3(cs))
|
||||||
|
.rotationEffect(.degrees(expanded ? 0 : -90))
|
||||||
|
.animation(KisaniSpring.micro, value: expanded)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14).padding(.vertical, 12)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture { withAnimation(KisaniSpring.snappy) { expanded.toggle() } }
|
||||||
|
|
||||||
|
if expanded {
|
||||||
|
ForEach(tasks) { task in
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button {
|
||||||
|
withAnimation(KisaniSpring.snappy) { onUndo(task) }
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "checkmark.square.fill")
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundColor(AppColors.text3(cs).opacity(0.6))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Text(task.title)
|
||||||
|
.font(AppFonts.sans(14))
|
||||||
|
.foregroundColor(AppColors.text3(cs))
|
||||||
|
.strikethrough(true, color: AppColors.text3(cs).opacity(0.7))
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
if let d = task.completedAt {
|
||||||
|
Text(dateFmt.string(from: d))
|
||||||
|
.font(AppFonts.sans(13))
|
||||||
|
.foregroundColor(AppColors.text3(cs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14).padding(.vertical, 10)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(AppColors.surface(cs))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Task Row
|
// MARK: - Task Row
|
||||||
struct TaskRowView: View {
|
struct TaskRowView: View {
|
||||||
@Environment(\.colorScheme) private var cs
|
@Environment(\.colorScheme) private var cs
|
||||||
|
|||||||
@@ -67,11 +67,25 @@ struct EventQuery: EntityQuery {
|
|||||||
|
|
||||||
// MARK: - Entry
|
// MARK: - Entry
|
||||||
|
|
||||||
|
// Tapping the countdown label cycles this unit (shared by all event widgets).
|
||||||
|
let countdownUnitKey = "kisani.widget.countdownUnit" // 0 = days, 1 = weeks, 2 = time
|
||||||
|
|
||||||
|
struct CycleCountdownUnitIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Change Countdown Unit"
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult {
|
||||||
|
let cur = UserDefaults.kisani.integer(forKey: countdownUnitKey)
|
||||||
|
UserDefaults.kisani.set((cur + 1) % 3, forKey: countdownUnitKey)
|
||||||
|
return .result()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct EventEntry: TimelineEntry {
|
struct EventEntry: TimelineEntry {
|
||||||
let date: Date
|
let date: Date
|
||||||
let eventName: String
|
let eventName: String
|
||||||
let start: Date
|
let start: Date
|
||||||
let target: Date
|
let target: Date
|
||||||
|
var unitMode: Int = 0 // 0 days, 1 weeks, 2 time
|
||||||
|
|
||||||
/// Fraction of the countdown window that has elapsed (0…1).
|
/// Fraction of the countdown window that has elapsed (0…1).
|
||||||
var progress: Double {
|
var progress: Double {
|
||||||
@@ -97,6 +111,30 @@ struct EventEntry: TimelineEntry {
|
|||||||
to: cal.startOfDay(for: target)).day ?? 0
|
to: cal.startOfDay(for: target)).day ?? 0
|
||||||
return max(0, d)
|
return max(0, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Countdown from the current date. Tapping the label cycles the unit:
|
||||||
|
/// days → weeks → time remaining.
|
||||||
|
var timeLeftLabel: String {
|
||||||
|
if target <= date { return "Today" }
|
||||||
|
let cal = Calendar.current
|
||||||
|
switch unitMode {
|
||||||
|
case 1: // weeks
|
||||||
|
let days = cal.dateComponents([.day], from: cal.startOfDay(for: date),
|
||||||
|
to: cal.startOfDay(for: target)).day ?? 0
|
||||||
|
if days < 7 { return "< 1 week left" }
|
||||||
|
let weeks = days / 7
|
||||||
|
return "\(weeks) week\(weeks == 1 ? "" : "s") left"
|
||||||
|
case 2: // time remaining (detailed)
|
||||||
|
let c = cal.dateComponents([.day, .hour], from: date, to: target)
|
||||||
|
let days = c.day ?? 0, hours = c.hour ?? 0
|
||||||
|
if days == 0 { return hours <= 0 ? "Due now" : "\(hours) hr left" }
|
||||||
|
return hours > 0 ? "\(days)d, \(hours) hr left" : "\(days) day\(days == 1 ? "" : "s") left"
|
||||||
|
default: // days
|
||||||
|
let days = cal.dateComponents([.day], from: cal.startOfDay(for: date),
|
||||||
|
to: cal.startOfDay(for: target)).day ?? 0
|
||||||
|
return "\(days) day\(days == 1 ? "" : "s") left"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Provider
|
// MARK: - Provider
|
||||||
@@ -133,7 +171,8 @@ struct EventProvider: AppIntentTimelineProvider {
|
|||||||
}
|
}
|
||||||
// Never let the start sit after the target.
|
// Never let the start sit after the target.
|
||||||
let start = min(config.startDate, target)
|
let start = min(config.startDate, target)
|
||||||
return EventEntry(date: .now, eventName: name, start: start, target: target)
|
let mode = UserDefaults.kisani.integer(forKey: countdownUnitKey)
|
||||||
|
return EventEntry(date: .now, eventName: name, start: start, target: target, unitMode: mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ struct LockScreenCircularWidget: Widget {
|
|||||||
struct KisaniWidgetBundle: WidgetBundle {
|
struct KisaniWidgetBundle: WidgetBundle {
|
||||||
var body: some Widget {
|
var body: some Widget {
|
||||||
EventCountdownWidget()
|
EventCountdownWidget()
|
||||||
|
MyTasksWidget()
|
||||||
DayProgressWidget()
|
DayProgressWidget()
|
||||||
TasksCompleteWidget()
|
TasksCompleteWidget()
|
||||||
LockScreenWidget()
|
LockScreenWidget()
|
||||||
|
|||||||
148
KisaniCalWidgets/MyTasksWidget.swift
Normal file
148
KisaniCalWidgets/MyTasksWidget.swift
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
|
// MARK: - Interactive intents
|
||||||
|
|
||||||
|
/// Tapping a checkbox toggles the task's completion directly in the App Group.
|
||||||
|
struct ToggleTaskIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Toggle Task Complete"
|
||||||
|
|
||||||
|
@Parameter(title: "Task ID")
|
||||||
|
var taskId: String
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
init(taskId: String) { self.taskId = taskId }
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult {
|
||||||
|
toggleWidgetTaskComplete(id: taskId)
|
||||||
|
return .result()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The "+" button opens the app and asks it to present the add-task sheet.
|
||||||
|
struct AddTaskFromWidgetIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Add Task"
|
||||||
|
static var openAppWhenRun = true
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult {
|
||||||
|
UserDefaults.kisani.set(true, forKey: "kisani.widget.openAddTask")
|
||||||
|
return .result()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Entry + Provider
|
||||||
|
|
||||||
|
struct TasksEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let tasks: [WidgetTask]
|
||||||
|
let openCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TasksProvider: TimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> TasksEntry {
|
||||||
|
TasksEntry(date: .now, tasks: [
|
||||||
|
WidgetTask(id: UUID(), title: "Pray for Uganda", dueDate: .now, isComplete: false,
|
||||||
|
quadrant: "Urgent + Important", category: "reminder"),
|
||||||
|
WidgetTask(id: UUID(), title: "Masters of the Universe", dueDate: .now, isComplete: false,
|
||||||
|
quadrant: "Schedule It", category: "reminder")
|
||||||
|
], openCount: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSnapshot(in context: Context, completion: @escaping (TasksEntry) -> Void) {
|
||||||
|
completion(loadEntry())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeline(in context: Context, completion: @escaping (Timeline<TasksEntry>) -> Void) {
|
||||||
|
let next = Calendar.current.startOfDay(for: Date().addingTimeInterval(86400))
|
||||||
|
completion(Timeline(entries: [loadEntry()], policy: .after(next)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadEntry() -> TasksEntry {
|
||||||
|
let tasks = todayWidgetTasks()
|
||||||
|
return TasksEntry(date: .now, tasks: tasks, openCount: tasks.filter { !$0.isComplete }.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Widget
|
||||||
|
|
||||||
|
struct MyTasksWidget: Widget {
|
||||||
|
let kind = "KisaniMyTasks"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: TasksProvider()) { entry in
|
||||||
|
MyTasksView(entry: entry)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("My Tasks")
|
||||||
|
.description("Today's tasks — check them off right from the widget.")
|
||||||
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MyTasksView: View {
|
||||||
|
let entry: TasksEntry
|
||||||
|
@Environment(\.widgetFamily) var family
|
||||||
|
|
||||||
|
private var maxRows: Int {
|
||||||
|
switch family {
|
||||||
|
case .systemLarge: return 9
|
||||||
|
case .systemMedium: return 4
|
||||||
|
default: return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("Today")
|
||||||
|
.font(.system(size: 18, weight: .bold))
|
||||||
|
.foregroundColor(Color(red: 0.42, green: 0.5, blue: 0.96))
|
||||||
|
Text("\(entry.openCount)")
|
||||||
|
.font(.system(size: 15, weight: .semibold))
|
||||||
|
.foregroundColor(Color(white: 0.5))
|
||||||
|
Spacer()
|
||||||
|
Button(intent: AddTaskFromWidgetIntent()) {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundColor(Color(red: 0.42, green: 0.5, blue: 0.96))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.tasks.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
Text("All clear for today 🎉")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(Color(white: 0.5))
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
Spacer()
|
||||||
|
} else {
|
||||||
|
ForEach(entry.tasks.prefix(maxRows)) { task in
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Button(intent: ToggleTaskIntent(taskId: task.id.uuidString)) {
|
||||||
|
Image(systemName: task.isComplete ? "checkmark.circle.fill" : "circle")
|
||||||
|
.font(.system(size: 18))
|
||||||
|
.foregroundColor(task.isComplete ? task.accentColor : Color(white: 0.45))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Text(task.title)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.foregroundColor(task.isComplete ? Color(white: 0.45) : .white)
|
||||||
|
.strikethrough(task.isComplete, color: Color(white: 0.45))
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entry.tasks.count > maxRows {
|
||||||
|
Text("+\(entry.tasks.count - maxRows) more")
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundColor(Color(white: 0.4))
|
||||||
|
.padding(.leading, 28)
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.black, for: .widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
// Minimal task model shared between widget and main app via App Group
|
// Minimal task model shared between widget and main app via App Group
|
||||||
struct WidgetTask: Codable, Identifiable {
|
struct WidgetTask: Codable, Identifiable {
|
||||||
@@ -84,6 +85,59 @@ func nextDueTask(from tasks: [WidgetTask]) -> WidgetTask? {
|
|||||||
.first
|
.first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func activeTasksKey() -> String {
|
||||||
|
let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared"
|
||||||
|
return "kisani.tasks.v2.\(uid)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks due today (incomplete first, then by due time) for the interactive list widget.
|
||||||
|
func todayWidgetTasks() -> [WidgetTask] {
|
||||||
|
let cal = Calendar.current
|
||||||
|
let start = cal.startOfDay(for: Date())
|
||||||
|
let end = cal.date(byAdding: .day, value: 1, to: start)!
|
||||||
|
return loadWidgetTasks()
|
||||||
|
.filter { t in
|
||||||
|
guard let due = t.dueDate else { return false }
|
||||||
|
return due >= start && due < end
|
||||||
|
}
|
||||||
|
.sorted {
|
||||||
|
if $0.isComplete != $1.isComplete { return !$0.isComplete }
|
||||||
|
return ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle a task's completion from the widget. Uses JSONSerialization so ALL task
|
||||||
|
// fields are preserved on write (the widget only models a subset of TaskItem).
|
||||||
|
func toggleWidgetTaskComplete(id: String) {
|
||||||
|
let key = activeTasksKey()
|
||||||
|
guard let data = UserDefaults.kisani.data(forKey: key),
|
||||||
|
var arr = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]]
|
||||||
|
else { return }
|
||||||
|
for i in arr.indices where (arr[i]["id"] as? String) == id {
|
||||||
|
let done = (arr[i]["isComplete"] as? Bool) ?? false
|
||||||
|
arr[i]["isComplete"] = !done
|
||||||
|
if !done {
|
||||||
|
// TaskItem uses the default (deferredToDate) Date strategy → seconds since reference date.
|
||||||
|
arr[i]["completedAt"] = Date().timeIntervalSinceReferenceDate
|
||||||
|
} else {
|
||||||
|
arr[i].removeValue(forKey: "completedAt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let out = try? JSONSerialization.data(withJSONObject: arr) {
|
||||||
|
UserDefaults.kisani.set(out, forKey: key)
|
||||||
|
CloudSyncManager_widgetPush(out, key)
|
||||||
|
}
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The widget extension can't see CloudSyncManager; mirror to iCloud KV store directly.
|
||||||
|
private func CloudSyncManager_widgetPush(_ data: Data, _ key: String) {
|
||||||
|
guard data.count < 900_000 else { return }
|
||||||
|
let kv = NSUbiquitousKeyValueStore.default
|
||||||
|
kv.set(data, forKey: key)
|
||||||
|
kv.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
// Today's task completion: (done, total) for tasks due today.
|
// Today's task completion: (done, total) for tasks due today.
|
||||||
func todayTaskCompletion() -> (done: Int, total: Int) {
|
func todayTaskCompletion() -> (done: Int, total: Int) {
|
||||||
let cal = Calendar.current
|
let cal = Calendar.current
|
||||||
|
|||||||
@@ -191,22 +191,31 @@ struct EventCountdownView: View {
|
|||||||
.containerBackground(.black, for: .widget)
|
.containerBackground(.black, for: .widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wide layout that mirrors the reference: title + percent, then a dot grid.
|
private var filled: Int { Int(entry.progress * Double(entry.totalDays)) }
|
||||||
|
|
||||||
|
// Wide layout: event name + days-left countdown, then the dot-grid progress.
|
||||||
private var medium: some View {
|
private var medium: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
HStack(alignment: .firstTextBaseline) {
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
Text(LocalizedStringKey(entry.eventName))
|
Text(LocalizedStringKey(entry.eventName))
|
||||||
.font(.system(size: 22, weight: .bold))
|
.font(.system(size: 20, weight: .bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(teal)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Spacer()
|
Spacer(minLength: 4)
|
||||||
Text("\(percent)%")
|
VStack(alignment: .trailing, spacing: 1) {
|
||||||
.font(.system(size: 22, weight: .bold))
|
Button(intent: CycleCountdownUnitIntent()) {
|
||||||
.foregroundColor(Color(white: 0.5))
|
Text(entry.timeLeftLabel)
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundColor(Color(white: 0.62))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
Text("\(percent)%")
|
||||||
|
.font(.system(size: 11, weight: .bold))
|
||||||
|
.foregroundColor(Color(white: 0.4))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DotGrid(total: entry.totalDays,
|
DotGrid(total: entry.totalDays, filled: filled, color: teal)
|
||||||
filled: Int(entry.progress * Double(entry.totalDays)),
|
|
||||||
color: teal)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 4)
|
.padding(.horizontal, 4)
|
||||||
}
|
}
|
||||||
@@ -215,20 +224,17 @@ struct EventCountdownView: View {
|
|||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(LocalizedStringKey(entry.eventName))
|
Text(LocalizedStringKey(entry.eventName))
|
||||||
.font(.system(size: 14, weight: .bold))
|
.font(.system(size: 14, weight: .bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(teal)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
Button(intent: CycleCountdownUnitIntent()) {
|
||||||
Text("\(entry.daysLeft)")
|
Text(entry.timeLeftLabel)
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(teal)
|
.foregroundColor(Color(white: 0.62))
|
||||||
Text(entry.daysLeft == 1 ? "day" : "days")
|
.lineLimit(1)
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(Color(white: 0.5))
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
Spacer(minLength: 2)
|
Spacer(minLength: 2)
|
||||||
DotGrid(total: entry.totalDays,
|
DotGrid(total: entry.totalDays, filled: filled, color: teal)
|
||||||
filled: Int(entry.progress * Double(entry.totalDays)),
|
|
||||||
color: teal)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user