// // MigrateView.swift // Passepartout // // Created by Davide De Rosa on 11/13/24. // Copyright (c) 2024 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn // // This file is part of Passepartout. // // Passepartout is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Passepartout is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Passepartout. If not, see . // import CommonLibrary import CommonUtils import PassepartoutKit import SwiftUI // FIXME: ###, migrations UI struct MigrateView: View { enum Style { case section case table } @EnvironmentObject private var migrationManager: MigrationManager let style: Style @State private var isFetching = true @State private var isMigrating = false @State private var profiles: [MigratableProfile] = [] @State private var excluded: Set = [] @State private var statuses: [UUID: MigrationStatus] = [:] @StateObject private var errorHandler: ErrorHandler = .default() var body: some View { Form { Subview( style: style, profiles: profiles, excluded: $excluded, statuses: statuses ) .disabled(isMigrating) } .themeForm() .themeProgress(if: isFetching) .themeEmptyContent(if: !isFetching && profiles.isEmpty, message: "Nothing to migrate") .navigationTitle(title) .toolbar(content: toolbarContent) .task { await fetch() } .withErrorHandler(errorHandler) } } private extension MigrateView { var title: String { Strings.Views.Migrate.title } func toolbarContent() -> some ToolbarContent { ToolbarItem(placement: .confirmationAction) { Button("Proceed") { Task { await migrate() } } } } } private extension MigrateView { func fetch() async { do { isFetching = true profiles = try await migrationManager.fetchMigratableProfiles() isFetching = false } catch { pp_log(.App.migration, .error, "Unable to fetch migratable profiles: \(error)") errorHandler.handle(error, title: title) isFetching = false } } func migrate() async { do { isMigrating = true let selection = Set(profiles.map(\.id)).symmetricDifference(excluded) let migrated = try await migrationManager.migrateProfiles(profiles, selection: selection) { statuses[$0] = $1 } print(">>> Migrated: \(migrated.count)") _ = migrated // FIXME: ###, import migrated } catch { pp_log(.App.migration, .error, "Unable to migrate profiles: \(error)") errorHandler.handle(error, title: title) } } } // MARK: - private extension MigrateView { struct Subview: View { let style: Style let profiles: [MigratableProfile] @Binding var excluded: Set let statuses: [UUID: MigrationStatus] var body: some View { switch style { case .section: MigrateView.SectionView( profiles: sortedProfiles, excluded: $excluded, statuses: statuses ) case .table: MigrateView.TableView( profiles: sortedProfiles, excluded: $excluded, statuses: statuses ) } } var sortedProfiles: [MigratableProfile] { profiles.sorted { $0.name.lowercased() < $1.name.lowercased() } } } } // MARK: - Previews #Preview("Before") { PrivatePreviews.MigratePreview( profiles: PrivatePreviews.profiles, statuses: [:] ) .withMockEnvironment() } #Preview("After") { PrivatePreviews.MigratePreview( profiles: PrivatePreviews.profiles, statuses: [ PrivatePreviews.profiles[0].id: .excluded, PrivatePreviews.profiles[1].id: .pending, PrivatePreviews.profiles[2].id: .failure, PrivatePreviews.profiles[3].id: .success ] ) .withMockEnvironment() } private struct PrivatePreviews { static let oneDay: TimeInterval = 24 * 60 * 60 static let profiles: [MigratableProfile] = [ .init(id: UUID(), name: "1One", lastUpdate: Date().addingTimeInterval(-oneDay)), .init(id: UUID(), name: "2Two", lastUpdate: Date().addingTimeInterval(-3 * oneDay)), .init(id: UUID(), name: "3Three", lastUpdate: Date().addingTimeInterval(-90 * oneDay)), .init(id: UUID(), name: "4Four", lastUpdate: Date().addingTimeInterval(-180 * oneDay)) ] struct MigratePreview: View { let profiles: [MigratableProfile] let statuses: [UUID: MigrationStatus] @State private var excluded: Set = [] #if os(iOS) private let style: MigrateView.Style = .section #else private let style: MigrateView.Style = .table #endif var body: some View { Form { MigrateView.Subview( style: style, profiles: profiles, excluded: $excluded, statuses: statuses ) } .navigationTitle("Migrate") .themeNavigationStack() } } }