ContentView.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import SwiftData
  2. import SwiftUI
  3. /// Root view — Playlists as the main screen, Library and Settings accessible from toolbar.
  4. struct ContentView: View {
  5. @Environment(PlayerViewModel.self) private var playerVM
  6. @Environment(PlaylistViewModel.self) private var playlistVM
  7. @EnvironmentObject private var libraryManager: LibraryManager
  8. @EnvironmentObject private var theme: AppTheme
  9. @EnvironmentObject private var syncManager: SyncManager
  10. @Environment(\.modelContext) private var modelContext
  11. @State private var showLibrary = false
  12. @State private var showSettings = false
  13. @State private var showCloudBrowser = false
  14. @Query(sort: \Playlist.dateModified, order: .reverse)
  15. private var playlists: [Playlist]
  16. var body: some View {
  17. VStack(spacing: 0) {
  18. // Main content: Playlists
  19. PlaylistListView()
  20. // Mini player at bottom
  21. if playerVM.currentTrack != nil || playerVM.isCloudPlayback {
  22. MiniPlayerView()
  23. }
  24. }
  25. .accessibilityIdentifier("ContentView")
  26. .overlay(alignment: .bottom) {
  27. // Undo queue replacement toast
  28. if playerVM.showUndoToast {
  29. HStack(spacing: 8) {
  30. Text(playerVM.undoMessage)
  31. .font(.subheadline)
  32. .foregroundStyle(theme.primaryText)
  33. Button("Undo") {
  34. playerVM.undoQueueReplacement()
  35. }
  36. .font(.subheadline.bold())
  37. .foregroundStyle(theme.accent)
  38. }
  39. .padding(.horizontal, 16)
  40. .padding(.vertical, 10)
  41. .background(theme.cardBackground.opacity(0.95))
  42. .clipShape(Capsule())
  43. .shadow(radius: 8)
  44. .padding(.bottom, (playerVM.currentTrack != nil || playerVM.isCloudPlayback) ? 90 : 60)
  45. .transition(.move(edge: .bottom).combined(with: .opacity))
  46. .animation(.easeInOut(duration: 0.3), value: playerVM.showUndoToast)
  47. }
  48. }
  49. .overlay(alignment: .bottom) {
  50. // Status toast
  51. if let status = playlistVM.statusMessage {
  52. HStack(spacing: 8) {
  53. Image(systemName: "checkmark.circle.fill")
  54. .foregroundStyle(theme.accent)
  55. Text(status)
  56. .font(.subheadline)
  57. .foregroundStyle(theme.primaryText)
  58. }
  59. .padding(.horizontal, 16)
  60. .padding(.vertical, 10)
  61. .background(theme.cardBackground.opacity(0.95))
  62. .clipShape(Capsule())
  63. .shadow(radius: 8)
  64. .padding(.bottom, (playerVM.currentTrack != nil || playerVM.isCloudPlayback) ? 90 : 60)
  65. .transition(.move(edge: .bottom).combined(with: .opacity))
  66. .animation(.easeInOut(duration: 0.3), value: playlistVM.statusMessage)
  67. }
  68. }
  69. .onAppear {
  70. libraryManager.setModelContext(modelContext)
  71. libraryManager.fixBadPathsIfNeeded()
  72. playlistVM.restoreTargetPlaylist(from: playlists)
  73. playerVM.modelContext = modelContext
  74. }
  75. .onChange(of: playlists.count) { _, newCount in
  76. guard newCount > 0 else { return }
  77. Task {
  78. try? await Task.sleep(for: .seconds(2))
  79. syncManager.exportPlaylists(playlists)
  80. }
  81. }
  82. .fullScreenCover(isPresented: Binding(
  83. get: { playerVM.showNowPlaying },
  84. set: { playerVM.showNowPlaying = $0 }
  85. )) {
  86. NowPlayingView()
  87. .environmentObject(theme)
  88. .environmentObject(libraryManager)
  89. }
  90. .sheet(isPresented: $showLibrary) {
  91. NavigationStack {
  92. LibraryView()
  93. }
  94. .environmentObject(theme)
  95. }
  96. .sheet(isPresented: $showSettings) {
  97. SettingsView()
  98. .environmentObject(theme)
  99. .environmentObject(syncManager)
  100. }
  101. .sheet(isPresented: $showCloudBrowser) {
  102. CloudBrowserView()
  103. .environmentObject(theme)
  104. }
  105. .sheet(isPresented: Bindable(playerVM).showQueue) {
  106. QueueView()
  107. .environment(playerVM)
  108. .environmentObject(theme)
  109. }
  110. }
  111. }