Files
Sportstime/SportsTime/Export/Views/SharePreviewView.swift
Trey t d034ee8612 fix: multiple bug fixes and improvements
- Fix suggested trips showing wrong sports for cross-country trips
- Remove quick start sections from home variants (Classic, Spotify)
- Remove dead quickActions code from HomeView
- Fix pace capsule animation in TripCreationView
- Add text wrapping to achievement descriptions
- Improve poll parsing with better error handling
- Various sharing system improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:35:18 -06:00

255 lines
7.7 KiB
Swift

//
// SharePreviewView.swift
// SportsTime
//
// Unified preview and customization UI for all shareable content.
//
import SwiftUI
struct SharePreviewView<Content: ShareableContent>: View {
let content: Content
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@State private var selectedTheme: ShareTheme
@State private var generatedImage: UIImage?
@State private var isGenerating = false
@State private var error: String?
@State private var showCopiedToast = false
init(content: Content) {
self.content = content
_selectedTheme = State(initialValue: ShareThemePreferences.theme(for: content.cardType))
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: Theme.Spacing.lg) {
// Preview
previewSection
// Theme selector
themeSelector
// Action buttons
actionButtons
}
.padding(Theme.Spacing.md)
}
.themedBackground()
.navigationTitle("Share")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
}
.alert("Error", isPresented: .constant(error != nil)) {
Button("OK") { error = nil }
} message: {
Text(error ?? "")
}
.overlay {
if showCopiedToast {
copiedToast
}
}
.task {
await generatePreview()
}
.onChange(of: selectedTheme) { _, _ in
Task { await generatePreview() }
}
}
}
// MARK: - Preview Section
private var previewSection: some View {
VStack(spacing: Theme.Spacing.sm) {
Text("Preview")
.font(.subheadline)
.foregroundStyle(Theme.textMuted(colorScheme))
if let image = generatedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(9/16, contentMode: .fit)
.frame(maxHeight: 400)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.shadow(color: .black.opacity(0.2), radius: 10, x: 0, y: 5)
} else if isGenerating {
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.fill(Theme.cardBackground(colorScheme))
.aspectRatio(9/16, contentMode: .fit)
.frame(maxHeight: 400)
.overlay {
ProgressView()
}
}
}
}
// MARK: - Theme Selector
private var themeSelector: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Theme")
.font(.subheadline)
.foregroundStyle(Theme.textMuted(colorScheme))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.sm) {
ForEach(ShareTheme.all) { theme in
themeButton(theme)
}
}
}
}
}
private func themeButton(_ theme: ShareTheme) -> some View {
Button {
withAnimation {
selectedTheme = theme
ShareThemePreferences.setTheme(theme, for: content.cardType)
}
} label: {
VStack(spacing: 4) {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
colors: theme.gradientColors,
startPoint: .top,
endPoint: .bottom
)
)
.frame(width: 50, height: 50)
Circle()
.fill(theme.accentColor)
.frame(width: 16, height: 16)
}
.overlay {
if selectedTheme.id == theme.id {
RoundedRectangle(cornerRadius: 8)
.stroke(Theme.warmOrange, lineWidth: 3)
}
}
Text(theme.name)
.font(.caption2)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
}
.buttonStyle(.plain)
}
// MARK: - Action Buttons
private var actionButtons: some View {
VStack(spacing: Theme.Spacing.sm) {
// Primary: Share to Instagram
Button {
shareToInstagram()
} label: {
HStack {
Image(systemName: "camera.fill")
Text("Share to Instagram")
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(Theme.warmOrange)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
.disabled(generatedImage == nil)
// Copy Image
Button {
copyImage()
} label: {
HStack {
Image(systemName: "doc.on.doc")
Text("Copy to Clipboard")
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(Theme.cardBackgroundElevated(colorScheme))
.foregroundStyle(Theme.textPrimary(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
.disabled(generatedImage == nil)
}
}
// MARK: - Copied Toast
private var copiedToast: some View {
VStack {
Spacer()
HStack {
Image(systemName: "checkmark.circle.fill")
Text("Copied to clipboard")
}
.padding()
.background(.ultraThinMaterial)
.clipShape(Capsule())
.padding(.bottom, 100)
}
.transition(.move(edge: .bottom).combined(with: .opacity))
}
// MARK: - Actions
private func generatePreview() async {
isGenerating = true
do {
generatedImage = try await content.render(theme: selectedTheme)
} catch {
self.error = error.localizedDescription
}
isGenerating = false
}
private func shareToInstagram() {
guard let image = generatedImage else { return }
if !ShareService.shared.shareToInstagram(image: image) {
// Fallback to system share
showSystemShare()
}
}
private func copyImage() {
guard let image = generatedImage else { return }
ShareService.shared.copyToClipboard(image: image)
withAnimation {
showCopiedToast = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
showCopiedToast = false
}
}
}
private func showSystemShare() {
guard let image = generatedImage,
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else { return }
ShareService.shared.presentShareSheet(image: image, from: rootVC)
}
}