Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent 20d0cfc0c7
commit 05f921d605
1043 changed files with 163875 additions and 68430 deletions

View File

@@ -0,0 +1,347 @@
//
// OnboardingCloudScreen.swift
// Yattee
//
// Second onboarding screen for iCloud sync configuration.
//
import CloudKit
import SwiftUI
struct OnboardingCloudScreen: View {
@Environment(\.appEnvironment) private var appEnvironment
let onContinue: () -> Void
private enum ScreenState: Equatable {
case initial // Show enable/skip buttons
case syncing // Show progress view
case complete // Sync finished, show continue
case error(String) // Show error with continue option
}
@State private var screenState: ScreenState = .initial
@State private var iCloudAvailable: Bool?
@State private var isChecking = true
private var settingsManager: SettingsManager? {
appEnvironment?.settingsManager
}
private var cloudKitSync: CloudKitSyncEngine? {
appEnvironment?.cloudKitSync
}
var body: some View {
VStack(spacing: 32) {
Spacer()
// Content based on state
switch screenState {
case .initial:
initialView
case .syncing:
syncingView
case .complete:
completeView
case .error(let message):
errorView(message)
}
Spacer()
// Buttons based on state
buttonsForState
}
.padding()
.task {
await checkiCloudAvailability()
}
.onChange(of: cloudKitSync?.uploadProgress?.isComplete) { _, newValue in
if newValue == true {
withAnimation {
screenState = .complete
}
}
}
}
// MARK: - Initial View
@ViewBuilder
private var initialView: some View {
// iCloud icon
Image(systemName: "icloud")
.font(.system(size: 80))
.foregroundStyle(Color.accentColor)
// Title and description
VStack(spacing: 12) {
Text(String(localized: "onboarding.cloud.title"))
.font(.title)
.fontWeight(.bold)
Text(String(localized: "onboarding.cloud.description"))
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
// iCloud status indicator
VStack(spacing: 16) {
if isChecking {
ProgressView()
.controlSize(.large)
} else if iCloudAvailable == false {
// iCloud unavailable
VStack(spacing: 12) {
Image(systemName: "exclamationmark.icloud")
.font(.title)
.foregroundStyle(.orange)
Text(String(localized: "onboarding.cloud.unavailable"))
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding()
#if os(tvOS)
.background(Color(.systemGray).opacity(0.2))
#elseif os(macOS)
.background(Color(nsColor: .controlBackgroundColor))
#else
.background(Color(uiColor: .secondarySystemBackground))
#endif
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal)
}
}
}
// MARK: - Syncing View
@ViewBuilder
private var syncingView: some View {
// Animated iCloud icon
Image(systemName: "icloud")
.font(.system(size: 80))
.foregroundStyle(Color.accentColor)
.symbolEffect(.pulse)
// Syncing title and progress
VStack(spacing: 12) {
Text(String(localized: "onboarding.cloud.syncing.title"))
.font(.title)
.fontWeight(.bold)
// Show appropriate progress text based on sync phase
if cloudKitSync?.isReceivingChanges == true {
Text(String(localized: "onboarding.cloud.syncing.downloading"))
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
} else if let progress = cloudKitSync?.uploadProgress {
Text(progress.displayText)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
} else {
Text(String(localized: "onboarding.cloud.syncing.preparing"))
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
ProgressView()
.controlSize(.large)
.padding(.top, 8)
}
}
// MARK: - Complete View
@ViewBuilder
private var completeView: some View {
// Checkmark iCloud icon
Image(systemName: "checkmark.icloud")
.font(.system(size: 80))
.foregroundStyle(.green)
// Complete title and description
VStack(spacing: 12) {
Text(String(localized: "onboarding.cloud.complete.title"))
.font(.title)
.fontWeight(.bold)
Text(String(localized: "onboarding.cloud.complete.description"))
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
// MARK: - Error View
@ViewBuilder
private func errorView(_ message: String) -> some View {
// Warning iCloud icon
Image(systemName: "exclamationmark.icloud")
.font(.system(size: 80))
.foregroundStyle(.orange)
// Error title and message
VStack(spacing: 12) {
Text(String(localized: "onboarding.cloud.error.title"))
.font(.title)
.fontWeight(.bold)
Text(message)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
// MARK: - Buttons
@ViewBuilder
private var buttonsForState: some View {
switch screenState {
case .initial:
if isChecking {
EmptyView()
} else if iCloudAvailable == true {
// Two buttons: Enable iCloud (primary) and Skip (secondary)
VStack(spacing: 12) {
// Primary: Enable iCloud
Button(action: enableAndStartSync) {
Text(String(localized: "onboarding.cloud.enable"))
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
#if os(tvOS)
.background(Color.accentColor.opacity(0.2))
#else
.background(Color.accentColor)
.foregroundStyle(.white)
#endif
.clipShape(RoundedRectangle(cornerRadius: 12))
}
#if os(tvOS)
.buttonStyle(.card)
#endif
// Secondary: Skip for now
Button(action: onContinue) {
Text(String(localized: "onboarding.cloud.skip"))
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
#if os(tvOS)
.background(Color(.systemGray).opacity(0.2))
#elseif os(macOS)
.background(Color(nsColor: .controlBackgroundColor))
#else
.background(Color(uiColor: .secondarySystemBackground))
#endif
.foregroundStyle(.primary)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
#if os(tvOS)
.buttonStyle(.card)
#endif
}
.padding(.horizontal)
.padding(.bottom)
} else {
// iCloud unavailable - just show Continue
Button(action: onContinue) {
Text(String(localized: "onboarding.continue"))
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
#if os(tvOS)
.background(Color.accentColor.opacity(0.2))
#else
.background(Color.accentColor)
.foregroundStyle(.white)
#endif
.clipShape(RoundedRectangle(cornerRadius: 12))
}
#if os(tvOS)
.buttonStyle(.card)
#endif
.padding(.horizontal)
.padding(.bottom)
}
case .syncing:
// No continue button during sync - user must wait
// (toolbar Skip still available to exit onboarding)
EmptyView()
case .complete, .error:
// Continue button
Button(action: onContinue) {
Text(String(localized: "onboarding.continue"))
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
#if os(tvOS)
.background(Color.accentColor.opacity(0.2))
#else
.background(Color.accentColor)
.foregroundStyle(.white)
#endif
.clipShape(RoundedRectangle(cornerRadius: 12))
}
#if os(tvOS)
.buttonStyle(.card)
#endif
.padding(.horizontal)
.padding(.bottom)
}
}
// MARK: - iCloud
private func checkiCloudAvailability() async {
do {
let status = try await CKContainer.default().accountStatus()
iCloudAvailable = (status == .available)
} catch {
iCloudAvailable = false
}
isChecking = false
}
private func enableAndStartSync() {
// Transition to syncing state
withAnimation {
screenState = .syncing
}
settingsManager?.iCloudSyncEnabled = true
settingsManager?.enableAllSyncCategories()
// Enable CloudKit sync engine then trigger initial upload
Task {
await appEnvironment?.cloudKitSync.enable()
await appEnvironment?.cloudKitSync.performInitialUpload()
}
// Sync non-CloudKit data
settingsManager?.replaceWithiCloudData()
appEnvironment?.instancesManager.replaceWithiCloudData()
appEnvironment?.mediaSourcesManager.replaceWithiCloudData()
}
}
// MARK: - Preview
#Preview {
OnboardingCloudScreen(onContinue: {})
.appEnvironment(.preview)
}