mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
276 lines
9.0 KiB
Swift
276 lines
9.0 KiB
Swift
//
|
|
// ButtonSettings.swift
|
|
// Yattee
|
|
//
|
|
// Settings types for configurable control buttons.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Spacer Settings
|
|
|
|
/// Configuration for spacer elements in the control layout.
|
|
struct SpacerSettings: Codable, Hashable, Sendable {
|
|
/// Whether the spacer is flexible (expands to fill available space).
|
|
var isFlexible: Bool
|
|
|
|
/// Fixed width in points when not flexible. Range: 4-100, step: 2.
|
|
var fixedWidth: Int
|
|
|
|
/// Default spacer settings (flexible).
|
|
init(isFlexible: Bool = true, fixedWidth: Int = 20) {
|
|
self.isFlexible = isFlexible
|
|
self.fixedWidth = Self.clampWidth(fixedWidth)
|
|
}
|
|
|
|
/// Clamps width to valid range (4-100) with 2pt step.
|
|
static func clampWidth(_ width: Int) -> Int {
|
|
let clamped = max(4, min(100, width))
|
|
return (clamped / 2) * 2
|
|
}
|
|
}
|
|
|
|
// MARK: - Slider Behavior
|
|
|
|
/// How a slider control (brightness/volume) behaves.
|
|
enum SliderBehavior: String, Codable, Hashable, Sendable, CaseIterable {
|
|
/// Button and slider are always visible together.
|
|
case alwaysVisible
|
|
|
|
/// Only button is visible; slider expands on tap.
|
|
case expandOnTap
|
|
|
|
/// Slider visible in landscape, expand on tap in portrait.
|
|
case autoExpandInLandscape
|
|
|
|
/// Localized display name.
|
|
var displayName: String {
|
|
switch self {
|
|
case .alwaysVisible:
|
|
return String(localized: "controls.slider.alwaysVisible")
|
|
case .expandOnTap:
|
|
return String(localized: "controls.slider.expandOnTap")
|
|
case .autoExpandInLandscape:
|
|
return String(localized: "controls.slider.autoExpandInLandscape")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration for slider buttons (brightness, volume).
|
|
struct SliderSettings: Codable, Hashable, Sendable {
|
|
/// How the slider behaves.
|
|
var sliderBehavior: SliderBehavior
|
|
|
|
/// Default slider settings (expand on tap).
|
|
init(sliderBehavior: SliderBehavior = .expandOnTap) {
|
|
self.sliderBehavior = sliderBehavior
|
|
}
|
|
}
|
|
|
|
// MARK: - Seek Direction
|
|
|
|
/// Direction for seek buttons in horizontal sections.
|
|
enum SeekDirection: String, Codable, Hashable, Sendable, CaseIterable {
|
|
case backward
|
|
case forward
|
|
|
|
/// Localized display name.
|
|
var displayName: String {
|
|
switch self {
|
|
case .backward:
|
|
return String(localized: "controls.seek.backward")
|
|
case .forward:
|
|
return String(localized: "controls.seek.forward")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Seek Settings
|
|
|
|
/// Configuration for seek buttons (backward/forward).
|
|
struct SeekSettings: Codable, Hashable, Sendable {
|
|
/// Number of seconds to seek. Default: 10.
|
|
var seconds: Int
|
|
|
|
/// Direction for horizontal section seek buttons. Default: .forward.
|
|
var direction: SeekDirection
|
|
|
|
/// Seek seconds that have dedicated SF Symbol icons.
|
|
private static let validSeekIconValues = [5, 10, 15, 30, 45, 60, 75, 90]
|
|
|
|
/// SF Symbol name for this seek button based on configured seconds and direction.
|
|
/// Uses numbered icon for standard values (5, 10, 15, 30, 45, 60, 75, 90),
|
|
/// plain arrow for other values.
|
|
var systemImage: String {
|
|
let arrowDirection = direction == .backward ? "counterclockwise" : "clockwise"
|
|
if Self.validSeekIconValues.contains(seconds) {
|
|
return "\(seconds).arrow.trianglehead.\(arrowDirection)"
|
|
}
|
|
return "arrow.trianglehead.\(arrowDirection)"
|
|
}
|
|
|
|
/// Default seek settings (10 seconds, forward direction).
|
|
init(seconds: Int = 10, direction: SeekDirection = .forward) {
|
|
self.seconds = max(1, seconds)
|
|
self.direction = direction
|
|
}
|
|
|
|
// MARK: - Codable (backward compatibility)
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case seconds
|
|
case direction
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
seconds = try container.decode(Int.self, forKey: .seconds)
|
|
// Default to .forward for backward compatibility with existing configs
|
|
direction = try container.decodeIfPresent(SeekDirection.self, forKey: .direction) ?? .forward
|
|
}
|
|
}
|
|
|
|
// MARK: - Title/Author Settings
|
|
|
|
/// Configuration for the title/author button.
|
|
struct TitleAuthorSettings: Codable, Hashable, Sendable {
|
|
/// Whether to show the source/author image.
|
|
var showSourceImage: Bool
|
|
|
|
/// Whether to show the video title.
|
|
var showTitle: Bool
|
|
|
|
/// Whether to show the source/author name.
|
|
var showSourceName: Bool
|
|
|
|
/// Default settings (all visible).
|
|
init(showSourceImage: Bool = true, showTitle: Bool = true, showSourceName: Bool = true) {
|
|
self.showSourceImage = showSourceImage
|
|
self.showTitle = showTitle
|
|
self.showSourceName = showSourceName
|
|
}
|
|
}
|
|
|
|
// MARK: - Time Display Format
|
|
|
|
/// How the time display shows current/total time.
|
|
enum TimeDisplayFormat: String, Codable, Hashable, Sendable, CaseIterable {
|
|
/// Shows only current time (e.g., "5:32").
|
|
case currentOnly
|
|
|
|
/// Shows current and total time (e.g., "5:32 / 12:45").
|
|
case currentAndTotal
|
|
|
|
/// Shows current and total, excluding SponsorBlock segments (e.g., "5:32 / 11:20").
|
|
case currentAndTotalExcludingSponsor
|
|
|
|
/// Shows current and remaining time (e.g., "5:32 / -7:13").
|
|
case currentAndRemaining
|
|
|
|
/// Shows current and remaining, excluding SponsorBlock segments.
|
|
case currentAndRemainingExcludingSponsor
|
|
|
|
/// Localized display name.
|
|
var displayName: String {
|
|
switch self {
|
|
case .currentOnly:
|
|
return String(localized: "controls.time.currentOnly")
|
|
case .currentAndTotal:
|
|
return String(localized: "controls.time.currentAndTotal")
|
|
case .currentAndTotalExcludingSponsor:
|
|
return String(localized: "controls.time.currentAndTotalExcludingSponsor")
|
|
case .currentAndRemaining:
|
|
return String(localized: "controls.time.currentAndRemaining")
|
|
case .currentAndRemainingExcludingSponsor:
|
|
return String(localized: "controls.time.currentAndRemainingExcludingSponsor")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration for time display.
|
|
struct TimeDisplaySettings: Codable, Hashable, Sendable {
|
|
/// Format for displaying time.
|
|
var format: TimeDisplayFormat
|
|
|
|
/// Default time display settings.
|
|
init(format: TimeDisplayFormat = .currentAndTotal) {
|
|
self.format = format
|
|
}
|
|
}
|
|
|
|
// MARK: - Button Settings
|
|
|
|
/// Settings for a configurable control button.
|
|
/// Uses associated values for type-specific settings.
|
|
enum ButtonSettings: Codable, Hashable, Sendable {
|
|
case spacer(SpacerSettings)
|
|
case slider(SliderSettings)
|
|
case seek(SeekSettings)
|
|
case timeDisplay(TimeDisplaySettings)
|
|
case titleAuthor(TitleAuthorSettings)
|
|
|
|
// MARK: - Codable
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case type
|
|
case spacer
|
|
case slider
|
|
case seek
|
|
case timeDisplay
|
|
case titleAuthor
|
|
}
|
|
|
|
private enum SettingsType: String, Codable {
|
|
case spacer
|
|
case slider
|
|
case seek
|
|
case timeDisplay
|
|
case titleAuthor
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let type = try container.decode(SettingsType.self, forKey: .type)
|
|
|
|
switch type {
|
|
case .spacer:
|
|
let settings = try container.decode(SpacerSettings.self, forKey: .spacer)
|
|
self = .spacer(settings)
|
|
case .slider:
|
|
let settings = try container.decode(SliderSettings.self, forKey: .slider)
|
|
self = .slider(settings)
|
|
case .seek:
|
|
let settings = try container.decode(SeekSettings.self, forKey: .seek)
|
|
self = .seek(settings)
|
|
case .timeDisplay:
|
|
let settings = try container.decode(TimeDisplaySettings.self, forKey: .timeDisplay)
|
|
self = .timeDisplay(settings)
|
|
case .titleAuthor:
|
|
let settings = try container.decode(TitleAuthorSettings.self, forKey: .titleAuthor)
|
|
self = .titleAuthor(settings)
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
switch self {
|
|
case .spacer(let settings):
|
|
try container.encode(SettingsType.spacer, forKey: .type)
|
|
try container.encode(settings, forKey: .spacer)
|
|
case .slider(let settings):
|
|
try container.encode(SettingsType.slider, forKey: .type)
|
|
try container.encode(settings, forKey: .slider)
|
|
case .seek(let settings):
|
|
try container.encode(SettingsType.seek, forKey: .type)
|
|
try container.encode(settings, forKey: .seek)
|
|
case .timeDisplay(let settings):
|
|
try container.encode(SettingsType.timeDisplay, forKey: .type)
|
|
try container.encode(settings, forKey: .timeDisplay)
|
|
case .titleAuthor(let settings):
|
|
try container.encode(SettingsType.titleAuthor, forKey: .type)
|
|
try container.encode(settings, forKey: .titleAuthor)
|
|
}
|
|
}
|
|
}
|