diff --git a/KisaniCal.xcodeproj/project.pbxproj b/KisaniCal.xcodeproj/project.pbxproj index ca712ce..7be4def 100644 --- a/KisaniCal.xcodeproj/project.pbxproj +++ b/KisaniCal.xcodeproj/project.pbxproj @@ -80,7 +80,7 @@ 0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLiveActivity.swift; sourceTree = ""; }; 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = ""; }; - 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 = ""; }; 1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = ""; }; 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = ""; }; @@ -104,7 +104,7 @@ 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = ""; }; 92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = ""; }; 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; - 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 = ""; }; AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -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; diff --git a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate index 14b2b42..beae5f6 100644 Binary files a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate and b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme index 9db359c..675309e 100644 --- a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme +++ b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme @@ -1,11 +1,10 @@ + LastUpgradeVersion = "2620" + version = "1.3"> + buildImplicitDependencies = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - diff --git a/KisaniCalWidgets/EventCountdownWidget.swift b/KisaniCalWidgets/EventCountdownWidget.swift index 45c4fab..71ae09e 100644 --- a/KisaniCalWidgets/EventCountdownWidget.swift +++ b/KisaniCalWidgets/EventCountdownWidget.swift @@ -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) }