import SwiftUI /// Your snippets at every visibility, or an owner's public ones. struct SnippetListView: View { @State private var model: SnippetListViewModel @State private var composing = false init(client: GitbayClient, owner: String?) { _model = State(initialValue: SnippetListViewModel(client: client, owner: owner)) } var body: some View { List { ForEach(model.state.value ?? []) { snippet in NavigationLink(value: SnippetRoute.snippet(id: snippet.id)) { SnippetRow(snippet: snippet, showsVisibility: model.owner == nil) } } PageFooter(list: model.list) } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle(model.owner.map { "\($0)'s snippets" } ?? "Snippets") .navigationBarTitleDisplayMode(.inline) .toolbar { if model.owner == nil { ToolbarItem(placement: .topBarTrailing) { Button { composing = true } label: { Image(systemName: "plus") } .accessibilityIdentifier("snippet-create-button") } } } .sheet(isPresented: $composing) { SnippetFileSheet( heading: "New Snippet", submitLabel: "Create", fileName: "", showsSettings: true, working: model.working, errorMessage: model.actionError ) { name, description, visibility, content in Task { if await model.create( filename: name, description: description, visibility: visibility, content: content) != nil { composing = false } } } } .task { await model.load() } .refreshable { await model.load() } } } struct SnippetRow: View { let snippet: Snippet let showsVisibility: Bool var body: some View { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 6) { Text(snippet.title) .font(.gbSans(.subheadline).weight(.medium)) .lineLimit(1) if showsVisibility, snippet.visibility != "public" { GBChip(snippet.visibility, .secondary) } } HStack(spacing: 6) { Text(snippet.fileNames) .font(.gbMono(.caption)) .lineLimit(1) Spacer() Text(snippet.updatedAt, format: .relative(presentation: .named)) } .font(.gbSans(.caption)) .foregroundStyle(.secondary) } .padding(.vertical, 2) } } /// A file's name and content, with the description and visibility when /// creating. Also used to add a file, or to replace one's content, in /// which case the name is fixed. struct SnippetFileSheet: View { let heading: String let submitLabel: String let fileName: String let showsSettings: Bool let working: Bool let errorMessage: String? /// name, description, visibility, content. let onSubmit: (String, String, String, String) -> Void @Environment(\.dismiss) private var dismiss @State private var name: String @State private var description = "" @State private var visibility = "unlisted" @State private var content: String init(heading: String, submitLabel: String, fileName: String, showsSettings: Bool, initialContent: String = "", working: Bool, errorMessage: String?, onSubmit: @escaping (String, String, String, String) -> Void) { self.heading = heading self.submitLabel = submitLabel self.fileName = fileName self.showsSettings = showsSettings self.working = working self.errorMessage = errorMessage self.onSubmit = onSubmit _name = State(initialValue: fileName) _content = State(initialValue: initialContent) } private var nameFixed: Bool { !fileName.isEmpty } var body: some View { NavigationStack { Form { Section { if nameFixed { Text(fileName) .font(.gbMono(.subheadline)) } else { TextField("File name", text: $name) .font(.gbMono(.subheadline)) .autocorrectionDisabled() .textInputAutocapitalization(.never) .accessibilityIdentifier("snippet-file-name") } if showsSettings { TextField("Description (optional)", text: $description) .autocorrectionDisabled() .accessibilityIdentifier("snippet-description") Picker("Visibility", selection: $visibility) { Text("unlisted").tag("unlisted") Text("public").tag("public") Text("private").tag("private") } .pickerStyle(.segmented) } } footer: { if !nameFixed { Text("Letters, digits and ._+- ; not starting with a dot.") } } Section { TextEditor(text: $content) .frame(minHeight: 200) .font(.gbMono(.caption)) .autocorrectionDisabled() .textInputAutocapitalization(.never) .accessibilityIdentifier("snippet-content") } if let errorMessage { Section { GBNotice(errorMessage) } } } .navigationTitle(heading) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } .disabled(working) } ToolbarItem(placement: .confirmationAction) { if working { ProgressView() } else { Button(submitLabel) { onSubmit(name, description, visibility, content) } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty || content.isEmpty) .accessibilityIdentifier("snippet-submit") } } } .interactiveDismissDisabled(working) } } } nonisolated enum SnippetRoute: Hashable { /// nil is the caller's own. case list(owner: String?) case snippet(id: String) }