oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import RastrilloApple
2import SwiftUI
3
4@MainActor struct DocsJumpControls: View {
5 @Binding var presented: Bool
6 var body: some View {
7 Button { presented = true } label: {
8 Label { Text("docs.jumpto.label", bundle: DocsResources.bundle) } icon: { Image(systemName: "magnifyingglass") }
9 }
10 .keyboardShortcut("k", modifiers: .command)
11 .accessibilityIdentifier("docs.jump")
12 }
13}
14
15private struct DocumentSearchResult: Decodable, Identifiable {
16 let title: String
17 let href: String
18 let when: String
19 var id: String { href }
20 var sqid: String? {
21 guard href.hasPrefix("/d/") else { return nil }
22 let value = String(href.dropFirst(3))
23 return !value.isEmpty && value.utf8.allSatisfy { (48...57).contains($0) || (65...90).contains($0) || (97...122).contains($0) } ? value : nil
24 }
25}
26
27@MainActor struct DocsJumpSheet: View {
28 let model: DocsModel
29 let client: NativeHTTP
30 @Environment(\.dismiss) private var dismiss
31 @FocusState private var searchFocused: Bool
32 @State private var query = ""
33 @State private var results: [DocumentSearchResult] = []
34 @State private var resultsQuery = ""
35 @State private var selected = 0
36 @State private var status: String?
37 @State private var working = false
38 var body: some View {
39 NavigationStack {
40 VStack(spacing: 0) {
41 TextField(String(localized: "docs.jumpto.placeholder", bundle: DocsResources.bundle), text: $query)
42 .textFieldStyle(.roundedBorder)
43 .focused($searchFocused)
44 .accessibilityIdentifier("docs.jump.query")
45 .accessibilityHint(Text("docs.jumpto.hint", bundle: DocsResources.bundle))
46 .onSubmit { openSelected() }
47 .onKeyPress(.return) { openSelected(); return .handled }
48 .onKeyPress(.upArrow) { move(-1); return .handled }
49 .onKeyPress(.downArrow) { move(1); return .handled }
50 .padding()
51 if let status { Text(LocalizedStringKey(status), bundle: DocsResources.bundle).foregroundStyle(.secondary).padding(.horizontal) }
52 if working { ProgressView().padding() }
53 ScrollViewReader { reader in
54 List(Array(results.enumerated()), id: \.element.id) { index, result in
55 Button { open(result) } label: {
56 VStack(alignment: .leading) {
57 Text(result.title)
58 Text(result.when).font(.caption).foregroundStyle(.secondary)
59 }.frame(maxWidth: .infinity, alignment: .leading).contentShape(Rectangle())
60 }
61 .buttonStyle(.plain)
62 .accessibilityAddTraits(index == selected ? .isSelected : [])
63 .listRowBackground(index == selected ? Color.accentColor.opacity(0.12) : Color.clear)
64 .id(result.id)
65 }
66 .onChange(of: selected) { if results.indices.contains(selected) { reader.scrollTo(results[selected].id) } }
67 }
68 }
69 .disabled(working)
70 .navigationTitle(Text("docs.jumpto.label", bundle: DocsResources.bundle))
71 .toolbar {
72 ToolbarItem(placement: .cancellationAction) {
73 Button { dismiss() } label: { Text("docs.jumpto.close", bundle: DocsResources.bundle) }
74 .keyboardShortcut(.cancelAction)
75 .disabled(working)
76 }
77 }
78 }
79 .task { searchFocused = true }
80 .task(id: query) { await search() }
81 .interactiveDismissDisabled(working)
82 #if os(macOS)
83 .frame(minWidth: 460, minHeight: 400)
84 #else
85 .presentationDetents([.medium, .large])
86 #endif
87 }
88 private func move(_ delta: Int) {
89 guard !results.isEmpty else { return }
90 selected = max(0, min(results.count - 1, selected + delta))
91 }
92 private func search() async {
93 guard !Task.isCancelled else { return }
94 let searching = query
95 results = []; selected = 0; status = nil
96 guard !searching.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
97 do {
98 try await Task.sleep(for: .milliseconds(120))
99 var components = URLComponents()
100 components.queryItems = [URLQueryItem(name: "q", value: searching)]
101 let encoded = (components.percentEncodedQuery ?? "").replacingOccurrences(of: "+", with: "%2B")
102 let response = try await client.responseDetails("/documents/search?\(encoded)")
103 guard response.status == 200 else { throw CRDTReplica.Failure.rejected }
104 let rows = try JSONDecoder().decode([DocumentSearchResult].self, from: response.data)
105 guard !Task.isCancelled, query == searching else { return }
106 results = rows.filter { $0.sqid != nil }
107 resultsQuery = searching
108 status = response.header("Docs-Catalog-Partial") == "1" ? "docs.team.partial" : (results.isEmpty ? "docs.jumpto.empty" : nil)
109 } catch {
110 guard !Task.isCancelled, query == searching else { return }
111 status = "docs.jumpto.failed"
112 }
113 }
114 private func openSelected() {
115 guard results.indices.contains(selected) else { return }
116 open(results[selected])
117 }
118 private func open(_ result: DocumentSearchResult) {
119 guard !working, query == resultsQuery, let sqid = result.sqid else { return }
120 working = true
121 Task {
122 defer { working = false }
123 guard await model.saveAll() else { status = "native.pending"; return }
124 await model.open(sqid)
125 if model.detail?.sqid == sqid { dismiss() }
126 else { status = "docs.error.generic" }
127 }
128 }
129}
130