134 lines
5.1 KiB
Swift
134 lines
5.1 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.layout(s.tabLayout), id: \.self) { t in
|
|
NavigationStack(path: ui.path(t)) {
|
|
root(t).miniPlayerRoom()
|
|
.navigationDestination(for: Route.self) { destination($0).miniPlayerRoom() }
|
|
}
|
|
.modifier(FloatingMiniPlayer())
|
|
.tabItem { Label(t.rawValue, systemImage: t.icon) }
|
|
.tag(t)
|
|
}
|
|
}
|
|
.modifier(GlassMiniPlayerHost())
|
|
.onChange(of: s.tabLayout) { _, _ in ui.ensureValidTab() }
|
|
.fullScreenCover(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 .Songs: SongsScreen()
|
|
case .Albums: AlbumsScreen()
|
|
case .Artists: ArtistsScreen()
|
|
case .Genres: GenresScreen()
|
|
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 .RecentlyAdded: SongsScreen(recent: true)
|
|
case .Playlists: PlaylistsScreen()
|
|
case .Genres: GenresScreen()
|
|
case .Downloads: DownloadsScreen()
|
|
case .DownloadQueue: DownloadQueueScreen()
|
|
case .TabBar: TabBarScreen()
|
|
case .WebRadio: WebRadioScreen()
|
|
case .GenrePage(let g): GenreScreen(genre: g)
|
|
case .Eq: EqScreen()
|
|
case .EqFilters(let id): EqFiltersScreen(profileId: id)
|
|
case .EqFilterPage(let id, let f): EqFilterPage(profileId: id, filterId: f)
|
|
case .Servers: ServersScreen()
|
|
case .ServerList: ServerListScreen()
|
|
case .Server(let id): ServerScreen(id: id)
|
|
case .StemServer: StemServerScreen()
|
|
case .Quality: QualityScreen()
|
|
case .Storage: StorageScreen()
|
|
case .Appearance: AppearanceScreen()
|
|
case .NowPlayingOptions: NowPlayingOptionsScreen()
|
|
case .StemEditor: StemEditorScreen()
|
|
case .About: AboutScreen()
|
|
case .ArtistPage(let a): ArtistScreen(artist: a)
|
|
case .AlbumPage(let a): AlbumScreen(album: a)
|
|
case .PlaylistPage(let pl): PlaylistScreen(playlist: pl)
|
|
case .StemPlaylistPage(let id): StemPlaylistScreen(id: id)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// iOS 26.1 and up put the mini player in the tab bar's glass accessory; older ones float a card over each tab.
|
|
private let glassAccessory: Bool = { if #available(iOS 26.1, *) { return true } else { return false } }()
|
|
|
|
private struct GlassMiniPlayerHost: ViewModifier {
|
|
private var g: AppGraph { .shared }
|
|
func body(content: Content) -> some View {
|
|
if #available(iOS 26.1, *) {
|
|
content
|
|
.tabViewBottomAccessory(isEnabled: g.playback.current != nil) { MiniPlayer() }
|
|
.tabBarMinimizeBehavior(.onScrollDown)
|
|
} else {
|
|
content
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Floats over the screens; each screen leaves room for it so nothing ends up underneath.
|
|
private struct FloatingMiniPlayer: ViewModifier {
|
|
private var g: AppGraph { .shared }
|
|
func body(content: Content) -> some View {
|
|
if glassAccessory {
|
|
content
|
|
} else {
|
|
content
|
|
.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)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct MiniPlayerRoom: ViewModifier {
|
|
private var g: AppGraph { .shared }
|
|
func body(content: Content) -> some View {
|
|
content.safeAreaInset(edge: .bottom, spacing: 0) {
|
|
if !glassAccessory && g.playback.current != nil { Color.clear.frame(height: 66) }
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension View {
|
|
func miniPlayerRoom() -> some View { modifier(MiniPlayerRoom()) }
|
|
}
|