import SwiftData import SwiftUI /// Main screen — list of playlists with Library and Settings in toolbar. struct PlaylistListView: View { @Environment(PlayerViewModel.self) private var playerVM @Environment(PlaylistViewModel.self) private var playlistVM @EnvironmentObject private var libraryManager: LibraryManager @EnvironmentObject private var theme: AppTheme @Environment(\.modelContext) private var modelContext @Query(sort: \Playlist.dateModified, order: .reverse) private var playlists: [Playlist] @State private var showNewPlaylist = false @State private var newPlaylistName = "" @State private var showLibrary = false @State private var showSettings = false @State private var showCloudBrowser = false var body: some View { NavigationStack { Group { if playlists.isEmpty { emptyState } else { playlistList } } .navigationTitle("MixBoard") .toolbar { ToolbarItem(placement: .topBarLeading) { HStack(spacing: 12) { Button { showLibrary = true } label: { Image(systemName: "music.note.list") } .accessibilityIdentifier("libraryButton") Button { showCloudBrowser = true } label: { Image(systemName: "cloud.fill") } .accessibilityIdentifier("cloudBrowserButton") Button { showSettings = true } label: { Image(systemName: "gearshape") } .accessibilityIdentifier("settingsButton") } } ToolbarItem(placement: .topBarTrailing) { Button { showNewPlaylist = true } label: { Image(systemName: "plus") } .accessibilityIdentifier("newPlaylistButton") .accessibilityIdentifier("newPlaylistButton") } } .alert("New Playlist", isPresented: $showNewPlaylist) { TextField("Playlist name", text: $newPlaylistName) Button("Cancel", role: .cancel) { newPlaylistName = "" } Button("Create") { guard !newPlaylistName.isEmpty else { return } let pl = playlistVM.createPlaylist(name: newPlaylistName, context: modelContext) playlistVM.selectedPlaylist = pl newPlaylistName = "" } } message: { Text("Enter a name for your new playlist") } .sheet(isPresented: $showLibrary) { LibraryView() .environmentObject(theme) } .sheet(isPresented: $showSettings) { SettingsView() .environmentObject(theme) } .sheet(isPresented: $showCloudBrowser) { CloudBrowserView() .environmentObject(theme) } } } // MARK: - Playlist List private var playlistList: some View { List { ForEach(playlists) { playlist in NavigationLink { PlaylistDetailView(playlist: playlist) } label: { PlaylistRowView(playlist: playlist) } .swipeActions(edge: .trailing) { Button(role: .destructive) { playlistVM.deletePlaylist(playlist, context: modelContext) } label: { Label("Delete", systemImage: "trash") } } .swipeActions(edge: .leading) { Button { playlistVM.targetPlaylist = playlist playlistVM.showStatus("Target: \(playlist.name)") } label: { Label("Target", systemImage: "star.fill") } .tint(.orange) } .contextMenu { Button { playlistVM.targetPlaylist = playlist playlistVM.showStatus("Target: \(playlist.name)") } label: { Label( playlistVM.targetPlaylist?.id == playlist.id ? "Current Target" : "Set as Target", systemImage: "star.fill" ) } Button(role: .destructive) { playlistVM.deletePlaylist(playlist, context: modelContext) } label: { Label("Delete", systemImage: "trash") } } } } .listStyle(.insetGrouped) .accessibilityIdentifier("playlistList") } // MARK: - Empty State private var emptyState: some View { VStack(spacing: 20) { Spacer() Image(systemName: "music.note.list") .font(.system(size: 60)) .foregroundStyle(theme.tertiaryText) Text("No playlists yet") .font(.title2) .foregroundStyle(theme.secondaryText) .accessibilityIdentifier("emptyStateTitle") Text("Create a playlist to start building your mix") .font(.subheadline) .foregroundStyle(theme.tertiaryText) Button { showNewPlaylist = true } label: { Label("New Playlist", systemImage: "plus.circle.fill") .font(.headline) .padding(.horizontal, 24) .padding(.vertical, 12) } .buttonStyle(.borderedProminent) .tint(theme.accent) .accessibilityIdentifier("emptyStateNewPlaylistButton") Spacer() } .accessibilityIdentifier("emptyState") } } // MARK: - Playlist Row struct PlaylistRowView: View { let playlist: Playlist @Environment(PlaylistViewModel.self) private var playlistVM @EnvironmentObject private var theme: AppTheme private var isTarget: Bool { guard let target = playlistVM.mixTargets.first(where: { $0?.id == playlist.id }) else { return false } return target != nil } var body: some View { HStack(spacing: 12) { // Color indicator Circle() .fill(Color(hex: playlist.color) ?? theme.accent) .frame(width: 12, height: 12) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text(playlist.name) .font(.headline) .foregroundStyle(theme.primaryText) if isTarget { Image(systemName: "star.fill") .font(.caption2) .foregroundStyle(.orange) } } HStack(spacing: 8) { Text("\(playlist.trackCount) tracks") .font(.caption) .foregroundStyle(theme.secondaryText) } } Spacer() } .padding(.vertical, 4) .accessibilityIdentifier("playlistRow_\(playlist.name)") } }