Files
stemify/ios/Stemify/UI/RootView.swift
T
cdnutterandClaude Opus 5.5 2ddfe051d8 iOS 1.0.1: mini player, Now Playing spacing, play spinner fix
- Mini player floats as a solid card and every screen leaves room for it,
  so it no longer covers the bottom of lists
- More space between title and scrubber; panel switches animate
- Play button no longer spins forever after changing tracks: the engine
  now always reports buffering on load
- Springier button presses and play/pause symbol transitions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:50:42 -07:00

87 lines
3.3 KiB
Swift

import SwiftUI
/// The tab bar, a navigation stack per tab, the mini player and the Now Playing sheet.
struct RootView: View {
@State private var ui = Ui(overlays: Overlays())
private var g: AppGraph { .shared }
var body: some View {
let s = g.settings
let p = Palette.of(s.theme, s.accent)
TabView(selection: Binding(get: { ui.tab }, set: { ui.selectTab($0) })) {
ForEach(Tab.allCases, id: \.self) { t in
NavigationStack(path: ui.path(t)) {
root(t).miniPlayerRoom()
.navigationDestination(for: Route.self) { destination($0).miniPlayerRoom() }
}
// Floats over the screens; each screen leaves room for it so nothing ends up underneath.
.overlay(alignment: .bottom) {
if g.playback.current != nil { MiniPlayer().transition(.move(edge: .bottom).combined(with: .opacity)) }
}
.animation(.spring(duration: 0.35), value: g.playback.current != nil)
.tabItem { Label(t.rawValue, systemImage: t.icon) }
.tag(t)
}
}
.sheet(isPresented: Binding(get: { ui.nowPlayingOpen }, set: { ui.nowPlayingOpen = $0 })) {
NowPlayingScreen()
.overlayHost(ui.overlays, level: 1)
.environment(ui)
.environment(\.palette, p)
.preferredColorScheme(s.theme == .White ? .light : .dark)
.tint(p.accent)
}
.overlayHost(ui.overlays, level: 0)
.onChange(of: g.playback.message) { _, m in
if let m { ui.overlays.hud(m.text, "info.circle") }
}
.environment(ui)
.environment(\.palette, p)
.preferredColorScheme(s.theme == .White ? .light : .dark)
.tint(p.accent)
}
@ViewBuilder private func root(_ t: Tab) -> some View {
switch t {
case .Library: LibraryHome()
case .Playlists: PlaylistsScreen()
case .Search: SearchScreen()
case .Downloads: DownloadsScreen()
case .Settings: SettingsScreen()
}
}
@ViewBuilder private func destination(_ r: Route) -> some View {
switch r {
case .Artists: ArtistsScreen()
case .Albums: AlbumsScreen()
case .Songs: SongsScreen()
case .Playlists: PlaylistsScreen()
case .Eq: EqScreen()
case .EqFilters(let id): EqFiltersScreen(profileId: id)
case .EqFilterPage(let id, let f): EqFilterPage(profileId: id, filterId: f)
case .PlexAccount: PlexAccountScreen()
case .StemServer: StemServerScreen()
case .Quality: QualityScreen()
case .Storage: StorageScreen()
case .Appearance: AppearanceScreen()
case .ArtistPage(let a): ArtistScreen(artist: a)
case .AlbumPage(let a): AlbumScreen(album: a)
case .PlaylistPage(let pl): PlaylistScreen(playlist: pl)
}
}
}
private struct MiniPlayerRoom: ViewModifier {
private var g: AppGraph { .shared }
func body(content: Content) -> some View {
content.safeAreaInset(edge: .bottom, spacing: 0) {
if g.playback.current != nil { Color.clear.frame(height: 66) }
}
}
}
private extension View {
func miniPlayerRoom() -> some View { modifier(MiniPlayerRoom()) }
}