@@ -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 ( ) }
0 commit comments