import SwiftUI struct RepoListView: View { @Environment(SessionStore.self) private var session @State private var model: RepoListViewModel @State private var createModel: RepoCreateViewModel @State private var composing = false init(client: GitbayClient) { _model = State(initialValue: RepoListViewModel(client: client)) _createModel = State(initialValue: RepoCreateViewModel(client: client)) } var body: some View { List { ForEach(model.visibleRepos) { repo in NavigationLink(value: RepoRoute.repo(repo.path)) { RepoRow(repo: repo) } } if model.searchText.isEmpty { PageFooter(list: model.list) } } .overlay { LoadStateOverlay(state: model.state, isEmpty: model.visibleRepos.isEmpty) } .searchable(text: Bindable(model).searchText, prompt: "Filter repositories") .navigationTitle("Repositories") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { composing = true } label: { Image(systemName: "plus") } .accessibilityIdentifier("repo-create-button") } } .sheet(isPresented: $composing) { RepoCreateSheet( model: createModel, ownerPrefix: session.current?.username ?? "" ) { composing = false Task { await model.load() } } } .task { await model.load() } .refreshable { await model.load() } } } /// `repo create [--private]` — the name carries the owner, /// so org repos are created by typing org/name. struct RepoCreateSheet: View { let model: RepoCreateViewModel let ownerPrefix: String let onCreated: () -> Void @Environment(\.dismiss) private var dismiss @State private var path = "" @State private var isPrivate = false @State private var seeded = false var body: some View { NavigationStack { Form { Section { TextField("owner/name", text: $path) .autocorrectionDisabled() .textInputAutocapitalization(.never) .accessibilityIdentifier("repo-create-path") } footer: { Text("Use org/name to create under an organization you can write to.") } Section { Toggle("Private", isOn: $isPrivate) } if let error = model.errorMessage { Section { GBNotice(error) } } } .navigationTitle("New Repository") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { if model.working { ProgressView() } else { Button("Create") { Task { if await model.create(path: path, isPrivate: isPrivate) != nil { onCreated() } } } .disabled(!path.contains("/") || path.hasSuffix("/") || path.hasPrefix("/")) .accessibilityIdentifier("repo-create-submit") } } } .interactiveDismissDisabled(model.working) .onAppear { if !seeded { path = ownerPrefix.isEmpty ? "" : ownerPrefix + "/" seeded = true } } } } } private struct RepoRow: View { let repo: RepoSummary var body: some View { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 6) { Text(repo.path) .font(.gbSans(.body).weight(.medium)) .lineLimit(1) if repo.isPrivate { Image(systemName: "lock.fill") .font(.gbSans(.caption2)) .foregroundStyle(.secondary) } if repo.isArchived { GBChip("archived", .secondary) } } if let description = repo.description, !description.isEmpty { Text(description) .font(.gbSans(.subheadline)) .foregroundStyle(.secondary) .lineLimit(2) } } .padding(.vertical, 2) } } /// The account switcher and sign-out. It lives on the My Profile tab /// only: the other tabs are about the instance, not about who you are. struct AccountMenu: ToolbarContent { @Environment(SessionStore.self) private var session var body: some ToolbarContent { ToolbarItem(placement: .topBarTrailing) { Menu { if let current = session.current { Section(current.label) { NavigationLink(value: RepoRoute.account) { Label("Keys & Email", systemImage: "key") } } } if !session.accounts.filter({ $0.id != session.current?.id }).isEmpty { Section("Switch account") { ForEach(session.accounts.filter { $0.id != session.current?.id }) { account in Button(account.label) { session.activate(account) } } } } Section { NavigationLink("Add Account", value: RepoRoute.addAccount) if let current = session.current { Button("Sign Out", role: .destructive) { session.remove(current) } } } } label: { Image(systemName: "person.crop.circle") } .accessibilityIdentifier("account-menu") } } }