a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Repos/RepoListView.swift

189 lines · 6646 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        }
 39        .sheet(isPresented: $composing) {
 40            RepoCreateSheet(
 41                model: createModel,
 42                ownerPrefix: session.current?.username ?? ""
 43            ) {
 44                composing = false
 45                Task { await model.load() }
 46            }
 47        }
 48        .task { await model.load() }
 49        .refreshable { await model.load() }
 50    }
 51}
 52
 53/// `repo create <owner/name> [--private]`  the name carries the owner,
 54/// so org repos are created by typing org/name.
 55struct RepoCreateSheet: View {
 56
 57    let model: RepoCreateViewModel
 58    let ownerPrefix: String
 59    let onCreated: () -> Void
 60
 61    @Environment(\.dismiss) private var dismiss
 62    @State private var path = ""
 63    @State private var isPrivate = false
 64    @State private var seeded = false
 65
 66    var body: some View {
 67        NavigationStack {
 68            Form {
 69                Section {
 70                    TextField("owner/name", text: $path)
 71                        .autocorrectionDisabled()
 72                        .textInputAutocapitalization(.never)
 73                        .accessibilityIdentifier("repo-create-path")
 74                } footer: {
 75                    Text("Use org/name to create under an organization you can write to.")
 76                }
 77                Section {
 78                    Toggle("Private", isOn: $isPrivate)
 79                }
 80                if let error = model.errorMessage {
 81                    Section {
 82                        GBNotice(error)
 83                    }
 84                }
 85            }
 86            .navigationTitle("New Repository")
 87            .navigationBarTitleDisplayMode(.inline)
 88            .toolbar {
 89                ToolbarItem(placement: .cancellationAction) {
 90                    Button("Cancel") { dismiss() }
 91                }
 92                ToolbarItem(placement: .confirmationAction) {
 93                    if model.working {
 94                        ProgressView()
 95                    } else {
 96                        Button("Create") {
 97                            Task {
 98                                if await model.create(path: path, isPrivate: isPrivate) != nil {
 99                                    onCreated()
100                                }
101                            }
102                        }
103                        .disabled(!path.contains("/")
104                            || path.hasSuffix("/")
105                            || path.hasPrefix("/"))
106                        .accessibilityIdentifier("repo-create-submit")
107                    }
108                }
109            }
110            .interactiveDismissDisabled(model.working)
111            .onAppear {
112                if !seeded {
113                    path = ownerPrefix.isEmpty ? "" : ownerPrefix + "/"
114                    seeded = true
115                }
116            }
117        }
118    }
119}
120
121private struct RepoRow: View {
122    let repo: RepoSummary
123
124    var body: some View {
125        VStack(alignment: .leading, spacing: 3) {
126            HStack(spacing: 6) {
127                Text(repo.path)
128                    .font(.gbSans(.body).weight(.medium))
129                    .lineLimit(1)
130                if repo.isPrivate {
131                    Image(systemName: "lock.fill")
132                        .font(.gbSans(.caption2))
133                        .foregroundStyle(.secondary)
134                }
135                if repo.isArchived {
136                    GBChip("archived", .secondary)
137                }
138            }
139            if let description = repo.description, !description.isEmpty {
140                Text(description)
141                    .font(.gbSans(.subheadline))
142                    .foregroundStyle(.secondary)
143                    .lineLimit(2)
144            }
145        }
146        .padding(.vertical, 2)
147    }
148}
149
150/// The account switcher and sign-out. It lives on the My Profile tab
151/// only: the other tabs are about the instance, not about who you are.
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.account) {
162                            Label("Keys & Email", systemImage: "key")
163                        }
164                    }
165                }
166                if !session.accounts.filter({ $0.id != session.current?.id }).isEmpty {
167                    Section("Switch account") {
168                        ForEach(session.accounts.filter { $0.id != session.current?.id }) { account in
169                            Button(account.label) {
170                                session.activate(account)
171                            }
172                        }
173                    }
174                }
175                Section {
176                    NavigationLink("Add Account", value: RepoRoute.addAccount)
177                    if let current = session.current {
178                        Button("Sign Out", role: .destructive) {
179                            session.remove(current)
180                        }
181                    }
182                }
183            } label: {
184                Image(systemName: "person.crop.circle")
185            }
186            .accessibilityIdentifier("account-menu")
187        }
188    }
189}