summaryrefslogtreecommitdiff
path: root/Hutch/Views/More/ManPageDetailView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-03 15:18:38 -0500
committerChristian Cleberg <[email protected]>2026-04-03 15:18:38 -0500
commit0914829ad4cfdbe85db9d54c4e5cc1fdd44b82be (patch)
tree4f5c1e7cea0737917e9db3fad458be58827842ba /Hutch/Views/More/ManPageDetailView.swift
parent18a9f62c9acadced60657addb61c2d4d8c1aab17 (diff)
downloadhutch-0914829ad4cfdbe85db9d54c4e5cc1fdd44b82be.tar.gz
hutch-0914829ad4cfdbe85db9d54c4e5cc1fdd44b82be.tar.bz2
hutch-0914829ad4cfdbe85db9d54c4e5cc1fdd44b82be.zip
feat: add in-app man pages browser
Add a native Man Pages section to the More tab with in-app browsing for man.sr.ht documentation and srht.site pages. - add ManPageService for fetching and extracting public docs content - add ManPageBrowserView and ManPageDetailView - add More tab navigation for Man Pages - support in-place navigation for internal documentation links - allow HTMLWebView to intercept internal links and use a base URL - support man.sr.ht and srht.site content extraction - remove heading permalink "#" anchors from man page rendering - fix relative links, fragment links, and srht.site CTA/icon layout
Diffstat (limited to 'Hutch/Views/More/ManPageDetailView.swift')
-rw-r--r--Hutch/Views/More/ManPageDetailView.swift103
1 files changed, 103 insertions, 0 deletions
diff --git a/Hutch/Views/More/ManPageDetailView.swift b/Hutch/Views/More/ManPageDetailView.swift
new file mode 100644
index 0000000..96a92fb
--- /dev/null
+++ b/Hutch/Views/More/ManPageDetailView.swift
@@ -0,0 +1,103 @@
+import SwiftUI
+
+/// Fetches and renders a single man.sr.ht page.
+/// Internal man.sr.ht links update the current URL in-place rather than
+/// opening the browser, so the user can follow wiki links without leaving
+/// the view.
+struct ManPageDetailView: View {
+ let initialURL: URL
+
+ @Environment(\.colorScheme) private var colorScheme
+ @State private var currentURL: URL
+ @State private var page: ManPage?
+ @State private var isLoading = false
+ @State private var error: String?
+
+ init(url: URL) {
+ initialURL = url
+ _currentURL = State(initialValue: url)
+ }
+
+ var body: some View {
+ ScrollView {
+ if isLoading {
+ SRHTLoadingStateView(message: "Loading page…")
+ .padding(.top, 40)
+ } else if let error {
+ SRHTErrorStateView(
+ title: "Couldn't Load Page",
+ message: error,
+ retryAction: { await loadPage() }
+ )
+ .padding()
+ } else if let page {
+ HTMLWebView(
+ html: page.contentHTML,
+ colorScheme: colorScheme,
+ style: .readme,
+ baseURL: page.url,
+ onInterceptURL: { url in
+ guard let destinationURL = normalizedManPageURL(for: url) else {
+ return false
+ }
+ currentURL = destinationURL
+ return true
+ }
+ )
+ .padding()
+ }
+ }
+ .navigationTitle(page?.title ?? "Documentation")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ if let page {
+ Link(destination: page.url) {
+ Image(systemName: "safari")
+ }
+ }
+ }
+ }
+ .task(id: currentURL) {
+ await loadPage()
+ }
+ }
+
+ private func loadPage() async {
+ isLoading = true
+ error = nil
+
+ do {
+ page = try await ManPageService.fetch(url: currentURL)
+ } catch {
+ self.error = error.localizedDescription
+ }
+
+ isLoading = false
+ }
+
+ private func normalizedManPageURL(for url: URL) -> URL? {
+ if ManPageService.isTrustedDocumentationURL(url) {
+ return url
+ }
+
+ guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+ let scheme = components.scheme?.lowercased(),
+ scheme == "about" || scheme == "file" else {
+ return nil
+ }
+
+ let rawPath = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
+ guard !rawPath.isEmpty else {
+ if currentURL.host?.lowercased() == "srht.site" {
+ return ManPageService.pagesBaseURL
+ }
+ return ManPageService.baseURL
+ }
+
+ if currentURL.host?.lowercased() == "srht.site" {
+ return URL(string: "https://srht.site/\(rawPath)") ?? ManPageService.pagesBaseURL
+ }
+ return URL(string: "https://man.sr.ht/\(rawPath)/") ?? ManPageService.baseURL
+ }
+}