Move comments pill mode into the comments page settings

The pill mode now lives on CommentsPageSettings and is edited in the
comments page editor instead of a standalone settings section. The
legacy value on PlayerPillSettings is kept for decoding old presets
and migrated by effectivePanelSettings. Comments are loaded when
either the comments page is enabled or the pill is not disabled.
This commit is contained in:
Arkadiusz Fal
2026-09-23 22:45:38 +02:00
parent 924facacc4
commit e7458ba629
10 changed files with 84 additions and 115 deletions

View File

@@ -147,11 +147,42 @@ struct InfoPageSettings: Codable, Hashable, Sendable {
var effectiveShowDescription: Bool { showDescription ?? true }
}
// MARK: - Comments Pill Mode
/// Controls how the comments pill is displayed in the player.
/// Stored on the comments page settings (moved from the player pill settings;
/// `PlayerControlsLayout.effectivePanelSettings` migrates the legacy value).
enum CommentsPillMode: String, Codable, Hashable, Sendable, CaseIterable {
case pill // Default - shows expanded pill, collapses on scroll
case button // Always show collapsed (button-only)
case disabled // No button/pill visible
/// Localized display name for settings UI.
var displayName: String {
switch self {
case .pill:
return String(localized: "commentsPill.mode.pill")
case .button:
return String(localized: "commentsPill.mode.button")
case .disabled:
return String(localized: "commentsPill.mode.disabled")
}
}
/// Whether the comments pill should always be collapsed (button mode).
var alwaysCollapsed: Bool {
self == .button
}
}
/// Element toggles for the comments page.
struct CommentsPageSettings: Codable, Hashable, Sendable {
var showAvatars: Bool?
/// How the comments pill (portrait/floating panel) is displayed.
var pillMode: CommentsPillMode?
var effectiveShowAvatars: Bool { showAvatars ?? true }
var effectivePillMode: CommentsPillMode { pillMode ?? .pill }
}
/// Element toggles for the related videos page.
@@ -418,6 +449,17 @@ struct PanelSettings: Codable, Hashable, Sendable {
enabledPages().contains { $0.pageType == pageType }
}
/// The comments pill mode from the comments page settings.
var commentsPillMode: CommentsPillMode {
configuration(for: .comments)?.commentsSettings.effectivePillMode ?? .pill
}
/// Whether comments should be loaded from the API: needed by the comments
/// page (when enabled) and by the pill (unless disabled).
var shouldLoadComments: Bool {
isPageEnabled(.comments) || commentsPillMode != .disabled
}
/// The stored configuration for a page type, if present.
func configuration(for pageType: PanelPageType) -> PanelPageConfiguration? {
normalizedPages.first { $0.pageType == pageType }

View File

@@ -100,8 +100,18 @@ struct PlayerControlsLayout: Codable, Hashable, Sendable {
// MARK: - Panel Settings
/// Returns the effective details panel settings, using defaults if not set.
/// Migrates the legacy comments pill mode (once stored on the player pill
/// settings) into the comments page settings; the migrated value persists
/// the first time the panel settings are edited and saved.
var effectivePanelSettings: PanelSettings {
panelSettings ?? .default
var settings = panelSettings ?? .default
if let legacyMode = playerPillSettings?.commentsPillMode,
settings.configuration(for: .comments)?.commentsSettings.pillMode == nil {
var comments = settings.configuration(for: .comments)?.commentsSettings ?? CommentsPageSettings()
comments.pillMode = legacyMode
settings.updateSettings(.comments(comments), for: .comments)
}
return settings
}
// MARK: - Defaults

View File

@@ -8,37 +8,6 @@
import Foundation
import SwiftUI
// MARK: - CommentsPillMode
/// Controls how the comments pill is displayed in the player.
enum CommentsPillMode: String, Codable, Hashable, Sendable, CaseIterable {
case pill // Default - shows expanded pill, collapses on scroll
case button // Always show collapsed (button-only)
case disabled // No button/pill visible, no API query
/// Localized display name for settings UI.
var displayName: String {
switch self {
case .pill:
return String(localized: "commentsPill.mode.pill")
case .button:
return String(localized: "commentsPill.mode.button")
case .disabled:
return String(localized: "commentsPill.mode.disabled")
}
}
/// Whether comments should be loaded from the API.
var shouldLoadComments: Bool {
self != .disabled
}
/// Whether the comments pill should always be collapsed (button mode).
var alwaysCollapsed: Bool {
self == .button
}
}
// MARK: - PillVisibility
/// Controls when the player pill is visible based on orientation.
@@ -89,7 +58,9 @@ struct PlayerPillSettings: Codable, Hashable, Sendable {
/// Ordered list of buttons to display in the pill.
var buttons: [ControlButtonConfiguration]
/// How the comments pill should be displayed (optional for backward compatibility).
/// 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
@@ -104,28 +75,6 @@ struct PlayerPillSettings: Codable, Hashable, Sendable {
self.commentsPillMode = commentsPillMode
}
// MARK: - Computed Properties
/// Returns the effective comments pill mode, defaulting to `.pill` when nil.
var effectiveCommentsPillMode: CommentsPillMode {
commentsPillMode ?? .pill
}
/// Whether comments should be loaded from the API.
var shouldLoadComments: Bool {
effectiveCommentsPillMode.shouldLoadComments
}
/// Whether the comments pill should always be shown collapsed.
var isCommentsPillAlwaysCollapsed: Bool {
effectiveCommentsPillMode.alwaysCollapsed
}
/// Whether the comments pill should be visible at all.
var shouldShowCommentsPill: Bool {
effectiveCommentsPillMode != .disabled
}
// MARK: - Mutation Helpers
/// Adds a button of the given type to the pill.

View File

@@ -625,19 +625,6 @@ final class PlayerControlsSettingsViewModel {
playerPillSettings.buttons
}
/// Current comments pill mode.
var commentsPillMode: CommentsPillMode {
playerPillSettings.effectiveCommentsPillMode
}
/// Synchronously updates the comments pill mode with immediate UI feedback.
/// - Parameter mode: The new comments pill mode.
func syncCommentsPillMode(_ mode: CommentsPillMode) {
updatePlayerPillSettingsSync { settings in
settings.commentsPillMode = mode
}
}
/// Synchronously updates the pill visibility with immediate UI feedback.
/// - Parameter visibility: The new visibility mode.
func syncPillVisibility(_ visibility: PillVisibility) {

View File

@@ -165,13 +165,14 @@ extension ExpandedPlayerSheet {
// Check if expanded comments pill will be visible (needs extra panel height)
let pillSettings = playerControlsLayout.effectivePlayerPillSettings
let commentsPillMode = playerControlsLayout.effectivePanelSettings.commentsPillMode
let hasCommentsPill = playerState?.commentsState == .loaded &&
!(playerState?.comments.isEmpty ?? true) &&
pillSettings.shouldShowCommentsPill
commentsPillMode != .disabled
let hasExpandedCommentsPill = hasCommentsPill &&
pillSettings.visibility.isVisible(isWideLayout: false) &&
!pillSettings.buttons.isEmpty &&
!pillSettings.isCommentsPillAlwaysCollapsed
!commentsPillMode.alwaysCollapsed
let minPanelHeight: CGFloat = useCompactPanel ? 150 : 200
// When compact panel but expanded comments pill visible, need taller panel (~250pt)

View File

@@ -70,9 +70,8 @@ extension ExpandedPlayerSheet {
func preloadComments() async {
guard let playerState, playerState.commentsState == .idle else { return }
// Check if comments pill is disabled in preset settings
let pillSettings = playerControlsLayout.effectivePlayerPillSettings
guard pillSettings.shouldLoadComments else {
// Skip when nothing displays comments (page disabled and pill disabled)
guard playerControlsLayout.effectivePanelSettings.shouldLoadComments else {
playerState.commentsState = .disabled
return
}

View File

@@ -69,15 +69,15 @@ struct FloatingDetailsPanel: View {
!playerPillSettings.buttons.isEmpty
}
// Comments pill helpers
// Comments pill helpers (mode lives on the comments page settings)
private var commentsPillMode: CommentsPillMode {
playerPillSettings.effectiveCommentsPillMode
panelSettings.commentsPillMode
}
private var shouldShowCommentsPill: Bool {
playerPillSettings.shouldShowCommentsPill
commentsPillMode != .disabled
}
private var isCommentsPillAlwaysCollapsed: Bool {
playerPillSettings.isCommentsPillAlwaysCollapsed
commentsPillMode.alwaysCollapsed
}
private var hasVideoDescription: Bool {

View File

@@ -72,15 +72,15 @@ struct PortraitDetailsPanel: View {
!playerPillSettings.buttons.isEmpty
}
// Comments pill helpers
// Comments pill helpers (mode lives on the comments page settings)
private var commentsPillMode: CommentsPillMode {
playerPillSettings.effectiveCommentsPillMode
panelSettings.commentsPillMode
}
private var shouldShowCommentsPill: Bool {
!hidesCommentsPill && playerPillSettings.shouldShowCommentsPill
!hidesCommentsPill && commentsPillMode != .disabled
}
private var isCommentsPillAlwaysCollapsed: Bool {
playerPillSettings.isCommentsPillAlwaysCollapsed
commentsPillMode.alwaysCollapsed
}
private var hasVideoDescription: Bool {

View File

@@ -20,6 +20,7 @@ struct PanelPageEditorView: View {
@State private var showStatsRow: Bool = true
@State private var showChannelRow: Bool = true
@State private var showAvatars: Bool = true
@State private var commentsPillMode: CommentsPillMode = .pill
@State private var showRelatedThumbnails: Bool = true
@State private var showChapterThumbnails: Bool = true
@State private var showHistory: Bool = true
@@ -167,6 +168,19 @@ struct PanelPageEditorView: View {
guard newValue != commentsSettings.effectiveShowAvatars else { return }
viewModel.updatePanelCommentsSettingsSync { $0.showAvatars = newValue }
}
Picker(
String(localized: "commentsPill.mode.title"),
selection: $commentsPillMode
) {
ForEach(CommentsPillMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.onChange(of: commentsPillMode) { _, newValue in
guard newValue != commentsSettings.effectivePillMode else { return }
viewModel.updatePanelCommentsSettingsSync { $0.pillMode = newValue }
}
} header: {
Text(String(localized: "panel.settings.elements.header"))
}
@@ -308,6 +322,7 @@ struct PanelPageEditorView: View {
showStatsRow = info.effectiveShowStatsRow
showChannelRow = info.effectiveShowChannelRow
showAvatars = commentsSettings.effectiveShowAvatars
commentsPillMode = commentsSettings.effectivePillMode
showRelatedThumbnails = relatedSettings.effectiveShowThumbnails
showChapterThumbnails = chaptersSettings.effectiveShowThumbnails
showHistory = queueSettings.effectiveShowHistory

View File

@@ -91,7 +91,6 @@ private struct PlayerControlsSettingsContent: View {
)
LayoutSectionsSection(viewModel: viewModel)
#if os(iOS)
CommentsPillSection(viewModel: viewModel)
GesturesSectionsSection(viewModel: viewModel)
#endif
SystemControlsSection(viewModel: viewModel)
@@ -426,39 +425,6 @@ private struct LayoutSectionsSection: View {
}
}
// MARK: - Comments Pill Section
private struct CommentsPillSection: View {
@Bindable var viewModel: PlayerControlsSettingsViewModel
// Local state for immediate UI updates
@State private var commentsPillMode: CommentsPillMode = .pill
var body: some View {
Section {
Picker(
String(localized: "commentsPill.mode.title"),
selection: $commentsPillMode
) {
ForEach(CommentsPillMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.disabled(!viewModel.canEditActivePreset)
.onChange(of: commentsPillMode) { _, newValue in
guard newValue != viewModel.commentsPillMode else { return }
viewModel.syncCommentsPillMode(newValue)
}
}
.onAppear {
commentsPillMode = viewModel.commentsPillMode
}
.onChange(of: viewModel.activePreset?.id) { _, _ in
commentsPillMode = viewModel.commentsPillMode
}
}
}
// MARK: - Gestures Sections Section (iOS only)
#if os(iOS)