mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
308 lines
11 KiB
Swift
308 lines
11 KiB
Swift
//
|
|
// LegacyDataImportView.swift
|
|
// Yattee
|
|
//
|
|
// Full-screen view for importing legacy v1 data from Advanced Settings.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct LegacyDataImportView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
|
|
@State private var items: [LegacyImportItem] = []
|
|
@State private var isLoading = true
|
|
@State private var isImporting = false
|
|
@State private var showingResultSheet = false
|
|
@State private var lastResult: MigrationResult?
|
|
@State private var showingUnreachableAlert = false
|
|
@State private var pendingUnreachableItem: LegacyImportItem?
|
|
|
|
private var legacyMigrationService: LegacyDataMigrationService? {
|
|
appEnvironment?.legacyMigrationService
|
|
}
|
|
|
|
private var selectedCount: Int {
|
|
items.filter(\.isSelected).count
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if isLoading {
|
|
ProgressView()
|
|
.controlSize(.large)
|
|
} else if items.isEmpty {
|
|
ContentUnavailableView(
|
|
String(localized: "migration.noDataFound"),
|
|
systemImage: "doc.questionmark",
|
|
description: Text(String(localized: "migration.noDataFoundDescription"))
|
|
)
|
|
} else {
|
|
importContent
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "settings.advanced.data.importLegacy"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.task {
|
|
loadLegacyData()
|
|
}
|
|
.sheet(isPresented: $showingResultSheet) {
|
|
resultSheet
|
|
}
|
|
.alert(String(localized: "migration.unreachableTitle"), isPresented: $showingUnreachableAlert) {
|
|
Button(String(localized: "migration.unreachableImport"), role: .destructive) {
|
|
// Keep the item selected
|
|
}
|
|
Button(String(localized: "common.cancel"), role: .cancel) {
|
|
if let item = pendingUnreachableItem,
|
|
let index = items.firstIndex(where: { $0.id == item.id }) {
|
|
items[index].isSelected = false
|
|
}
|
|
}
|
|
} message: {
|
|
Text(String(localized: "migration.unreachableMessage"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Import Content
|
|
|
|
@ViewBuilder
|
|
private var importContent: some View {
|
|
VStack(spacing: 0) {
|
|
List {
|
|
Section {
|
|
ForEach(items) { item in
|
|
MigrationImportRow(item: item) {
|
|
toggleItem(item)
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "migration.selectToImport"))
|
|
} footer: {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(String(localized: "migration.accountsHint"))
|
|
Text(String(localized: "migration.settingsFooter"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bottom bar with import button
|
|
VStack(spacing: 12) {
|
|
Divider()
|
|
|
|
Button(action: performImport) {
|
|
if isImporting {
|
|
HStack(spacing: 8) {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
Text(String(localized: "migration.importing"))
|
|
}
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.accentColor)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
} else {
|
|
Text(String(localized: "migration.import"))
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(selectedCount > 0 ? Color.accentColor : Color.gray)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
.disabled(selectedCount == 0 || isImporting)
|
|
.padding(.horizontal)
|
|
.padding(.bottom, 8)
|
|
}
|
|
#if os(tvOS)
|
|
.background(Color(.systemGray).opacity(0.2))
|
|
#elseif os(macOS)
|
|
.background(Color(nsColor: .controlBackgroundColor))
|
|
#else
|
|
.background(Color(uiColor: .systemBackground))
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Result Sheet
|
|
|
|
@ViewBuilder
|
|
private var resultSheet: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 24) {
|
|
if let result = lastResult {
|
|
Spacer()
|
|
|
|
Image(systemName: result.isFullSuccess ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(result.isFullSuccess ? .green : .orange)
|
|
|
|
Text(String(localized: "migration.partialTitle"))
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
Text(String(
|
|
format: NSLocalizedString("migration.partialMessage %lld %lld", comment: "Import result count"),
|
|
result.succeeded.count,
|
|
result.totalProcessed
|
|
))
|
|
.font(.body)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
|
|
if !result.failed.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(String(localized: "migration.failedItems"))
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
|
|
ForEach(result.failed, id: \.item.id) { failure in
|
|
HStack {
|
|
Text(failure.item.displayName)
|
|
.font(.caption)
|
|
Spacer()
|
|
Text(failure.error.localizedDescription)
|
|
.font(.caption)
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
}
|
|
.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: 8))
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
VStack(spacing: 12) {
|
|
if !result.failed.isEmpty {
|
|
Button(action: retryFailed) {
|
|
Text(String(localized: "migration.retry"))
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.accentColor)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
|
|
Button(action: finishImport) {
|
|
Text(String(localized: "migration.continue"))
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(result.failed.isEmpty ? Color.accentColor : Color.secondary)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.bottom, 20)
|
|
}
|
|
}
|
|
.padding()
|
|
.interactiveDismissDisabled()
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
private func loadLegacyData() {
|
|
items = legacyMigrationService?.parseLegacyData() ?? []
|
|
isLoading = false
|
|
}
|
|
|
|
private func toggleItem(_ item: LegacyImportItem) {
|
|
guard let index = items.firstIndex(where: { $0.id == item.id }) else { return }
|
|
|
|
let wasSelected = items[index].isSelected
|
|
items[index].isSelected.toggle()
|
|
|
|
if !wasSelected && items[index].reachabilityStatus == .unknown {
|
|
checkReachability(for: items[index])
|
|
}
|
|
}
|
|
|
|
private func checkReachability(for item: LegacyImportItem) {
|
|
guard let index = items.firstIndex(where: { $0.id == item.id }) else { return }
|
|
|
|
items[index].reachabilityStatus = .checking
|
|
|
|
Task {
|
|
let isReachable = await legacyMigrationService?.checkReachability(for: item) ?? false
|
|
|
|
guard let currentIndex = items.firstIndex(where: { $0.id == item.id }) else { return }
|
|
items[currentIndex].reachabilityStatus = isReachable ? .reachable : .unreachable
|
|
|
|
if !isReachable && items[currentIndex].isSelected {
|
|
pendingUnreachableItem = items[currentIndex]
|
|
showingUnreachableAlert = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performImport() {
|
|
guard let service = legacyMigrationService else { return }
|
|
|
|
isImporting = true
|
|
|
|
Task {
|
|
let result = await service.importItems(items)
|
|
lastResult = result
|
|
|
|
isImporting = false
|
|
|
|
if result.isFullSuccess {
|
|
dismiss()
|
|
} else {
|
|
showingResultSheet = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func retryFailed() {
|
|
guard let result = lastResult else { return }
|
|
|
|
for index in items.indices {
|
|
let isFailed = result.failed.contains(where: { $0.item.id == items[index].id })
|
|
items[index].isSelected = isFailed
|
|
}
|
|
|
|
showingResultSheet = false
|
|
|
|
Task {
|
|
try? await Task.sleep(for: .milliseconds(300))
|
|
performImport()
|
|
}
|
|
}
|
|
|
|
private func finishImport() {
|
|
showingResultSheet = false
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
LegacyDataImportView()
|
|
}
|
|
.appEnvironment(.preview)
|
|
}
|