| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- 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
- @State private var navigationPath = NavigationPath()
- @State private var hasRestoredNavigation = false
- var body: some View {
- NavigationStack(path: $navigationPath) {
- 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(value: playlist.id) {
- 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")
- .navigationDestination(for: UUID.self) { playlistID in
- if let playlist = playlists.first(where: { $0.id == playlistID }) {
- PlaylistDetailView(playlist: playlist)
- }
- }
- .onAppear {
- guard !hasRestoredNavigation else { return }
- hasRestoredNavigation = true
- // Restore last playlist on launch
- if let lastIDString = UserDefaults.standard.string(forKey: "appState.lastPlaylistID"),
- let lastID = UUID(uuidString: lastIDString),
- playlists.contains(where: { $0.id == lastID }) {
- navigationPath.append(lastID)
- }
- }
- }
- // 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)")
- }
- }
|