mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
347
Yattee/Views/Onboarding/OnboardingCloudScreen.swift
Normal file
347
Yattee/Views/Onboarding/OnboardingCloudScreen.swift
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user