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:
@@ -80,7 +80,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -104,7 +104,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -289,7 +289,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1500;
|
||||
LastUpgradeCheck = 2620;
|
||||
TargetAttributes = {
|
||||
28B43516AD88946E21D9BFE0 = {
|
||||
DevelopmentTeam = K8BLMMR883;
|
||||
@@ -405,6 +405,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||
@@ -416,13 +417,14 @@
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -469,6 +471,7 @@
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -483,6 +486,7 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -495,10 +499,16 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -545,6 +555,7 @@
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -566,6 +577,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -578,10 +590,16 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -597,6 +615,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||
@@ -608,13 +627,14 @@
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -27,8 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -40,8 +38,6 @@
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -63,8 +59,6 @@
|
||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -82,8 +76,6 @@
|
||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -10,12 +10,32 @@ import AppIntents
|
||||
// persisted per-instance by WidgetKit, the widget is fully self-contained and does
|
||||
// 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 {
|
||||
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")
|
||||
var event: EventEntity?
|
||||
|
||||
@@ -27,6 +47,25 @@ struct EventConfigIntent: WidgetConfigurationIntent {
|
||||
|
||||
@Parameter(title: "Counting from", default: Date.now)
|
||||
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)
|
||||
@@ -141,10 +180,11 @@ struct EventEntry: TimelineEntry {
|
||||
|
||||
struct EventProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in context: Context) -> EventEntry {
|
||||
// ~90-day window, ~60% elapsed → a clear, legible dot grid in the gallery.
|
||||
EventEntry(date: .now,
|
||||
eventName: "Mum's Birthday",
|
||||
start: Date().addingTimeInterval(-86400 * 320),
|
||||
target: Date().addingTimeInterval(86400 * 60))
|
||||
eventName: "IELTS",
|
||||
start: Date().addingTimeInterval(-86400 * 54),
|
||||
target: Date().addingTimeInterval(86400 * 36))
|
||||
}
|
||||
|
||||
func snapshot(for configuration: EventConfigIntent, in context: Context) async -> EventEntry {
|
||||
@@ -158,19 +198,43 @@ struct EventProvider: AppIntentTimelineProvider {
|
||||
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 {
|
||||
let name: String
|
||||
let target: Date
|
||||
if let ev = config.event, let due = ev.dueDate {
|
||||
// A picked event supplies its own name + date.
|
||||
var name: String
|
||||
var target: 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
|
||||
target = due
|
||||
} else {
|
||||
// Fully custom event.
|
||||
name = config.eventName.isEmpty ? "My Event" : config.eventName
|
||||
target = config.targetDate
|
||||
}
|
||||
// Never let the start sit after the target.
|
||||
let start = min(config.startDate, target)
|
||||
|
||||
// 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)
|
||||
return EventEntry(date: .now, eventName: name, start: start, target: target, unitMode: mode)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user