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.
127 lines
4.9 KiB
Swift
127 lines
4.9 KiB
Swift
import SwiftUI
|
|
|
|
enum TvColors {
|
|
static let bg = Color(red: 0.02, green: 0.02, blue: 0.025)
|
|
static let bgElevated = Color(red: 0.06, green: 0.06, blue: 0.07)
|
|
static let surface = Color(red: 0.11, green: 0.11, blue: 0.125)
|
|
static let surfaceSoft = Color(red: 0.16, green: 0.16, blue: 0.18)
|
|
/// Netflix-red #E50914
|
|
static let accent = Color(red: 0.898, green: 0.035, blue: 0.078)
|
|
static let accentSoft = Color(red: 0.898, green: 0.035, blue: 0.078).opacity(0.28)
|
|
static let text = Color(red: 0.97, green: 0.97, blue: 0.98)
|
|
static let textDim = Color(red: 0.62, green: 0.62, blue: 0.68)
|
|
static let danger = Color(red: 1.0, green: 0.42, blue: 0.42)
|
|
static let success = Color(red: 0.275, green: 0.827, blue: 0.412)
|
|
}
|
|
|
|
enum TvFonts {
|
|
static let brand = Font.system(size: 72, weight: .black, design: .rounded)
|
|
static let hero = Font.system(size: 56, weight: .black, design: .rounded)
|
|
static let title = Font.system(size: 42, weight: .bold, design: .rounded)
|
|
static let headline = Font.system(size: 28, weight: .semibold, design: .rounded)
|
|
static let body = Font.system(size: 24, weight: .regular, design: .rounded)
|
|
static let caption = Font.system(size: 20, weight: .medium, design: .rounded)
|
|
static let nav = Font.system(size: 22, weight: .semibold, design: .rounded)
|
|
static let code = Font.system(size: 64, weight: .bold, design: .monospaced)
|
|
}
|
|
|
|
struct FocusableCardStyle: ButtonStyle {
|
|
var scale: CGFloat = 1.08
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
FocusableCardLabel(configuration: configuration, scale: scale)
|
|
}
|
|
}
|
|
|
|
private struct FocusableCardLabel: View {
|
|
let configuration: ButtonStyleConfiguration
|
|
let scale: CGFloat
|
|
@Environment(\.isFocused) private var isFocused
|
|
|
|
var body: some View {
|
|
configuration.label
|
|
.scaleEffect(isFocused ? scale : (configuration.isPressed ? 0.98 : 1.0))
|
|
.shadow(color: isFocused ? TvColors.accent.opacity(0.55) : .clear, radius: isFocused ? 28 : 0, y: 10)
|
|
.animation(.easeOut(duration: 0.18), value: isFocused)
|
|
.animation(.easeOut(duration: 0.12), value: configuration.isPressed)
|
|
}
|
|
}
|
|
|
|
/// Top-nav: duidelijke focus-ring, aparte “actieve tab”-styling.
|
|
struct NavTabButtonStyle: ButtonStyle {
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
NavTabButtonLabel(configuration: configuration)
|
|
}
|
|
}
|
|
|
|
private struct NavTabButtonLabel: View {
|
|
let configuration: ButtonStyleConfiguration
|
|
@Environment(\.isFocused) private var isFocused
|
|
|
|
var body: some View {
|
|
configuration.label
|
|
.scaleEffect(isFocused ? 1.06 : (configuration.isPressed ? 0.97 : 1.0))
|
|
.shadow(color: isFocused ? Color.white.opacity(0.35) : .clear, radius: isFocused ? 16 : 0, y: 0)
|
|
.animation(.easeOut(duration: 0.15), value: isFocused)
|
|
.animation(.easeOut(duration: 0.1), value: configuration.isPressed)
|
|
}
|
|
}
|
|
|
|
struct TvPrimaryButtonStyle: ButtonStyle {
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
TvPrimaryButtonLabel(configuration: configuration)
|
|
}
|
|
}
|
|
|
|
private struct TvPrimaryButtonLabel: View {
|
|
let configuration: ButtonStyleConfiguration
|
|
@Environment(\.isFocused) private var isFocused
|
|
|
|
var body: some View {
|
|
configuration.label
|
|
.font(TvFonts.headline)
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 36)
|
|
.padding(.vertical, 18)
|
|
.background(
|
|
LinearGradient(
|
|
colors: [TvColors.accent, TvColors.accent.opacity(0.82)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
in: Capsule()
|
|
)
|
|
.scaleEffect(isFocused ? 1.06 : (configuration.isPressed ? 0.97 : 1))
|
|
.shadow(color: TvColors.accent.opacity(isFocused ? 0.55 : 0.28), radius: isFocused ? 22 : 10, y: 6)
|
|
.animation(.easeOut(duration: 0.18), value: isFocused)
|
|
}
|
|
}
|
|
|
|
struct TvSecondaryButtonStyle: ButtonStyle {
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
TvSecondaryButtonLabel(configuration: configuration)
|
|
}
|
|
}
|
|
|
|
private struct TvSecondaryButtonLabel: View {
|
|
let configuration: ButtonStyleConfiguration
|
|
@Environment(\.isFocused) private var isFocused
|
|
|
|
var body: some View {
|
|
configuration.label
|
|
.font(TvFonts.headline)
|
|
.foregroundStyle(TvColors.text)
|
|
.padding(.horizontal, 32)
|
|
.padding(.vertical, 16)
|
|
.background(
|
|
.ultraThinMaterial,
|
|
in: Capsule()
|
|
)
|
|
.overlay {
|
|
Capsule()
|
|
.strokeBorder(isFocused ? TvColors.accent.opacity(0.7) : Color.white.opacity(0.12), lineWidth: isFocused ? 2 : 1)
|
|
}
|
|
.scaleEffect(isFocused ? 1.05 : 1)
|
|
.animation(.easeOut(duration: 0.18), value: isFocused)
|
|
}
|
|
}
|