stremio/apps/ios/VMCTV/UI/TopNav.swift
Jos Vooges | STH ac951617cb Add Live Events across API/admin/Android TV, mobile ClearKey/DASH fixes, and tvOS app.
Wire FAAB schedule-events into master-api and admin IPTV settings, add Android TV Events UI, fix mobile ClearKey/stream playback (DASH + bump 1.0.23), and scaffold the Apple TV (VMCTV) client with docs/scripts.
2026-09-18 23:34:42 +02:00

170 lines
5.2 KiB
Swift

import SwiftUI
struct TopNav: View {
@EnvironmentObject private var app: AppModel
@State private var now = Date()
private let clockTimer = Timer.publish(every: 30, on: .main, in: .common).autoconnect()
var body: some View {
HStack(spacing: 8) {
BrandLogo(size: 34)
.padding(.trailing, 20)
HStack(spacing: 10) {
ForEach(visibleTabs) { tab in
if tab == .search || tab == .settings {
navIconButton(tab)
} else {
navTextButton(tab)
}
}
}
.focusSection()
Spacer(minLength: 16)
Text(timeString)
.font(.system(size: 22, weight: .semibold, design: .rounded))
.foregroundStyle(TvColors.textDim)
.monospacedDigit()
.padding(.trailing, 12)
Button {
app.tab = .settings
} label: {
FocusAwareProfileBadge(avatarId: app.store.profileAvatarId)
}
.buttonStyle(NavTabButtonStyle())
}
.padding(.horizontal, 48)
.padding(.vertical, 18)
.background {
Rectangle()
.fill(.ultraThinMaterial)
.overlay(
LinearGradient(
colors: [Color.black.opacity(0.55), Color.black.opacity(0.15)],
startPoint: .top,
endPoint: .bottom
)
)
.ignoresSafeArea(edges: .top)
}
.onReceive(clockTimer) { now = $0 }
}
private var timeString: String {
let f = DateFormatter()
f.locale = Locale(identifier: "nl_NL")
f.dateFormat = "HH:mm"
return f.string(from: now)
}
private var visibleTabs: [AppModel.MainTab] {
AppModel.MainTab.allCases.filter { tab in
if tab == .tv { return app.iptvEnabled }
return true
}
}
private func navTextButton(_ tab: AppModel.MainTab) -> some View {
Button {
withAnimation(.easeOut(duration: 0.2)) { app.tab = tab }
} label: {
FocusAwareNavLabel(title: tab.title, selected: app.tab == tab)
}
.buttonStyle(NavTabButtonStyle())
}
private func navIconButton(_ tab: AppModel.MainTab) -> some View {
Button {
withAnimation(.easeOut(duration: 0.2)) { app.tab = tab }
} label: {
FocusAwareNavIcon(systemName: tab.icon, selected: app.tab == tab)
}
.buttonStyle(NavTabButtonStyle())
.accessibilityLabel(tab.title)
}
}
private struct FocusAwareNavLabel: View {
let title: String
let selected: Bool
@Environment(\.isFocused) private var isFocused
var body: some View {
Text(title)
.font(TvFonts.nav)
.foregroundStyle(isFocused || selected ? TvColors.text : TvColors.textDim)
.padding(.horizontal, 20)
.padding(.vertical, 12)
.background(
Capsule().fill(backgroundFill)
)
.overlay {
Capsule()
.strokeBorder(borderColor, lineWidth: isFocused ? 3 : (selected ? 1.5 : 0))
}
}
private var backgroundFill: Color {
if isFocused { return Color.white.opacity(0.18) }
if selected { return TvColors.accent.opacity(0.28) }
return .clear
}
private var borderColor: Color {
if isFocused { return Color.white }
if selected { return TvColors.accent.opacity(0.7) }
return .clear
}
}
private struct FocusAwareNavIcon: View {
let systemName: String
let selected: Bool
@Environment(\.isFocused) private var isFocused
var body: some View {
Image(systemName: systemName)
.font(.system(size: 24, weight: .semibold))
.foregroundStyle(isFocused || selected ? Color.white : TvColors.textDim)
.frame(width: 54, height: 54)
.background(Circle().fill(backgroundFill))
.overlay {
Circle()
.strokeBorder(borderColor, lineWidth: isFocused ? 3 : (selected ? 1.5 : 0))
}
}
private var backgroundFill: Color {
if isFocused { return Color.white.opacity(0.18) }
if selected { return TvColors.accent.opacity(0.28) }
return .clear
}
private var borderColor: Color {
if isFocused { return Color.white }
if selected { return TvColors.accent.opacity(0.7) }
return .clear
}
}
private struct FocusAwareProfileBadge: View {
let avatarId: String?
@Environment(\.isFocused) private var isFocused
var body: some View {
Text(ProfileAvatars.emoji(for: avatarId))
.font(.system(size: 32))
.frame(width: 56, height: 56)
.background(.ultraThinMaterial, in: Circle())
.overlay(
Circle().strokeBorder(
isFocused ? Color.white : TvColors.accent.opacity(0.45),
lineWidth: isFocused ? 3 : 1.5
)
)
}
}