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