- 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>
255 lines
7.7 KiB
Swift
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)
|
|
}
|
|
}
|