a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Repos/RepoView.swift

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