gitbay/Views/Repos/RepoListView.swift

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

183 lines · 6344 bytes

  1import SwiftUI
  2
  3struct RepoListView: View {
  4
  5    @Environment(SessionStore.self) private var session
  6    @State private var model: RepoListViewModel
  7    @State private var createModel: RepoCreateViewModel
  8    @State private var composing = false
  9
 10    init(client: GitbayClient) {
 11        _model = State(initialValue: RepoListViewModel(client: client))
 12        _createModel = State(initialValue: RepoCreateViewModel(client: client))
 13    }
 14
 15    var body: some View {
 16        List {
 17            ForEach(model.visibleRepos) { repo in
 18                NavigationLink(value: RepoRoute.repo(repo.path)) {
 19                    RepoRow(repo: repo)
 20                }
 21            }
 22            if model.searchText.isEmpty {
 23                PageFooter(list: model.list)
 24            }
 25        }
 26        .overlay { LoadStateOverlay(state: model.state, isEmpty: model.visibleRepos.isEmpty) }
 27        .searchable(text: Bindable(model).searchText, prompt: "Filter repositories")
 28        .navigationTitle("Repositories")
 29        .toolbar {
 30            ToolbarItem(placement: .topBarTrailing) {
 31                Button {
 32                    composing = true
 33                } label: {
 34                    Image(systemName: "plus")
 35                }
 36                .accessibilityIdentifier("repo-create-button")
 37            }
 38            AccountMenu()
 39        }
 40        .sheet(isPresented: $composing) {
 41            RepoCreateSheet(
 42                model: createModel,
 43                ownerPrefix: session.current?.username ?? ""
 44            ) {
 45                composing = false
 46                Task { await model.load() }
 47            }
 48        }
 49        .task { await model.load() }
 50        .refreshable { await model.load() }
 51    }
 52}
 53
 54/// `repo create <owner/name> [--private]` — the name carries the owner,
 55/// so org repos are created by typing org/name.
 56private struct RepoCreateSheet: View {
 57
 58    let model: RepoCreateViewModel
 59    let ownerPrefix: String
 60    let onCreated: () -> Void
 61
 62    @Environment(\.dismiss) private var dismiss
 63    @State private var path = ""
 64    @State private var isPrivate = false
 65    @State private var seeded = false
 66
 67    var body: some View {
 68        NavigationStack {
 69            Form {
 70                Section {
 71                    TextField("owner/name", text: $path)
 72                        .autocorrectionDisabled()
 73                        .textInputAutocapitalization(.never)
 74                        .accessibilityIdentifier("repo-create-path")
 75                } footer: {
 76                    Text("Use org/name to create under an organization you can write to.")
 77                }
 78                Section {
 79                    Toggle("Private", isOn: $isPrivate)
 80                }
 81                if let error = model.errorMessage {
 82                    Section {
 83                        Label(error, systemImage: "exclamationmark.triangle")
 84                            .foregroundStyle(.red)
 85                            .font(.subheadline)
 86                    }
 87                }
 88            }
 89            .navigationTitle("New Repository")
 90            .navigationBarTitleDisplayMode(.inline)
 91            .toolbar {
 92                ToolbarItem(placement: .cancellationAction) {
 93                    Button("Cancel") { dismiss() }
 94                }
 95                ToolbarItem(placement: .confirmationAction) {
 96                    if model.working {
 97                        ProgressView()
 98                    } else {
 99                        Button("Create") {
100                            Task {
101                                if await model.create(path: path, isPrivate: isPrivate) != nil {
102                                    onCreated()
103                                }
104                            }
105                        }
106                        .disabled(!path.contains("/")
107                            || path.hasSuffix("/")
108                            || path.hasPrefix("/"))
109                        .accessibilityIdentifier("repo-create-submit")
110                    }
111                }
112            }
113            .interactiveDismissDisabled(model.working)
114            .onAppear {
115                if !seeded {
116                    path = ownerPrefix.isEmpty ? "" : ownerPrefix + "/"
117                    seeded = true
118                }
119            }
120        }
121    }
122}
123
124private struct RepoRow: View {
125    let repo: RepoSummary
126
127    var body: some View {
128        VStack(alignment: .leading, spacing: 3) {
129            HStack(spacing: 6) {
130                Text(repo.path)
131                    .font(.body.weight(.medium))
132                    .lineLimit(1)
133                if repo.isPrivate {
134                    Image(systemName: "lock.fill")
135                        .font(.caption2)
136                        .foregroundStyle(.secondary)
137                }
138                if repo.isArchived {
139                    Text("archived")
140                        .font(.caption2)
141                        .padding(.horizontal, 5)
142                        .padding(.vertical, 1)
143                        .background(.quaternary, in: Capsule())
144                }
145            }
146            if let description = repo.description, !description.isEmpty {
147                Text(description)
148                    .font(.subheadline)
149                    .foregroundStyle(.secondary)
150                    .lineLimit(2)
151            }
152        }
153        .padding(.vertical, 2)
154    }
155}
156
157/// The account switcher and sign-out, on every top-level screen.
158struct AccountMenu: ToolbarContent {
159
160    @Environment(SessionStore.self) private var session
161
162    var body: some ToolbarContent {
163        ToolbarItem(placement: .topBarTrailing) {
164            Menu {
165                if let current = session.current {
166                    Section(current.label) {
167                        Button("Sign Out", role: .destructive) {
168                            session.remove(current)
169                        }
170                    }
171                }
172                ForEach(session.accounts.filter { $0.id != session.current?.id }) { account in
173                    Button(account.label) {
174                        session.activate(account)
175                    }
176                }
177                NavigationLink("Add Account", value: RepoRoute.addAccount)
178            } label: {
179                Image(systemName: "person.crop.circle")
180            }
181        }
182    }
183}