mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
348 lines
11 KiB
Swift
348 lines
11 KiB
Swift
//
|
|
// 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)
|
|
}
|