import SwiftUI /// Compact track row for lists — adapts to current skin. /// Shows 3 quick-add mix buttons on the right side. struct TrackRow: View { let track: Track @EnvironmentObject private var theme: AppTheme @Environment(PlayerViewModel.self) private var playerVM @Environment(PlaylistViewModel.self) private var playlistVM @Environment(\.modelContext) private var modelContext private var isCurrentlyPlaying: Bool { playerVM.currentTrack?.id == track.id } private static let mixColors: [Color] = [ Color(red: 0.95, green: 0.3, blue: 0.3), // Red Color(red: 0.3, green: 0.75, blue: 0.95), // Blue Color(red: 0.95, green: 0.75, blue: 0.2), // Yellow/Gold ] var body: some View { HStack(spacing: 10) { // Album art ArtworkThumbnail(track: track) .frame(width: 44, height: 44) // Track info VStack(alignment: .leading, spacing: 2) { Text(track.title) .font(.system(size: theme.dataFontSize, weight: isCurrentlyPlaying ? .semibold : .regular)) .foregroundStyle(isCurrentlyPlaying ? theme.playingHighlight : theme.primaryText) .lineLimit(1) HStack(spacing: 6) { if !track.artist.isEmpty { Text(track.artist) .font(.system(size: theme.smallFontSize)) .foregroundStyle(isCurrentlyPlaying ? theme.playingHighlight.opacity(0.7) : theme.secondaryText) .lineLimit(1) } if let bpm = track.bpm { Text("•") .foregroundStyle(theme.tertiaryText) Text("\(String(format: "%.0f", bpm)) BPM") .font(.system(size: theme.smallFontSize, design: .monospaced)) .foregroundStyle(theme.tertiaryText) } if let key = track.musicalKey { Text(key) .font(.system(size: theme.smallFontSize, design: .monospaced)) .foregroundStyle(theme.tertiaryText) } } } Spacer(minLength: 4) // 3 Mix buttons HStack(spacing: 4) { ForEach(0..<3, id: \.self) { slot in Button { _ = playlistVM.quickAddToMix(slot: slot, track: track, context: modelContext) } label: { Text("\(slot + 1)") .font(.system(size: 11, weight: .bold, design: .rounded)) .frame(width: 26, height: 26) .foregroundStyle( playlistVM.mixTargets[slot] != nil ? Self.mixColors[slot] : theme.tertiaryText ) .background( playlistVM.mixTargets[slot] != nil ? Self.mixColors[slot].opacity(0.15) : theme.tertiaryText.opacity(0.08) ) .clipShape(RoundedRectangle(cornerRadius: 6)) } .buttonStyle(.plain) } } // Duration + format VStack(alignment: .trailing, spacing: 2) { Text(track.formattedDuration) .font(.system(size: theme.smallFontSize, design: .monospaced)) .foregroundStyle(theme.secondaryText) Text(track.fileFormat) .font(.system(size: 9, weight: .medium, design: .monospaced)) .foregroundStyle(theme.tertiaryText) .padding(.horizontal, 4) .padding(.vertical, 1) .background(theme.tertiaryText.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 3)) } if isCurrentlyPlaying && playerVM.isPlaying { Image(systemName: "speaker.wave.2.fill") .font(.caption) .foregroundStyle(theme.playingHighlight) } } .padding(.vertical, 4) .contextMenu { Button { playerVM.loadAndPlay(track) playerVM.showNowPlaying = true } label: { Label("Play Now", systemImage: "play.fill") } Button { playerVM.playNextInQueue(QueueEntry.from(track: track)) } label: { Label("Play Next", systemImage: "text.insert") } Button { playerVM.addToQueue(QueueEntry.from(track: track)) } label: { Label("Add to Queue", systemImage: "text.append") } } } } // MARK: - Artwork Thumbnail struct ArtworkThumbnail: View { let track: Track @EnvironmentObject private var theme: AppTheme @State private var artwork: UIImage? var body: some View { Group { if let image = artwork { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fill) } else { ZStack { RoundedRectangle(cornerRadius: theme.cornerRadius / 2) .fill(theme.cardBackground) Image(systemName: "music.note") .font(.system(size: 16)) .foregroundStyle(theme.tertiaryText) } } } .clipShape(RoundedRectangle(cornerRadius: theme.cornerRadius / 2)) .task { let url = track.fileURL artwork = await ArtworkService.shared.artwork(for: url) } } }