PlaylistViewConfig.swift 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import Foundation
  2. /// Configuration for how the playlist view displays tracks.
  3. /// Persisted to UserDefaults so it remembers across sessions.
  4. /// Use `PlaylistViewConfig.shared` for a single app-wide instance.
  5. final class PlaylistViewConfig: ObservableObject {
  6. static let shared = PlaylistViewConfig()
  7. // MARK: - Column Visibility
  8. /// All possible metadata columns.
  9. enum Column: String, CaseIterable, Identifiable, Codable {
  10. case artwork = "Artwork"
  11. case trackNumber = "#"
  12. case title = "Title"
  13. case artist = "Artist"
  14. case album = "Album"
  15. case genre = "Genre"
  16. case bpm = "BPM"
  17. case key = "Key"
  18. case duration = "Duration"
  19. case format = "Format"
  20. case sampleRate = "Sample Rate"
  21. case bitDepth = "Bit Depth"
  22. case fileSize = "File Size"
  23. case rating = "Rating"
  24. case dateAdded = "Date Added"
  25. case playCount = "Play Count"
  26. case crossfade = "Crossfade"
  27. case gain = "Gain"
  28. var id: String { rawValue }
  29. /// Default width hint for each column.
  30. var defaultWidth: CGFloat {
  31. switch self {
  32. case .artwork: return 50
  33. case .trackNumber: return 30
  34. case .title: return 220
  35. case .artist: return 150
  36. case .album: return 150
  37. case .genre: return 100
  38. case .bpm: return 55
  39. case .key: return 55
  40. case .duration: return 55
  41. case .format: return 50
  42. case .sampleRate: return 70
  43. case .bitDepth: return 50
  44. case .fileSize: return 70
  45. case .rating: return 80
  46. case .dateAdded: return 90
  47. case .playCount: return 50
  48. case .crossfade: return 120
  49. case .gain: return 120
  50. }
  51. }
  52. }
  53. // MARK: - Artwork Size
  54. enum ArtworkSize: String, CaseIterable, Identifiable, Codable {
  55. case small = "Small" // 32pt
  56. case medium = "Medium" // 48pt
  57. case large = "Large" // 64pt
  58. var id: String { rawValue }
  59. var points: CGFloat {
  60. switch self {
  61. case .small: return 32
  62. case .medium: return 48
  63. case .large: return 64
  64. }
  65. }
  66. }
  67. // MARK: - Published State
  68. @Published var visibleColumns: [Column] {
  69. didSet { save() }
  70. }
  71. @Published var showArtwork: Bool {
  72. didSet { save() }
  73. }
  74. @Published var artworkSize: ArtworkSize {
  75. didSet { save() }
  76. }
  77. @Published var cursorFollowsPlayback: Bool {
  78. didSet { save() }
  79. }
  80. @Published var playbackFollowsCursor: Bool {
  81. didSet { save() }
  82. }
  83. // MARK: - Defaults
  84. static let defaultColumns: [Column] = [
  85. .artwork, .trackNumber, .title, .artist, .bpm, .key, .duration, .crossfade
  86. ]
  87. // MARK: - Init
  88. init() {
  89. let defaults = UserDefaults.standard
  90. if let data = defaults.data(forKey: "playlistVisibleColumns"),
  91. let decoded = try? JSONDecoder().decode([Column].self, from: data) {
  92. visibleColumns = decoded
  93. } else {
  94. visibleColumns = Self.defaultColumns
  95. }
  96. showArtwork = defaults.object(forKey: "playlistShowArtwork") as? Bool ?? true
  97. cursorFollowsPlayback = defaults.object(forKey: "playlistCursorFollowsPlayback") as? Bool ?? true
  98. playbackFollowsCursor = defaults.object(forKey: "playlistPlaybackFollowsCursor") as? Bool ?? false
  99. if let raw = defaults.string(forKey: "playlistArtworkSize"),
  100. let size = ArtworkSize(rawValue: raw) {
  101. artworkSize = size
  102. } else {
  103. artworkSize = .medium
  104. }
  105. }
  106. // MARK: - Persistence
  107. private func save() {
  108. let defaults = UserDefaults.standard
  109. if let data = try? JSONEncoder().encode(visibleColumns) {
  110. defaults.set(data, forKey: "playlistVisibleColumns")
  111. }
  112. defaults.set(showArtwork, forKey: "playlistShowArtwork")
  113. defaults.set(cursorFollowsPlayback, forKey: "playlistCursorFollowsPlayback")
  114. defaults.set(playbackFollowsCursor, forKey: "playlistPlaybackFollowsCursor")
  115. defaults.set(artworkSize.rawValue, forKey: "playlistArtworkSize")
  116. }
  117. // MARK: - Helpers
  118. func isColumnVisible(_ column: Column) -> Bool {
  119. visibleColumns.contains(column)
  120. }
  121. func toggleColumn(_ column: Column) {
  122. if let idx = visibleColumns.firstIndex(of: column) {
  123. visibleColumns.remove(at: idx)
  124. } else {
  125. visibleColumns.append(column)
  126. }
  127. }
  128. func resetToDefaults() {
  129. visibleColumns = Self.defaultColumns
  130. showArtwork = true
  131. artworkSize = .medium
  132. cursorFollowsPlayback = true
  133. playbackFollowsCursor = false
  134. }
  135. }