diff options
| author | Christian Cleberg <[email protected]> | 2026-08-07 03:16:14 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-08-07 03:16:14 -0500 |
| commit | 6c26cf048119bf4ff6af039991d3e34d76bac2ad (patch) | |
| tree | 22c3280dca5f85923ddc3cc85bff8751ad46b7fe /Hutch/App | |
| parent | 044c33f7f43db1f38d6c44dd8f483298d9efcc06 (diff) | |
| parent | 9c11d69002d71556fea1e2a938e02d4f4d503ea5 (diff) | |
| download | hutch-6c26cf048119bf4ff6af039991d3e34d76bac2ad.tar.gz hutch-6c26cf048119bf4ff6af039991d3e34d76bac2ad.tar.bz2 hutch-6c26cf048119bf4ff6af039991d3e34d76bac2ad.zip | |
Merge pull request #31 from krazywarez/app-intents-expansion
Expand App Intents: dialogs, search type, mutating intents (#17)
Diffstat (limited to 'Hutch/App')
| -rw-r--r-- | Hutch/App/DeepLink.swift | 21 | ||||
| -rw-r--r-- | Hutch/App/HutchIntents.swift | 147 | ||||
| -rw-r--r-- | Hutch/App/RootView.swift | 12 |
3 files changed, 123 insertions, 57 deletions
diff --git a/Hutch/App/DeepLink.swift b/Hutch/App/DeepLink.swift index 7467fc8..a0331a0 100644 --- a/Hutch/App/DeepLink.swift +++ b/Hutch/App/DeepLink.swift @@ -25,7 +25,7 @@ enum HutchRoute: Equatable, Sendable { case trackers case systemStatus case lookup - case search(query: String) + case search(query: String, type: LookupType?) case projectDashboard(id: String, title: String?) init?(url: URL) { @@ -103,7 +103,8 @@ enum HutchRoute: Equatable, Sendable { case "lookup": if let query = queryValue("q"), !query.isEmpty { - self = .search(query: query) + let type = queryValue("type").flatMap(LookupType.init(rawValue:)) + self = .search(query: query, type: type) } else { self = .lookup } @@ -148,8 +149,12 @@ enum HutchRoute: Equatable, Sendable { return Self.makeURL(host: "status") case .lookup: return Self.makeURL(host: "lookup") - case .search(let query): - return Self.makeURL(host: "lookup", queryItems: [URLQueryItem(name: "q", value: query)]) + case .search(let query, let type): + var items = [URLQueryItem(name: "q", value: query)] + if let type { + items.append(URLQueryItem(name: "type", value: type.rawValue)) + } + return Self.makeURL(host: "lookup", queryItems: items) case .projectDashboard(let id, let title): return Self.makeURL( host: "projects", @@ -205,8 +210,8 @@ enum DeepLink: Equatable { case systemStatus /// hutch://lookup case lookup - /// hutch://lookup?q=<query> - case search(query: String) + /// hutch://lookup?q=<query>&type=<type> + case search(query: String, type: LookupType?) /// hutch://builds?filter=failed case failedBuilds /// hutch://projects/<rid> @@ -253,8 +258,8 @@ enum DeepLink: Equatable { self = .systemStatus case .lookup: self = .lookup - case .search(let query): - self = .search(query: query) + case .search(let query, let type): + self = .search(query: query, type: type) case .projectDashboard(let id, let title): self = .projectDashboard(id: id, title: title) } diff --git a/Hutch/App/HutchIntents.swift b/Hutch/App/HutchIntents.swift index 947ec04..fe4ebdc 100644 --- a/Hutch/App/HutchIntents.swift +++ b/Hutch/App/HutchIntents.swift @@ -132,11 +132,14 @@ struct SearchHutchIntent: AppIntent { @Parameter(title: "Query") var query: String + @Parameter(title: "Search Type", default: .user) + var searchType: LookupType + var route: HutchRoute { let normalized = query.trimmingCharacters(in: .whitespacesAndNewlines) - // Routes to Lookup for now; repoint at a global content search when Hutch - // gains one — tracked in ROADMAP.md § "App Intent gaps". - return normalized.isEmpty ? .lookup : .search(query: normalized) + // Routes to Lookup, pre-selecting the search type, until Hutch gains a + // global content search. + return normalized.isEmpty ? .lookup : .search(query: normalized, type: searchType) } @MainActor @@ -147,7 +150,7 @@ struct SearchHutchIntent: AppIntent { } // An OpenSavedSearchIntent belongs here once Hutch has global saved-search -// persistence — tracked in ROADMAP.md § "App Intent gaps". +// persistence. // MARK: - App Entities @@ -201,7 +204,7 @@ struct ProjectEntityQuery: EntityQuery { } } -private enum HutchIntentEntityStore { +enum HutchIntentEntityStore { static func pinnedResources() -> [PinnedResourceEntity] { pins().compactMap { makePinnedResource(from: $0) } } @@ -213,22 +216,27 @@ private enum HutchIntentEntityStore { } } - private static func pins() -> [HomePinRecord] { + /// The active account's key, or `nil` when no account is signed in. + static func currentUserKey() -> String? { guard let userKey = ContributionWidgetContextStore.loadActor(), !userKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - return [] + return nil } - - return HomePinStore.loadPins(for: userKey, defaults: activeAccountDefaults) + return userKey } - private static var activeAccountDefaults: UserDefaults { + static var accountDefaults: UserDefaults { let activeID = UserDefaults.standard.string(forKey: AppStorageKeys.activeAccountID) ?? "" guard !activeID.isEmpty else { return .standard } return AccountDefaultsStore.userDefaults(for: activeID) } + private static func pins() -> [HomePinRecord] { + guard let userKey = currentUserKey() else { return [] } + return HomePinStore.loadPins(for: userKey, defaults: accountDefaults) + } + private static func makePinnedResource(from pin: HomePinRecord) -> PinnedResourceEntity? { guard let route = route(for: pin) else { return nil } return PinnedResourceEntity( @@ -274,20 +282,22 @@ struct CheckSystemStatusIntent: AppIntent { static var description = IntentDescription("Returns the current SourceHut system status.") @MainActor - func perform() async throws -> some IntentResult & ReturnsValue<String> { - guard let snapshot = SystemStatusWidgetSnapshotStore.load() else { - return .result(value: "System status is unavailable. Open Hutch to refresh.") - } - - if snapshot.hasDisruption { - let disrupted = snapshot.services - .filter { $0.requiresAttention } - .map { "\($0.name): \($0.status)" } - .joined(separator: ", ") - return .result(value: "SourceHut disruption detected: \(disrupted)") + func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { + let message: String + if let snapshot = SystemStatusWidgetSnapshotStore.load() { + if snapshot.hasDisruption { + let disrupted = snapshot.services + .filter { $0.requiresAttention } + .map { "\($0.name): \($0.status)" } + .joined(separator: ", ") + message = "SourceHut disruption detected: \(disrupted)" + } else { + message = "All SourceHut services operational." + } + } else { + message = "System status is unavailable. Open Hutch to refresh." } - - return .result(value: "All SourceHut services operational.") + return .result(value: message, dialog: IntentDialog(stringLiteral: message)) } } @@ -296,34 +306,85 @@ struct CheckBuildsIntent: AppIntent { static var description = IntentDescription("Returns a summary of your recent build status.") @MainActor - func perform() async throws -> some IntentResult & ReturnsValue<String> { - guard let snapshot = NeedsAttentionSnapshotStore.load() else { - return .result(value: "Build status unavailable. Open Hutch to refresh.") - } + func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { + let message: String + if let snapshot = NeedsAttentionSnapshotStore.load() { + var parts: [String] = [] + + if let failed = snapshot.failedBuilds { + if failed > 0 { + parts.append("\(failed) failed build\(failed == 1 ? "" : "s")") + } else { + parts.append("No failed builds") + } + } - var parts: [String] = [] + if let unread = snapshot.unreadInboxThreads, unread > 0 { + parts.append("\(unread) unread thread\(unread == 1 ? "" : "s")") + } - if let failed = snapshot.failedBuilds { - if failed > 0 { - parts.append("\(failed) failed build\(failed == 1 ? "" : "s")") - } else { - parts.append("No failed builds") + if let assigned = snapshot.assignedOpenTickets, assigned > 0 { + parts.append("\(assigned) assigned ticket\(assigned == 1 ? "" : "s")") } - } - if let unread = snapshot.unreadInboxThreads, unread > 0 { - parts.append("\(unread) unread thread\(unread == 1 ? "" : "s")") + message = parts.isEmpty ? "No recent data. Open Hutch to refresh." : parts.joined(separator: ". ") + "." + } else { + message = "Build status unavailable. Open Hutch to refresh." } + return .result(value: message, dialog: IntentDialog(stringLiteral: message)) + } +} - if let assigned = snapshot.assignedOpenTickets, assigned > 0 { - parts.append("\(assigned) assigned ticket\(assigned == 1 ? "" : "s")") - } +// MARK: - Search Type - if parts.isEmpty { - return .result(value: "No recent data. Open Hutch to refresh.") - } +extension LookupType: @retroactive AppEnum { + public nonisolated static var typeDisplayRepresentation: TypeDisplayRepresentation { + TypeDisplayRepresentation(name: "Search Type") + } + + public nonisolated static var caseDisplayRepresentations: [LookupType: DisplayRepresentation] { + [ + .user: "User", + .gitRepo: "Git Repository", + .hgRepo: "Mercurial Repository", + .mailingList: "Mailing List", + .tracker: "Tracker", + .buildJob: "Build Job" + ] + } +} + +// MARK: - Mutating Intents + +struct ClearRecentActivityIntent: AppIntent { + static var title: LocalizedStringResource = "Clear Recent Activity" + static var description = IntentDescription("Clears the Recent list on the Hutch Home tab.") + + @MainActor + func perform() async throws -> some IntentResult & ProvidesDialog { + RecentActivityStore.clear(defaults: HutchIntentEntityStore.accountDefaults) + return .result(dialog: "Cleared recent activity.") + } +} + +struct UnpinResourceIntent: AppIntent { + static var title: LocalizedStringResource = "Unpin Resource" + static var description = IntentDescription("Removes a pinned resource from the Hutch Home tab.") - return .result(value: parts.joined(separator: ". ") + ".") + @Parameter(title: "Pinned Resource") + var pinnedResource: PinnedResourceEntity + + @MainActor + func perform() async throws -> some IntentResult & ProvidesDialog { + guard let userKey = HutchIntentEntityStore.currentUserKey() else { + return .result(dialog: "No active Hutch account.") + } + HomePinStore.removePin( + id: pinnedResource.id, + for: userKey, + defaults: HutchIntentEntityStore.accountDefaults + ) + return .result(dialog: "Unpinned \(pinnedResource.name).") } } diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift index 2e16718..a3f9e7e 100644 --- a/Hutch/App/RootView.swift +++ b/Hutch/App/RootView.swift @@ -247,12 +247,12 @@ struct RootView: View { appState.pendingBuildListFilter = .failed appState.selectedTab = .builds - case .search(let query): + case .search(let query, let type): morePath = NavigationPath() appState.selectedTab = .more Task { await settleNavigationTransition() - morePath.append(MoreRoute.lookup(query: query)) + morePath.append(MoreRoute.lookup(query: query, type: type)) } case .lookup: @@ -260,7 +260,7 @@ struct RootView: View { appState.selectedTab = .more Task { await settleNavigationTransition() - morePath.append(MoreRoute.lookup(query: nil)) + morePath.append(MoreRoute.lookup(query: nil, type: nil)) } case .buildsTab: @@ -431,7 +431,7 @@ enum MoreDestination: Hashable { } enum MoreRoute: Hashable { - case lookup(query: String?) + case lookup(query: String?, type: LookupType?) case projects case lists case pastes @@ -454,8 +454,8 @@ private struct MoreNavigationRoot: View { MoreView() .navigationDestination(for: MoreRoute.self) { route in switch route { - case .lookup(let query): - LookupView(initialQuery: query ?? "") + case .lookup(let query, let type): + LookupView(initialQuery: query ?? "", initialType: type) case .projects: ProjectsListView() case .lists: |
