a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Repos/RepoView.swift

173 lines · 7158 bytes

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