All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
91 lines
3.6 KiB
Swift
91 lines
3.6 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Root tab container with persistent mini-player overlay
|
|
|
|
struct RootTabView: View {
|
|
@EnvironmentObject var authStore: AuthStore
|
|
@EnvironmentObject var audioPlayer: AudioPlayerService
|
|
|
|
@State private var selectedTab: Tab = .home
|
|
@State private var showFullPlayer: Bool = false
|
|
@State private var readerIsActive: Bool = false
|
|
@State private var fullPlayerDragOffset: CGFloat = 0
|
|
|
|
enum Tab: Hashable {
|
|
case home, library, browse, search, profile
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .bottom) {
|
|
TabView(selection: $selectedTab) {
|
|
HomeView()
|
|
.tabItem { Label("Home", systemImage: "house.fill") }
|
|
.tag(Tab.home)
|
|
|
|
LibraryView()
|
|
.tabItem { Label("Library", systemImage: "book.pages.fill") }
|
|
.tag(Tab.library)
|
|
|
|
BrowseView()
|
|
.tabItem { Label("Discover", systemImage: "sparkles") }
|
|
.tag(Tab.browse)
|
|
|
|
SearchView()
|
|
.tabItem { Label("Search", systemImage: "magnifyingglass") }
|
|
.tag(Tab.search)
|
|
|
|
ProfileView()
|
|
.tabItem { Label("Profile", systemImage: "person.fill") }
|
|
.tag(Tab.profile)
|
|
}
|
|
|
|
// Mini player bar — sits above the tab bar
|
|
if audioPlayer.isActive && !showFullPlayer && !readerIsActive {
|
|
MiniPlayerBar(showFullPlayer: $showFullPlayer)
|
|
.padding(.bottom, 49)
|
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
|
.animation(.spring(response: 0.35, dampingFraction: 0.8), value: audioPlayer.isActive)
|
|
}
|
|
|
|
// Full player — slides up from the bottom
|
|
if showFullPlayer {
|
|
FullPlayerView(onDismiss: {
|
|
withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
|
|
showFullPlayer = false
|
|
fullPlayerDragOffset = 0
|
|
}
|
|
})
|
|
.offset(y: max(fullPlayerDragOffset, 0))
|
|
.gesture(
|
|
DragGesture(minimumDistance: 10)
|
|
.onChanged { value in
|
|
if value.translation.height > 0 {
|
|
fullPlayerDragOffset = value.translation.height
|
|
}
|
|
}
|
|
.onEnded { value in
|
|
let velocity = value.predictedEndTranslation.height - value.translation.height
|
|
if value.translation.height > 120 || velocity > 400 {
|
|
withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
|
|
showFullPlayer = false
|
|
fullPlayerDragOffset = 0
|
|
}
|
|
} else {
|
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
|
|
fullPlayerDragOffset = 0
|
|
}
|
|
}
|
|
}
|
|
)
|
|
.transition(.move(edge: .bottom))
|
|
.animation(.spring(response: 0.45, dampingFraction: 0.85), value: showFullPlayer)
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
.animation(.spring(response: 0.45, dampingFraction: 0.85), value: showFullPlayer)
|
|
.onPreferenceChange(HideMiniPlayerKey.self) { hide in
|
|
readerIsActive = hide
|
|
}
|
|
}
|
|
}
|