krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.2.0: Hutch/Views/More/ManPageDetailView.swift · raw
1import SwiftUI
2
3/// Fetches and renders a single man.sr.ht page.
4/// Internal man.sr.ht links update the current URL in-place rather than
5/// opening the browser, so the user can follow wiki links without leaving
6/// the view.
7struct ManPageDetailView: View {
8 let initialURL: URL
9
10 @Environment(\.colorScheme) private var colorScheme
11 @State private var currentURL: URL
12 @State private var page: ManPage?
13 @State private var isLoading = false
14 @State private var error: String?
15
16 init(url: URL) {
17 initialURL = url
18 _currentURL = State(initialValue: url)
19 }
20
21 var body: some View {
22 ScrollView {
23 if isLoading {
24 SRHTLoadingStateView(message: "Loading page…")
25 .padding(.top, 40)
26 } else if let error {
27 SRHTErrorStateView(
28 title: "Couldn't Load Page",
29 message: error,
30 retryAction: { await loadPage() }
31 )
32 .padding()
33 } else if let page {
34 HTMLWebView(
35 html: page.contentHTML,
36 colorScheme: colorScheme,
37 style: .readme,
38 baseURL: page.url,
39 onInterceptURL: { url in
40 guard let destinationURL = normalizedManPageURL(for: url) else {
41 return false
42 }
43 currentURL = destinationURL
44 return true
45 }
46 )
47 .padding()
48 }
49 }
50 .navigationTitle(page?.title ?? "Documentation")
51 .navigationBarTitleDisplayMode(.inline)
52 .toolbar {
53 ToolbarItem(placement: .topBarTrailing) {
54 if let page {
55 Link(destination: page.url) {
56 Image(systemName: "safari")
57 }
58 }
59 }
60 }
61 .task(id: currentURL) {
62 await loadPage()
63 }
64 }
65
66 private func loadPage() async {
67 isLoading = true
68 error = nil
69
70 do {
71 page = try await ManPageService.fetch(url: currentURL)
72 } catch {
73 self.error = error.localizedDescription
74 }
75
76 isLoading = false
77 }
78
79 private func normalizedManPageURL(for url: URL) -> URL? {
80 if ManPageService.isTrustedDocumentationURL(url) {
81 return url
82 }
83
84 guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
85 let scheme = components.scheme?.lowercased(),
86 scheme == "about" || scheme == "file" else {
87 return nil
88 }
89
90 let rawPath = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
91 guard !rawPath.isEmpty else {
92 if currentURL.host?.lowercased() == "srht.site" {
93 return ManPageService.pagesBaseURL
94 }
95 return ManPageService.baseURL
96 }
97
98 if currentURL.host?.lowercased() == "srht.site" {
99 return URL(string: "https://srht.site/\(rawPath)") ?? ManPageService.pagesBaseURL
100 }
101 return URL(string: "https://man.sr.ht/\(rawPath)/") ?? ManPageService.baseURL
102 }
103}