About was a bare "Punktfunk / Version x" header over 885 KB of license text — it answered the one question nobody opens About to ask. It is now the shape every Mac user already knows: the app's icon, its name, the version (selectable, because a bug report is worth more with it), the tagline, and then the ways out — documentation, community, source, licenses. The license wall is still `AcknowledgementsView`, one push in on iOS/tvOS and a sheet on macOS, where a preferences tab has no navigation stack to push onto. Every platform hides the app icon somewhere different and tvOS can't hand it over at all (layered assets, no single image), so there's a drawn fallback in the same brand gradient and Geist monogram as the host cards — where the real icon is unavailable it reads as a mark, not as a broken image. tvOS also has no browser: the addresses are text to read off the screen rather than links to nowhere. And profile colours were half-built by me: the catalog stored `accent`, the chips tinted from it, and nothing in the app ever SET one. The scope menu grows "Color ▸" over a fixed palette — the chip is small tinted text on a tinted capsule, and a colour picked freehand lands somewhere unreadable often enough to matter. The palette is what this client OFFERS, not what it accepts: any `#RRGGBB` another platform writes still renders, which is what Android needs from this field. The colour now shows wherever the profile is named — the scope switcher's dot, the card chips, and the HUD's session line, which tints to the same colour the card that launched it wore. A colour is an identifier, and an identifier that changes between surfaces isn't one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
61 lines
2.7 KiB
Swift
61 lines
2.7 KiB
Swift
// A profile's chip colour (design/client-settings-profiles.md §5.1 "Change color"). The catalog
|
|
// stores `accent` as `#RRGGBB` — a plain string, so the Rust and Kotlin catalogs read the same
|
|
// value — and this is the palette the Apple client offers for it.
|
|
//
|
|
// A fixed palette rather than a full colour well, for two reasons. The chip is small, tinted text
|
|
// on a tinted capsule, so a colour picked freehand can land somewhere unreadable; and a profile's
|
|
// colour is an IDENTIFIER — "the orange one" — which works when there are eight of them and stops
|
|
// working when there are sixteen million. Any `#RRGGBB` a newer client (or another platform)
|
|
// writes still renders: the palette is what this client OFFERS, not what it accepts.
|
|
|
|
import SwiftUI
|
|
|
|
public struct ProfileAccent: Identifiable, Sendable, Hashable {
|
|
/// `#RRGGBB`, lowercase — exactly what lands in the catalog.
|
|
public let hex: String
|
|
public let name: String
|
|
|
|
public var id: String { hex }
|
|
|
|
public var color: Color { Color(hex: hex) ?? .brand }
|
|
|
|
/// Hues that stay legible as tinted text on their own tinted capsule, in both appearances,
|
|
/// and that stay tellable apart from each other at chip size.
|
|
public static let palette: [ProfileAccent] = [
|
|
ProfileAccent(hex: "#8b5cf6", name: "Violet"),
|
|
ProfileAccent(hex: "#3aa0ff", name: "Blue"),
|
|
ProfileAccent(hex: "#2bb8a6", name: "Teal"),
|
|
ProfileAccent(hex: "#35c759", name: "Green"),
|
|
ProfileAccent(hex: "#e0a800", name: "Amber"),
|
|
ProfileAccent(hex: "#ff8800", name: "Orange"),
|
|
ProfileAccent(hex: "#ff4f6d", name: "Red"),
|
|
ProfileAccent(hex: "#ff4f9a", name: "Pink"),
|
|
ProfileAccent(hex: "#8e8e93", name: "Graphite"),
|
|
]
|
|
|
|
public static func named(_ hex: String?) -> ProfileAccent? {
|
|
guard let hex else { return nil }
|
|
return palette.first { $0.hex.caseInsensitiveCompare(hex) == .orderedSame }
|
|
}
|
|
}
|
|
|
|
public extension Color {
|
|
/// Parse a `#RRGGBB` string. nil for anything else — a profile with an accent this build
|
|
/// can't read falls back to the brand tint rather than rendering as black.
|
|
init?(hex: String) {
|
|
guard hex.hasPrefix("#"), hex.count == 7,
|
|
let value = UInt32(hex.dropFirst(), radix: 16)
|
|
else { return nil }
|
|
self.init(
|
|
red: Double((value >> 16) & 0xFF) / 255,
|
|
green: Double((value >> 8) & 0xFF) / 255,
|
|
blue: Double(value & 0xFF) / 255)
|
|
}
|
|
}
|
|
|
|
public extension StreamProfile {
|
|
/// This profile's colour, or the brand tint when it has none — every surface that shows a
|
|
/// profile reads it through here, so "no colour chosen" looks deliberate everywhere.
|
|
var accentColor: Color { Color(hex: accent ?? "") ?? .brand }
|
|
}
|