diff --git a/KisaniCal.xcodeproj/project.pbxproj b/KisaniCal.xcodeproj/project.pbxproj index 85b6af9..922cc75 100644 --- a/KisaniCal.xcodeproj/project.pbxproj +++ b/KisaniCal.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; }; 1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; }; 3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72FDF9C8DD37134576356B89 /* Assets.xcassets */; }; + 497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */; }; 67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106EEF572C6F8990408329F0 /* OnboardingView.swift */; }; 6921CB73A3257502FF778381 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */; }; 7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; }; @@ -32,6 +33,7 @@ 106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = ""; }; 23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = ""; }; + 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = ""; }; 5722CC4B59E3939724142710 /* ExerciseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseModels.swift; sourceTree = ""; }; 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 72308FEE0226F45414C04DDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -135,6 +137,7 @@ isa = PBXGroup; children = ( 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */, + 326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */, ); path = Managers; sourceTree = ""; @@ -222,6 +225,7 @@ 67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */, A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */, 8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */, + 497732557745AE9BDA44FB2F /* ShortcutHandler.swift in Sources */, 6921CB73A3257502FF778381 /* SplashView.swift in Sources */, 096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */, C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */, diff --git a/KisaniCal/ContentView.swift b/KisaniCal/ContentView.swift index 67c351a..8af19d9 100644 --- a/KisaniCal/ContentView.swift +++ b/KisaniCal/ContentView.swift @@ -9,9 +9,11 @@ struct ContentView: View { @AppStorage("showMatrixTab") private var showMatrixTab = true @AppStorage("showWorkoutTab") private var showWorkoutTab = true @AppStorage("hasOnboarded") private var hasOnboarded = false - @State private var showOnboarding = false - @State private var selectedTab = 0 - @StateObject private var tabState = FloatingTabState() + @State private var showOnboarding = false + @State private var selectedTab = 0 + @State private var showQuickAddTask = false + @StateObject private var tabState = FloatingTabState() + @ObservedObject private var shortcuts = ShortcutHandler.shared @State private var iPadSelection: Int? = 0 @State private var calIconImage: Image = ContentView.buildCalIcon() @@ -92,6 +94,27 @@ struct ContentView: View { .onChange(of: workoutVM.doneSets) { _ in NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM) } + .onChange(of: shortcuts.pending) { action in + guard let action else { return } + switch action { + case .addTask: + showQuickAddTask = true + case .today: + withAnimation(KisaniSpring.micro) { selectedTab = 0 } + case .calendar: + withAnimation(KisaniSpring.micro) { selectedTab = 1 } + case .workout: + withAnimation(KisaniSpring.micro) { selectedTab = showWorkoutTab ? 3 : 0 } + } + shortcuts.consume() + } + .sheet(isPresented: $showQuickAddTask) { + AddTaskSheet(prefilledDate: Date()) + .environmentObject(taskVM) + .presentationDetents([.height(150)]) + .presentationDragIndicator(.visible) + } + .onChange(of: showWorkoutTab) { _ in ShortcutHandler.registerShortcuts() } } // MARK: - iPhone diff --git a/KisaniCal/KisaniCalApp.swift b/KisaniCal/KisaniCalApp.swift index ee75e6e..a79f146 100644 --- a/KisaniCal/KisaniCalApp.swift +++ b/KisaniCal/KisaniCalApp.swift @@ -25,6 +25,22 @@ final class AppDelegate: NSObject, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { NotificationManager.shared.requestPermission() + ShortcutHandler.registerShortcuts() + + // Cold launch from a shortcut + if let item = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem { + ShortcutHandler.shared.handle(item.type) + return false // tells UIKit we handled it + } return true } + + func application( + _ application: UIApplication, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void + ) { + ShortcutHandler.shared.handle(shortcutItem.type) + completionHandler(true) + } } diff --git a/KisaniCal/Managers/ShortcutHandler.swift b/KisaniCal/Managers/ShortcutHandler.swift new file mode 100644 index 0000000..3abd7a0 --- /dev/null +++ b/KisaniCal/Managers/ShortcutHandler.swift @@ -0,0 +1,70 @@ +import UIKit +import SwiftUI + +// MARK: - Quick action identifiers + +enum QuickAction: String { + case addTask = "com.kutesir.kisanical.addTask" + case today = "com.kutesir.kisanical.today" + case calendar = "com.kutesir.kisanical.calendar" + case workout = "com.kutesir.kisanical.workout" +} + +// MARK: - Singleton observable handler + +final class ShortcutHandler: ObservableObject { + static let shared = ShortcutHandler() + @Published var pending: QuickAction? + + private init() {} + + func handle(_ type: String) { + guard let action = QuickAction(rawValue: type) else { return } + DispatchQueue.main.async { self.pending = action } + } + + func consume() { + pending = nil + } +} + +// MARK: - Shortcut registration + +extension ShortcutHandler { + static func registerShortcuts() { + let ud = UserDefaults.standard + let showWorkout = ud.object(forKey: "showWorkoutTab") as? Bool ?? true + + var items: [UIApplicationShortcutItem] = [ + UIApplicationShortcutItem( + type: QuickAction.addTask.rawValue, + localizedTitle: "Add Task", + localizedSubtitle: "Create a new reminder", + icon: UIApplicationShortcutIcon(systemImageName: "plus.circle.fill") + ), + UIApplicationShortcutItem( + type: QuickAction.today.rawValue, + localizedTitle: "Today", + localizedSubtitle: "Tasks & agenda", + icon: UIApplicationShortcutIcon(systemImageName: "checkmark.circle") + ), + UIApplicationShortcutItem( + type: QuickAction.calendar.rawValue, + localizedTitle: "Calendar", + localizedSubtitle: "Month view", + icon: UIApplicationShortcutIcon(systemImageName: "calendar") + ), + ] + + if showWorkout { + items.append(UIApplicationShortcutItem( + type: QuickAction.workout.rawValue, + localizedTitle: "Workout", + localizedSubtitle: "Today's session", + icon: UIApplicationShortcutIcon(systemImageName: "dumbbell.fill") + )) + } + + UIApplication.shared.shortcutItems = items + } +}