// Screens the app can open, and the `wristcode://` URLs the complication uses to open them. import Foundation enum Route: Hashable, Sendable { case thread(hostId: String, threadId: String) case newThread /// Every thread in Recent, past the feed's first 10. case recent /// The status overview, and from it one host or the notifications page. case status case host(id: String) case notifications /// All threads: every host, then one host's projects, then one project's threads. case allThreads case hostProjects(hostId: String) case projectThreads(hostId: String, projectId: String) /// Threads on every host matching a search from All threads. case search(query: String) static let scheme = "wristcode" /// `wristcode://thread//`, `wristcode://new`, `wristcode://recent`, `wristcode://status`, /// `wristcode://host/`, `wristcode://notifications`, `wristcode://threads`, /// `wristcode://projects/`, `wristcode://project//` or `wristcode://search?q=`. var url: URL { var components = URLComponents() components.scheme = Self.scheme func path(_ parts: String...) -> String { "/" + parts.map { $0.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? $0 }.joined(separator: "/") } switch self { case let .thread(hostId, threadId): components.host = "thread" components.percentEncodedPath = path(hostId, threadId) case .newThread: components.host = "new" case .recent: components.host = "recent" case .status: components.host = "status" case let .host(id): components.host = "host" components.percentEncodedPath = path(id) case .notifications: components.host = "notifications" case .allThreads: components.host = "threads" case let .hostProjects(hostId): components.host = "projects" components.percentEncodedPath = path(hostId) case let .projectThreads(hostId, projectId): components.host = "project" components.percentEncodedPath = path(hostId, projectId) case let .search(query): components.host = "search" components.queryItems = [URLQueryItem(name: "q", value: query)] } return components.url! } /// The route a `wristcode://` URL names, or nil when it names none. init?(url: URL) { guard url.scheme == Self.scheme, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil } let parts = components.percentEncodedPath.split(separator: "/").map { $0.removingPercentEncoding ?? String($0) } switch components.host { case "thread" where parts.count == 2 && parts.allSatisfy { !$0.isEmpty }: self = .thread(hostId: parts[0], threadId: parts[1]) case "new": self = .newThread case "recent": self = .recent case "status": self = .status case "host" where parts.count == 1 && !parts[0].isEmpty: self = .host(id: parts[0]) case "notifications": self = .notifications case "threads": self = .allThreads case "projects" where parts.count == 1 && !parts[0].isEmpty: self = .hostProjects(hostId: parts[0]) case "project" where parts.count == 2 && parts.allSatisfy { !$0.isEmpty }: self = .projectThreads(hostId: parts[0], projectId: parts[1]) case "search": guard let query = components.queryItems?.first(where: { $0.name == "q" })?.value, !query.isEmpty else { return nil } self = .search(query: query) default: return nil } } }