From 61786ec6f7067c905f9dfbeafd238dd11c3e1ec1 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 27 Sep 2026 14:27:11 +0200 Subject: [PATCH] Move the wide-layout pin button into the panel page bar The pin now sits as an icon-only pill at the leading edge of the page bar, before the page pills, instead of floating on the resize grabber. It stays fixed while the pages scroll and fills with the accent color while pinned. The page bar is kept visible when a pin action is present even if only one page is available, so the control never disappears. iPhone (floating-only) still shows no pin. Removes PanelPinButton, which had no other users. --- .../Views/Player/FloatingDetailsPanel.swift | 26 +++----- Yattee/Views/Player/PanelPinButton.swift | 59 ----------------- Yattee/Views/Player/Panels/PanelPageBar.swift | 63 ++++++++++++++++--- .../Panels/PlayerPanelContentView.swift | 9 ++- 4 files changed, 69 insertions(+), 88 deletions(-) delete mode 100644 Yattee/Views/Player/PanelPinButton.swift diff --git a/Yattee/Views/Player/FloatingDetailsPanel.swift b/Yattee/Views/Player/FloatingDetailsPanel.swift index adc35b0c..068247cc 100644 --- a/Yattee/Views/Player/FloatingDetailsPanel.swift +++ b/Yattee/Views/Player/FloatingDetailsPanel.swift @@ -94,9 +94,10 @@ struct FloatingDetailsPanel: View { PanelSettings.isSectionsOnlyDevice } - /// iPhone is floating-only: no pin button on the grabber. - private var showsPinButton: Bool { - !PanelSettings.isSectionsOnlyDevice + /// iPhone is floating-only: no pin button in the page bar. + private var pageBarPinAction: PanelPageBarPinAction? { + guard !PanelSettings.isSectionsOnlyDevice else { return nil } + return PanelPageBarPinAction(isPinned: isPinned, toggle: onPinToggle) } /// Whether the comments pill should route to the comments page instead of @@ -142,7 +143,7 @@ struct FloatingDetailsPanel: View { HStack(spacing: 0) { // Resize grabber on left when panel is on right side if panelSide == .right { - grabberWithPinButton + grabber } // Main panel content @@ -150,7 +151,7 @@ struct FloatingDetailsPanel: View { // Resize grabber on right when panel is on left side if panelSide == .left { - grabberWithPinButton + grabber } } .environment(\.colorScheme, isPinned ? systemColorScheme : .dark) @@ -183,7 +184,7 @@ struct FloatingDetailsPanel: View { } @ViewBuilder - private var grabberWithPinButton: some View { + private var grabber: some View { PanelResizeGrabber( panelWidth: $panelWidth, minWidth: Self.minPanelWidth, @@ -192,18 +193,6 @@ struct FloatingDetailsPanel: View { isPinned: isPinned, isDragActive: $isDragHandleActive ) - .overlay(alignment: .center) { - if showsPinButton { - PanelPinButton( - isPinned: isPinned, - panelSide: panelSide, - onPinToggle: onPinToggle, - isDragHandleActive: $isDragHandleActive - ) - .offset(y: -65) - .zIndex(10) // Ensure pin button appears above panel content - } - } .zIndex(10) // Ensure grabber overlay appears above panel } @@ -230,6 +219,7 @@ struct FloatingDetailsPanel: View { ), pageBarTopPadding: 12, showPageBar: !usesInlineSections, + pageBarPinAction: pageBarPinAction, inlineInfoSections: usesInlineSections ) } diff --git a/Yattee/Views/Player/PanelPinButton.swift b/Yattee/Views/Player/PanelPinButton.swift deleted file mode 100644 index 12ea81c5..00000000 --- a/Yattee/Views/Player/PanelPinButton.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// PanelPinButton.swift -// Yattee -// -// Floating pin button for the details panel in wide layout. -// - -import SwiftUI - -#if os(iOS) || os(macOS) - -/// A floating pin button that appears on the divider edge between player and panel. -/// Features auto-hide behavior with 3s timer, reappearing on drag handle interaction. -struct PanelPinButton: View { - @Environment(\.appEnvironment) private var appEnvironment - - let isPinned: Bool - let panelSide: FloatingPanelSide - let onPinToggle: () -> Void - - /// Whether the drag handle is currently active (being dragged or hovered). - @Binding var isDragHandleActive: Bool - - /// Button diameter - private static let buttonSize: CGFloat = 36 - - private var accentColor: Color { - appEnvironment?.settingsManager.resolvedAccentColor ?? .accentColor - } - - var body: some View { - Button { - triggerHaptic() - onPinToggle() - } label: { - Image(systemName: isPinned ? "pin.fill" : "pin") - .font(.system(size: 16, weight: .medium)) - .foregroundStyle(isPinned ? accentColor : .primary) - .frame(width: Self.buttonSize, height: Self.buttonSize) - .glassBackground(.regular, in: .circle, fallback: .thinMaterial) - .shadow(color: .black.opacity(0.15), radius: 8, y: 2) - .contentShape(Circle()) - } - .buttonStyle(.plain) - .accessibilityLabel(isPinned - ? String(localized: "wideLayoutPanel.pinButton.unpin") - : String(localized: "wideLayoutPanel.pinButton.pin")) - } - - // MARK: - Haptic Feedback - - private func triggerHaptic() { - #if os(iOS) - appEnvironment?.settingsManager.triggerHapticFeedback(for: .subscribeButton) - #endif - } -} - -#endif diff --git a/Yattee/Views/Player/Panels/PanelPageBar.swift b/Yattee/Views/Player/Panels/PanelPageBar.swift index b3792772..7f6e3124 100644 --- a/Yattee/Views/Player/Panels/PanelPageBar.swift +++ b/Yattee/Views/Player/Panels/PanelPageBar.swift @@ -9,13 +9,23 @@ import SwiftUI #if os(iOS) || os(macOS) +/// Pin control shown at the leading edge of the page bar. +struct PanelPageBarPinAction { + let isPinned: Bool + let toggle: () -> Void +} + struct PanelPageBar: View { let pages: [PanelPageConfiguration] @Binding var selectedPage: PanelPageType let accentColor: Color var showLabels: Bool = true + /// Optional pin control rendered as an icon-only pill before the pages + /// (wide layout: pinned side-by-side vs. floating). + var pinAction: PanelPageBarPinAction? = nil @Environment(\.panelHorizontalInsets) private var insets + @Environment(\.appEnvironment) private var appEnvironment /// Pills sit 4pt closer to the edge than page content (12 vs 16 with the /// standard inset); a host that trims the content inset (corner side @@ -24,16 +34,24 @@ struct PanelPageBar: View { var body: some View { ScrollViewReader { proxy in - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(pages) { page in - pageButton(page.pageType) - .id(page.pageType) - } + HStack(spacing: 8) { + if let pinAction { + pinButton(pinAction) + .padding(.leading, max(0, insets.leading - Self.pillEdgeOffset)) + .padding(.vertical, 6) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(pages) { page in + pageButton(page.pageType) + .id(page.pageType) + } + } + .padding(.leading, pinAction == nil ? max(0, insets.leading - Self.pillEdgeOffset) : 0) + .padding(.trailing, max(0, insets.trailing - Self.pillEdgeOffset)) + .padding(.vertical, 6) } - .padding(.leading, max(0, insets.leading - Self.pillEdgeOffset)) - .padding(.trailing, max(0, insets.trailing - Self.pillEdgeOffset)) - .padding(.vertical, 6) } .onChange(of: selectedPage) { _, newValue in withAnimation(.easeInOut(duration: 0.2)) { @@ -43,6 +61,33 @@ struct PanelPageBar: View { } } + /// Icon-only pill toggling the pinned state; filled with the accent + /// color while pinned so it reads like a selected page. + @ViewBuilder + private func pinButton(_ pin: PanelPageBarPinAction) -> some View { + Button { + #if os(iOS) + appEnvironment?.settingsManager.triggerHapticFeedback(for: .subscribeButton) + #endif + pin.toggle() + } label: { + Image(systemName: pin.isPinned ? "pin.fill" : "pin") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(pin.isPinned ? Color.white : Color.primary) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background { + Capsule() + .fill(pin.isPinned ? AnyShapeStyle(accentColor) : AnyShapeStyle(.quaternary.opacity(0.6))) + } + .contentShape(Capsule()) + } + .buttonStyle(.plain) + .accessibilityLabel(pin.isPinned + ? String(localized: "wideLayoutPanel.pinButton.unpin") + : String(localized: "wideLayoutPanel.pinButton.pin")) + } + @ViewBuilder private func pageButton(_ pageType: PanelPageType) -> some View { let isSelected = pageType == selectedPage diff --git a/Yattee/Views/Player/Panels/PlayerPanelContentView.swift b/Yattee/Views/Player/Panels/PlayerPanelContentView.swift index ff27d584..649148cd 100644 --- a/Yattee/Views/Player/Panels/PlayerPanelContentView.swift +++ b/Yattee/Views/Player/Panels/PlayerPanelContentView.swift @@ -28,6 +28,10 @@ struct PlayerPanelContentView: View { /// hide the top page bar. var showPageBar: Bool = true + /// Pin toggle rendered before the page pills (wide layout hosts). Keeps + /// the bar visible even when only one page is available. + var pageBarPinAction: PanelPageBarPinAction? = nil + /// The info page shows chapters and related as collapsible inline /// sections in configured order; those pages are then dropped from the /// page list (iPhone in every orientation, stacked iPad portrait). @@ -113,7 +117,7 @@ struct PlayerPanelContentView: View { let pages = visiblePages VStack(spacing: 0) { - if showPageBar, pages.count >= 2 { + if showPageBar, pages.count >= 2 || pageBarPinAction != nil { // Highlight follows the rendered page (the selection when // visible, else the fallback); taps still set the selection. PanelPageBar( @@ -123,7 +127,8 @@ struct PlayerPanelContentView: View { set: { selectedPage = $0 } ), accentColor: context.accentColor, - showLabels: panelSettings.effectiveShowPageBarLabels + showLabels: panelSettings.effectiveShowPageBarLabels, + pinAction: pageBarPinAction ) .padding(.top, pageBarTopPadding) }