import SwiftUI struct RepoView: View { @State private var model: RepoDetailViewModel private let path: String init(client: GitbayClient, path: String) { self.path = path _model = State(initialValue: RepoDetailViewModel(client: client, path: path)) } @State private var confirmingArchive = false var body: some View { List { if let detail = model.state.value { if let error = model.actionError { Section { GBNotice(error, .gbWarn) } } header(detail) Section { NavigationLink(value: RepoRoute.tree(repo: path, directory: "", ref: nil)) { Label("Files", systemImage: "folder") } NavigationLink(value: RepoRoute.log(repo: path, ref: nil, path: nil)) { Label("History", systemImage: "clock") } NavigationLink(value: RepoRoute.refs(repo: path)) { Label("Branches & Tags", systemImage: "arrow.triangle.branch") } NavigationLink(value: MRRoute.list(repo: path)) { Label("Merge Requests", systemImage: "arrow.triangle.merge") } NavigationLink(value: IssueRoute.list(repo: path)) { Label("Issues", systemImage: "smallcircle.filled.circle") } NavigationLink(value: RepoRoute.milestones(repo: path)) { Label("Milestones", systemImage: "flag") } NavigationLink(value: BuildRoute.list(repo: path)) { Label("Builds", systemImage: "hammer") } NavigationLink(value: ReleaseRoute.list(repo: path)) { Label("Releases", systemImage: "shippingbox") } NavigationLink(value: RepoRoute.wiki(repo: path)) { Label("Wiki", systemImage: "book") } NavigationLink(value: RepoRoute.grep(repo: path)) { Label("Search in Files", systemImage: "text.magnifyingglass") } NavigationLink(value: RepoRoute.settings(repo: path)) { Label("Settings", systemImage: "gearshape") } NavigationLink(value: RepoRoute.profile(String(path.split(separator: "/").first ?? ""))) { Label(String(path.split(separator: "/").first ?? ""), systemImage: "person.crop.circle") } } if let readme = model.readme { Section("README") { ReadmeView(name: model.readmeName ?? "README.md", content: readme) .padding(.vertical, 4) } } } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle(String(path.split(separator: "/").last ?? "")) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } .task { await model.load() } .refreshable { await model.load() } .confirmationDialog( model.state.value?.isArchived == true ? "Unarchive \(path)?" : "Archive \(path)? Pushes and issue/MR writes will be refused.", isPresented: $confirmingArchive ) { Button( model.state.value?.isArchived == true ? "Unarchive" : "Archive", role: model.state.value?.isArchived == true ? nil : .destructive ) { Task { await model.setArchived(!(model.state.value?.isArchived ?? false)) } } Button("Cancel", role: .cancel) {} } } @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .topBarTrailing) { if let detail = model.state.value { Menu { if let pinned = model.isPinned { Button { Task { await model.setPinned(!pinned) } } label: { Label(pinned ? "Unpin" : "Pin", systemImage: pinned ? "pin.slash" : "pin") } } Divider() Button(role: detail.isArchived ? nil : .destructive) { confirmingArchive = true } label: { Label(detail.isArchived ? "Unarchive" : "Archive", systemImage: "archivebox") } } label: { if model.working { ProgressView() } else { Image(systemName: "ellipsis.circle") } } .disabled(model.working) .accessibilityIdentifier("repo-actions-menu") } } } @ViewBuilder private func header(_ detail: RepoDetail) -> some View { Section { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 6) { Text(detail.path) .font(.gbSans(.headline)) if detail.visibility == "private" { Image(systemName: "lock.fill") .font(.gbSans(.caption)) .foregroundStyle(.secondary) } if detail.isArchived { GBChip("archived", .secondary) } } if let description = detail.description, !description.isEmpty { Text(description) .font(.gbSans(.subheadline)) .foregroundStyle(.secondary) } if let website = detail.website, let url = URL(string: website) { Link(website, destination: url) .font(.gbSans(.caption)) .lineLimit(1) } HStack(spacing: 12) { Label(detail.defaultBranch, systemImage: "arrow.triangle.branch") if let topics = detail.topics, !topics.isEmpty { Text(topics.joined(separator: " ยท ")) .lineLimit(1) } } .font(.gbSans(.caption)) .foregroundStyle(.secondary) } .padding(.vertical, 4) } } }