gitbay/Views/Repos/RepoView.swift

5411753023a5e2958f17b523903abce380941107
gitbay-ios/gitbay/Views/Repos/RepoView.swift history · blame · raw

163 lines · 6630 bytes

  1import SwiftUI
  2
  3struct RepoView: View {
  4
  5    @State private var model: RepoDetailViewModel
  6    private let path: String
  7
  8    init(client: GitbayClient, path: String) {
  9        self.path = path
 10        _model = State(initialValue: RepoDetailViewModel(client: client, path: path))
 11    }
 12
 13    @State private var confirmingArchive = false
 14
 15    var body: some View {
 16        List {
 17            if let detail = model.state.value {
 18                if let error = model.actionError {
 19                    Section {
 20                        Label(error, systemImage: "hand.raised")
 21                            .foregroundStyle(.orange)
 22                            .font(.subheadline)
 23                    }
 24                }
 25                header(detail)
 26
 27                Section {
 28                    NavigationLink(value: RepoRoute.tree(repo: path, directory: "", ref: nil)) {
 29                        Label("Files", systemImage: "folder")
 30                    }
 31                    NavigationLink(value: RepoRoute.log(repo: path)) {
 32                        Label("History", systemImage: "clock")
 33                    }
 34                    NavigationLink(value: MRRoute.list(repo: path)) {
 35                        Label("Merge Requests", systemImage: "arrow.triangle.merge")
 36                    }
 37                    NavigationLink(value: IssueRoute.list(repo: path)) {
 38                        Label("Issues", systemImage: "smallcircle.filled.circle")
 39                    }
 40                    NavigationLink(value: BuildRoute.list(repo: path)) {
 41                        Label("Builds", systemImage: "hammer")
 42                    }
 43                    NavigationLink(value: ReleaseRoute.list(repo: path)) {
 44                        Label("Releases", systemImage: "shippingbox")
 45                    }
 46                    NavigationLink(value: RepoRoute.grep(repo: path)) {
 47                        Label("Search in Files", systemImage: "text.magnifyingglass")
 48                    }
 49                    NavigationLink(value: RepoRoute.settings(repo: path)) {
 50                        Label("Settings", systemImage: "gearshape")
 51                    }
 52                    NavigationLink(value: RepoRoute.profile(String(path.split(separator: "/").first ?? ""))) {
 53                        Label(String(path.split(separator: "/").first ?? ""), systemImage: "person.crop.circle")
 54                    }
 55                }
 56
 57                if let readme = model.readme {
 58                    Section("README") {
 59                        MarkdownView(markdown: readme)
 60                            .padding(.vertical, 4)
 61                    }
 62                }
 63            }
 64        }
 65        .overlay { LoadStateOverlay(state: model.state) }
 66        .navigationTitle(String(path.split(separator: "/").last ?? ""))
 67        .navigationBarTitleDisplayMode(.inline)
 68        .toolbar { toolbar }
 69        .task { await model.load() }
 70        .refreshable { await model.load() }
 71        .confirmationDialog(
 72            model.state.value?.isArchived == true
 73                ? "Unarchive \(path)?"
 74                : "Archive \(path)? Pushes and issue/MR writes will be refused.",
 75            isPresented: $confirmingArchive
 76        ) {
 77            Button(
 78                model.state.value?.isArchived == true ? "Unarchive" : "Archive",
 79                role: model.state.value?.isArchived == true ? nil : .destructive
 80            ) {
 81                Task { await model.setArchived(!(model.state.value?.isArchived ?? false)) }
 82            }
 83            Button("Cancel", role: .cancel) {}
 84        }
 85    }
 86
 87    @ToolbarContentBuilder
 88    private var toolbar: some ToolbarContent {
 89        ToolbarItem(placement: .topBarTrailing) {
 90            if let detail = model.state.value {
 91                Menu {
 92                    if let pinned = model.isPinned {
 93                        Button {
 94                            Task { await model.setPinned(!pinned) }
 95                        } label: {
 96                            Label(pinned ? "Unpin" : "Pin",
 97                                  systemImage: pinned ? "pin.slash" : "pin")
 98                        }
 99                    }
100                    Divider()
101                    Button(role: detail.isArchived ? nil : .destructive) {
102                        confirmingArchive = true
103                    } label: {
104                        Label(detail.isArchived ? "Unarchive" : "Archive",
105                              systemImage: "archivebox")
106                    }
107                } label: {
108                    if model.working {
109                        ProgressView()
110                    } else {
111                        Image(systemName: "ellipsis.circle")
112                    }
113                }
114                .disabled(model.working)
115                .accessibilityIdentifier("repo-actions-menu")
116            }
117        }
118    }
119
120    @ViewBuilder
121    private func header(_ detail: RepoDetail) -> some View {
122        Section {
123            VStack(alignment: .leading, spacing: 8) {
124                HStack(spacing: 6) {
125                    Text(detail.path)
126                        .font(.headline)
127                    if detail.visibility == "private" {
128                        Image(systemName: "lock.fill")
129                            .font(.caption)
130                            .foregroundStyle(.secondary)
131                    }
132                    if detail.isArchived {
133                        Text("archived")
134                            .font(.caption2)
135                            .padding(.horizontal, 5)
136                            .padding(.vertical, 1)
137                            .background(.quaternary, in: Capsule())
138                    }
139                }
140                if let description = detail.description, !description.isEmpty {
141                    Text(description)
142                        .font(.subheadline)
143                        .foregroundStyle(.secondary)
144                }
145                if let website = detail.website, let url = URL(string: website) {
146                    Link(website, destination: url)
147                        .font(.caption)
148                        .lineLimit(1)
149                }
150                HStack(spacing: 12) {
151                    Label(detail.defaultBranch, systemImage: "arrow.triangle.branch")
152                    if let topics = detail.topics, !topics.isEmpty {
153                        Text(topics.joined(separator: " · "))
154                            .lineLimit(1)
155                    }
156                }
157                .font(.caption)
158                .foregroundStyle(.secondary)
159            }
160            .padding(.vertical, 4)
161        }
162    }
163}