import SwiftUI import UIKit struct ShareSheetView: View { let image: UIImage? var imageData: Data? = nil var errorMessage: String? = nil let onClose: () -> Void @State private var repos: [GiteaRepo] = RepoCache.repos @State private var loadingRepos = false @State private var loadError: String? @State private var search = "" @State private var selectedRepo: GiteaRepo? @State private var title = "" @State private var notes = "" @State private var submitting = false @State private var submitError: String? @State private var submitted: GiteaIssue? var body: some View { NavigationStack { Form { if let error = errorMessage { Section { Label(error, systemImage: "exclamationmark.triangle.fill") .foregroundStyle(.red) } } if let image { Section { Image(uiImage: image) .resizable() .scaledToFit() .frame(maxHeight: 160) .frame(maxWidth: .infinity) .clipShape(RoundedRectangle(cornerRadius: 12)) } } Section("Repository") { if loadingRepos && repos.isEmpty { HStack { ProgressView(); Text("Loading repos…") } } else if let loadError { Label(loadError, systemImage: "xmark.octagon") .foregroundStyle(.red) } else { repoPicker } } Section("Issue") { TextField("Title", text: $title) .textInputAutocapitalization(.sentences) TextField("Notes (optional)", text: $notes, axis: .vertical) .lineLimit(3...8) } if let submitError { Section { Label(submitError, systemImage: "exclamationmark.triangle") .foregroundStyle(.red) } } if let issue = submitted { Section { Label("Created issue #\(issue.number)", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) Link("Open in Gitea", destination: URL(string: issue.htmlUrl)!) } } } .navigationTitle("New issue") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { onClose() } } ToolbarItem(placement: .confirmationAction) { Button(submitting ? "Sending…" : "Submit") { submit() } .disabled(!canSubmit) } } .task { await loadReposIfNeeded() } } } private var canSubmit: Bool { selectedRepo != nil && !title.trimmingCharacters(in: .whitespaces).isEmpty && !submitting && submitted == nil } @ViewBuilder private var repoPicker: some View { TextField("Search repos", text: $search) .textInputAutocapitalization(.never) .autocorrectionDisabled() let filtered = filteredRepos let recents = recentRepos.filter { search.isEmpty || $0.fullName.localizedCaseInsensitiveContains(search) } if !recents.isEmpty { ForEach(recents) { repo in row(repo, badge: "Recent") } } ForEach(filtered.filter { r in !recents.contains(where: { $0.id == r.id }) }) { repo in row(repo, badge: nil) } Button { Task { await refreshRepos(force: true) } } label: { Label("Refresh repo list", systemImage: "arrow.clockwise") } } private func row(_ repo: GiteaRepo, badge: String?) -> some View { Button { selectedRepo = repo } label: { HStack { VStack(alignment: .leading) { Text(repo.fullName) .foregroundStyle(.primary) if let badge { Text(badge).font(.caption2).foregroundStyle(.secondary) } } Spacer() if selectedRepo?.id == repo.id { Image(systemName: "checkmark").foregroundStyle(.tint) } } } } private var filteredRepos: [GiteaRepo] { let sorted = repos.sorted { $0.fullName.localizedCaseInsensitiveCompare($1.fullName) == .orderedAscending } guard !search.isEmpty else { return sorted } return sorted.filter { $0.fullName.localizedCaseInsensitiveContains(search) } } private var recentRepos: [GiteaRepo] { let names = RepoCache.recentFullNames return names.compactMap { name in repos.first(where: { $0.fullName == name }) } } private func loadReposIfNeeded() async { if repos.isEmpty || RepoCache.isStale { await refreshRepos(force: false) } } private func refreshRepos(force: Bool) async { loadingRepos = true loadError = nil do { let client = try GiteaClient() let fresh = try await client.searchRepos() await MainActor.run { self.repos = fresh RepoCache.repos = fresh self.loadingRepos = false } } catch { await MainActor.run { self.loadError = error.localizedDescription self.loadingRepos = false } } } private func submit() { guard let repo = selectedRepo, let imageData else { submitError = "Pick a repo and make sure the screenshot loaded." return } submitting = true submitError = nil Task { do { let client = try GiteaClient() let bodyText = notes.trimmingCharacters(in: .whitespacesAndNewlines) let issue = try await client.createIssue( owner: repo.owner.login, repo: repo.name, title: title.trimmingCharacters(in: .whitespacesAndNewlines), body: bodyText ) let asset = try await client.uploadAsset( owner: repo.owner.login, repo: repo.name, issueNumber: issue.number, image: imageData, filename: "screenshot-\(Int(Date().timeIntervalSince1970)).png" ) let updatedBody = (bodyText.isEmpty ? "" : bodyText + "\n\n") + "![screenshot](\(asset.browserDownloadUrl))" try await client.updateIssueBody( owner: repo.owner.login, repo: repo.name, issueNumber: issue.number, body: updatedBody ) RepoCache.touch(repo.fullName) await MainActor.run { submitting = false submitted = issue UINotificationFeedbackGenerator().notificationOccurred(.success) } try? await Task.sleep(nanoseconds: 800_000_000) await MainActor.run { onClose() } } catch { await MainActor.run { submitting = false submitError = error.localizedDescription UINotificationFeedbackGenerator().notificationOccurred(.error) } } } } }