quick fix
This commit is contained in:
parent
db9d79f023
commit
aae53a17d8
2 changed files with 114 additions and 115 deletions
114
iOS/Views/Companion/ConflictModels.swift
Normal file
114
iOS/Views/Companion/ConflictModels.swift
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import SwiftUI
|
||||
|
||||
// MARK: - Conflict Models
|
||||
// Kept in a separate file so both CompanionAPIService and LibraryConflictsView can reference these types.
|
||||
|
||||
struct LibraryConflict: Codable, Identifiable {
|
||||
let type: String
|
||||
let severity: String
|
||||
let title: String
|
||||
let detail: String
|
||||
let affected_paths: [String]
|
||||
let fix_action: String?
|
||||
let fix_data: [String: AnyCodable]?
|
||||
|
||||
var id: String { "\(type)|\(title)" }
|
||||
|
||||
var severityColor: Color {
|
||||
severity == "error" ? Color(red: 1, green: 0.176, blue: 0.333) : .yellow
|
||||
}
|
||||
|
||||
var severityIcon: String {
|
||||
severity == "error" ? "xmark.circle.fill" : "exclamationmark.triangle.fill"
|
||||
}
|
||||
|
||||
var typeIcon: String {
|
||||
switch type {
|
||||
case "duplicate_album": return "rectangle.on.rectangle.fill"
|
||||
case "missing_files": return "doc.badge.questionmark"
|
||||
case "picard_legacy_tags": return "tag.slash.fill"
|
||||
case "orphaned_tracks": return "link.badge.plus"
|
||||
case "duplicate_track": return "music.note.list"
|
||||
case "stale_companion_paths": return "externaldrive.badge.xmark"
|
||||
default: return "exclamationmark.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConflictsResponse: Codable {
|
||||
let total: Int
|
||||
let errors: Int
|
||||
let warnings: Int
|
||||
let issues: [LibraryConflict]
|
||||
}
|
||||
|
||||
// MARK: - AnyCodable
|
||||
// Wraps heterogeneous values in the fix_data dictionary (String, Int, or [String]).
|
||||
|
||||
struct AnyCodable: Codable {
|
||||
let value: Any
|
||||
|
||||
init(_ value: Any) { self.value = value }
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let v = try? container.decode([String].self) { value = v; return }
|
||||
if let v = try? container.decode([String: String].self) { value = v; return }
|
||||
if let v = try? container.decode(String.self) { value = v; return }
|
||||
if let v = try? container.decode(Int.self) { value = v; return }
|
||||
value = ""
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch value {
|
||||
case let v as [String]: try container.encode(v)
|
||||
case let v as [String: String]: try container.encode(v)
|
||||
case let v as String: try container.encode(v)
|
||||
case let v as Int: try container.encode(v)
|
||||
default: try container.encode("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conflict Manager
|
||||
|
||||
@MainActor
|
||||
class ConflictManager: ObservableObject {
|
||||
static let shared = ConflictManager()
|
||||
|
||||
@Published var conflicts: ConflictsResponse?
|
||||
@Published var isLoading = false
|
||||
@Published var lastError: String?
|
||||
@Published var isFixing: String? = nil
|
||||
|
||||
private let api = CompanionAPIService()
|
||||
|
||||
var badgeCount: Int { conflicts?.errors ?? 0 }
|
||||
var totalCount: Int { conflicts?.total ?? 0 }
|
||||
|
||||
func refresh() async {
|
||||
guard CompanionSettings.shared.isEnabled else { return }
|
||||
isLoading = true
|
||||
lastError = nil
|
||||
do {
|
||||
conflicts = try await api.fetchConflicts()
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func fix(_ conflict: LibraryConflict) async {
|
||||
guard let action = conflict.fix_action else { return }
|
||||
isFixing = conflict.id
|
||||
do {
|
||||
try await api.fixConflict(action: action, fixData: conflict.fix_data)
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
await refresh()
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
isFixing = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +1,5 @@
|
|||
import SwiftUI
|
||||
|
||||
// MARK: - Conflict Models
|
||||
|
||||
struct LibraryConflict: Codable, Identifiable {
|
||||
let type: String
|
||||
let severity: String
|
||||
let title: String
|
||||
let detail: String
|
||||
let affected_paths: [String]
|
||||
let fix_action: String?
|
||||
let fix_data: [String: AnyCodable]?
|
||||
|
||||
var id: String { "\(type)|\(title)" }
|
||||
|
||||
var severityColor: Color {
|
||||
severity == "error" ? Color(red: 1, green: 0.176, blue: 0.333) : .yellow
|
||||
}
|
||||
|
||||
var severityIcon: String {
|
||||
severity == "error" ? "xmark.circle.fill" : "exclamationmark.triangle.fill"
|
||||
}
|
||||
|
||||
var typeIcon: String {
|
||||
switch type {
|
||||
case "duplicate_album": return "rectangle.on.rectangle.fill"
|
||||
case "missing_files": return "doc.badge.questionmark"
|
||||
case "picard_legacy_tags": return "tag.slash.fill"
|
||||
case "orphaned_tracks": return "link.badge.plus"
|
||||
case "duplicate_track": return "music.note.list"
|
||||
case "stale_companion_paths": return "externaldrive.badge.xmark"
|
||||
default: return "exclamationmark.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConflictsResponse: Codable {
|
||||
let total: Int
|
||||
let errors: Int
|
||||
let warnings: Int
|
||||
let issues: [LibraryConflict]
|
||||
}
|
||||
|
||||
// AnyCodable wrapper for heterogeneous fix_data dict
|
||||
struct AnyCodable: Codable {
|
||||
let value: Any
|
||||
|
||||
init(_ value: Any) { self.value = value }
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let v = try? container.decode([String].self) { value = v; return }
|
||||
if let v = try? container.decode([String: String].self) { value = v; return }
|
||||
if let v = try? container.decode(String.self) { value = v; return }
|
||||
if let v = try? container.decode(Int.self) { value = v; return }
|
||||
value = ""
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch value {
|
||||
case let v as [String]: try container.encode(v)
|
||||
case let v as [String: String]: try container.encode(v)
|
||||
case let v as String: try container.encode(v)
|
||||
case let v as Int: try container.encode(v)
|
||||
default: try container.encode("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conflict Manager
|
||||
|
||||
@MainActor
|
||||
class ConflictManager: ObservableObject {
|
||||
static let shared = ConflictManager()
|
||||
|
||||
@Published var conflicts: ConflictsResponse?
|
||||
@Published var isLoading = false
|
||||
@Published var lastError: String?
|
||||
@Published var isFixing: String? = nil // currently fixing conflict id
|
||||
|
||||
private let api = CompanionAPIService()
|
||||
|
||||
var badgeCount: Int { conflicts?.errors ?? 0 }
|
||||
var totalCount: Int { conflicts?.total ?? 0 }
|
||||
|
||||
func refresh() async {
|
||||
guard CompanionSettings.shared.isEnabled else { return }
|
||||
isLoading = true
|
||||
lastError = nil
|
||||
do {
|
||||
conflicts = try await api.fetchConflicts()
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func fix(_ conflict: LibraryConflict) async {
|
||||
guard let action = conflict.fix_action else { return }
|
||||
isFixing = conflict.id
|
||||
do {
|
||||
try await api.fixConflict(action: action, fixData: conflict.fix_data)
|
||||
// Refresh after fix
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
await refresh()
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
isFixing = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Issues & Conflicts View
|
||||
|
||||
struct LibraryConflictsView: View {
|
||||
|
|
@ -165,7 +54,6 @@ struct LibraryConflictsView: View {
|
|||
Text("Summary")
|
||||
}
|
||||
|
||||
// Group by severity — errors first
|
||||
let sorted = conflicts.issues.sorted {
|
||||
($0.severity == "error" ? 0 : 1) < ($1.severity == "error" ? 0 : 1)
|
||||
}
|
||||
|
|
@ -212,7 +100,6 @@ struct LibraryConflictsView: View {
|
|||
private func conflictRow(_ conflict: LibraryConflict) -> some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Header
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: conflict.typeIcon)
|
||||
.font(.system(size: 16))
|
||||
|
|
@ -231,7 +118,6 @@ struct LibraryConflictsView: View {
|
|||
.foregroundColor(.gray)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
// Affected paths (collapsed, show first 3)
|
||||
if !conflict.affected_paths.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
ForEach(conflict.affected_paths.prefix(3), id: \.self) { path in
|
||||
|
|
@ -251,7 +137,6 @@ struct LibraryConflictsView: View {
|
|||
.cornerRadius(6)
|
||||
}
|
||||
|
||||
// Fix button
|
||||
if let action = conflict.fix_action {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
|
|
|||
Loading…
Reference in a new issue