import Foundation /// Configuration for how the playlist view displays tracks. /// Persisted to UserDefaults so it remembers across sessions. /// Use `PlaylistViewConfig.shared` for a single app-wide instance. final class PlaylistViewConfig: ObservableObject { static let shared = PlaylistViewConfig() // MARK: - Column Visibility /// All possible metadata columns. enum Column: String, CaseIterable, Identifiable, Codable { case artwork = "Artwork" case trackNumber = "#" case title = "Title" case artist = "Artist" case album = "Album" case genre = "Genre" case bpm = "BPM" case key = "Key" case duration = "Duration" case format = "Format" case sampleRate = "Sample Rate" case bitDepth = "Bit Depth" case fileSize = "File Size" case rating = "Rating" case dateAdded = "Date Added" case playCount = "Play Count" case crossfade = "Crossfade" case gain = "Gain" var id: String { rawValue } /// Default width hint for each column. var defaultWidth: CGFloat { switch self { case .artwork: return 50 case .trackNumber: return 30 case .title: return 220 case .artist: return 150 case .album: return 150 case .genre: return 100 case .bpm: return 55 case .key: return 55 case .duration: return 55 case .format: return 50 case .sampleRate: return 70 case .bitDepth: return 50 case .fileSize: return 70 case .rating: return 80 case .dateAdded: return 90 case .playCount: return 50 case .crossfade: return 120 case .gain: return 120 } } } // MARK: - Artwork Size enum ArtworkSize: String, CaseIterable, Identifiable, Codable { case small = "Small" // 32pt case medium = "Medium" // 48pt case large = "Large" // 64pt var id: String { rawValue } var points: CGFloat { switch self { case .small: return 32 case .medium: return 48 case .large: return 64 } } } // MARK: - Published State @Published var visibleColumns: [Column] { didSet { save() } } @Published var showArtwork: Bool { didSet { save() } } @Published var artworkSize: ArtworkSize { didSet { save() } } @Published var cursorFollowsPlayback: Bool { didSet { save() } } @Published var playbackFollowsCursor: Bool { didSet { save() } } // MARK: - Defaults static let defaultColumns: [Column] = [ .artwork, .trackNumber, .title, .artist, .bpm, .key, .duration, .crossfade ] // MARK: - Init init() { let defaults = UserDefaults.standard if let data = defaults.data(forKey: "playlistVisibleColumns"), let decoded = try? JSONDecoder().decode([Column].self, from: data) { visibleColumns = decoded } else { visibleColumns = Self.defaultColumns } showArtwork = defaults.object(forKey: "playlistShowArtwork") as? Bool ?? true cursorFollowsPlayback = defaults.object(forKey: "playlistCursorFollowsPlayback") as? Bool ?? true playbackFollowsCursor = defaults.object(forKey: "playlistPlaybackFollowsCursor") as? Bool ?? false if let raw = defaults.string(forKey: "playlistArtworkSize"), let size = ArtworkSize(rawValue: raw) { artworkSize = size } else { artworkSize = .medium } } // MARK: - Persistence private func save() { let defaults = UserDefaults.standard if let data = try? JSONEncoder().encode(visibleColumns) { defaults.set(data, forKey: "playlistVisibleColumns") } defaults.set(showArtwork, forKey: "playlistShowArtwork") defaults.set(cursorFollowsPlayback, forKey: "playlistCursorFollowsPlayback") defaults.set(playbackFollowsCursor, forKey: "playlistPlaybackFollowsCursor") defaults.set(artworkSize.rawValue, forKey: "playlistArtworkSize") } // MARK: - Helpers func isColumnVisible(_ column: Column) -> Bool { visibleColumns.contains(column) } func toggleColumn(_ column: Column) { if let idx = visibleColumns.firstIndex(of: column) { visibleColumns.remove(at: idx) } else { visibleColumns.append(column) } } func resetToDefaults() { visibleColumns = Self.defaultColumns showArtwork = true artworkSize = .medium cursorFollowsPlayback = true playbackFollowsCursor = false } }