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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
304 lines · 10.2 KB Raw History
import SwiftUI

/// A round mark: the picture at `url`, or else a pattern in a colour that `did`
/// picks, the account's own wherever it shows, as on the pages, or `name` for
/// one with no account.
struct Avatar: View {
    let url: String?
    let name: String
    var did: String?
    /// An image in the app, as a coding agent's own face.
    var face: String?
    var size: CGFloat = 24

    var body: some View {
        Group {
            if let face {
                Image(face).resizable().scaledToFill()
            } else if let url, let link = URL(string: url) {
                KeptFace(link: link) { pattern }
            } else {
                pattern
            }
        }
        .frame(width: size, height: size)
        .clipShape(Circle())
        .accessibilityHidden(true)
    }

    private var pattern: some View {
        let seed = did ?? name
        // hsl(hue 45% 52%), as the pages' `.av.id rect`, in the terms SwiftUI takes.
        let color = Color(hue: Double(Identicon.hue(seed)) / 360, saturation: 0.587, brightness: 0.736)
        return Canvas { context, area in
            let unit = area.width / 7
            var cells = Path()
            for cell in Identicon.cells(seed) {
                cells.addRect(CGRect(x: CGFloat(cell.x + 1) * unit, y: CGFloat(cell.y + 1) * unit, width: unit, height: unit))
            }
            context.fill(cells, with: .color(color), style: FillStyle(antialiased: false))
        }
        .background(Color(light: 0xf4f4f4, dark: 0x1a1a1a))
    }
}

/// Loads what a screen shows, and says when it can't.
struct Loading<Value: Sendable, Content: View>: View {
    let load: @Sendable () async throws -> Value
    /// Moved to read it again. What's on screen stays until the answer comes,
    /// and stays if none does.
    var refresh = 0
    @ViewBuilder let content: (Value) -> Content

    @State private var value: Value?
    @State private var failure: String?
    @State private var loaded: Int?

    var body: some View {
        Group {
            if let value {
                content(value)
            } else if let failure {
                ContentUnavailableView {
                    SwiftUI.Label("This didn't load", systemImage: "wifi.exclamationmark")
                } description: {
                    Text(failure)
                } actions: {
                    Button("Try again") { Task { await run() } }
                }
            } else {
                ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
        .task(id: refresh) { if value == nil || loaded != refresh { await run() } }
    }

    private func run() async {
        failure = nil
        do {
            value = try await load()
            loaded = refresh
        } catch is CancellationError {
        } catch let error as URLError where error.code == .cancelled {
        } catch {
            if value == nil { failure = error.localizedDescription }
        }
    }
}

struct StatePill: View {
    let style: StateStyle

    var body: some View {
        HStack(spacing: Space.s1_5) {
            Image(style.icon).renderingMode(.template).resizable().scaledToFit().frame(width: 14, height: 14)
            Text(style.label)
        }
            .font(.subheadline.weight(.semibold))
            .foregroundStyle(Color.onSolid)
            .padding(.horizontal, Space.s2_5)
            .padding(.vertical, Space.s1_5)
            .background(style.pillColor, in: Capsule())
    }
}

extension StateStyle {
    /// A solid fill under `onSolid` text: in iOS's own colours dark in light and
    /// dark alike, so white reads on it; in a theme, its own state colours, under its background.
    var pillColor: Color {
        switch label {
        case "Merged": Color(themed: \.done, otherwise: UIColor(hex: 0x7048b5))
        case "Open": Color.successFill
        case "Closed": icon == "oct-issue-closed"
            ? Color(themed: \.done, otherwise: UIColor(hex: 0x7048b5))
            : Color(themed: \.danger, otherwise: UIColor(hex: 0xcf222e))
        default: Color(themed: \.neutral, otherwise: UIColor(hex: 0x636363))
        }
    }
}

/// An icon close beside its words, where a list would space them apart.
struct TightLabel: LabelStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack(spacing: Space.s1) {
            configuration.icon
            configuration.title
        }
    }
}

struct BranchChip: View {
    let name: String

    var body: some View {
        Text(name)
            .font(.footnote.monospaced())
            .foregroundStyle(Color.accent)
            .lineLimit(1)
            .padding(.horizontal, Space.s2)
            .padding(.vertical, Space.s1)
            .background(Color.accentSoft, in: RoundedRectangle(cornerRadius: Radius.small))
    }
}

struct StateIcon: View {
    let style: StateStyle
    var size: CGFloat = 20

    var body: some View {
        Image(style.icon).renderingMode(.template).resizable().scaledToFit()
            .frame(width: size, height: size)
            .foregroundStyle(style.color)
            .accessibilityLabel(style.label)
    }
}

/// An Octicon, as the web pages use them, tinted by what's around it.
struct Oct: View {
    let name: String
    var size: CGFloat = Icon.md

    var body: some View {
        Image("oct-" + name).renderingMode(.template).resizable().scaledToFit().frame(width: size, height: size)
    }
}

struct IconSquare: View {
    let image: Image
    let color: Color

    var body: some View {
        image.renderingMode(.template).resizable().scaledToFit()
            .frame(width: 16, height: 16)
            .foregroundStyle(Color.onSolid)
            .frame(width: 30, height: 30)
            .background(color, in: RoundedRectangle(cornerRadius: Radius.medium))
    }
}

struct RepoRow: View {
    let repo: Repo

    var body: some View {
        NavigationLink(value: Route.repo(repo.ref)) {
            HStack(spacing: Space.s3) {
                Avatar(url: repo.owner.avatar, name: repo.owner.handle, did: repo.owner.did, size: Size.Avatar.regular)
                VStack(alignment: .leading, spacing: Space.s0_5) {
                    Text(repo.owner.handle).font(.footnote).foregroundStyle(.secondary).lineLimit(1)
                    HStack(spacing: Space.s1_5) {
                        Text(repo.name).font(.body.weight(.medium)).lineLimit(1)
                        if repo.private == true {
                            Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.secondary)
                                .accessibilityLabel("Private")
                        }
                    }
                }
            }
        }
    }
}

struct CheckSummary {
    let passed: Int, failed: Int, running: Int

    init(_ checks: [Check]) {
        passed = checks.filter { ["success", "cancelled"].contains($0.status) }.count
        failed = checks.filter { ["failure", "error"].contains($0.status) }.count
        running = checks.count - passed - failed
    }

    var symbol: String { failed > 0 ? "x-circle-fill" : running > 0 ? "clock" : "check-circle-fill" }
    var color: Color { failed > 0 ? .danger : running > 0 ? .attention : .success }
    var text: String {
        if failed > 0 { return failed == 1 ? "1 check failed" : "\(failed) checks failed" }
        if running > 0 { return running == 1 ? "1 check running" : "\(running) checks running" }
        return passed == 1 ? "1 check passed" : "All \(passed) checks passed"
    }
}

/// An account's picture, at once if it has been seen since the app opened. A
/// plain ground until it arrives: its identicon would flash first, though that
/// isn't what it looks like. The identicon only if the picture won't load.
private struct KeptFace<Fallback: View>: View {
    let link: URL
    @ViewBuilder let fallback: () -> Fallback

    @State private var image: UIImage?
    @State private var failed = false

    var body: some View {
        Group {
            if let shown = image ?? Faces.kept(link) {
                Image(uiImage: shown).resizable().scaledToFill()
            } else if failed {
                fallback()
            } else {
                Color.fill
            }
        }
        .task(id: link) {
            image = Faces.kept(link)
            failed = false
            guard image == nil else { return }
            do {
                image = try await Faces.shared.load(link)
            } catch is CancellationError {
                // It's off the screen; the next look asks again.
            } catch {
                failed = true
            }
        }
    }
}

/// A button beside the main one: in a theme, on its surface with its text, as
/// the pages' plain `.btn`, so only the main one takes a colour; in iOS's own
/// colours, iOS's bordered button.
private struct QuietButton: ViewModifier {
    let large: Bool
    let radius: CGFloat
    /// Read, so a change of theme draws it again.
    @Environment(\.themeGeneration) private var generation

    func body(content: Content) -> some View {
        if Themes.current != nil {
            content.buttonStyle(QuietStyle(large: large, radius: radius))
        } else {
            content.buttonStyle(.bordered).buttonBorderShape(.roundedRectangle(radius: radius))
        }
    }
}

private struct QuietStyle: ButtonStyle {
    let large: Bool
    let radius: CGFloat

    func makeBody(configuration: Configuration) -> some View {
        Drawn(configuration: configuration, large: large, radius: radius)
    }

    private struct Drawn: View {
        let configuration: Configuration
        let large: Bool
        let radius: CGFloat
        @Environment(\.isEnabled) private var enabled

        var body: some View {
            let shape = RoundedRectangle(cornerRadius: radius)
            configuration.label
                .foregroundStyle(Color.fg)
                .padding(.horizontal, large ? Space.s4 : Space.s3)
                .padding(.vertical, large ? Space.s3_5 : Space.s1_5)
                .background(Color.raised, in: shape)
                .overlay(shape.stroke(Color.line))
                .opacity(enabled ? (configuration.isPressed ? 0.7 : 1) : 0.5)
                .contentShape(shape)
        }
    }
}

extension View {
    func quietButton(large: Bool = false, radius: CGFloat) -> some View {
        modifier(QuietButton(large: large, radius: radius))
    }
}