macOS: move instance update info to third row

This commit is contained in:
Peter Steinberger
2025-12-18 09:36:07 +01:00
parent fceab511b3
commit 24009ed00f
3 changed files with 103 additions and 34 deletions

View File

@@ -31,7 +31,11 @@ enum DeviceModelCatalog {
return DevicePresentation(title: title, symbol: symbol) return DevicePresentation(title: title, symbol: symbol)
} }
static func symbol(deviceFamily familyRaw: String, modelIdentifier modelIdentifierRaw: String, friendlyName: String?) -> String? { static func symbol(
deviceFamily familyRaw: String,
modelIdentifier modelIdentifierRaw: String,
friendlyName: String?) -> String?
{
let family = familyRaw.trimmingCharacters(in: .whitespacesAndNewlines) let family = familyRaw.trimmingCharacters(in: .whitespacesAndNewlines)
let modelIdentifier = modelIdentifierRaw.trimmingCharacters(in: .whitespacesAndNewlines) let modelIdentifier = modelIdentifierRaw.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -80,7 +84,7 @@ enum DeviceModelCatalog {
case "mac": case "mac":
return "laptopcomputer" return "laptopcomputer"
case "android": case "android":
return "logo.android" return "android"
case "linux": case "linux":
return "cpu" return "cpu"
default: default:

View File

@@ -64,15 +64,11 @@ struct InstancesSettings: View {
let device = DeviceModelCatalog.presentation( let device = DeviceModelCatalog.presentation(
deviceFamily: inst.deviceFamily, deviceFamily: inst.deviceFamily,
modelIdentifier: inst.modelIdentifier) modelIdentifier: inst.modelIdentifier)
let leadingSymbol = self.leadingDeviceSymbol(inst, device: device)
HStack(alignment: .top, spacing: 12) { HStack(alignment: .top, spacing: 12) {
Image(systemName: leadingSymbol) self.leadingDeviceIcon(inst, device: device)
.font(.system(size: 26, weight: .regular))
.foregroundStyle(.secondary)
.frame(width: 28, height: 28, alignment: .center) .frame(width: 28, height: 28, alignment: .center)
.padding(.top, 1) .padding(.top, 1)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -80,38 +76,38 @@ struct InstancesSettings: View {
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") } if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
} }
HStack(alignment: .firstTextBaseline, spacing: 12) { HStack(spacing: 8) {
HStack(spacing: 8) { if let version = inst.version {
if let version = inst.version { self.label(icon: "shippingbox", text: version)
self.label(icon: "shippingbox", text: version) }
}
if let device { if let device {
// Avoid showing generic "Mac"/"iPhone"/etc; prefer the concrete model name. // Avoid showing generic "Mac"/"iPhone"/etc; prefer the concrete model name.
let family = (inst.deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let family = (inst.deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let isGeneric = !family.isEmpty && device.title == family let isGeneric = !family.isEmpty && device.title == family
if !isGeneric { if !isGeneric {
if let prettyPlatform { if let prettyPlatform {
self.label(icon: device.symbol, text: "\(device.title) · \(prettyPlatform)") self.label(icon: device.symbol, text: "\(device.title) · \(prettyPlatform)")
} else { } else {
self.label(icon: device.symbol, text: device.title) self.label(icon: device.symbol, text: device.title)
}
} else if let prettyPlatform, let platform = inst.platform {
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
} }
} else if let prettyPlatform, let platform = inst.platform { } else if let prettyPlatform, let platform = inst.platform {
self.label(icon: self.platformIcon(platform), text: prettyPlatform) self.label(icon: self.platformIcon(platform), text: prettyPlatform)
} }
} else if let prettyPlatform, let platform = inst.platform {
if let mode = inst.mode { self.label(icon: "network", text: mode) } self.label(icon: self.platformIcon(platform), text: prettyPlatform)
} }
.layoutPriority(1)
Spacer(minLength: 0) if let mode = inst.mode { self.label(icon: "network", text: mode) }
}
.layoutPriority(1)
if !isGateway, self.shouldShowUpdateRow(inst) {
HStack(spacing: 8) { HStack(spacing: 8) {
Spacer(minLength: 0)
// Last local input is helpful for interactive nodes, but noisy/meaningless for the gateway. // Last local input is helpful for interactive nodes, but noisy/meaningless for the gateway.
if !isGateway, let secs = inst.lastInputSeconds { if let secs = inst.lastInputSeconds {
self.label(icon: "clock", text: "\(secs)s ago") self.label(icon: "clock", text: "\(secs)s ago")
} }
@@ -136,18 +132,42 @@ struct InstancesSettings: View {
private func label(icon: String?, text: String) -> some View { private func label(icon: String?, text: String) -> some View {
HStack(spacing: 4) { HStack(spacing: 4) {
if let icon, self.isSystemSymbolAvailable(icon) { if let icon {
Image(systemName: icon).foregroundStyle(.secondary).font(.caption) if icon == Self.androidSymbolToken {
AndroidMark()
.foregroundStyle(.secondary)
.frame(width: 12, height: 12, alignment: .center)
} else if self.isSystemSymbolAvailable(icon) {
Image(systemName: icon).foregroundStyle(.secondary).font(.caption)
}
} }
Text(text) Text(text)
} }
.font(.footnote) .font(.footnote)
} }
@ViewBuilder
private func leadingDeviceIcon(_ inst: InstanceInfo, device: DevicePresentation?) -> some View {
let symbol = self.leadingDeviceSymbol(inst, device: device)
if symbol == Self.androidSymbolToken {
AndroidMark()
.foregroundStyle(.secondary)
.frame(width: 24, height: 24, alignment: .center)
.accessibilityHidden(true)
} else {
Image(systemName: symbol)
.font(.system(size: 26, weight: .regular))
.foregroundStyle(.secondary)
.accessibilityHidden(true)
}
}
private static let androidSymbolToken = "android"
private func leadingDeviceSymbol(_ inst: InstanceInfo, device: DevicePresentation?) -> String { private func leadingDeviceSymbol(_ inst: InstanceInfo, device: DevicePresentation?) -> String {
let family = (inst.deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let family = (inst.deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if family == "android" { if family == "android" {
return self.safeSystemSymbol("logo.android", fallback: "cpu") return Self.androidSymbolToken
} }
if let title = device?.title.lowercased() { if let title = device?.title.lowercased() {
@@ -176,6 +196,12 @@ struct InstancesSettings: View {
return "cpu" return "cpu"
} }
private func shouldShowUpdateRow(_ inst: InstanceInfo) -> Bool {
if inst.lastInputSeconds != nil { return true }
if self.updateSummaryText(inst, isGateway: false) != nil { return true }
return false
}
private func safeSystemSymbol(_ preferred: String, fallback: String) -> String { private func safeSystemSymbol(_ preferred: String, fallback: String) -> String {
if self.isSystemSymbolAvailable(preferred) { return preferred } if self.isSystemSymbolAvailable(preferred) { return preferred }
return fallback return fallback
@@ -185,6 +211,46 @@ struct InstancesSettings: View {
NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil
} }
private struct AndroidMark: View {
var body: some View {
GeometryReader { geo in
let w = geo.size.width
let h = geo.size.height
let headHeight = h * 0.68
let headWidth = w * 0.92
let headY = h * 0.18
let corner = headHeight * 0.28
ZStack {
RoundedRectangle(cornerRadius: corner, style: .continuous)
.frame(width: headWidth, height: headHeight)
.position(x: w / 2, y: headY + headHeight / 2)
Circle()
.frame(width: max(1, w * 0.1), height: max(1, w * 0.1))
.position(x: w * 0.38, y: headY + headHeight * 0.55)
.blendMode(.destinationOut)
Circle()
.frame(width: max(1, w * 0.1), height: max(1, w * 0.1))
.position(x: w * 0.62, y: headY + headHeight * 0.55)
.blendMode(.destinationOut)
Rectangle()
.frame(width: max(1, w * 0.08), height: max(1, h * 0.18))
.rotationEffect(.degrees(-25))
.position(x: w * 0.34, y: h * 0.12)
Rectangle()
.frame(width: max(1, w * 0.08), height: max(1, h * 0.18))
.rotationEffect(.degrees(25))
.position(x: w * 0.66, y: h * 0.12)
}
.compositingGroup()
}
}
}
private func platformIcon(_ raw: String) -> String { private func platformIcon(_ raw: String) -> String {
let (prefix, _) = self.parsePlatform(raw) let (prefix, _) = self.parsePlatform(raw)
switch prefix { switch prefix {

View File

@@ -27,8 +27,7 @@ struct DeviceModelCatalogTests {
@Test @Test
func symbolFallsBackToDeviceFamily() { func symbolFallsBackToDeviceFamily() {
#expect(DeviceModelCatalog.symbol(deviceFamily: "Android", modelIdentifier: "", friendlyName: nil) == "logo.android") #expect(DeviceModelCatalog.symbol(deviceFamily: "Android", modelIdentifier: "", friendlyName: nil) == "android")
#expect(DeviceModelCatalog.symbol(deviceFamily: "Linux", modelIdentifier: "", friendlyName: nil) == "cpu") #expect(DeviceModelCatalog.symbol(deviceFamily: "Linux", modelIdentifier: "", friendlyName: nil) == "cpu")
} }
} }