From 31db452ca943db3d30b810db2262c74d97ea0e0a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 30 Jul 2026 00:53:46 +0200 Subject: [PATCH] feat(client/apple): the host tile wears its OS mark where the initial was MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple parity with the Android card (a94b1d3c's sibling): the OS mark moves out of the status row and into the tile, replacing the monogram. The initial says nothing the name beside it doesn't already say — twice over on a row of home-worker-N boxes — while the mark identifies the machine at a glance. Both host surfaces, because on tvOS the console home is the only one there is: the touch cards (saved and discovered) and GamepadHomeView's badge, which needed the chain carried on HomeTile to reach it. Sized to the monogram's own point size, so it lands at ~48% of the tile everywhere, and tinted through the same foregroundStyle the letter used — the assets are template imagesets, so they follow it like an SF Symbol. A host that advertises no OS chain, or one we ship no art for, keeps its letter: `osIconImage` already returns nil for both, so a mixed row still reads as one set. The mark carries the accessibility label now, since the status row it used to ride no longer names the OS. Untouched: the widget draws no monogram, and AboutView's is the app's own logo, not a host's. Co-Authored-By: Claude Opus 5 (1M context) --- .../Home/GamepadHomeView.swift | 14 ++++++ .../PunktfunkClient/Home/HostCards.swift | 49 ++++++++++--------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift index 3b1caa4d..e94b356f 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift @@ -54,6 +54,9 @@ private struct HomeTile: Identifiable { var hasLibrary = false /// Shows this SF symbol in the badge instead of the title monogram (the Add Host tile). var icon: String? + /// The host's OS-identity chain. When we ship art for it, the badge wears the OS mark instead + /// of the title's initial — the same substitution the touch cards make. + var osChain: String? /// Offline saved host we hold a MAC for (and WoL is available) — activating it wakes first. var canWake = false let activate: () -> Void @@ -284,6 +287,7 @@ struct GamepadHomeView: View { // A pinned card is a shortcut, not a second host — Y (library) stays on the // host's own tile, where the host-level actions live. hasLibrary: profile == nil, + osChain: host.osChain, canWake: autoWakeEnabled && PunktfunkConnection.wakeOnLANAvailable && !online && !host.wakeMacs.isEmpty, activate: { @@ -297,6 +301,7 @@ struct GamepadHomeView: View { title: d.name, subtitle: "\(d.host):\(String(d.port))", isOnline: true, + osChain: d.osChain, activate: { connectDiscovered(d) }) } let add = HomeTile( @@ -420,6 +425,15 @@ private struct GamepadHostTile: View { Image(systemName: icon) .font(.system(size: Self.iconFont, weight: .semibold)) .foregroundStyle(Color.brand) + } else if let mark = osIconImage(for: tile.osChain) { + // The OS mark stands in for the initial (template asset — tints like the text it + // replaces), and carries the label, since nothing else on the tile names the OS. + mark + .resizable() + .scaledToFit() + .frame(width: Self.monogramFont, height: Self.monogramFont) + .foregroundStyle(tile.filled ? .white : Color.brand) + .accessibilityLabel(tile.osChain ?? "") } else { Text(monogram(tile.title)) .font(.geistFixed(Self.monogramFont, .bold)) diff --git a/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift b/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift index 1aee3cb5..d3aac670 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift @@ -38,9 +38,16 @@ private func monogram(_ name: String) -> String { return String(first).uppercased() } -/// The squared monogram tile. `filled` = a solid brand-purple chip (saved hosts); otherwise a -/// tinted outline (discovered hosts). Shows a spinner in place of the glyph while connecting. -private func monogramTile(_ letter: String, m: CardMetrics, connecting: Bool, filled: Bool) -> some View { +/// The squared host tile. `filled` = a solid brand-purple chip (saved hosts); otherwise a tinted +/// outline (discovered hosts). Shows a spinner in place of the glyph while connecting. +/// +/// `mark` is the host's OS mark, and it REPLACES the monogram when we have one: it identifies the +/// machine better than its initial ever did, and on a row of similarly-named boxes the initial says +/// nothing the name beneath it doesn't already say. A host that advertises no OS chain — or one we +/// ship no art for — keeps its letter, so a mixed row still reads as one set. +private func monogramTile( + _ letter: String, osChain: String?, m: CardMetrics, connecting: Bool, filled: Bool +) -> some View { let shape = RoundedRectangle(cornerRadius: m.radius - 3, style: .continuous) return ZStack { shape.fill(filled @@ -50,6 +57,16 @@ private func monogramTile(_ letter: String, m: CardMetrics, connecting: Bool, fi : AnyShapeStyle(Color.brand.opacity(0.14))) if connecting { ProgressView().tint(filled ? .white : Color.brand) + } else if let mark = osIconImage(for: osChain) { + // Template asset — tints from foregroundStyle exactly like the letter it stands in for. + // Labelled, because this is where the OS is now announced: it used to ride the status + // row below, which no longer carries it. + mark + .resizable() + .scaledToFit() + .frame(width: m.monogram, height: m.monogram) + .foregroundStyle(filled ? Color.white : Color.brand) + .accessibilityLabel(osChain ?? "") } else { // Fixed size (not Dynamic Type): the glyph is pinned inside a fixed tile, so it must // not scale up and spill out at large accessibility text sizes. minimumScaleFactor + @@ -133,7 +150,8 @@ struct HostCardView: View { let m = CardMetrics.current return Button(action: onConnect) { HStack(spacing: m.spacing) { - monogramTile(monogram(host.displayName), m: m, connecting: isConnecting, filled: true) + monogramTile(monogram(host.displayName), osChain: host.osChain, + m: m, connecting: isConnecting, filled: true) VStack(alignment: .leading, spacing: 4) { // The chip rides the TITLE line, anchored to the card's trailing edge — not // trailing the name, where it read as part of the title, and not on a line of @@ -295,16 +313,7 @@ struct HostCardView: View { /// certificate is pinned (the lock state, spelled out). @ViewBuilder private func statusRow(_ m: CardMetrics) -> some View { HStack(spacing: 6) { - // The host's OS mark leads the row (template asset — tints like an SF Symbol); - // absent entirely for a host that never advertised one, so those cards render - // exactly as they always did. - if let mark = osIconImage(for: host.osChain) { - mark - .resizable() - .scaledToFit() - .frame(width: m.status + 2, height: m.status + 2) - .accessibilityLabel(host.osChain ?? "") - } + // The OS mark used to lead this row; it is the tile's glyph now (see monogramTile). RoundedRectangle(cornerRadius: 1.5) .fill(isOnline ? Color.green : Color.secondary.opacity(0.4)) .frame(width: 6, height: 6) @@ -364,7 +373,8 @@ struct DiscoveredCardView: View { let m = CardMetrics.current return Button(action: onConnect) { HStack(spacing: m.spacing) { - monogramTile(monogram(discovered.name), m: m, connecting: false, filled: false) + monogramTile(monogram(discovered.name), osChain: discovered.osChain, + m: m, connecting: false, filled: false) VStack(alignment: .leading, spacing: 4) { Text(discovered.name) .font(.geist(m.name, .bold, relativeTo: .title3)) @@ -375,14 +385,7 @@ struct DiscoveredCardView: View { .foregroundStyle(.secondary) .lineLimit(1) HStack(spacing: 6) { - // Same leading OS mark as a saved card's status row — live from the advert. - if let mark = osIconImage(for: discovered.osChain) { - mark - .resizable() - .scaledToFit() - .frame(width: m.status + 2, height: m.status + 2) - .accessibilityLabel(discovered.osChain) - } + // The advert's OS mark is the tile's glyph now (see monogramTile). Image(systemName: discovered.requiresPairing ? "lock.fill" : "antenna.radiowaves.left.and.right") .font(.system(size: m.status))