krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

main: 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}