johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
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))
}
}