Skip to content

Commit bc82e27

Browse files
authored
feat(app): show device marketing names and add component browser (#365)
1 parent 63f7fe8 commit bc82e27

7 files changed

Lines changed: 402 additions & 61 deletions

File tree

macOSdb.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
CD7EEB8A2F5F8038005A8490 /* ChipSupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7EEB7C2F5F8038005A8490 /* ChipSupportView.swift */; };
1717
CD7EEB8B2F5F8038005A8490 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7EEB782F5F8038005A8490 /* AppState.swift */; };
1818
CD7EEB8E2F5F80EE005A8490 /* macOSdbKit in Frameworks */ = {isa = PBXBuildFile; productRef = CD7EEB8D2F5F80EE005A8490 /* macOSdbKit */; };
19+
CD9549252F836EB100E57666 /* ComponentDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9549242F836EB100E57666 /* ComponentDetailView.swift */; };
1920
CDAA00072F6A0001005A8490 /* ListCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAA00012F6A0001005A8490 /* ListCommand.swift */; };
2021
CDAA00082F6A0001005A8490 /* CompareCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAA00022F6A0001005A8490 /* CompareCommand.swift */; };
2122
CDAA00092F6A0001005A8490 /* ShowCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAA00032F6A0001005A8490 /* ShowCommand.swift */; };
@@ -36,6 +37,7 @@
3637
CD7EEB7F2F5F8038005A8490 /* ReleaseDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseDetailView.swift; sourceTree = "<group>"; };
3738
CD7EEB802F5F8038005A8490 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
3839
CD7EEB822F5F8038005A8490 /* macOSdbApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = macOSdbApp.swift; sourceTree = "<group>"; };
40+
CD9549242F836EB100E57666 /* ComponentDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentDetailView.swift; sourceTree = "<group>"; };
3941
CDAA00012F6A0001005A8490 /* ListCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCommand.swift; sourceTree = "<group>"; };
4042
CDAA00022F6A0001005A8490 /* CompareCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareCommand.swift; sourceTree = "<group>"; };
4143
CDAA00032F6A0001005A8490 /* ShowCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowCommand.swift; sourceTree = "<group>"; };
@@ -90,6 +92,7 @@
9092
children = (
9193
CD7EEB7C2F5F8038005A8490 /* ChipSupportView.swift */,
9294
CD7EEB7D2F5F8038005A8490 /* CompareView.swift */,
95+
CD9549242F836EB100E57666 /* ComponentDetailView.swift */,
9396
CD7EEB7E2F5F8038005A8490 /* ContentView.swift */,
9497
CD7EEB7F2F5F8038005A8490 /* ReleaseDetailView.swift */,
9598
CD7EEB802F5F8038005A8490 /* SidebarView.swift */,
@@ -249,6 +252,7 @@
249252
CDAA000C2F6A0001005A8490 /* EntryPoint.swift in Sources */,
250253
CDAA00072F6A0001005A8490 /* ListCommand.swift in Sources */,
251254
CDC3BCAD2F8360AC00380644 /* ValidateCommand.swift in Sources */,
255+
CD9549252F836EB100E57666 /* ComponentDetailView.swift in Sources */,
252256
CDAA00082F6A0001005A8490 /* CompareCommand.swift in Sources */,
253257
CDAA00092F6A0001005A8490 /* ShowCommand.swift in Sources */,
254258
CDAA000A2F6A0001005A8490 /* ScanCommand.swift in Sources */,

macOSdbApp/Models/AppState.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@ final class AppState {
2323
var isComparing = false
2424
var showBetas = true
2525
var showDeviceSpecific = false
26+
var sidebarMode: SidebarMode = .releases
27+
var selectedComponentName: String?
28+
29+
// MARK: - Types
30+
31+
enum SidebarMode: String, CaseIterable {
32+
case releases = "Releases"
33+
case components = "Components"
34+
}
35+
36+
struct ComponentSummary: Identifiable, Hashable {
37+
let name: String
38+
let latestVersion: String
39+
let source: ComponentSource
40+
let path: String
41+
var id: String { name }
42+
}
43+
44+
struct ComponentVersionEntry: Identifiable {
45+
let version: String
46+
let releaseName: String
47+
let releaseDate: String?
48+
let isBeta: Bool
49+
let isRC: Bool
50+
let direction: ChangeDirection?
51+
var id: String { "\(releaseName)-\(version)" }
52+
}
2653

2754
// MARK: - Derived
2855

@@ -58,6 +85,72 @@ final class AppState {
5885
return VersionComparer.compare(from: from, to: target)
5986
}
6087

88+
var allComponents: [ComponentSummary] {
89+
let filtered = releases.filter { release in
90+
(showBetas || !release.isPrerelease) && (showDeviceSpecific || !release.isDeviceSpecific)
91+
}
92+
let sorted = filtered.sorted(by: >)
93+
94+
var seen = Set<String>()
95+
var result: [ComponentSummary] = []
96+
for release in sorted {
97+
for comp in release.components where seen.insert(comp.name).inserted {
98+
result.append(ComponentSummary(
99+
name: comp.name,
100+
latestVersion: comp.displayVersion,
101+
source: comp.source,
102+
path: comp.path
103+
))
104+
}
105+
}
106+
return result.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
107+
}
108+
109+
var filteredComponents: [ComponentSummary] {
110+
let search = searchText.trimmingCharacters(in: .whitespaces)
111+
guard !search.isEmpty else { return allComponents }
112+
return allComponents.filter {
113+
$0.name.localizedCaseInsensitiveContains(search)
114+
|| $0.latestVersion.localizedCaseInsensitiveContains(search)
115+
}
116+
}
117+
118+
func componentHistory(for name: String) -> [ComponentVersionEntry] {
119+
let filtered = releases.filter { release in
120+
(showBetas || !release.isPrerelease) && (showDeviceSpecific || !release.isDeviceSpecific)
121+
}
122+
123+
var versionMap: [String: Release] = [:]
124+
for release in filtered.sorted(by: <) {
125+
guard let comp = release.component(named: name) else { continue }
126+
let ver = comp.displayVersion
127+
if versionMap[ver] == nil {
128+
versionMap[ver] = release
129+
}
130+
}
131+
132+
let sorted = versionMap.keys.sorted {
133+
VersionComparer.compareVersionStrings($0, $1) == .upgraded
134+
}
135+
136+
let entries: [ComponentVersionEntry] = sorted.enumerated().map { index, version in
137+
let release = versionMap[version]!
138+
let direction: ChangeDirection? = index == 0
139+
? nil
140+
: VersionComparer.compareVersionStrings(sorted[index - 1], version)
141+
return ComponentVersionEntry(
142+
version: version,
143+
releaseName: release.displayName,
144+
releaseDate: release.releaseDate,
145+
isBeta: release.isBeta,
146+
isRC: release.isRC,
147+
direction: direction
148+
)
149+
}
150+
151+
return entries.reversed()
152+
}
153+
61154
// MARK: - Data provider
62155

63156
private let dataProvider: DataProvider
@@ -106,6 +199,7 @@ final class AppState {
106199
selectedProduct = product
107200
selectedRelease = nil
108201
compareRelease = nil
202+
selectedComponentName = nil
109203
isComparing = false
110204
releases = []
111205
Task { await refresh() }

macOSdbApp/Views/ChipSupportView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ private struct ChipCard: View {
136136

137137
VStack(alignment: .leading, spacing: 2) {
138138
ForEach(devices, id: \.self) { device in
139-
Text(device)
139+
Text(DeviceRegistry.info(for: device)?.marketingName ?? device)
140140
.font(.caption)
141141
.foregroundStyle(.secondary)
142+
.help(device)
142143
}
143144
}
144145
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import macOSdbKit
2+
import SwiftUI
3+
4+
struct ComponentDetailView: View {
5+
@Environment(AppState.self)
6+
private var appState
7+
8+
let componentName: String
9+
10+
private var component: AppState.ComponentSummary? {
11+
appState.allComponents.first { $0.name == componentName }
12+
}
13+
14+
var body: some View {
15+
if let component {
16+
ScrollView {
17+
VStack(alignment: .leading, spacing: 20) {
18+
componentHeader(component)
19+
versionHistorySection
20+
}
21+
.padding()
22+
}
23+
.navigationTitle(componentName)
24+
}
25+
}
26+
27+
// MARK: - Header
28+
29+
@ViewBuilder
30+
private func componentHeader(_ component: AppState.ComponentSummary) -> some View {
31+
VStack(alignment: .leading, spacing: 6) {
32+
Text(component.name)
33+
.font(.largeTitle)
34+
.fontWeight(.bold)
35+
36+
HStack(spacing: 16) {
37+
Label(component.latestVersion, systemImage: "tag")
38+
Label(component.source.rawValue, systemImage: "archivebox")
39+
Label(component.path, systemImage: "folder")
40+
}
41+
.font(.callout)
42+
.foregroundStyle(.secondary)
43+
}
44+
}
45+
46+
// MARK: - Version History
47+
48+
@ViewBuilder private var versionHistorySection: some View {
49+
let history = appState.componentHistory(for: componentName)
50+
51+
VStack(alignment: .leading, spacing: 8) {
52+
Text("Version History")
53+
.font(.title2)
54+
.fontWeight(.semibold)
55+
56+
Text("\(history.count) version\(history.count == 1 ? "" : "s") tracked")
57+
.font(.callout)
58+
.foregroundStyle(.secondary)
59+
60+
if history.isEmpty {
61+
ContentUnavailableView(
62+
"No History",
63+
systemImage: "clock",
64+
description: Text("No version changes found for this component.")
65+
)
66+
} else {
67+
versionTable(history)
68+
}
69+
}
70+
}
71+
72+
@ViewBuilder
73+
private func versionTable(_ history: [AppState.ComponentVersionEntry]) -> some View {
74+
Table(history) {
75+
TableColumn("Version") { entry in
76+
Text(entry.version)
77+
.fontDesign(.monospaced)
78+
}
79+
.width(min: 80, ideal: 120)
80+
81+
TableColumn("Introduced In") { entry in
82+
HStack(spacing: 6) {
83+
Text(entry.releaseName)
84+
if entry.isBeta {
85+
Text("Beta")
86+
.font(.caption2)
87+
.fontWeight(.medium)
88+
.foregroundStyle(.orange)
89+
} else if entry.isRC {
90+
Text("RC")
91+
.font(.caption2)
92+
.fontWeight(.medium)
93+
.foregroundStyle(.green)
94+
}
95+
}
96+
}
97+
.width(min: 150, ideal: 200)
98+
99+
TableColumn("Date") { entry in
100+
Text(entry.releaseDate ?? "")
101+
.foregroundStyle(.secondary)
102+
}
103+
.width(min: 80, ideal: 100)
104+
105+
TableColumn("Status") { entry in
106+
StatusLabel(direction: entry.direction)
107+
}
108+
.width(min: 80, ideal: 110)
109+
}
110+
.frame(minHeight: 300)
111+
}
112+
}
113+
114+
// MARK: - Status Label
115+
116+
private struct StatusLabel: View {
117+
let direction: ChangeDirection?
118+
119+
var body: some View {
120+
switch direction {
121+
case .upgraded:
122+
Label("Upgraded", systemImage: "arrow.up.circle.fill")
123+
.foregroundStyle(.green)
124+
.font(.caption)
125+
case .downgraded:
126+
Label("Downgraded", systemImage: "arrow.down.circle.fill")
127+
.foregroundStyle(.orange)
128+
.font(.caption)
129+
case .unchanged:
130+
EmptyView()
131+
case nil:
132+
Label("First tracked", systemImage: "plus.circle.fill")
133+
.foregroundStyle(.blue)
134+
.font(.caption)
135+
}
136+
}
137+
}

macOSdbApp/Views/ContentView.swift

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,41 @@ struct ContentView: View {
1616
} detail: {
1717
if appState.isComparing, appState.comparison != nil {
1818
CompareView()
19+
} else if appState.sidebarMode == .components, let name = appState.selectedComponentName {
20+
ComponentDetailView(componentName: name)
1921
} else if appState.selectedRelease != nil {
2022
ReleaseDetailView()
2123
} else {
2224
ContentUnavailableView(
23-
"Select a Release",
24-
systemImage: "apple.logo",
25-
description: Text("Choose a release from the sidebar to view its components.")
25+
appState.sidebarMode == .components ? "Select a Component" : "Select a Release",
26+
systemImage: appState.sidebarMode == .components ? "shippingbox" : "apple.logo",
27+
description: Text(
28+
appState.sidebarMode == .components
29+
? "Choose a component from the sidebar to view its version history."
30+
: "Choose a release from the sidebar to view its components."
31+
)
2632
)
2733
}
2834
}
2935
.searchable(text: $state.searchText, prompt: "Filter components")
3036
.toolbar {
3137
ToolbarItemGroup(placement: .primaryAction) {
32-
if appState.isComparing {
33-
Button {
34-
appState.endCompare()
35-
} label: {
36-
Label("Done", systemImage: "xmark.circle")
38+
if appState.sidebarMode == .releases {
39+
if appState.isComparing {
40+
Button {
41+
appState.endCompare()
42+
} label: {
43+
Label("Done", systemImage: "xmark.circle")
44+
}
45+
.help("Exit comparison mode")
46+
} else if appState.selectedRelease != nil {
47+
Button {
48+
appState.startCompare()
49+
} label: {
50+
Label("Compare", systemImage: "square.split.2x1")
51+
}
52+
.help("Compare with another release")
3753
}
38-
.help("Exit comparison mode")
39-
} else if appState.selectedRelease != nil {
40-
Button {
41-
appState.startCompare()
42-
} label: {
43-
Label("Compare", systemImage: "square.split.2x1")
44-
}
45-
.help("Compare with another release")
4654
}
4755
}
4856
}

macOSdbApp/Views/ReleaseDetailView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,11 @@ private struct KernelCard: View {
293293
Text(kernel.arch)
294294
.font(.caption)
295295
.foregroundStyle(.secondary)
296-
Text(kernel.devices.joined(separator: ", "))
296+
Text(
297+
kernel.devices
298+
.map { DeviceRegistry.info(for: $0)?.marketingName ?? $0 }
299+
.joined(separator: ", ")
300+
)
297301
.font(.caption2)
298302
.foregroundStyle(.tertiary)
299303
}

0 commit comments

Comments
 (0)