mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
487
Yattee/Models/PlayerControls/GlobalLayoutSettings.swift
Normal file
487
Yattee/Models/PlayerControls/GlobalLayoutSettings.swift
Normal file
@@ -0,0 +1,487 @@
|
||||
//
|
||||
// GlobalLayoutSettings.swift
|
||||
// Yattee
|
||||
//
|
||||
// Global settings that apply to all control buttons in the player layout.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Theme options for player controls appearance.
|
||||
enum ControlsTheme: String, Codable, Hashable, Sendable, CaseIterable {
|
||||
/// Follow system appearance
|
||||
case system
|
||||
|
||||
/// Always use light appearance
|
||||
case light
|
||||
|
||||
/// Always use dark appearance
|
||||
case dark
|
||||
|
||||
/// Localized display name.
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .system:
|
||||
return String(localized: "controls.theme.system")
|
||||
case .light:
|
||||
return String(localized: "controls.theme.light")
|
||||
case .dark:
|
||||
return String(localized: "controls.theme.dark")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ColorScheme to apply, or nil for system.
|
||||
var colorScheme: ColorScheme? {
|
||||
switch self {
|
||||
case .system:
|
||||
return nil
|
||||
case .light:
|
||||
return .light
|
||||
case .dark:
|
||||
return .dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Size options for control buttons.
|
||||
enum ButtonSize: String, Codable, Hashable, Sendable, CaseIterable {
|
||||
case small
|
||||
case medium
|
||||
case large
|
||||
|
||||
/// The point size for buttons at this size.
|
||||
var pointSize: CGFloat {
|
||||
switch self {
|
||||
case .small:
|
||||
return 36
|
||||
case .medium:
|
||||
return 44
|
||||
case .large:
|
||||
return 52
|
||||
}
|
||||
}
|
||||
|
||||
/// The icon size relative to button size.
|
||||
var iconSize: CGFloat {
|
||||
switch self {
|
||||
case .small:
|
||||
return 18
|
||||
case .medium:
|
||||
return 22
|
||||
case .large:
|
||||
return 26
|
||||
}
|
||||
}
|
||||
|
||||
/// Localized display name.
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .small:
|
||||
return String(localized: "controls.size.small")
|
||||
case .medium:
|
||||
return String(localized: "controls.size.medium")
|
||||
case .large:
|
||||
return String(localized: "controls.size.large")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background style options for control buttons.
|
||||
enum ButtonBackgroundStyle: String, Codable, Hashable, Sendable, CaseIterable {
|
||||
/// No background (default, current behavior)
|
||||
case none
|
||||
|
||||
/// Clear glass - subtle circular glass background
|
||||
case clearGlass
|
||||
|
||||
/// Regular glass - more visible circular glass background
|
||||
case regularGlass
|
||||
|
||||
/// Localized display name.
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return String(localized: "controls.buttonBackground.none")
|
||||
case .clearGlass:
|
||||
return String(localized: "controls.buttonBackground.clearGlass")
|
||||
case .regularGlass:
|
||||
return String(localized: "controls.buttonBackground.regularGlass")
|
||||
}
|
||||
}
|
||||
|
||||
/// The GlassStyle to use for this background style.
|
||||
var glassStyle: GlassStyle? {
|
||||
switch self {
|
||||
case .none:
|
||||
return nil
|
||||
case .clearGlass:
|
||||
return .clear
|
||||
case .regularGlass:
|
||||
return .regular
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overall style for player controls appearance.
|
||||
enum ControlsStyle: String, Codable, Hashable, Sendable, CaseIterable {
|
||||
/// No background, follows system theme
|
||||
case plain
|
||||
|
||||
/// Regular glass background, always dark theme
|
||||
case glass
|
||||
|
||||
/// Localized display name.
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .plain:
|
||||
return String(localized: "controls.style.plain")
|
||||
case .glass:
|
||||
return String(localized: "controls.style.glass")
|
||||
}
|
||||
}
|
||||
|
||||
/// The theme to apply for this style.
|
||||
var theme: ControlsTheme {
|
||||
switch self {
|
||||
case .plain:
|
||||
return .system
|
||||
case .glass:
|
||||
return .dark
|
||||
}
|
||||
}
|
||||
|
||||
/// The button background to apply for this style.
|
||||
var buttonBackground: ButtonBackgroundStyle {
|
||||
switch self {
|
||||
case .plain:
|
||||
return .none
|
||||
case .glass:
|
||||
return .regularGlass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable Color
|
||||
|
||||
/// A color stored as RGBA components for Codable support.
|
||||
struct CodableColor: Codable, Hashable, Sendable {
|
||||
var red: Double
|
||||
var green: Double
|
||||
var blue: Double
|
||||
var opacity: Double
|
||||
|
||||
/// Creates a CodableColor from RGBA components.
|
||||
/// - Parameters:
|
||||
/// - red: Red component (0-1).
|
||||
/// - green: Green component (0-1).
|
||||
/// - blue: Blue component (0-1).
|
||||
/// - opacity: Opacity component (0-1). Defaults to 1.
|
||||
init(red: Double, green: Double, blue: Double, opacity: Double = 1.0) {
|
||||
self.red = red
|
||||
self.green = green
|
||||
self.blue = blue
|
||||
self.opacity = opacity
|
||||
}
|
||||
|
||||
/// Creates a CodableColor from a SwiftUI Color.
|
||||
/// - Parameter color: The SwiftUI Color to convert.
|
||||
init(_ color: Color) {
|
||||
let resolved = color.resolve(in: EnvironmentValues())
|
||||
self.red = Double(resolved.red)
|
||||
self.green = Double(resolved.green)
|
||||
self.blue = Double(resolved.blue)
|
||||
self.opacity = Double(resolved.opacity)
|
||||
}
|
||||
|
||||
/// Converts to a SwiftUI Color.
|
||||
var color: Color {
|
||||
Color(red: red, green: green, blue: blue, opacity: opacity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SponsorBlock Segment Settings
|
||||
|
||||
/// Settings for a single SponsorBlock category's appearance on the progress bar.
|
||||
struct SponsorBlockCategorySettings: Codable, Hashable, Sendable {
|
||||
/// Whether this category is visible on the progress bar.
|
||||
var isVisible: Bool
|
||||
|
||||
/// The color to use for this category's segments.
|
||||
var color: CodableColor
|
||||
|
||||
/// Creates category settings.
|
||||
/// - Parameters:
|
||||
/// - isVisible: Whether the category is visible. Defaults to `true`.
|
||||
/// - color: The color for segments. Defaults to gray.
|
||||
init(isVisible: Bool = true, color: CodableColor = CodableColor(red: 0.5, green: 0.5, blue: 0.5)) {
|
||||
self.isVisible = isVisible
|
||||
self.color = color
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings for SponsorBlock segment display on the progress bar.
|
||||
struct SponsorBlockSegmentSettings: Codable, Hashable, Sendable {
|
||||
/// Global toggle for showing sponsor segments on the progress bar.
|
||||
var showSegments: Bool
|
||||
|
||||
/// Per-category settings, keyed by SponsorBlockCategory raw value.
|
||||
var categorySettings: [String: SponsorBlockCategorySettings]
|
||||
|
||||
/// Creates sponsor block segment settings.
|
||||
/// - Parameters:
|
||||
/// - showSegments: Whether to show segments. Defaults to `true`.
|
||||
/// - categorySettings: Per-category settings. Defaults to default colors for all categories.
|
||||
init(
|
||||
showSegments: Bool = true,
|
||||
categorySettings: [String: SponsorBlockCategorySettings]? = nil
|
||||
) {
|
||||
self.showSegments = showSegments
|
||||
self.categorySettings = categorySettings ?? Self.defaultCategorySettings
|
||||
}
|
||||
|
||||
/// Default settings with all categories visible using their default overlay colors.
|
||||
static let `default` = SponsorBlockSegmentSettings()
|
||||
|
||||
/// Default category settings using default colors for each category.
|
||||
/// Colors match the SponsorBlockCategory.overlayColor values.
|
||||
static var defaultCategorySettings: [String: SponsorBlockCategorySettings] {
|
||||
[
|
||||
"sponsor": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.green)),
|
||||
"selfpromo": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.yellow)),
|
||||
"interaction": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.purple)),
|
||||
"intro": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.cyan)),
|
||||
"outro": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.blue)),
|
||||
"preview": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.indigo)),
|
||||
"music_offtopic": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.orange)),
|
||||
"filler": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.gray)),
|
||||
"poi_highlight": SponsorBlockCategorySettings(isVisible: true, color: CodableColor(.pink)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Gets settings for a specific category key, returning defaults if not found.
|
||||
/// - Parameter categoryKey: The category raw value key.
|
||||
/// - Returns: The category settings.
|
||||
func settings(forKey categoryKey: String) -> SponsorBlockCategorySettings {
|
||||
categorySettings[categoryKey] ?? Self.defaultCategorySettings[categoryKey]
|
||||
?? SponsorBlockCategorySettings()
|
||||
}
|
||||
|
||||
/// Returns a copy with updated settings for a specific category key.
|
||||
/// - Parameters:
|
||||
/// - categoryKey: The category raw value key.
|
||||
/// - settings: The new settings.
|
||||
/// - Returns: Updated SponsorBlockSegmentSettings.
|
||||
func withUpdatedSettings(
|
||||
forKey categoryKey: String,
|
||||
_ settings: SponsorBlockCategorySettings
|
||||
) -> SponsorBlockSegmentSettings {
|
||||
var newCategorySettings = categorySettings
|
||||
newCategorySettings[categoryKey] = settings
|
||||
return SponsorBlockSegmentSettings(
|
||||
showSegments: showSegments,
|
||||
categorySettings: newCategorySettings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SponsorBlockCategory Integration
|
||||
|
||||
extension SponsorBlockSegmentSettings {
|
||||
/// Gets settings for a specific category, returning defaults if not found.
|
||||
/// - Parameter category: The category to get settings for.
|
||||
/// - Returns: The category settings.
|
||||
func settings(for category: SponsorBlockCategory) -> SponsorBlockCategorySettings {
|
||||
settings(forKey: category.rawValue)
|
||||
}
|
||||
|
||||
/// Returns a copy with updated settings for a specific category.
|
||||
/// - Parameters:
|
||||
/// - category: The category to update.
|
||||
/// - settings: The new settings.
|
||||
/// - Returns: Updated SponsorBlockSegmentSettings.
|
||||
func withUpdatedSettings(
|
||||
for category: SponsorBlockCategory,
|
||||
_ settings: SponsorBlockCategorySettings
|
||||
) -> SponsorBlockSegmentSettings {
|
||||
withUpdatedSettings(forKey: category.rawValue, settings)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Font Style
|
||||
|
||||
/// Font style options for player controls text.
|
||||
enum ControlsFontStyle: String, Codable, Hashable, Sendable, CaseIterable {
|
||||
/// System default font with monospaced digits.
|
||||
case system
|
||||
|
||||
/// Fully monospaced font.
|
||||
case monospaced
|
||||
|
||||
/// Rounded system font with monospaced digits.
|
||||
case rounded
|
||||
|
||||
/// Localized display name.
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .system:
|
||||
return String(localized: "controls.fontStyle.system")
|
||||
case .monospaced:
|
||||
return String(localized: "controls.fontStyle.monospaced")
|
||||
case .rounded:
|
||||
return String(localized: "controls.fontStyle.rounded")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Font for the given text style.
|
||||
func font(_ style: Font.TextStyle = .caption) -> Font {
|
||||
switch self {
|
||||
case .system:
|
||||
return .system(style).monospacedDigit()
|
||||
case .monospaced:
|
||||
return .system(style, design: .monospaced)
|
||||
case .rounded:
|
||||
return .system(style, design: .rounded).monospacedDigit()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Font for the given size and weight.
|
||||
func font(size: CGFloat, weight: Font.Weight = .regular) -> Font {
|
||||
switch self {
|
||||
case .system:
|
||||
return .system(size: size, weight: weight).monospacedDigit()
|
||||
case .monospaced:
|
||||
return .system(size: size, weight: weight, design: .monospaced)
|
||||
case .rounded:
|
||||
return .system(size: size, weight: weight, design: .rounded).monospacedDigit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings for the progress bar appearance.
|
||||
struct ProgressBarSettings: Codable, Hashable, Sendable {
|
||||
/// Color for the played portion of the progress bar.
|
||||
var playedColor: CodableColor
|
||||
|
||||
/// Whether to show chapter markers on the progress bar.
|
||||
var showChapters: Bool
|
||||
|
||||
/// Settings for SponsorBlock segment display.
|
||||
var sponsorBlockSettings: SponsorBlockSegmentSettings
|
||||
|
||||
/// Creates progress bar settings.
|
||||
/// - Parameters:
|
||||
/// - playedColor: Color for the played portion. Defaults to red.
|
||||
/// - showChapters: Whether to show chapter markers. Defaults to `true`.
|
||||
/// - sponsorBlockSettings: Settings for SponsorBlock segments. Defaults to `.default`.
|
||||
init(
|
||||
playedColor: CodableColor = CodableColor(.red),
|
||||
showChapters: Bool = true,
|
||||
sponsorBlockSettings: SponsorBlockSegmentSettings = .default
|
||||
) {
|
||||
self.playedColor = playedColor
|
||||
self.showChapters = showChapters
|
||||
self.sponsorBlockSettings = sponsorBlockSettings
|
||||
}
|
||||
|
||||
/// Default progress bar settings.
|
||||
static let `default` = ProgressBarSettings()
|
||||
}
|
||||
|
||||
/// Global settings that apply to all control buttons.
|
||||
struct GlobalLayoutSettings: Codable, Hashable, Sendable {
|
||||
/// Overall style for player controls.
|
||||
var style: ControlsStyle
|
||||
|
||||
/// Size of control buttons.
|
||||
var buttonSize: ButtonSize
|
||||
|
||||
/// Font style for text elements in player controls.
|
||||
var fontStyle: ControlsFontStyle
|
||||
|
||||
/// Opacity of the controls background fade (0-1).
|
||||
var controlsFadeOpacity: Double
|
||||
|
||||
/// Mode for system control buttons (Control Center, Lock Screen).
|
||||
var systemControlsMode: SystemControlsMode
|
||||
|
||||
/// Duration for seek operations when systemControlsMode is .seek.
|
||||
var systemControlsSeekDuration: SystemControlsSeekDuration
|
||||
|
||||
/// How volume is controlled during playback.
|
||||
var volumeMode: VolumeMode
|
||||
|
||||
/// Theme derived from style (for backwards compatibility).
|
||||
var theme: ControlsTheme { style.theme }
|
||||
|
||||
/// Button background derived from style (for backwards compatibility).
|
||||
var buttonBackground: ButtonBackgroundStyle { style.buttonBackground }
|
||||
|
||||
/// Cached settings for instant access (avoids flash on view recreation).
|
||||
/// Updated whenever settings are loaded from the active preset.
|
||||
nonisolated(unsafe) static var cached: GlobalLayoutSettings = .default
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case style
|
||||
case buttonSize
|
||||
case buttonBackground // Legacy, not used for encoding
|
||||
case theme // Legacy, not used for encoding
|
||||
case fontStyle
|
||||
case controlsFadeOpacity
|
||||
case systemControlsMode
|
||||
case systemControlsSeekDuration
|
||||
case volumeMode
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
style = try container.decodeIfPresent(ControlsStyle.self, forKey: .style) ?? .glass
|
||||
buttonSize = try container.decodeIfPresent(ButtonSize.self, forKey: .buttonSize) ?? .medium
|
||||
fontStyle = try container.decodeIfPresent(ControlsFontStyle.self, forKey: .fontStyle) ?? .system
|
||||
controlsFadeOpacity = try container.decodeIfPresent(Double.self, forKey: .controlsFadeOpacity) ?? 0.5
|
||||
systemControlsMode = try container.decodeIfPresent(SystemControlsMode.self, forKey: .systemControlsMode) ?? .seek
|
||||
systemControlsSeekDuration = try container.decodeIfPresent(SystemControlsSeekDuration.self, forKey: .systemControlsSeekDuration) ?? .tenSeconds
|
||||
volumeMode = try container.decodeIfPresent(VolumeMode.self, forKey: .volumeMode) ?? .mpv
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(style, forKey: .style)
|
||||
try container.encode(buttonSize, forKey: .buttonSize)
|
||||
try container.encode(fontStyle, forKey: .fontStyle)
|
||||
try container.encode(controlsFadeOpacity, forKey: .controlsFadeOpacity)
|
||||
try container.encode(systemControlsMode, forKey: .systemControlsMode)
|
||||
try container.encode(systemControlsSeekDuration, forKey: .systemControlsSeekDuration)
|
||||
try container.encode(volumeMode, forKey: .volumeMode)
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Creates global layout settings.
|
||||
init(
|
||||
style: ControlsStyle = .glass,
|
||||
buttonSize: ButtonSize = .medium,
|
||||
fontStyle: ControlsFontStyle = .system,
|
||||
controlsFadeOpacity: Double = 0.5,
|
||||
systemControlsMode: SystemControlsMode = .seek,
|
||||
systemControlsSeekDuration: SystemControlsSeekDuration = .tenSeconds,
|
||||
volumeMode: VolumeMode = .mpv
|
||||
) {
|
||||
self.style = style
|
||||
self.buttonSize = buttonSize
|
||||
self.fontStyle = fontStyle
|
||||
self.controlsFadeOpacity = controlsFadeOpacity
|
||||
self.systemControlsMode = systemControlsMode
|
||||
self.systemControlsSeekDuration = systemControlsSeekDuration
|
||||
self.volumeMode = volumeMode
|
||||
}
|
||||
|
||||
// MARK: - Defaults
|
||||
|
||||
/// Default global layout settings.
|
||||
static let `default` = GlobalLayoutSettings()
|
||||
}
|
||||
Reference in New Issue
Block a user