From 3f3010a8fdbe28ebb2f56784a6f9ca5a41509b82 Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Sat, 11 Apr 2026 18:15:10 -0700 Subject: [PATCH] fixed dynamicminiplayer placement --- iOS/Views/Common/MainTabView.swift | 141 +++++++++++++++++------------ 1 file changed, 81 insertions(+), 60 deletions(-) diff --git a/iOS/Views/Common/MainTabView.swift b/iOS/Views/Common/MainTabView.swift index af1b3dc..36b11e6 100644 --- a/iOS/Views/Common/MainTabView.swift +++ b/iOS/Views/Common/MainTabView.swift @@ -10,6 +10,7 @@ struct MainTabView: View { @State private var navigateToAlbumId: String? @State private var navigateToArtistId: String? @State var showNowPlaying = false + @State private var wasInDynamicIsland = false // restore DI mode on NowPlaying dismiss // Dynamic Island morph @Namespace private var playerNamespace @@ -173,6 +174,15 @@ struct MainTabView: View { .opacity(showNowPlaying ? 0 : 1) } } + .onChange(of: showNowPlaying) { _, isShowing in + // If we were in DI mode when NowPlaying opened, restore it on dismiss + if !isShowing && wasInDynamicIsland { + wasInDynamicIsland = false + withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { + isDynamicIsland = true + } + } + } .onChange(of: debugLogger.isPiP) { _, newVal in // DebugConsoleView's PiP button sets logger.isPiP; sync to local state if newVal { debugPipMode = true } @@ -694,87 +704,98 @@ struct DynamicIslandView: View { var body: some View { VStack(spacing: 0) { - Spacer().frame(height: 12) // Sit directly below Dynamic Island - - // The pill — only this is tappable for NowPlaying - HStack(spacing: 0) { - Group { - if let song = audioPlayer.currentSong { - AsyncCoverArt(coverArtId: song.coverArt, size: 48) - } else { - Color.gray.opacity(0.3) + Spacer().frame(height: 12) + + HStack(spacing: 8) { + // ── Left pill: album art + song info ───────────────────────── + // Left-aligned, sits directly under the Dynamic Island + HStack(spacing: 8) { + Group { + if let song = audioPlayer.currentSong { + AsyncCoverArt(coverArtId: song.coverArt, size: 44) + } else { + Color.gray.opacity(0.3) + } + } + .frame(width: 30, height: 30) + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + .matchedGeometryEffect(id: "albumArt", in: namespace) + + VStack(alignment: .leading, spacing: 1) { + Text(audioPlayer.currentSong?.title ?? "") + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.white) + .lineLimit(1) + Text(audioPlayer.currentSong?.artist ?? "") + .font(.system(size: 9)) + .foregroundColor(.white.opacity(0.55)) + .lineLimit(1) + } + .frame(maxWidth: 140, alignment: .leading) + } + .padding(.horizontal, 10) + .frame(height: 44) + .background( + RoundedRectangle(cornerRadius: 22, style: .continuous) + .fill(Color.black.opacity(0.85)) + .shadow(color: themeColor.opacity(0.3), radius: 6, y: 2) + ) + .contentShape(RoundedRectangle(cornerRadius: 22)) + .onTapGesture { + wasInDynamicIsland = true + withAnimation(.easeInOut(duration: 0.35)) { + showNowPlaying = true + isDynamicIsland = false } } - .frame(width: 32, height: 32) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) - .matchedGeometryEffect(id: "albumArt", in: namespace) - .padding(.leading, 10) - - VStack(alignment: .leading, spacing: 0) { - Text(audioPlayer.currentSong?.title ?? "") - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(.white) - .lineLimit(1) - Text(audioPlayer.currentSong?.artist ?? "") - .font(.system(size: 10)) - .foregroundColor(.white.opacity(0.6)) - .lineLimit(1) - } - .padding(.leading, 8) - .frame(maxWidth: .infinity, alignment: .leading) - - if VisualizerSettings.shared.enabled { + + // ── Visualizer fill ─────────────────────────────────────────── + if VisualizerSettings.shared.enabled && VisualizerSettings.shared.miniPlayerEnabled { CompactVisualizerView( isPlaying: audioPlayer.isPlaying, isSongLoaded: audioPlayer.currentSong != nil, accentColor: themeColor, - height: 32 + height: 44 ) .matchedGeometryEffect(id: "visWave", in: namespace) - .frame(width: 64, height: 32) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } else { - Button(action: { audioPlayer.togglePlayPause() }) { - Image(systemName: audioPlayer.isPlaying ? "pause.fill" : "play.fill") - .font(.system(size: 16)) - .foregroundColor(.white) - } - .frame(width: 40, height: 32) - } - - Spacer().frame(width: 10) - } - .frame(height: 44) - .background( - RoundedRectangle(cornerRadius: 22, style: .continuous) - .fill( - LinearGradient( - colors: [Color.black, themeColor.opacity(0.25)], - startPoint: .leading, - endPoint: .trailing - ) + .frame(maxWidth: .infinity) + .frame(height: 44) + .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) + .background( + RoundedRectangle(cornerRadius: 22, style: .continuous) + .fill(Color.black.opacity(0.7)) ) - .shadow(color: themeColor.opacity(0.4), radius: 8, y: 2) - ) - .padding(.horizontal, 56) - .contentShape(RoundedRectangle(cornerRadius: 22)) - .onTapGesture { - withAnimation(.easeInOut(duration: 0.35)) { - showNowPlaying = true - isDynamicIsland = false + } else { + Spacer() + } + + // ── Play/pause circle button — matches debug PiP circle style ─ + Button(action: { audioPlayer.togglePlayPause() }) { + Image(systemName: audioPlayer.isPlaying ? "pause.fill" : "play.fill") + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(.white) + .frame(width: 44, height: 44) + .background( + Circle() + .fill(Color.black.opacity(0.85)) + .shadow(color: themeColor.opacity(0.3), radius: 6, y: 2) + ) } } + .padding(.leading, 16) + .padding(.trailing, 16) .gesture( DragGesture(minimumDistance: 8) .onEnded { value in if value.translation.height > 25 { + wasInDynamicIsland = false withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { isDynamicIsland = false } } } ) - + Spacer() } // Status bar stays visible — no .statusBarHidden