Commit c108ce3805
Unsigned
Layout: unified · split
gitbay/ContentView.swift +15 −3
| @@ -12,6 +12,9 @@ struct ContentView: View { | |||
| 12 | @State private var selectedTab = Tabs.dashboard | 12 | @State private var selectedTab = Tabs.dashboard |
| 13 | @State private var dashboardPath = NavigationPath() | 13 | @State private var dashboardPath = NavigationPath() |
| 14 | @State private var repoPath = NavigationPath() | 14 | @State private var repoPath = NavigationPath() |
| 15 | @State private var feedPath = NavigationPath() | ||
| 16 | @State private var explorePath = NavigationPath() | ||
| 17 | @State private var profilePath = NavigationPath() | ||
| 15 | @State private var unread = 0 | 18 | @State private var unread = 0 |
| 16 | 19 | ||
| 17 | /// whoami's admin flag, read on the My Profile tab, decides whether | 20 | /// whoami's admin flag, read on the My Profile tab, decides whether |
| @@ -40,14 +43,16 @@ struct ContentView: View { | |||
| 40 | DashboardView(client: client, unread: $unread) | 43 | DashboardView(client: client, unread: $unread) |
| 41 | .navigationDestinations(client: client) | 44 | .navigationDestinations(client: client) |
| 42 | } | 45 | } |
| 46 | .pushesRoutes(into: $dashboardPath) | ||
| 43 | } | 47 | } |
| 44 | } | 48 | } |
| 45 | .badge(unread) | 49 | .badge(unread) |
| 46 | Tab("Feed", systemImage: "bolt", value: Tabs.feed) { | 50 | Tab("Feed", systemImage: "bolt", value: Tabs.feed) { |
| 47 | NavigationStack { | 51 | NavigationStack(path: $feedPath) { |
| 48 | FeedView(client: client) | 52 | FeedView(client: client) |
| 49 | .navigationDestinations(client: client) | 53 | .navigationDestinations(client: client) |
| 50 | } | 54 | } |
| 55 | .pushesRoutes(into: $feedPath) | ||
| 51 | } | 56 | } |
| 52 | Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) { | 57 | Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) { |
| 53 | if sizeClass == .regular { | 58 | if sizeClass == .regular { |
| @@ -59,16 +64,18 @@ struct ContentView: View { | |||
| 59 | RepoListView(client: client) | 64 | RepoListView(client: client) |
| 60 | .navigationDestinations(client: client) | 65 | .navigationDestinations(client: client) |
| 61 | } | 66 | } |
| 67 | .pushesRoutes(into: $repoPath) | ||
| 62 | } | 68 | } |
| 63 | } | 69 | } |
| 64 | Tab("Explore", systemImage: "safari", value: Tabs.explore) { | 70 | Tab("Explore", systemImage: "safari", value: Tabs.explore) { |
| 65 | NavigationStack { | 71 | NavigationStack(path: $explorePath) { |
| 66 | ExploreView(client: client) | 72 | ExploreView(client: client) |
| 67 | .navigationDestinations(client: client) | 73 | .navigationDestinations(client: client) |
| 68 | } | 74 | } |
| 75 | .pushesRoutes(into: $explorePath) | ||
| 69 | } | 76 | } |
| 70 | Tab("My Profile", systemImage: "person.crop.circle", value: Tabs.profile) { | 77 | Tab("My Profile", systemImage: "person.crop.circle", value: Tabs.profile) { |
| 71 | NavigationStack { | 78 | NavigationStack(path: $profilePath) { |
| 72 | ProfileView(client: client, name: account.username, | 79 | ProfileView(client: client, name: account.username, |
| 73 | title: "My Profile") | 80 | title: "My Profile") |
| 74 | .toolbar { AccountMenu(isAdmin: isAdmin) } | 81 | .toolbar { AccountMenu(isAdmin: isAdmin) } |
| @@ -78,6 +85,7 @@ struct ContentView: View { | |||
| 78 | } | 85 | } |
| 79 | .navigationDestinations(client: client) | 86 | .navigationDestinations(client: client) |
| 80 | } | 87 | } |
| 88 | .pushesRoutes(into: $profilePath) | ||
| 81 | } | 89 | } |
| 82 | } | 90 | } |
| 83 | .id(account.id) // a fresh tab bar and stacks per account | 91 | .id(account.id) // a fresh tab bar and stacks per account |
| @@ -123,6 +131,9 @@ struct ContentView: View { | |||
| 123 | if pathAccount != account.id { | 131 | if pathAccount != account.id { |
| 124 | dashboardPath = NavigationPath() | 132 | dashboardPath = NavigationPath() |
| 125 | repoPath = NavigationPath() | 133 | repoPath = NavigationPath() |
| 134 | feedPath = NavigationPath() | ||
| 135 | explorePath = NavigationPath() | ||
| 136 | profilePath = NavigationPath() | ||
| 126 | selectedTab = .dashboard | 137 | selectedTab = .dashboard |
| 127 | pathAccount = account.id | 138 | pathAccount = account.id |
| 128 | } | 139 | } |
| @@ -229,6 +240,7 @@ private struct SplitStack<Sidebar: View>: View { | |||
| 229 | } | 240 | } |
| 230 | .navigationDestinations(client: client) | 241 | .navigationDestinations(client: client) |
| 231 | } | 242 | } |
| 243 | .pushesRoutes(into: $path) | ||
| 232 | } | 244 | } |
| 233 | } | 245 | } |
| 234 | } | 246 | } |
gitbay/Views/Account/NotificationsView.swift +8 −6
| @@ -17,13 +17,15 @@ struct NotificationsView: View { | |||
| 17 | 17 | ||
| 18 | var body: some View { | 18 | var body: some View { |
| 19 | List { | 19 | List { |
| 20 | Picker("Filter", selection: Bindable(model).showAll) { | 20 | Section { |
| 21 | Text("Unread").tag(false) | 21 | Picker("Filter", selection: Bindable(model).showAll) { |
| 22 | Text("All").tag(true) | 22 | Text("Unread").tag(false) |
| 23 | Text("All").tag(true) | ||
| 24 | } | ||
| 25 | .pickerStyle(.segmented) | ||
| 26 | .listRowBackground(Color.clear) | ||
| 27 | .listRowInsets(EdgeInsets()) | ||
| 23 | } | 28 | } |
| 24 | .pickerStyle(.segmented) | ||
| 25 | .listRowBackground(Color.clear) | ||
| 26 | .listRowInsets(EdgeInsets()) | ||
| 27 | 29 | ||
| 28 | if let actionError = model.actionError { | 30 | if let actionError = model.actionError { |
| 29 | Section { GBNotice(actionError) } | 31 | Section { GBNotice(actionError) } |
gitbay/Views/Admin/AdminUsersView.swift +9 −7
| @@ -20,15 +20,17 @@ struct AdminUsersView: View { | |||
| 20 | 20 | ||
| 21 | var body: some View { | 21 | var body: some View { |
| 22 | List { | 22 | List { |
| 23 | Picker("State", selection: Bindable(model).filter) { | 23 | Section { |
| 24 | ForEach(AdminUserFilter.allCases) { filter in | 24 | Picker("State", selection: Bindable(model).filter) { |
| 25 | Text(filter.label).tag(filter) | 25 | ForEach(AdminUserFilter.allCases) { filter in |
| 26 | Text(filter.label).tag(filter) | ||
| 27 | } | ||
| 26 | } | 28 | } |
| 29 | .pickerStyle(.segmented) | ||
| 30 | .listRowBackground(Color.clear) | ||
| 31 | .listRowInsets(EdgeInsets()) | ||
| 32 | .accessibilityIdentifier("admin-user-filter") | ||
| 27 | } | 33 | } |
| 28 | .pickerStyle(.segmented) | ||
| 29 | .listRowBackground(Color.clear) | ||
| 30 | .listRowInsets(EdgeInsets()) | ||
| 31 | .accessibilityIdentifier("admin-user-filter") | ||
| 32 | 34 | ||
| 33 | if let error = model.actionError { | 35 | if let error = model.actionError { |
| 34 | Section { | 36 | Section { |
gitbay/Views/Builds/BuildDetailView.swift +24 −14
| @@ -29,6 +29,9 @@ struct BuildDetailView: View { | |||
| 29 | .padding(.vertical, 8) | 29 | .padding(.vertical, 8) |
| 30 | } | 30 | } |
| 31 | header(detail.build) | 31 | header(detail.build) |
| 32 | if !detail.log.isEmpty { | ||
| 33 | jumpBar(failed: detail.lines.failed != nil) | ||
| 34 | } | ||
| 32 | Divider() | 35 | Divider() |
| 33 | log(detail) | 36 | log(detail) |
| 34 | } | 37 | } |
| @@ -39,20 +42,6 @@ struct BuildDetailView: View { | |||
| 39 | .navigationBarTitleDisplayMode(.inline) | 42 | .navigationBarTitleDisplayMode(.inline) |
| 40 | .searchable(text: $query, prompt: "Filter log lines") | 43 | .searchable(text: $query, prompt: "Filter log lines") |
| 41 | .toolbar { | 44 | .toolbar { |
| 42 | ToolbarItemGroup(placement: .topBarTrailing) { | ||
| 43 | if let detail = model.state.value, !detail.log.isEmpty { | ||
| 44 | if detail.lines.failed != nil { | ||
| 45 | Button("Jump to failure", systemImage: "exclamationmark.triangle") { | ||
| 46 | jump = .failure | ||
| 47 | } | ||
| 48 | .accessibilityIdentifier("build-jump-failure") | ||
| 49 | } | ||
| 50 | Button("Jump to end", systemImage: "arrow.down.to.line") { | ||
| 51 | jump = .end | ||
| 52 | } | ||
| 53 | .accessibilityIdentifier("build-jump-end") | ||
| 54 | } | ||
| 55 | } | ||
| 56 | ToolbarItem(placement: .topBarTrailing) { | 45 | ToolbarItem(placement: .topBarTrailing) { |
| 57 | if model.isCancellable { | 46 | if model.isCancellable { |
| 58 | if model.working { | 47 | if model.working { |
| @@ -89,6 +78,27 @@ struct BuildDetailView: View { | |||
| 89 | } | 78 | } |
| 90 | } | 79 | } |
| 91 | 80 | ||
| 81 | private func jumpBar(failed: Bool) -> some View { | ||
| 82 | HStack(spacing: 20) { | ||
| 83 | if failed { | ||
| 84 | Button("Jump to failure", systemImage: "exclamationmark.triangle") { | ||
| 85 | jump = .failure | ||
| 86 | } | ||
| 87 | .accessibilityIdentifier("build-jump-failure") | ||
| 88 | } | ||
| 89 | Button("Jump to end", systemImage: "arrow.down.to.line") { | ||
| 90 | jump = .end | ||
| 91 | } | ||
| 92 | .accessibilityIdentifier("build-jump-end") | ||
| 93 | Spacer() | ||
| 94 | } | ||
| 95 | .font(.gbSans(.caption)) | ||
| 96 | .foregroundStyle(Color.gbAccent) | ||
| 97 | .buttonStyle(.borderless) | ||
| 98 | .padding(.horizontal, 16) | ||
| 99 | .padding(.bottom, 10) | ||
| 100 | } | ||
| 101 | |||
| 92 | private func header(_ build: Build) -> some View { | 102 | private func header(_ build: Build) -> some View { |
| 93 | VStack(alignment: .leading, spacing: 6) { | 103 | VStack(alignment: .leading, spacing: 6) { |
| 94 | HStack(spacing: 8) { | 104 | HStack(spacing: 8) { |
gitbay/Views/Discovery/ExploreView.swift +8 −6
| @@ -42,14 +42,16 @@ struct ExploreView: View { | |||
| 42 | var body: some View { | 42 | var body: some View { |
| 43 | List { | 43 | List { |
| 44 | if search.isSearching { | 44 | if search.isSearching { |
| 45 | Picker("Kind", selection: Bindable(search).kind) { | 45 | Section { |
| 46 | ForEach(SearchKind.allCases) { kind in | 46 | Picker("Kind", selection: Bindable(search).kind) { |
| 47 | Text(kind.label).tag(kind) | 47 | ForEach(SearchKind.allCases) { kind in |
| 48 | Text(kind.label).tag(kind) | ||
| 49 | } | ||
| 48 | } | 50 | } |
| 51 | .pickerStyle(.segmented) | ||
| 52 | .listRowBackground(Color.clear) | ||
| 53 | .listRowInsets(EdgeInsets()) | ||
| 49 | } | 54 | } |
| 50 | .pickerStyle(.segmented) | ||
| 51 | .listRowBackground(Color.clear) | ||
| 52 | .listRowInsets(EdgeInsets()) | ||
| 53 | 55 | ||
| 54 | ForEach(search.state.value ?? []) { result in | 56 | ForEach(search.state.value ?? []) { result in |
| 55 | searchRow(for: result) | 57 | searchRow(for: result) |
gitbay/Views/Issues/IssueListView.swift +10 −11
| @@ -9,7 +9,7 @@ struct IssueListView: View { | |||
| 9 | @State private var draftTitle = "" | 9 | @State private var draftTitle = "" |
| 10 | @State private var draftBody = "" | 10 | @State private var draftBody = "" |
| 11 | @State private var draftFormat = "md" | 11 | @State private var draftFormat = "md" |
| 12 | @State private var createdIssue: Int64? | 12 | @Environment(\.pushRoute) private var pushRoute |
| 13 | 13 | ||
| 14 | private let client: GitbayClient | 14 | private let client: GitbayClient |
| 15 | 15 | ||
| @@ -21,14 +21,16 @@ struct IssueListView: View { | |||
| 21 | 21 | ||
| 22 | var body: some View { | 22 | var body: some View { |
| 23 | List { | 23 | List { |
| 24 | Picker("State", selection: Bindable(model).filter.state) { | 24 | Section { |
| 25 | ForEach(IssueFilter.State.allCases) { filter in | 25 | Picker("State", selection: Bindable(model).filter.state) { |
| 26 | Text(filter.label).tag(filter) | 26 | ForEach(IssueFilter.State.allCases) { filter in |
| 27 | Text(filter.label).tag(filter) | ||
| 28 | } | ||
| 27 | } | 29 | } |
| 30 | .pickerStyle(.segmented) | ||
| 31 | .listRowBackground(Color.clear) | ||
| 32 | .listRowInsets(EdgeInsets()) | ||
| 28 | } | 33 | } |
| 29 | .pickerStyle(.segmented) | ||
| 30 | .listRowBackground(Color.clear) | ||
| 31 | .listRowInsets(EdgeInsets()) | ||
| 32 | 34 | ||
| 33 | ForEach(model.state.value ?? []) { issue in | 35 | ForEach(model.state.value ?? []) { issue in |
| 34 | NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) { | 36 | NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) { |
| @@ -101,15 +103,12 @@ struct IssueListView: View { | |||
| 101 | draftBody = "" | 103 | draftBody = "" |
| 102 | draftFormat = "md" | 104 | draftFormat = "md" |
| 103 | composing = false | 105 | composing = false |
| 104 | createdIssue = number | 106 | pushRoute(IssueRoute.issue(repo: model.repoPath, number: number)) |
| 105 | await model.load() | 107 | await model.load() |
| 106 | } | 108 | } |
| 107 | } | 109 | } |
| 108 | } | 110 | } |
| 109 | } | 111 | } |
| 110 | .navigationDestination(item: $createdIssue) { number in | ||
| 111 | IssueView(client: client, repo: model.repoPath, number: number) | ||
| 112 | } | ||
| 113 | .task { await model.loadIfNeeded() } | 112 | .task { await model.loadIfNeeded() } |
| 114 | .task { await createModel.loadTemplates() } | 113 | .task { await createModel.loadTemplates() } |
| 115 | .refreshable { await model.load() } | 114 | .refreshable { await model.load() } |
gitbay/Views/Issues/MilestoneListView.swift +8 −6
| @@ -16,14 +16,16 @@ struct MilestoneListView: View { | |||
| 16 | 16 | ||
| 17 | var body: some View { | 17 | var body: some View { |
| 18 | List { | 18 | List { |
| 19 | Picker("State", selection: Bindable(model).filter) { | 19 | Section { |
| 20 | ForEach(MilestoneListViewModel.StateFilter.allCases) { filter in | 20 | Picker("State", selection: Bindable(model).filter) { |
| 21 | Text(filter.rawValue.capitalized).tag(filter) | 21 | ForEach(MilestoneListViewModel.StateFilter.allCases) { filter in |
| 22 | Text(filter.rawValue.capitalized).tag(filter) | ||
| 23 | } | ||
| 22 | } | 24 | } |
| 25 | .pickerStyle(.segmented) | ||
| 26 | .listRowBackground(Color.clear) | ||
| 27 | .listRowInsets(EdgeInsets()) | ||
| 23 | } | 28 | } |
| 24 | .pickerStyle(.segmented) | ||
| 25 | .listRowBackground(Color.clear) | ||
| 26 | .listRowInsets(EdgeInsets()) | ||
| 27 | 29 | ||
| 28 | if let error = model.actionError { | 30 | if let error = model.actionError { |
| 29 | Section { | 31 | Section { |
gitbay/Views/MRs/MRListView.swift +8 −6
| @@ -14,14 +14,16 @@ struct MRListView: View { | |||
| 14 | 14 | ||
| 15 | var body: some View { | 15 | var body: some View { |
| 16 | List { | 16 | List { |
| 17 | Picker("State", selection: Bindable(model).filter.state) { | 17 | Section { |
| 18 | ForEach(MRFilter.State.allCases) { filter in | 18 | Picker("State", selection: Bindable(model).filter.state) { |
| 19 | Text(filter.label).tag(filter) | 19 | ForEach(MRFilter.State.allCases) { filter in |
| 20 | Text(filter.label).tag(filter) | ||
| 21 | } | ||
| 20 | } | 22 | } |
| 23 | .pickerStyle(.segmented) | ||
| 24 | .listRowBackground(Color.clear) | ||
| 25 | .listRowInsets(EdgeInsets()) | ||
| 21 | } | 26 | } |
| 22 | .pickerStyle(.segmented) | ||
| 23 | .listRowBackground(Color.clear) | ||
| 24 | .listRowInsets(EdgeInsets()) | ||
| 25 | 27 | ||
| 26 | ForEach(model.state.value ?? []) { mr in | 28 | ForEach(model.state.value ?? []) { mr in |
| 27 | NavigationLink(value: MRRoute.mr(repo: model.repoPath, number: mr.number)) { | 29 | NavigationLink(value: MRRoute.mr(repo: model.repoPath, number: mr.number)) { |
gitbay/Views/Repos/RepoView.swift +24 −30
| @@ -19,10 +19,9 @@ struct RepoView: View { | |||
| 19 | @State private var confirmingArchive = false | 19 | @State private var confirmingArchive = false |
| 20 | @State private var confirmingFork = false | 20 | @State private var confirmingFork = false |
| 21 | @State private var forkName = "" | 21 | @State private var forkName = "" |
| 22 | @State private var forkDestination: RepoRoute? | ||
| 23 | @Environment(\.dynamicTypeSize) private var typeSize | 22 | @Environment(\.dynamicTypeSize) private var typeSize |
| 24 | @ScaledMetric(relativeTo: .title3) private var statValueHeight: CGFloat = 26 | 23 | @ScaledMetric(relativeTo: .title3) private var statValueHeight: CGFloat = 26 |
| 25 | @State private var destination: Destination? | 24 | @Environment(\.pushRoute) private var pushRoute |
| 26 | 25 | ||
| 27 | /// Pushed from buttons rather than NavigationLinks: several share a | 26 | /// Pushed from buttons rather than NavigationLinks: several share a |
| 28 | /// list row, and a link there claims the whole row and adds a chevron. | 27 | /// list row, and a link there claims the whole row and adds a chevron. |
| @@ -31,6 +30,24 @@ struct RepoView: View { | |||
| 31 | case history, refs, milestones, labels, releases, wiki, grep | 30 | case history, refs, milestones, labels, releases, wiki, grep |
| 32 | } | 31 | } |
| 33 | 32 | ||
| 33 | private func open(_ target: Destination) { | ||
| 34 | switch target { | ||
| 35 | case .files: pushRoute(RepoRoute.tree(repo: path, directory: "", ref: nil)) | ||
| 36 | case .mergeRequests: pushRoute(MRRoute.list(repo: path)) | ||
| 37 | case .issues: pushRoute(IssueRoute.list(repo: path)) | ||
| 38 | case .builds: pushRoute(BuildRoute.list(repo: path)) | ||
| 39 | case .owner: pushRoute(RepoRoute.profile(owner)) | ||
| 40 | case .settings: pushRoute(RepoRoute.settings(repo: path)) | ||
| 41 | case .history: pushRoute(RepoRoute.log(repo: path, ref: nil, path: nil)) | ||
| 42 | case .refs: pushRoute(RepoRoute.refs(repo: path)) | ||
| 43 | case .milestones: pushRoute(RepoRoute.milestones(repo: path)) | ||
| 44 | case .labels: pushRoute(RepoRoute.labels(repo: path)) | ||
| 45 | case .releases: pushRoute(ReleaseRoute.list(repo: path)) | ||
| 46 | case .wiki: pushRoute(RepoRoute.wiki(repo: path)) | ||
| 47 | case .grep: pushRoute(RepoRoute.grep(repo: path)) | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 34 | var body: some View { | 51 | var body: some View { |
| 35 | List { | 52 | List { |
| 36 | if let detail = model.state.value { | 53 | if let detail = model.state.value { |
| @@ -80,28 +97,6 @@ struct RepoView: View { | |||
| 80 | .toolbar { toolbar } | 97 | .toolbar { toolbar } |
| 81 | .task { await model.load() } | 98 | .task { await model.load() } |
| 82 | .refreshable { await model.load() } | 99 | .refreshable { await model.load() } |
| 83 | .navigationDestination(item: $forkDestination) { route in | ||
| 84 | if case .repo(let forkedPath) = route { | ||
| 85 | RepoView(client: client, path: forkedPath) | ||
| 86 | } | ||
| 87 | } | ||
| 88 | .navigationDestination(item: $destination) { destination in | ||
| 89 | switch destination { | ||
| 90 | case .files: TreeView(client: client, repo: path, directory: "", ref: nil) | ||
| 91 | case .mergeRequests: MRListView(client: client, repo: path) | ||
| 92 | case .issues: IssueListView(client: client, repo: path) | ||
| 93 | case .builds: BuildListView(client: client, repo: path) | ||
| 94 | case .owner: ProfileView(client: client, name: owner) | ||
| 95 | case .settings: RepoSettingsView(client: client, repo: path) | ||
| 96 | case .history: LogView(client: client, repo: path, ref: nil, path: nil) | ||
| 97 | case .refs: RefsView(client: client, repo: path) | ||
| 98 | case .milestones: MilestoneListView(client: client, repo: path) | ||
| 99 | case .labels: LabelListView(client: client, repo: path) | ||
| 100 | case .releases: ReleaseListView(client: client, repo: path) | ||
| 101 | case .wiki: WikiView(client: client, repo: path) | ||
| 102 | case .grep: GrepView(client: client, repo: path) | ||
| 103 | } | ||
| 104 | } | ||
| 105 | .alert("Fork \(path)", isPresented: $confirmingFork) { | 100 | .alert("Fork \(path)", isPresented: $confirmingFork) { |
| 106 | TextField("New name (optional)", text: $forkName) | 101 | TextField("New name (optional)", text: $forkName) |
| 107 | .autocorrectionDisabled() | 102 | .autocorrectionDisabled() |
| @@ -109,7 +104,7 @@ struct RepoView: View { | |||
| 109 | Button("Fork") { | 104 | Button("Fork") { |
| 110 | Task { | 105 | Task { |
| 111 | if let result = await actionsModel.fork(named: forkName) { | 106 | if let result = await actionsModel.fork(named: forkName) { |
| 112 | forkDestination = .repo(result.path) | 107 | pushRoute(RepoRoute.repo(result.path)) |
| 113 | } | 108 | } |
| 114 | } | 109 | } |
| 115 | } | 110 | } |
| @@ -208,7 +203,7 @@ struct RepoView: View { | |||
| 208 | } | 203 | } |
| 209 | Divider() | 204 | Divider() |
| 210 | Button { | 205 | Button { |
| 211 | destination = .settings | 206 | open(.settings) |
| 212 | } label: { | 207 | } label: { |
| 213 | Label("Settings", systemImage: "gearshape") | 208 | Label("Settings", systemImage: "gearshape") |
| 214 | } | 209 | } |
| @@ -239,7 +234,7 @@ struct RepoView: View { | |||
| 239 | VStack(alignment: .leading, spacing: 8) { | 234 | VStack(alignment: .leading, spacing: 8) { |
| 240 | HStack(spacing: 6) { | 235 | HStack(spacing: 6) { |
| 241 | HStack(spacing: 0) { | 236 | HStack(spacing: 0) { |
| 242 | Button(owner) { destination = .owner } | 237 | Button(owner) { open(.owner) } |
| 243 | .buttonStyle(.borderless) | 238 | .buttonStyle(.borderless) |
| 244 | .accessibilityIdentifier("repo-owner-link") | 239 | .accessibilityIdentifier("repo-owner-link") |
| 245 | Text("/" + (detail.path.split(separator: "/").last.map(String.init) ?? "")) | 240 | Text("/" + (detail.path.split(separator: "/").last.map(String.init) ?? "")) |
| @@ -348,7 +343,7 @@ struct RepoView: View { | |||
| 348 | _ title: String, label: String, value: String? = nil, id: String, | 343 | _ title: String, label: String, value: String? = nil, id: String, |
| 349 | to target: Destination, first: Bool, @ViewBuilder top: () -> some View | 344 | to target: Destination, first: Bool, @ViewBuilder top: () -> some View |
| 350 | ) -> some View { | 345 | ) -> some View { |
| 351 | Button { destination = target } label: { | 346 | Button { open(target) } label: { |
| 352 | VStack(spacing: 2) { | 347 | VStack(spacing: 2) { |
| 353 | top() | 348 | top() |
| 354 | .font(.gbSans(.title3)) | 349 | .font(.gbSans(.title3)) |
| @@ -409,7 +404,7 @@ struct RepoView: View { | |||
| 409 | } | 404 | } |
| 410 | 405 | ||
| 411 | private func pill(_ title: String, systemImage: String, to target: Destination) -> some View { | 406 | private func pill(_ title: String, systemImage: String, to target: Destination) -> some View { |
| 412 | Button { destination = target } label: { | 407 | Button { open(target) } label: { |
| 413 | Label { | 408 | Label { |
| 414 | Text(title).lineLimit(1).minimumScaleFactor(0.8) | 409 | Text(title).lineLimit(1).minimumScaleFactor(0.8) |
| 415 | } icon: { | 410 | } icon: { |
| @@ -419,7 +414,6 @@ struct RepoView: View { | |||
| 419 | .padding(.horizontal, 10) | 414 | .padding(.horizontal, 10) |
| 420 | .padding(.vertical, 8) | 415 | .padding(.vertical, 8) |
| 421 | .frame(maxWidth: .infinity, alignment: .leading) | 416 | .frame(maxWidth: .infinity, alignment: .leading) |
| 422 | .overlay(RoundedRectangle(cornerRadius: 2).stroke(Color.gbLine, lineWidth: 0.5)) | ||
| 423 | .contentShape(Rectangle()) | 417 | .contentShape(Rectangle()) |
| 424 | } | 418 | } |
| 425 | .buttonStyle(.plain) | 419 | .buttonStyle(.plain) |
gitbay/Views/Shared/BodyReferences.swift +6 −15
| @@ -87,27 +87,18 @@ extension View { | |||
| 87 | 87 | ||
| 88 | private struct ReferenceNavigation: ViewModifier { | 88 | private struct ReferenceNavigation: ViewModifier { |
| 89 | 89 | ||
| 90 | @Environment(SessionStore.self) private var session | 90 | @Environment(\.pushRoute) private var pushRoute |
| 91 | @State private var target: NotificationDestination? | ||
| 92 | 91 | ||
| 93 | func body(content: Content) -> some View { | 92 | func body(content: Content) -> some View { |
| 94 | content | 93 | content |
| 95 | .environment(\.openURL, OpenURLAction { url in | 94 | .environment(\.openURL, OpenURLAction { url in |
| 96 | guard let destination = referenceDestination(url) else { return .systemAction } | 95 | guard let destination = referenceDestination(url) else { return .systemAction } |
| 97 | target = destination | 96 | switch destination { |
| 97 | case .issue(let repo, let number): pushRoute(IssueRoute.issue(repo: repo, number: number)) | ||
| 98 | case .mr(let repo, let number): pushRoute(MRRoute.mr(repo: repo, number: number)) | ||
| 99 | case .build(let repo, let number): pushRoute(BuildRoute.detail(repo: repo, number: number)) | ||
| 100 | } | ||
| 98 | return .handled | 101 | return .handled |
| 99 | }) | 102 | }) |
| 100 | .navigationDestination(item: $target) { destination in | ||
| 101 | if let client = session.client { | ||
| 102 | switch destination { | ||
| 103 | case .issue(let repo, let number): | ||
| 104 | IssueView(client: client, repo: repo, number: number) | ||
| 105 | case .mr(let repo, let number): | ||
| 106 | MRView(client: client, repo: repo, number: number) | ||
| 107 | case .build(let repo, let number): | ||
| 108 | BuildDetailView(client: client, repo: repo, number: number) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | 103 | } |
| 113 | } | 104 | } |
gitbay/Views/Shared/SidebarRouting.swift +12
| @@ -4,6 +4,18 @@ extension EnvironmentValues { | |||
| 4 | /// Set on a list shown as a `NavigationSplitView` sidebar: what it | 4 | /// Set on a list shown as a `NavigationSplitView` sidebar: what it |
| 5 | /// selects opens in the detail column's stack. Nil in a plain stack. | 5 | /// selects opens in the detail column's stack. Nil in a plain stack. |
| 6 | @Entry var sidebarSelection: Binding<AnyHashable?>? | 6 | @Entry var sidebarSelection: Binding<AnyHashable?>? |
| 7 | |||
| 8 | /// Appends a route to the enclosing stack's path. Screens that push | ||
| 9 | /// from a button use it instead of `navigationDestination(item:)`, | ||
| 10 | /// which puts any later value-link push underneath the pushed screen. | ||
| 11 | @Entry var pushRoute: (any Hashable) -> Void = { _ in } | ||
| 12 | } | ||
| 13 | |||
| 14 | extension View { | ||
| 15 | /// Lets screens in this stack push onto `path` with `pushRoute`. | ||
| 16 | func pushesRoutes(into path: Binding<NavigationPath>) -> some View { | ||
| 17 | environment(\.pushRoute) { path.wrappedValue.append($0) } | ||
| 18 | } | ||
| 7 | } | 19 | } |
| 8 | 20 | ||
| 9 | /// A `List` whose rows select into `sidebarSelection` when there is one. | 21 | /// A `List` whose rows select into `sidebarSelection` when there is one. |