mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
Remove dead legacy-import pipeline and unused LegacyAccount.password
The manual account-import flow made the old silent instance-import pipeline unreachable. Drop the orphaned parsing, reachability checks, batch import, result/error types, and the old import row view, plus the httpClient dependency they were the only consumer of. Also remove the LegacyAccount.password field, which was parsed but never read.
This commit is contained in:
@@ -296,8 +296,7 @@ final class AppEnvironment {
|
||||
invidiousCredentialsManager: invidiousCreds,
|
||||
pipedCredentialsManager: pipedCreds,
|
||||
invidiousAPI: invidiousAPI,
|
||||
pipedAPI: pipedAPI,
|
||||
httpClient: client
|
||||
pipedAPI: pipedAPI
|
||||
)
|
||||
|
||||
// Initialize Sources Settings
|
||||
|
||||
@@ -29,16 +29,9 @@ final class LegacyDataMigrationService {
|
||||
private let pipedCredentialsManager: PipedCredentialsManager
|
||||
private let invidiousAPI: InvidiousAPI
|
||||
private let pipedAPI: PipedAPI
|
||||
private let httpClient: HTTPClient
|
||||
|
||||
// MARK: - State
|
||||
|
||||
/// Whether an import is currently in progress
|
||||
private(set) var isImporting = false
|
||||
|
||||
/// Progress of the current import (0.0 to 1.0)
|
||||
private(set) var importProgress: Double = 0.0
|
||||
|
||||
/// Changes whenever legacy account defaults are resolved, so SwiftUI views refresh.
|
||||
private(set) var legacyAccountsRevision = 0
|
||||
|
||||
@@ -50,8 +43,7 @@ final class LegacyDataMigrationService {
|
||||
invidiousCredentialsManager: InvidiousCredentialsManager,
|
||||
pipedCredentialsManager: PipedCredentialsManager,
|
||||
invidiousAPI: InvidiousAPI,
|
||||
pipedAPI: PipedAPI,
|
||||
httpClient: HTTPClient = HTTPClient()
|
||||
pipedAPI: PipedAPI
|
||||
) {
|
||||
self.instancesManager = instancesManager
|
||||
self.basicAuthCredentialsManager = basicAuthCredentialsManager
|
||||
@@ -59,18 +51,10 @@ final class LegacyDataMigrationService {
|
||||
self.pipedCredentialsManager = pipedCredentialsManager
|
||||
self.invidiousAPI = invidiousAPI
|
||||
self.pipedAPI = pipedAPI
|
||||
self.httpClient = httpClient
|
||||
}
|
||||
|
||||
// MARK: - Detection
|
||||
|
||||
/// Checks if there is legacy v1 data available for migration.
|
||||
/// - Returns: true if v1 data exists and can be parsed
|
||||
func hasLegacyData() -> Bool {
|
||||
guard let items = parseLegacyData() else { return false }
|
||||
return !items.isEmpty
|
||||
}
|
||||
|
||||
/// Whether there are legacy accounts left for the user to review.
|
||||
func hasLegacyAccountsToImport() -> Bool {
|
||||
_ = legacyAccountsRevision
|
||||
@@ -87,47 +71,6 @@ final class LegacyDataMigrationService {
|
||||
UserDefaults.standard.set(true, forKey: legacyAccountsPromptShownKey)
|
||||
}
|
||||
|
||||
/// Parses legacy v1 data from UserDefaults.
|
||||
/// - Returns: Array of import items, or nil if data is corrupted or doesn't exist
|
||||
func parseLegacyData() -> [LegacyImportItem]? {
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
// Check if legacy data exists
|
||||
guard defaults.object(forKey: legacyInstancesKey) != nil ||
|
||||
defaults.object(forKey: legacyAccountsKey) != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse instances only (credentials are not imported)
|
||||
let legacyInstances = parseLegacyInstances(from: defaults)
|
||||
|
||||
// Build import items - one per unique instance
|
||||
var items: [LegacyImportItem] = []
|
||||
|
||||
for instance in legacyInstances {
|
||||
// Skip PeerTube (not supported in migration)
|
||||
guard let instanceType = instance.instanceType,
|
||||
instanceType != .peertube else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = instance.url else { continue }
|
||||
|
||||
let item = LegacyImportItem(
|
||||
id: UUID(),
|
||||
legacyInstanceID: instance.id,
|
||||
instanceType: instanceType,
|
||||
url: url,
|
||||
name: instance.name.isEmpty ? nil : instance.name,
|
||||
proxiesVideos: instance.proxiesVideos
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
// Return nil if no valid items were found (treat as no data)
|
||||
return items.isEmpty ? nil : items
|
||||
}
|
||||
|
||||
// MARK: - Parsing Helpers
|
||||
|
||||
private func parseLegacyInstances(from defaults: UserDefaults) -> [LegacyInstance] {
|
||||
@@ -188,78 +131,8 @@ final class LegacyDataMigrationService {
|
||||
return importItems
|
||||
}
|
||||
|
||||
// MARK: - Reachability
|
||||
|
||||
/// Checks if an instance is reachable.
|
||||
/// - Parameter item: The import item to check
|
||||
/// - Returns: true if the instance responds, false otherwise
|
||||
func checkReachability(for item: LegacyImportItem) async -> Bool {
|
||||
// Build the appropriate health check endpoint based on instance type
|
||||
let endpoint: GenericEndpoint
|
||||
switch item.instanceType {
|
||||
case .invidious:
|
||||
endpoint = GenericEndpoint(path: "/api/v1/stats", timeout: 10)
|
||||
case .piped:
|
||||
endpoint = GenericEndpoint(path: "/healthcheck", timeout: 10)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
_ = try await httpClient.fetchData(endpoint, baseURL: item.url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Import
|
||||
|
||||
/// Imports the selected items into the v2 system.
|
||||
/// - Parameter items: The items to import (only selected items will be processed)
|
||||
/// - Returns: The result of the import operation
|
||||
func importItems(_ items: [LegacyImportItem]) async -> MigrationResult {
|
||||
isImporting = true
|
||||
importProgress = 0.0
|
||||
|
||||
let selectedItems = items.filter(\.isSelected)
|
||||
var succeeded: [LegacyImportItem] = []
|
||||
var failed: [(item: LegacyImportItem, error: MigrationError)] = []
|
||||
var skippedDuplicates: [LegacyImportItem] = []
|
||||
|
||||
let total = selectedItems.count
|
||||
|
||||
for (index, item) in selectedItems.enumerated() {
|
||||
// Update progress
|
||||
importProgress = Double(index) / Double(max(total, 1))
|
||||
|
||||
// Check for duplicates
|
||||
if isDuplicate(item) {
|
||||
skippedDuplicates.append(item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Perform import
|
||||
do {
|
||||
try importItem(item)
|
||||
succeeded.append(item)
|
||||
} catch let error as MigrationError {
|
||||
failed.append((item, error))
|
||||
} catch {
|
||||
failed.append((item, .unknown(error.localizedDescription)))
|
||||
}
|
||||
}
|
||||
|
||||
importProgress = 1.0
|
||||
isImporting = false
|
||||
|
||||
return MigrationResult(
|
||||
succeeded: succeeded,
|
||||
failed: failed,
|
||||
skippedDuplicates: skippedDuplicates
|
||||
)
|
||||
}
|
||||
|
||||
/// Re-creates a legacy account by signing in with fresh credentials.
|
||||
/// The matching source is created if needed; otherwise the existing matching source is reused.
|
||||
/// - Parameters:
|
||||
@@ -307,44 +180,6 @@ final class LegacyDataMigrationService {
|
||||
return instance
|
||||
}
|
||||
|
||||
/// Imports a single item into the v2 system.
|
||||
/// If the legacy URL contains embedded basic-auth credentials
|
||||
/// (e.g. `https://user:pass@host`), they are stripped from the URL
|
||||
/// and stored in the Keychain via `BasicAuthCredentialsManager`.
|
||||
private func importItem(_ item: LegacyImportItem) throws {
|
||||
let (cleanURL, credentials) = Self.splitCredentials(from: item.url)
|
||||
|
||||
let instance = Instance(
|
||||
id: UUID(),
|
||||
type: item.instanceType,
|
||||
url: cleanURL,
|
||||
name: item.name,
|
||||
isEnabled: true,
|
||||
proxiesVideos: item.proxiesVideos
|
||||
)
|
||||
|
||||
instancesManager.add(instance)
|
||||
|
||||
if let credentials {
|
||||
basicAuthCredentialsManager.setCredentials(
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
for: instance
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if an import item would be a duplicate of an existing instance.
|
||||
private func isDuplicate(_ item: LegacyImportItem) -> Bool {
|
||||
let (cleanURL, _) = Self.splitCredentials(from: item.url)
|
||||
for existing in instancesManager.instances {
|
||||
if existing.url.host == cleanURL.host && existing.type == item.instanceType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func instanceForAccountImport(_ item: LegacyAccountImportItem) -> Instance {
|
||||
let (cleanURL, _) = Self.splitCredentials(from: item.url)
|
||||
if let existing = instancesManager.instances.first(where: { existing in
|
||||
@@ -416,32 +251,8 @@ final class LegacyDataMigrationService {
|
||||
return "Basic \(encoded)"
|
||||
}
|
||||
|
||||
// MARK: - Auto-Import
|
||||
|
||||
/// Silently imports any legacy v1 data on first launch.
|
||||
/// Skips unreachable-checks and UI; just imports everything and deletes the legacy keys.
|
||||
/// Safe to call repeatedly — if there is no legacy data left, this is a no-op.
|
||||
func autoImportIfNeeded() async {
|
||||
// Silent migration is intentionally disabled. Legacy accounts require
|
||||
// explicit review because v2 stores fresh per-instance session tokens.
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
/// Deletes the legacy v1 data from UserDefaults.
|
||||
/// Call this after a successful import or when the user confirms they don't want to import.
|
||||
func deleteLegacyData() {
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
// Remove legacy keys
|
||||
defaults.removeObject(forKey: legacyInstancesKey)
|
||||
defaults.removeObject(forKey: legacyAccountsKey)
|
||||
|
||||
// Note: We don't delete the old Keychain items as they may be needed
|
||||
// if the user reinstalls v1 or for debugging purposes.
|
||||
// The old Keychain service name is different so there's no conflict.
|
||||
}
|
||||
|
||||
/// Removes one legacy account after successful import or explicit user dismissal.
|
||||
/// If this was the last account, the account defaults key is removed.
|
||||
func removeLegacyAccount(_ item: LegacyAccountImportItem) {
|
||||
|
||||
@@ -100,9 +100,6 @@ struct LegacyAccount: Sendable {
|
||||
/// Stored username/email, when present in UserDefaults.
|
||||
let username: String
|
||||
|
||||
/// Password value from very old defaults exports, if present.
|
||||
let password: String?
|
||||
|
||||
/// Parses a dictionary from v1 UserDefaults format.
|
||||
/// - Parameter dictionary: The serialized account dictionary
|
||||
/// - Returns: A LegacyAccount if parsing succeeds, nil otherwise
|
||||
@@ -115,57 +112,17 @@ struct LegacyAccount: Sendable {
|
||||
|
||||
let instanceID = dictionary["instanceID"] as? String ?? ""
|
||||
let name = dictionary["name"] as? String ?? ""
|
||||
let password = dictionary["password"] as? String
|
||||
|
||||
return LegacyAccount(
|
||||
id: id,
|
||||
instanceID: instanceID,
|
||||
name: name,
|
||||
apiURL: apiURL,
|
||||
username: username,
|
||||
password: password?.isEmpty == true ? nil : password
|
||||
username: username
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Legacy Import Item
|
||||
|
||||
/// Represents an instance to be imported from v1 data.
|
||||
/// Only instances are imported - users need to re-add their accounts after import.
|
||||
struct LegacyImportItem: Identifiable, Sendable {
|
||||
/// New UUID for UI identification
|
||||
let id: UUID
|
||||
|
||||
/// The original v1 instance ID
|
||||
let legacyInstanceID: String
|
||||
|
||||
/// The type of instance (Invidious or Piped)
|
||||
let instanceType: InstanceType
|
||||
|
||||
/// The instance URL
|
||||
let url: URL
|
||||
|
||||
/// User-defined name for the instance
|
||||
let name: String?
|
||||
|
||||
/// Whether this instance proxies videos
|
||||
var proxiesVideos: Bool = false
|
||||
|
||||
/// Whether this item is selected for import
|
||||
var isSelected: Bool = true
|
||||
|
||||
/// Current reachability status
|
||||
var reachabilityStatus: ReachabilityStatus = .unknown
|
||||
|
||||
/// Display name for the UI
|
||||
var displayName: String {
|
||||
if let name, !name.isEmpty {
|
||||
return name
|
||||
}
|
||||
return url.host ?? url.absoluteString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Legacy Account Import Item
|
||||
|
||||
/// Represents a legacy account that can be re-created by signing in to v2.
|
||||
@@ -216,58 +173,3 @@ struct LegacyAccountImportItem: Identifiable, Sendable {
|
||||
return url.host ?? url.absoluteString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reachability Status
|
||||
|
||||
/// Status of an instance's reachability check.
|
||||
enum ReachabilityStatus: Sendable {
|
||||
/// Not yet checked
|
||||
case unknown
|
||||
/// Currently checking
|
||||
case checking
|
||||
/// Instance is reachable
|
||||
case reachable
|
||||
/// Instance is unreachable
|
||||
case unreachable
|
||||
}
|
||||
|
||||
// MARK: - Migration Result
|
||||
|
||||
/// Result of an import operation.
|
||||
struct MigrationResult {
|
||||
/// Items that were successfully imported
|
||||
let succeeded: [LegacyImportItem]
|
||||
|
||||
/// Items that failed to import with their errors
|
||||
let failed: [(item: LegacyImportItem, error: MigrationError)]
|
||||
|
||||
/// Items that were skipped because they already exist
|
||||
let skippedDuplicates: [LegacyImportItem]
|
||||
|
||||
/// Whether all selected items were successfully imported
|
||||
var isFullSuccess: Bool {
|
||||
failed.isEmpty
|
||||
}
|
||||
|
||||
/// Total number of items processed
|
||||
var totalProcessed: Int {
|
||||
succeeded.count + failed.count + skippedDuplicates.count
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Migration Error
|
||||
|
||||
/// Errors that can occur during migration.
|
||||
enum MigrationError: LocalizedError, Sendable {
|
||||
case invalidURL
|
||||
case unknown(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL:
|
||||
return String(localized: "migration.error.invalidURL")
|
||||
case .unknown(let message):
|
||||
return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
//
|
||||
// MigrationImportRow.swift
|
||||
// Yattee
|
||||
//
|
||||
// Reusable row component for displaying a legacy import item.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MigrationImportRow: View {
|
||||
let item: LegacyImportItem
|
||||
let onToggle: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onToggle) {
|
||||
HStack(spacing: 12) {
|
||||
// Checkbox
|
||||
Image(systemName: item.isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.font(.title2)
|
||||
.foregroundStyle(item.isSelected ? Color.accentColor : .secondary)
|
||||
|
||||
// Instance type icon
|
||||
instanceIcon
|
||||
.font(.title2)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 28)
|
||||
|
||||
// Instance details
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.displayName)
|
||||
.font(.body)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(item.url.host ?? item.url.absoluteString)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Reachability indicator
|
||||
reachabilityIndicator
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
@ViewBuilder
|
||||
private var instanceIcon: some View {
|
||||
switch item.instanceType {
|
||||
case .invidious:
|
||||
Image(systemName: "server.rack")
|
||||
case .piped:
|
||||
Image(systemName: "cloud")
|
||||
default:
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var reachabilityIndicator: some View {
|
||||
switch item.reachabilityStatus {
|
||||
case .unknown:
|
||||
EmptyView()
|
||||
case .checking:
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
case .reachable:
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
case .unreachable:
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
MigrationImportRow(
|
||||
item: LegacyImportItem(
|
||||
id: UUID(),
|
||||
legacyInstanceID: "test",
|
||||
instanceType: .invidious,
|
||||
url: URL(string: "https://invidious.example.com")!,
|
||||
name: "My Invidious",
|
||||
isSelected: true,
|
||||
reachabilityStatus: .reachable
|
||||
),
|
||||
onToggle: {}
|
||||
)
|
||||
|
||||
MigrationImportRow(
|
||||
item: LegacyImportItem(
|
||||
id: UUID(),
|
||||
legacyInstanceID: "test2",
|
||||
instanceType: .piped,
|
||||
url: URL(string: "https://piped.example.com")!,
|
||||
name: nil,
|
||||
isSelected: false,
|
||||
reachabilityStatus: .unreachable
|
||||
),
|
||||
onToggle: {}
|
||||
)
|
||||
|
||||
MigrationImportRow(
|
||||
item: LegacyImportItem(
|
||||
id: UUID(),
|
||||
legacyInstanceID: "test3",
|
||||
instanceType: .invidious,
|
||||
url: URL(string: "https://another.invidious.com")!,
|
||||
name: "Another Instance",
|
||||
isSelected: true,
|
||||
reachabilityStatus: .checking
|
||||
),
|
||||
onToggle: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user