Event widget: upcoming-queue auto-select + cleaner config & preview

- Add "Upcoming queue" toggle to the Event Countdown widget. When on, it
  auto-tracks the Nth nearest upcoming event (Nearest / 2nd … 5th, new
  AutoSelectRank enum). When off, track a picked or fully custom event.
- parameterSummary now shows only the relevant fields per mode (toggle +
  auto-select, or event/name/date), so the date input only appears for
  custom events.
- Replace the dense 380-day placeholder with a ~90-day window so the
  gallery preview shows a legible dot grid.

(The interactive task-checkbox widget already exists as "My Tasks".)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-07 12:43:02 +03:00
parent 72f4566d91
commit 7b98be1d8d
4 changed files with 109 additions and 33 deletions

View File

@@ -80,7 +80,7 @@
0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.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>"; }; 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; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; }; 106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; }; 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>"; }; 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = "<group>"; };
@@ -104,7 +104,7 @@
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>"; }; 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; explicitFileType = wrapper.application; includeInIndex = 0; 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>"; };
@@ -289,7 +289,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1500; LastUpgradeCheck = 2620;
TargetAttributes = { TargetAttributes = {
28B43516AD88946E21D9BFE0 = { 28B43516AD88946E21D9BFE0 = {
DevelopmentTeam = K8BLMMR883; DevelopmentTeam = K8BLMMR883;
@@ -405,6 +405,7 @@
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements; CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal"; INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
@@ -416,13 +417,14 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2;
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_VERSION = 5.9; SWIFT_VERSION = 5.9;
@@ -469,6 +471,7 @@
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;
@@ -483,6 +486,7 @@
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;
@@ -495,10 +499,16 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements; CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
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 = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2;
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;
@@ -545,6 +555,7 @@
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;
@@ -566,6 +577,7 @@
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;
@@ -578,10 +590,16 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements; CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
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 = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2;
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;
@@ -597,6 +615,7 @@
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements; CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal"; INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
@@ -608,13 +627,14 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2;
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_VERSION = 5.9; SWIFT_VERSION = 5.9;

View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1500" LastUpgradeVersion = "2620"
version = "1.7"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -27,8 +26,7 @@
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"
@@ -40,8 +38,6 @@
</MacroExpansion> </MacroExpansion>
<Testables> <Testables>
</Testables> </Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -63,8 +59,6 @@
ReferencedContainer = "container:KisaniCal.xcodeproj"> ReferencedContainer = "container:KisaniCal.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -82,8 +76,6 @@
ReferencedContainer = "container:KisaniCal.xcodeproj"> ReferencedContainer = "container:KisaniCal.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">

View File

@@ -10,12 +10,32 @@ import AppIntents
// persisted per-instance by WidgetKit, the widget is fully self-contained and does // persisted per-instance by WidgetKit, the widget is fully self-contained and does
// not depend on App Group data sharing. // not depend on App Group data sharing.
// Which upcoming event to auto-track when the queue is on.
enum AutoSelectRank: Int, AppEnum {
case first = 1, second, third, fourth, fifth
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Auto Select"
static var caseDisplayRepresentations: [AutoSelectRank: DisplayRepresentation] = [
.first: "Nearest to finish",
.second: "2nd nearest to finish",
.third: "3rd nearest to finish",
.fourth: "4th nearest to finish",
.fifth: "5th nearest to finish",
]
}
struct EventConfigIntent: WidgetConfigurationIntent { struct EventConfigIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Event Countdown" static var title: LocalizedStringResource = "Event Countdown"
static var description = IntentDescription("Count down to any upcoming event with a dot grid.") static var description = IntentDescription("Count down to an event with a dot grid.")
// When on, the widget auto-tracks one of your upcoming events. When off, you
// track a specific event you pick, or a fully custom name + date.
@Parameter(title: "Upcoming queue", default: true)
var upcomingQueue: Bool
@Parameter(title: "Auto select", default: .first)
var autoSelect: AutoSelectRank
// Pick one of your existing events (tasks with a due date). When set, it
// overrides the manual name/date fields below.
@Parameter(title: "Track event") @Parameter(title: "Track event")
var event: EventEntity? var event: EventEntity?
@@ -27,6 +47,25 @@ struct EventConfigIntent: WidgetConfigurationIntent {
@Parameter(title: "Counting from", default: Date.now) @Parameter(title: "Counting from", default: Date.now)
var startDate: Date var startDate: Date
// Show only the relevant fields depending on the toggle like a clean,
// single-purpose config screen instead of every field at once.
static var parameterSummary: some ParameterSummary {
When(\.$upcomingQueue, .equalTo, true) {
Summary("Track upcoming event") {
\.$upcomingQueue
\.$autoSelect
}
} otherwise: {
Summary("Track a custom event") {
\.$upcomingQueue
\.$event
\.$eventName
\.$targetDate
\.$startDate
}
}
}
} }
// MARK: - Event picker (reads your tasks from the App Group) // MARK: - Event picker (reads your tasks from the App Group)
@@ -141,10 +180,11 @@ struct EventEntry: TimelineEntry {
struct EventProvider: AppIntentTimelineProvider { struct EventProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> EventEntry { func placeholder(in context: Context) -> EventEntry {
// ~90-day window, ~60% elapsed a clear, legible dot grid in the gallery.
EventEntry(date: .now, EventEntry(date: .now,
eventName: "Mum's Birthday", eventName: "IELTS",
start: Date().addingTimeInterval(-86400 * 320), start: Date().addingTimeInterval(-86400 * 54),
target: Date().addingTimeInterval(86400 * 60)) target: Date().addingTimeInterval(86400 * 36))
} }
func snapshot(for configuration: EventConfigIntent, in context: Context) async -> EventEntry { func snapshot(for configuration: EventConfigIntent, in context: Context) async -> EventEntry {
@@ -158,19 +198,43 @@ struct EventProvider: AppIntentTimelineProvider {
return Timeline(entries: [entry], policy: .after(nextMidnight)) return Timeline(entries: [entry], policy: .after(nextMidnight))
} }
/// Upcoming events (incomplete tasks with a future due date), nearest first.
private func upcomingEvents() -> [EventEntity] {
loadWidgetTasks()
.filter { !$0.isComplete && ($0.dueDate ?? .distantPast) > Date() }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
.map { EventEntity(id: $0.id.uuidString, title: $0.title, dueDate: $0.dueDate) }
}
private func entry(for config: EventConfigIntent) -> EventEntry { private func entry(for config: EventConfigIntent) -> EventEntry {
let name: String var name: String
let target: Date var target: Date
if let ev = config.event, let due = ev.dueDate {
// A picked event supplies its own name + date. if config.upcomingQueue {
// Auto-track the Nth nearest upcoming event.
let upcoming = upcomingEvents()
let idx = max(0, config.autoSelect.rawValue - 1)
if idx < upcoming.count, let due = upcoming[idx].dueDate {
name = upcoming[idx].title
target = due
} else {
name = "No upcoming event"
target = Date().addingTimeInterval(86400)
}
} else if let ev = config.event, let due = ev.dueDate {
// A specifically picked event supplies its own name + date.
name = ev.title name = ev.title
target = due target = due
} else { } else {
// Fully custom event.
name = config.eventName.isEmpty ? "My Event" : config.eventName name = config.eventName.isEmpty ? "My Event" : config.eventName
target = config.targetDate target = config.targetDate
} }
// Never let the start sit after the target.
let start = min(config.startDate, target) // Counting anchor: custom mode honors the picker; auto mode counts from
// "now" so the grid fills toward the event. Never start after the target.
let anchor = config.upcomingQueue ? Date() : config.startDate
let start = min(anchor, target)
let mode = UserDefaults.kisani.integer(forKey: countdownUnitKey) let mode = UserDefaults.kisani.integer(forKey: countdownUnitKey)
return EventEntry(date: .now, eventName: name, start: start, target: target, unitMode: mode) return EventEntry(date: .now, eventName: name, start: start, target: target, unitMode: mode)
} }