NavidromeApp/iOS/Views/Companion/TrackEditorView.swift

224 lines
9.1 KiB
Swift

import SwiftUI
struct TrackEditorView: View {
let song: Song
@Environment(\.dismiss) private var dismiss
// Field values
@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
// Checkmarks only checked fields get sent to the API
@State private var editTitle = false
@State private var editArtist = false
@State private var editAlbum = false
@State private var editAlbumArtist = false
@State private var editGenre = false
@State private var editYear = false
@State private var editTrackNumber = false
@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)
private var hasSelection: Bool {
editTitle || editArtist || editAlbum || editAlbumArtist || editGenre || editYear || editTrackNumber
}
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!)" : "")
}
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.")
.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 with checkmarks
Section {
checkField("Title", text: $title, isEnabled: $editTitle)
checkField("Artist", text: $artist, isEnabled: $editArtist)
checkField("Album", text: $album, isEnabled: $editAlbum)
checkField("Album Artist", text: $albumArtist, isEnabled: $editAlbumArtist)
} header: {
Text("Basic Info")
} footer: {
Text("Check the fields you want to change. Only checked fields will be updated.")
}
Section {
checkField("Genre", text: $genre, isEnabled: $editGenre)
checkField("Year", text: $year, isEnabled: $editYear, keyboard: .numberPad)
checkField("Track #", text: $trackNumber, isEnabled: $editTrackNumber, keyboard: .numberPad)
} header: {
Text("Details")
}
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(hasSelection ? accentPink : .gray)
}
}
.disabled(isSaving || !hasSelection || 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: editTitle ? (title.isEmpty ? nil : title) : nil,
artist: editArtist ? (artist.isEmpty ? nil : artist) : nil,
album: editAlbum ? (album.isEmpty ? nil : album) : nil,
albumArtist: editAlbumArtist ? (albumArtist.isEmpty ? nil : albumArtist) : nil,
genre: editGenre ? (genre.isEmpty ? nil : genre) : nil,
year: editYear ? Int(year) : nil,
trackNumber: editTrackNumber ? Int(trackNumber) : nil
)
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
}
}
}
}
// MARK: - Checkmark Field
private func checkField(_ label: String, text: Binding<String>, isEnabled: Binding<Bool>, keyboard: UIKeyboardType = .default) -> some View {
HStack(spacing: 10) {
Button(action: { isEnabled.wrappedValue.toggle() }) {
Image(systemName: isEnabled.wrappedValue ? "checkmark.circle.fill" : "circle")
.font(.system(size: 20))
.foregroundColor(isEnabled.wrappedValue ? accentPink : .gray.opacity(0.4))
}
.buttonStyle(.plain)
Text(label)
.foregroundColor(isEnabled.wrappedValue ? .white : .gray)
.frame(width: 85, alignment: .leading)
TextField(label, text: text)
.keyboardType(keyboard)
.multilineTextAlignment(.trailing)
.foregroundColor(isEnabled.wrappedValue ? .white : .gray.opacity(0.5))
.disabled(!isEnabled.wrappedValue)
}
.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))
}
}