216 lines
8.1 KiB
Swift
216 lines
8.1 KiB
Swift
|
|
import SwiftUI
|
||
|
|
|
||
|
|
struct TrackEditorView: View {
|
||
|
|
let song: Song
|
||
|
|
@Environment(\.dismiss) private var dismiss
|
||
|
|
|
||
|
|
@State private var title: String
|
||
|
|
@State private var artist: String
|
||
|
|
@State private var album: String
|
||
|
|
@State private var albumArtist: String
|
||
|
|
@State private var genre: String
|
||
|
|
@State private var year: String
|
||
|
|
@State private var trackNumber: String
|
||
|
|
@State private var discNumber: String
|
||
|
|
@State private var comment: String
|
||
|
|
|
||
|
|
@State private var isSaving = false
|
||
|
|
@State private var errorMessage: String?
|
||
|
|
@State private var showSuccess = false
|
||
|
|
|
||
|
|
@ObservedObject private var settings = CompanionSettings.shared
|
||
|
|
|
||
|
|
private let api = CompanionAPIService()
|
||
|
|
private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333)
|
||
|
|
|
||
|
|
init(song: Song) {
|
||
|
|
self.song = song
|
||
|
|
_title = State(initialValue: song.title)
|
||
|
|
_artist = State(initialValue: song.artist ?? "")
|
||
|
|
_album = State(initialValue: song.album ?? "")
|
||
|
|
_albumArtist = State(initialValue: "")
|
||
|
|
_genre = State(initialValue: song.genre ?? "")
|
||
|
|
_year = State(initialValue: song.year != nil ? "\(song.year!)" : "")
|
||
|
|
_trackNumber = State(initialValue: song.track != nil ? "\(song.track!)" : "")
|
||
|
|
_discNumber = State(initialValue: song.discNumber != nil ? "\(song.discNumber!)" : "")
|
||
|
|
_comment = State(initialValue: "")
|
||
|
|
}
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
NavigationStack {
|
||
|
|
List {
|
||
|
|
if !settings.isEnabled {
|
||
|
|
Section {
|
||
|
|
VStack(spacing: 8) {
|
||
|
|
Image(systemName: "exclamationmark.triangle")
|
||
|
|
.font(.system(size: 28))
|
||
|
|
.foregroundColor(.yellow)
|
||
|
|
Text("Companion API Required")
|
||
|
|
.font(.system(size: 15, weight: .medium))
|
||
|
|
Text("Configure the Companion API in Settings to edit track metadata remotely.")
|
||
|
|
.font(.system(size: 13))
|
||
|
|
.foregroundColor(.gray)
|
||
|
|
.multilineTextAlignment(.center)
|
||
|
|
}
|
||
|
|
.frame(maxWidth: .infinity)
|
||
|
|
.padding(.vertical, 12)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// File info (read-only)
|
||
|
|
Section {
|
||
|
|
infoRow("File", song.path ?? song.id)
|
||
|
|
if let suffix = song.suffix {
|
||
|
|
infoRow("Format", suffix.uppercased())
|
||
|
|
}
|
||
|
|
if let bitRate = song.bitRate {
|
||
|
|
infoRow("Bit Rate", "\(bitRate) kbps")
|
||
|
|
}
|
||
|
|
} header: {
|
||
|
|
Text("File Info")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Editable fields
|
||
|
|
Section {
|
||
|
|
editField("Title", text: $title)
|
||
|
|
editField("Artist", text: $artist)
|
||
|
|
editField("Album", text: $album)
|
||
|
|
editField("Album Artist", text: $albumArtist)
|
||
|
|
} header: {
|
||
|
|
Text("Basic Info")
|
||
|
|
}
|
||
|
|
|
||
|
|
Section {
|
||
|
|
editField("Genre", text: $genre)
|
||
|
|
editField("Year", text: $year, keyboard: .numberPad)
|
||
|
|
editField("Track #", text: $trackNumber, keyboard: .numberPad)
|
||
|
|
editField("Disc #", text: $discNumber, keyboard: .numberPad)
|
||
|
|
} header: {
|
||
|
|
Text("Details")
|
||
|
|
}
|
||
|
|
|
||
|
|
Section {
|
||
|
|
TextEditor(text: $comment)
|
||
|
|
.frame(minHeight: 60)
|
||
|
|
.font(.system(size: 14))
|
||
|
|
} header: {
|
||
|
|
Text("Comment")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Error / Success
|
||
|
|
if let error = errorMessage {
|
||
|
|
Section {
|
||
|
|
Text(error)
|
||
|
|
.font(.system(size: 13))
|
||
|
|
.foregroundColor(.red)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.navigationTitle("Edit Tags")
|
||
|
|
.navigationBarTitleDisplayMode(.inline)
|
||
|
|
.toolbar {
|
||
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
||
|
|
Button("Cancel") { dismiss() }
|
||
|
|
.foregroundColor(.gray)
|
||
|
|
}
|
||
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||
|
|
if settings.isEnabled {
|
||
|
|
Button(action: save) {
|
||
|
|
if isSaving {
|
||
|
|
ProgressView()
|
||
|
|
.tint(accentPink)
|
||
|
|
} else {
|
||
|
|
Text("Save")
|
||
|
|
.fontWeight(.semibold)
|
||
|
|
.foregroundColor(accentPink)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.disabled(isSaving || song.path == nil)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.overlay {
|
||
|
|
if showSuccess {
|
||
|
|
VStack(spacing: 12) {
|
||
|
|
Image(systemName: "checkmark.circle.fill")
|
||
|
|
.font(.system(size: 44))
|
||
|
|
.foregroundColor(.green)
|
||
|
|
Text("Tags Updated")
|
||
|
|
.font(.system(size: 16, weight: .medium))
|
||
|
|
.foregroundColor(.white)
|
||
|
|
}
|
||
|
|
.padding(30)
|
||
|
|
.background(.ultraThinMaterial)
|
||
|
|
.cornerRadius(16)
|
||
|
|
.transition(.scale.combined(with: .opacity))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Save
|
||
|
|
|
||
|
|
private func save() {
|
||
|
|
guard let path = song.path else { return }
|
||
|
|
isSaving = true
|
||
|
|
errorMessage = nil
|
||
|
|
|
||
|
|
Task {
|
||
|
|
do {
|
||
|
|
let request = MetadataEditRequest(
|
||
|
|
relativePath: path,
|
||
|
|
title: title.isEmpty ? nil : title,
|
||
|
|
artist: artist.isEmpty ? nil : artist,
|
||
|
|
album: album.isEmpty ? nil : album
|
||
|
|
)
|
||
|
|
|
||
|
|
try await api.editMetadata(request)
|
||
|
|
|
||
|
|
await MainActor.run {
|
||
|
|
isSaving = false
|
||
|
|
withAnimation { showSuccess = true }
|
||
|
|
DebugLogger.shared.log("Tags updated: \(title) — \(artist)", category: "Companion")
|
||
|
|
}
|
||
|
|
|
||
|
|
try? await Task.sleep(for: .seconds(1.2))
|
||
|
|
await MainActor.run {
|
||
|
|
dismiss()
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch {
|
||
|
|
await MainActor.run {
|
||
|
|
isSaving = false
|
||
|
|
errorMessage = error.localizedDescription
|
||
|
|
DebugLogger.shared.log("Tag edit failed: \(error.localizedDescription)", category: "Companion")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Helpers
|
||
|
|
|
||
|
|
private func editField(_ label: String, text: Binding<String>, keyboard: UIKeyboardType = .default) -> some View {
|
||
|
|
HStack {
|
||
|
|
Text(label)
|
||
|
|
.foregroundColor(.gray)
|
||
|
|
.frame(width: 90, alignment: .leading)
|
||
|
|
TextField(label, text: text)
|
||
|
|
.keyboardType(keyboard)
|
||
|
|
.multilineTextAlignment(.trailing)
|
||
|
|
}
|
||
|
|
.font(.system(size: 14))
|
||
|
|
}
|
||
|
|
|
||
|
|
private func infoRow(_ label: String, _ value: String) -> some View {
|
||
|
|
HStack {
|
||
|
|
Text(label)
|
||
|
|
.foregroundColor(.gray)
|
||
|
|
Spacer()
|
||
|
|
Text(value)
|
||
|
|
.foregroundColor(.white.opacity(0.7))
|
||
|
|
.lineLimit(1)
|
||
|
|
}
|
||
|
|
.font(.system(size: 13))
|
||
|
|
}
|
||
|
|
}
|