| @@ -5,11 +5,13 @@ struct ContentView: View { |
| 5 | @Environment(SessionStore.self) private var session |
5 | @Environment(SessionStore.self) private var session |
| 6 | @Environment(PushRouter.self) private var router |
6 | @Environment(PushRouter.self) private var router |
| 7 | @Environment(\.openURL) private var openURL |
7 | @Environment(\.openURL) private var openURL |
| |
8 | @Environment(\.horizontalSizeClass) private var sizeClass |
| 8 | |
9 | |
| 9 | enum Tabs { case dashboard, feed, repositories, explore, profile } |
10 | enum Tabs { case dashboard, feed, repositories, explore, profile } |
| 10 | |
11 | |
| 11 | @State private var selectedTab = Tabs.dashboard |
12 | @State private var selectedTab = Tabs.dashboard |
| 12 | @State private var dashboardPath = NavigationPath() |
13 | @State private var dashboardPath = NavigationPath() |
| |
14 | @State private var repoPath = NavigationPath() |
| 13 | @State private var unread = 0 |
15 | @State private var unread = 0 |
| 14 | |
16 | |
| 15 | /// whoami's admin flag, read on the My Profile tab, decides whether |
17 | /// whoami's admin flag, read on the My Profile tab, decides whether |
| @@ -29,9 +31,15 @@ struct ContentView: View { |
| 29 | if let client = session.client, let account = session.current { |
31 | if let client = session.client, let account = session.current { |
| 30 | TabView(selection: $selectedTab) { |
32 | TabView(selection: $selectedTab) { |
| 31 | Tab("Dashboard", systemImage: "square.grid.2x2", value: Tabs.dashboard) { |
33 | Tab("Dashboard", systemImage: "square.grid.2x2", value: Tabs.dashboard) { |
| 32 | NavigationStack(path: $dashboardPath) { |
34 | if sizeClass == .regular { |
| 33 | DashboardView(client: client, unread: $unread) |
35 | SplitStack(path: $dashboardPath, client: client, placeholder: "Nothing selected") { |
| 34 | .navigationDestinations(client: client) |
36 | DashboardView(client: client, unread: $unread) |
| |
37 | } |
| |
38 | } else { |
| |
39 | NavigationStack(path: $dashboardPath) { |
| |
40 | DashboardView(client: client, unread: $unread) |
| |
41 | .navigationDestinations(client: client) |
| |
42 | } |
| 35 | } |
43 | } |
| 36 | } |
44 | } |
| 37 | .badge(unread) |
45 | .badge(unread) |
| @@ -42,9 +50,15 @@ struct ContentView: View { |
| 42 | } |
50 | } |
| 43 | } |
51 | } |
| 44 | Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) { |
52 | Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) { |
| 45 | NavigationStack { |
53 | if sizeClass == .regular { |
| 46 | RepoListView(client: client) |
54 | SplitStack(path: $repoPath, client: client, placeholder: "No repository selected") { |
| 47 | .navigationDestinations(client: client) |
55 | RepoListView(client: client) |
| |
56 | } |
| |
57 | } else { |
| |
58 | NavigationStack(path: $repoPath) { |
| |
59 | RepoListView(client: client) |
| |
60 | .navigationDestinations(client: client) |
| |
61 | } |
| 48 | } |
62 | } |
| 49 | } |
63 | } |
| 50 | Tab("Explore", systemImage: "safari", value: Tabs.explore) { |
64 | Tab("Explore", systemImage: "safari", value: Tabs.explore) { |
| @@ -108,6 +122,7 @@ struct ContentView: View { |
| 108 | private func routePendingNotification(_ account: Account) async { |
122 | private func routePendingNotification(_ account: Account) async { |
| 109 | if pathAccount != account.id { |
123 | if pathAccount != account.id { |
| 110 | dashboardPath = NavigationPath() |
124 | dashboardPath = NavigationPath() |
| |
125 | repoPath = NavigationPath() |
| 111 | selectedTab = .dashboard |
126 | selectedTab = .dashboard |
| 112 | pathAccount = account.id |
127 | pathAccount = account.id |
| 113 | } |
128 | } |
| @@ -178,6 +193,46 @@ struct ContentView: View { |
| 178 | |
193 | |
| 179 | } |
194 | } |
| 180 | |
195 | |
| |
196 | /// A list as a sidebar at regular width. What it selects is the root of |
| |
197 | /// the detail column's stack; the stack's path is the one the compact |
| |
198 | /// layout's stack uses, so notification and link routing, which append |
| |
199 | /// to it, work at either width. |
| |
200 | private struct SplitStack<Sidebar: View>: View { |
| |
201 | |
| |
202 | @Binding var path: NavigationPath |
| |
203 | let client: GitbayClient |
| |
204 | let placeholder: String |
| |
205 | @ViewBuilder let sidebar: () -> Sidebar |
| |
206 | |
| |
207 | @State private var selection: AnyHashable? |
| |
208 | |
| |
209 | var body: some View { |
| |
210 | NavigationSplitView { |
| |
211 | sidebar() |
| |
212 | .environment(\.sidebarSelection, Binding( |
| |
213 | get: { selection }, |
| |
214 | set: { route in |
| |
215 | selection = route |
| |
216 | path = NavigationPath() |
| |
217 | } |
| |
218 | )) |
| |
219 | } detail: { |
| |
220 | NavigationStack(path: $path) { |
| |
221 | Group { |
| |
222 | if let selection { |
| |
223 | RouteDestinations(client: client).destination(selection) |
| |
224 | } else { |
| |
225 | Text(placeholder) |
| |
226 | .font(.gbSans(.subheadline)) |
| |
227 | .foregroundStyle(.secondary) |
| |
228 | } |
| |
229 | } |
| |
230 | .navigationDestinations(client: client) |
| |
231 | } |
| |
232 | } |
| |
233 | } |
| |
234 | } |
| |
235 | |
| 181 | /// Every value-routed screen, attachable to any stack root. |
236 | /// Every value-routed screen, attachable to any stack root. |
| 182 | private struct RouteDestinations: ViewModifier { |
237 | private struct RouteDestinations: ViewModifier { |
| 183 | |
238 | |
| @@ -208,6 +263,21 @@ private struct RouteDestinations: ViewModifier { |
| 208 | } |
263 | } |
| 209 | } |
264 | } |
| 210 | |
265 | |
| |
266 | /// A type-erased route's screen, for a sidebar selection. |
| |
267 | @ViewBuilder |
| |
268 | func destination(_ route: AnyHashable) -> some View { |
| |
269 | switch route.base { |
| |
270 | case let r as RepoRoute: destination(r, client: client) |
| |
271 | case let r as MRRoute: destination(r, client: client) |
| |
272 | case let r as IssueRoute: destination(r, client: client) |
| |
273 | case let r as BuildRoute: destination(r, client: client) |
| |
274 | case let r as ReleaseRoute: destination(r, client: client) |
| |
275 | case let r as OrgRoute: destination(r, client: client) |
| |
276 | case let r as SnippetRoute: destination(r, client: client) |
| |
277 | default: EmptyView() |
| |
278 | } |
| |
279 | } |
| |
280 | |
| 211 | @ViewBuilder |
281 | @ViewBuilder |
| 212 | private func destination(_ route: RepoRoute, client: GitbayClient) -> some View { |
282 | private func destination(_ route: RepoRoute, client: GitbayClient) -> some View { |
| 213 | switch route { |
283 | switch route { |