gitbay/Views/Repos/RepoListView.swift

1c5622210d0f72efd86582ce195a6741401e7939
gitbay-ios/gitbay/Views/Repos/RepoListView.swift history · blame · raw

192 lines · 6790 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.
 56struct 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                        GBNotice(error)
 84                    }
 85                }
 86            }
 87            .navigationTitle("New Repository")
 88            .navigationBarTitleDisplayMode(.inline)
 89            .toolbar {
 90                ToolbarItem(placement: .cancellationAction) {
 91                    Button("Cancel") { dismiss() }
 92                }
 93                ToolbarItem(placement: .confirmationAction) {
 94                    if model.working {
 95                        ProgressView()
 96                    } else {
 97                        Button("Create") {
 98                            Task {
 99                                if await model.create(path: path, isPrivate: isPrivate) != nil {
100                                    onCreated()
101                                }
102                            }
103                        }
104                        .disabled(!path.contains("/")
105                            || path.hasSuffix("/")
106                            || path.hasPrefix("/"))
107                        .accessibilityIdentifier("repo-create-submit")
108                    }
109                }
110            }
111            .interactiveDismissDisabled(model.working)
112            .onAppear {
113                if !seeded {
114                    path = ownerPrefix.isEmpty ? "" : ownerPrefix + "/"
115                    seeded = true
116                }
117            }
118        }
119    }
120}
121
122private struct RepoRow: View {
123    let repo: RepoSummary
124
125    var body: some View {
126        VStack(alignment: .leading, spacing: 3) {
127            HStack(spacing: 6) {
128                Text(repo.path)
129                    .font(.gbSans(.body).weight(.medium))
130                    .lineLimit(1)
131                if repo.isPrivate {
132                    Image(systemName: "lock.fill")
133                        .font(.gbSans(.caption2))
134                        .foregroundStyle(.secondary)
135                }
136                if repo.isArchived {
137                    GBChip("archived", .secondary)
138                }
139            }
140            if let description = repo.description, !description.isEmpty {
141                Text(description)
142                    .font(.gbSans(.subheadline))
143                    .foregroundStyle(.secondary)
144                    .lineLimit(2)
145            }
146        }
147        .padding(.vertical, 2)
148    }
149}
150
151/// The account switcher and sign-out, on every top-level screen.
152struct AccountMenu: ToolbarContent {
153
154    @Environment(SessionStore.self) private var session
155
156    var body: some ToolbarContent {
157        ToolbarItem(placement: .topBarTrailing) {
158            Menu {
159                if let current = session.current {
160                    Section(current.label) {
161                        NavigationLink(value: RepoRoute.profile(current.username)) {
162                            Label("My Profile", systemImage: "person.crop.circle")
163                        }
164                        NavigationLink(value: RepoRoute.account) {
165                            Label("Keys & Email", systemImage: "key")
166                        }
167                    }
168                }
169                if !session.accounts.filter({ $0.id != session.current?.id }).isEmpty {
170                    Section("Switch account") {
171                        ForEach(session.accounts.filter { $0.id != session.current?.id }) { account in
172                            Button(account.label) {
173                                session.activate(account)
174                            }
175                        }
176                    }
177                }
178                Section {
179                    NavigationLink("Add Account", value: RepoRoute.addAccount)
180                    if let current = session.current {
181                        Button("Sign Out", role: .destructive) {
182                            session.remove(current)
183                        }
184                    }
185                }
186            } label: {
187                Image(systemName: "person.crop.circle")
188            }
189            .accessibilityIdentifier("account-menu")
190        }
191    }
192}