| @@ -5,11 +5,13 @@ struct ContentView: View { |
| 5 | 5 | @Environment(SessionStore.self) private var session |
| 6 | 6 | @Environment(PushRouter.self) private var router |
| 7 | 7 | @Environment(\.openURL) private var openURL |
| 8 | @Environment(\.horizontalSizeClass) private var sizeClass |
| 8 | 9 | |
| 9 | 10 | enum Tabs { case dashboard, feed, repositories, explore, profile } |
| 10 | 11 | |
| 11 | 12 | @State private var selectedTab = Tabs.dashboard |
| 12 | 13 | @State private var dashboardPath = NavigationPath() |
| 14 | @State private var repoPath = NavigationPath() |
| 13 | 15 | @State private var unread = 0 |
| 14 | 16 | |
| 15 | 17 | /// whoami's admin flag, read on the My Profile tab, decides whether |
| @@ -29,9 +31,15 @@ struct ContentView: View { |
| 29 | 31 | if let client = session.client, let account = session.current { |
| 30 | 32 | TabView(selection: $selectedTab) { |
| 31 | 33 | Tab("Dashboard", systemImage: "square.grid.2x2", value: Tabs.dashboard) { |
| 32 | | NavigationStack(path: $dashboardPath) { |
| 33 | | DashboardView(client: client, unread: $unread) |
| 34 | | .navigationDestinations(client: client) |
| 34 | if sizeClass == .regular { |
| 35 | SplitStack(path: $dashboardPath, client: client, placeholder: "Nothing selected") { |
| 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 | 45 | .badge(unread) |
| @@ -42,9 +50,15 @@ struct ContentView: View { |
| 42 | 50 | } |
| 43 | 51 | } |
| 44 | 52 | Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) { |
| 45 | | NavigationStack { |
| 46 | | RepoListView(client: client) |
| 47 | | .navigationDestinations(client: client) |
| 53 | if sizeClass == .regular { |
| 54 | SplitStack(path: $repoPath, client: client, placeholder: "No repository selected") { |
| 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 | 64 | Tab("Explore", systemImage: "safari", value: Tabs.explore) { |
| @@ -108,6 +122,7 @@ struct ContentView: View { |
| 108 | 122 | private func routePendingNotification(_ account: Account) async { |
| 109 | 123 | if pathAccount != account.id { |
| 110 | 124 | dashboardPath = NavigationPath() |
| 125 | repoPath = NavigationPath() |
| 111 | 126 | selectedTab = .dashboard |
| 112 | 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 | 236 | /// Every value-routed screen, attachable to any stack root. |
| 182 | 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 | 281 | @ViewBuilder |
| 212 | 282 | private func destination(_ route: RepoRoute, client: GitbayClient) -> some View { |
| 213 | 283 | switch route { |