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.
This commit is contained in:
Arkadiusz Fal
2026-09-27 14:27:11 +02:00
parent 1370f68c37
commit 61786ec6f7
4 changed files with 69 additions and 88 deletions

View File

@@ -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
)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}