~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
129 lines · 4.8 KB Raw History
import SwiftUI
import WebKit

/// A picture Markdown names, at its own size unless that's wider than the
/// page. One that won't load says what it was of.
struct MarkdownImage: View {
    let urls: [URL]
    let alt: String

    private enum Loaded {
        case bitmap(UIImage)
        case svg(Data, CGSize)
    }

    @State private var loaded: Loaded?
    @State private var failed = false

    var body: some View {
        Group {
            switch loaded {
            case .bitmap(let image):
                Image(uiImage: image).resizable().sized(image.size)
            case .svg(let data, let size):
                SVGView(data: data).sized(size)
            case nil:
                if failed {
                    if !alt.isEmpty { Text(alt).foregroundStyle(.secondary) }
                } else {
                    RoundedRectangle(cornerRadius: Radius.small).fill(Color.raised).frame(width: 120, height: 24)
                }
            }
        }
        .accessibilityLabel(alt)
        .task(id: urls) { await load() }
    }

    private func load() async {
        for url in urls {
            guard let data = try? await Client.shared.picture(url) else { continue }
            if let image = UIImage(data: data), image.size.width > 0 {
                loaded = .bitmap(image)
                return
            }
            if let size = SVGSize.of(data) {
                loaded = .svg(data, size)
                return
            }
        }
        failed = true
    }
}

private extension View {
    func sized(_ size: CGSize) -> some View {
        aspectRatio(size.width / size.height, contentMode: .fit).frame(maxWidth: size.width)
    }
}

/// What an SVG says its size is, from its width and height, or its viewBox.
enum SVGSize {
    static func of(_ data: Data) -> CGSize? {
        guard let text = String(data: data.prefix(4096), encoding: .utf8),
              let open = text.firstMatch(of: #/<svg\b[^>]*>/#)?.output else { return nil }
        let tag = String(open)
        func number(_ name: String) -> CGFloat? {
            guard let found = try? Regex("\\s\(name)=[\"']([0-9.]+)(px)?[\"']").firstMatch(in: tag),
                  let value = found[1].substring else { return nil }
            return Double(value).map { CGFloat($0) }
        }
        if let w = number("width"), let h = number("height"), w > 0, h > 0 { return CGSize(width: w, height: h) }
        if let box = tag.firstMatch(of: #/viewBox=["'][-0-9.]+[\s,]+[-0-9.]+[\s,]+([0-9.]+)[\s,]+([0-9.]+)["']/#),
           let w = Double(box.1), let h = Double(box.2), w > 0, h > 0 {
            return CGSize(width: w, height: h)
        }
        return CGSize(width: 240, height: 240)
    }
}

/// An SVG drawn by WebKit as an image, in which it can run no script and fetch nothing.
private struct SVGView: UIViewRepresentable {
    let data: Data

    func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView(frame: .zero)
        view.isOpaque = false
        view.backgroundColor = .clear
        view.scrollView.isScrollEnabled = false
        view.isUserInteractionEnabled = false
        view.loadHTMLString(
            "<!doctype html><meta name=viewport content='width=device-width,initial-scale=1'><style>html,body{margin:0;background:transparent}img{display:block;width:100%;height:auto}</style><img src='data:image/svg+xml;base64,\(data.base64EncodedString())'>",
            baseURL: nil)
        return view
    }

    func updateUIView(_ view: WKWebView, context: Context) {}
}

/// Pictures side by side, wrapping as words do, as a README's row of badges.
struct FlowRow: Layout {
    var spacing: CGFloat = 6

    private func rows(_ width: CGFloat, _ subviews: Subviews) -> (CGSize, [(Int, CGPoint, CGSize)]) {
        var placed: [(Int, CGPoint, CGSize)] = []
        var x: CGFloat = 0, y: CGFloat = 0, tallest: CGFloat = 0, widest: CGFloat = 0
        for (i, view) in subviews.enumerated() {
            let size = view.sizeThatFits(ProposedViewSize(width: width, height: nil))
            if x > 0, x + size.width > width {
                x = 0
                y += tallest + spacing
                tallest = 0
            }
            placed.append((i, CGPoint(x: x, y: y), size))
            x += size.width + spacing
            tallest = max(tallest, size.height)
            widest = max(widest, x - spacing)
        }
        return (CGSize(width: widest, height: y + tallest), placed)
    }

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        rows(proposal.width ?? .infinity, subviews).0
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        for (i, at, size) in rows(bounds.width, subviews).1 {
            subviews[i].place(at: CGPoint(x: bounds.minX + at.x, y: bounds.minY + at.y), proposal: ProposedViewSize(size))
        }
    }
}