| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- 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)
- }
- }
- }
|