Files
yattee/Yattee/Models/PlayerControls/PlayerPillSettings.swift
Arkadiusz Fal 7f74b50e94 Make the player pill an on/off setting on iPad, drop the dead wide-layout pill overlay
On iPad both orientations show the video next to a panel, the corner
layout hosts the portrait panel in landscape, and Split View windows make
"portrait" mean "narrow", so the portrait/landscape visibility split never
matched what the user saw. PillVisibility now treats any mode except Never
as visible on iPad, and the pill editor shows a Show pill toggle there
(writes Both/Never, reads synced orientation modes as on). iPhone keeps
the four-way picker.

ExpandedPlayerSheet's bottomPillsOverlay was unreferenced; the floating
panel renders its own pill row.
2026-09-27 16:36:42 +02:00

139 lines
4.8 KiB
Swift

//
// PlayerPillSettings.swift
// Yattee
//
// Settings for the player pill component (visibility and buttons).
//
import Foundation
import SwiftUI
// MARK: - PillVisibility
/// Controls when the player pill is visible based on orientation.
enum PillVisibility: String, Codable, Hashable, Sendable, CaseIterable {
case portraitOnly // Default - shown in portrait orientation only
case landscapeOnly // Shown in wide/landscape orientation only
case both // Shown in all orientations
case never // Pill disabled
/// Localized display name for settings UI.
var displayName: String {
switch self {
case .portraitOnly:
return String(localized: "pill.visibility.portraitOnly")
case .landscapeOnly:
return String(localized: "pill.visibility.landscapeOnly")
case .both:
return String(localized: "pill.visibility.both")
case .never:
return String(localized: "pill.visibility.never")
}
}
/// iPad treats the pill as on/off: both orientations show the video next
/// to a panel, the corner layout hosts the portrait panel in landscape,
/// and Split View windows make "portrait" mean "narrow" — so an
/// orientation split never matches what the user sees.
static var isOrientationAgnostic: Bool {
#if os(iOS)
return UIDevice.current.userInterfaceIdiom == .pad
#else
return false
#endif
}
/// Whether the pill is enabled at all (any mode except `.never`).
var isEnabled: Bool { self != .never }
/// Returns whether the pill should be visible for the given layout context.
/// - Parameter isWideLayout: True if in wide/landscape layout, false for portrait.
/// - Returns: Whether the pill should be shown.
func isVisible(isWideLayout: Bool) -> Bool {
if Self.isOrientationAgnostic { return isEnabled }
switch self {
case .portraitOnly:
return !isWideLayout
case .landscapeOnly:
return isWideLayout
case .both:
return true
case .never:
return false
}
}
}
// MARK: - PlayerPillSettings
/// Complete settings for the player pill component.
struct PlayerPillSettings: Codable, Hashable, Sendable {
/// When to show the pill.
var visibility: PillVisibility
/// Ordered list of buttons to display in the pill.
var buttons: [ControlButtonConfiguration]
/// Legacy: the comments pill mode now lives on the comments page settings
/// (`CommentsPageSettings.pillMode`). Kept only so old presets decode;
/// `PlayerControlsLayout.effectivePanelSettings` migrates it.
var commentsPillMode: CommentsPillMode?
// MARK: - Initialization
init(
visibility: PillVisibility = .portraitOnly,
buttons: [ControlButtonConfiguration] = [],
commentsPillMode: CommentsPillMode? = nil
) {
self.visibility = visibility
self.buttons = buttons
self.commentsPillMode = commentsPillMode
}
// MARK: - Mutation Helpers
/// Adds a button of the given type to the pill.
/// - Parameter buttonType: The type of button to add.
mutating func add(buttonType: ControlButtonType) {
let config = ControlButtonConfiguration(buttonType: buttonType)
buttons.append(config)
}
/// Removes the button at the given index.
/// - Parameter index: The index of the button to remove.
mutating func remove(at index: Int) {
guard buttons.indices.contains(index) else { return }
buttons.remove(at: index)
}
/// Moves buttons within the list.
/// - Parameters:
/// - source: Source indices to move from.
/// - destination: Destination index to move to.
mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) {
buttons.move(fromOffsets: source, toOffset: destination)
}
/// Updates a button configuration by matching ID.
/// - Parameter configuration: The updated configuration.
mutating func update(_ configuration: ControlButtonConfiguration) {
guard let index = buttons.firstIndex(where: { $0.id == configuration.id }) else { return }
buttons[index] = configuration
}
// MARK: - Default Configuration
/// Default player pill settings matching the original queue pill behavior.
static let `default` = PlayerPillSettings(
visibility: .portraitOnly,
buttons: [
ControlButtonConfiguration(buttonType: .queue),
ControlButtonConfiguration(buttonType: .playPrevious),
ControlButtonConfiguration(buttonType: .playPause),
ControlButtonConfiguration(buttonType: .playNext),
ControlButtonConfiguration(buttonType: .close)
]
)
}