PhotosPicker (bis 3 Bilder) + Thumbnails + Entfernen; FeedbackImagePrep (ImageIO/CoreGraphics, iOS+macOS, EXIF-frei). FeedbackFormModel haelt pickedItems/images. Braucht ManaCore >= 2.2.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
4 KiB
Swift
125 lines
4 KiB
Swift
import Foundation
|
|
import ManaCore
|
|
import Observation
|
|
import PhotosUI
|
|
import SwiftUI
|
|
|
|
/// State-Maschine für ``ManaFeedbackSheet``. Wraps ``FeedbackClient``.
|
|
///
|
|
/// Bei `isLoggedIn == false` wird zusätzlich ein optionales Kontakt-Mail-Feld
|
|
/// angeboten (Rückfrage-Adresse) und der Hinweis gezeigt, dass die
|
|
/// Einreichung erst durch die Moderation geht.
|
|
@MainActor
|
|
@Observable
|
|
public final class FeedbackFormModel {
|
|
public enum Status: Equatable, Sendable {
|
|
case idle
|
|
case sending
|
|
case sent
|
|
case error(String)
|
|
}
|
|
|
|
public var kind: FeedbackKind = .feedback
|
|
public var title: String = ""
|
|
public var details: String = ""
|
|
public var contactEmail: String = ""
|
|
public private(set) var status: Status = .idle
|
|
|
|
/// Höchstzahl Screenshot-Anhänge — deckt sich mit Web + Backend (`MAX_IMAGES`).
|
|
public let maxImages = 3
|
|
/// Auswahl aus dem ``PhotosPicker``; ``loadPickedImages()`` verarbeitet sie.
|
|
public var pickedItems: [PhotosPickerItem] = []
|
|
/// Aufbereitete (herunterskalierte, EXIF-freie) JPEG-Bytes — Quelle für
|
|
/// Thumbnails **und** den Upload.
|
|
public private(set) var images: [Data] = []
|
|
|
|
private let client: FeedbackClient
|
|
public let isLoggedIn: Bool
|
|
|
|
public init(client: FeedbackClient, isLoggedIn: Bool) {
|
|
self.client = client
|
|
self.isLoggedIn = isLoggedIn
|
|
}
|
|
|
|
public var canAddImages: Bool { images.count < maxImages }
|
|
|
|
/// Lädt die `PhotosPicker`-Auswahl, skaliert herunter (EXIF-frei) und füllt
|
|
/// ``images``. Wird `onChange(of: pickedItems)` aufgerufen.
|
|
public func loadPickedImages() async {
|
|
guard !pickedItems.isEmpty else { return }
|
|
var collected: [Data] = []
|
|
for item in pickedItems {
|
|
if images.count + collected.count >= maxImages { break }
|
|
guard let raw = try? await item.loadTransferable(type: Data.self),
|
|
let prepped = FeedbackImagePrep.prepare(raw) else { continue }
|
|
collected.append(prepped)
|
|
}
|
|
images = Array((images + collected).prefix(maxImages))
|
|
pickedItems = []
|
|
}
|
|
|
|
public func removeImage(at index: Int) {
|
|
guard images.indices.contains(index) else { return }
|
|
images.remove(at: index)
|
|
}
|
|
|
|
/// Anonyme Einreichung → Kontakt-Feld + Moderations-Hinweis zeigen.
|
|
public var showsContactField: Bool { !isLoggedIn }
|
|
|
|
public var canSubmit: Bool {
|
|
guard trimmed(title).count >= 3 else { return false }
|
|
if case .sending = status { return false }
|
|
return true
|
|
}
|
|
|
|
public var isSending: Bool {
|
|
if case .sending = status { return true }
|
|
return false
|
|
}
|
|
|
|
public var errorMessage: String? {
|
|
if case .error(let message) = status { return message }
|
|
return nil
|
|
}
|
|
|
|
public func submit() async {
|
|
guard canSubmit else { return }
|
|
status = .sending
|
|
|
|
let submission = FeedbackSubmission(
|
|
kind: kind,
|
|
title: trimmed(title),
|
|
description: trimmed(details),
|
|
contactEmail: isLoggedIn ? nil : nilIfEmpty(trimmed(contactEmail)),
|
|
images: images.map { FeedbackImage(mime: "image/jpeg", b64: $0.base64EncodedString()) }
|
|
)
|
|
|
|
do {
|
|
_ = try await client.submit(submission)
|
|
status = .sent
|
|
} catch {
|
|
let message = (error as? LocalizedError)?.errorDescription
|
|
?? "Senden fehlgeschlagen. Bitte später erneut versuchen."
|
|
status = .error(message)
|
|
}
|
|
}
|
|
|
|
private func trimmed(_ value: String) -> String {
|
|
value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
private func nilIfEmpty(_ value: String) -> String? {
|
|
value.isEmpty ? nil : value
|
|
}
|
|
}
|
|
|
|
extension FeedbackKind {
|
|
/// Deutsche Anzeige-Bezeichnung — neutral, nicht „Wunsch"-lastig.
|
|
public var displayName: String {
|
|
switch self {
|
|
case .wunsch: return "Wunsch"
|
|
case .bug: return "Problem"
|
|
case .feedback: return "Feedback"
|
|
}
|
|
}
|
|
}
|