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.
170 lines
5.2 KiB
Swift
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
|
|
)
|
|
)
|
|
}
|
|
}
|