mirror of
https://github.com/yattee/yattee.git
synced 2026-11-05 03:17:19 +00:00
Add corner player layout on iPad and macOS
Preset-selected two-region layout: video in the top-left, a resizable side column, and an optional region below the video. Pages are assigned to regions per preset (Location picker in the page editor); the layout falls back to the stacked widescreen layout below 900pt width, so Split View and narrow windows degrade automatically. - CornerPlayerLayout hosts the shared playerAreaForWidescreen builder, so the MPV hosting path is identical to the stacked layout - Layout resolver in ExpandedPlayerSheet picks corner per layout pass; NavigationCoordinator.isCornerLayoutActive feeds the macOS window manager, which drops the aspect-ratio lock and auto-fit while corner is active (and restores them on exit, incl. fullscreen replay and window re-mounts) - Side column width persisted via new cornerPanelColumnWidth setting (280-560pt, video column kept >= 480pt) - Settings: layout mode picker (iPad/macOS) with automatic-fallback footer, per-page region picker when corner is on, region shown in the pages list
This commit is contained in:
218
Yattee/Views/Player/CornerPlayerLayout.swift
Normal file
218
Yattee/Views/Player/CornerPlayerLayout.swift
Normal file
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// CornerPlayerLayout.swift
|
||||
// Yattee
|
||||
//
|
||||
// Corner player layout for iPad/macOS: video in the top-left, a resizable
|
||||
// side column on the right, and an optional region below the video —
|
||||
// YouTube-style. Pages are assigned to regions in the preset's panel
|
||||
// settings; the layout resolver in ExpandedPlayerSheet falls back to the
|
||||
// stacked layout when the window is too narrow.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
|
||||
struct CornerPlayerLayout<PlayerContent: View>: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
|
||||
let playerControlsLayout: PlayerControlsLayout
|
||||
|
||||
/// Video + controls area builder, sized by the layout.
|
||||
@ViewBuilder let playerContent: (
|
||||
_ onTogglePanel: @escaping () -> Void,
|
||||
_ isPanelVisible: Bool,
|
||||
_ width: CGFloat,
|
||||
_ height: CGFloat
|
||||
) -> PlayerContent
|
||||
|
||||
let onChannelTap: (() -> Void)?
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
/// Side column width bounds.
|
||||
static var minColumnWidth: CGFloat { 280 }
|
||||
static var maxColumnWidthCap: CGFloat { 560 }
|
||||
/// Minimum width kept for the video column.
|
||||
static var minVideoColumnWidth: CGFloat { 480 }
|
||||
/// Maximum fraction of the window height the video area may take when a
|
||||
/// below-video region needs space.
|
||||
static var maxVideoHeightFraction: CGFloat { 0.72 }
|
||||
|
||||
// MARK: - State
|
||||
|
||||
@State private var isPanelVisible = true
|
||||
@State private var columnWidth: CGFloat = SettingsManager.defaultCornerPanelColumnWidth
|
||||
@State private var primaryPage: PanelPageType = .related
|
||||
@State private var belowVideoPage: PanelPageType = .info
|
||||
@State private var isGrabberActive = false
|
||||
|
||||
private var settingsManager: SettingsManager? { appEnvironment?.settingsManager }
|
||||
private var playerState: PlayerState? { appEnvironment?.playerService.state }
|
||||
private var video: Video? { playerState?.currentVideo }
|
||||
private var accentColor: Color { settingsManager?.resolvedAccentColor ?? .accentColor }
|
||||
|
||||
private var panelSettings: PanelSettings {
|
||||
playerControlsLayout.effectivePanelSettings
|
||||
}
|
||||
|
||||
/// Region emptiness is decided at the config level: runtime-empty pages
|
||||
/// (no chapters, no related yet) are hidden inside the panel content, not
|
||||
/// by collapsing regions — layout jumps during loading would be jarring.
|
||||
private var hasPrimaryRegion: Bool {
|
||||
!panelSettings.enabledPages(in: .primary).isEmpty
|
||||
}
|
||||
|
||||
private var hasBelowVideoRegion: Bool {
|
||||
!panelSettings.enabledPages(in: .belowVideo).isEmpty
|
||||
}
|
||||
|
||||
private var panelBackgroundColor: Color {
|
||||
#if os(iOS)
|
||||
Color(uiColor: .systemBackground)
|
||||
#else
|
||||
Color(nsColor: .windowBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func clampColumnWidth(_ width: CGFloat, availableWidth: CGFloat) -> CGFloat {
|
||||
let maxWidth = max(
|
||||
Self.minColumnWidth,
|
||||
min(Self.maxColumnWidthCap, availableWidth * 0.45, availableWidth - Self.minVideoColumnWidth)
|
||||
)
|
||||
return min(max(width, Self.minColumnWidth), maxWidth)
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let width = max(1, geometry.size.width)
|
||||
let height = max(1, geometry.size.height)
|
||||
|
||||
let showsPrimary = isPanelVisible && hasPrimaryRegion
|
||||
let showsBelowVideo = isPanelVisible && hasBelowVideoRegion
|
||||
|
||||
let effectiveColumnWidth = clampColumnWidth(columnWidth, availableWidth: width)
|
||||
// Grabber sits on the column's leading edge, inside the column background.
|
||||
let totalColumnWidth = effectiveColumnWidth + 20
|
||||
|
||||
let videoWidth = showsPrimary ? max(1, width - totalColumnWidth) : width
|
||||
let videoHeight: CGFloat = {
|
||||
guard showsBelowVideo else { return height }
|
||||
let rawAspectRatio = playerState?.displayAspectRatio ?? (16.0 / 9.0)
|
||||
let aspectRatio = rawAspectRatio > 0 ? rawAspectRatio : (16.0 / 9.0)
|
||||
return max(1, min(videoWidth / aspectRatio, height * Self.maxVideoHeightFraction))
|
||||
}()
|
||||
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea(.all)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
playerContent(
|
||||
togglePanel,
|
||||
isPanelVisible,
|
||||
videoWidth,
|
||||
videoHeight
|
||||
)
|
||||
.frame(width: videoWidth, height: videoHeight)
|
||||
.clipped()
|
||||
|
||||
if showsBelowVideo, let video {
|
||||
regionPanel(
|
||||
region: .belowVideo,
|
||||
selectedPage: $belowVideoPage,
|
||||
video: video
|
||||
)
|
||||
.frame(width: videoWidth)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
if showsPrimary, let video {
|
||||
HStack(spacing: 0) {
|
||||
PanelResizeGrabber(
|
||||
panelWidth: $columnWidth,
|
||||
minWidth: Self.minColumnWidth,
|
||||
maxWidth: clampColumnWidth(.infinity, availableWidth: width),
|
||||
panelSide: .right,
|
||||
isPinned: true,
|
||||
isDragActive: $isGrabberActive
|
||||
)
|
||||
|
||||
regionPanel(
|
||||
region: .primary,
|
||||
selectedPage: $primaryPage,
|
||||
video: video
|
||||
)
|
||||
.frame(width: effectiveColumnWidth)
|
||||
}
|
||||
.background(panelBackgroundColor.ignoresSafeArea(edges: .vertical))
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: width) { _, newWidth in
|
||||
// Re-clamp when the window shrinks (still above the corner
|
||||
// threshold — below it the resolver switches layouts).
|
||||
columnWidth = clampColumnWidth(columnWidth, availableWidth: newWidth)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isPanelVisible = settingsManager?.landscapeDetailsPanelVisible ?? true
|
||||
if let savedWidth = settingsManager?.cornerPanelColumnWidth, savedWidth > 0 {
|
||||
columnWidth = savedWidth
|
||||
}
|
||||
}
|
||||
.onChange(of: columnWidth) { _, newWidth in
|
||||
settingsManager?.cornerPanelColumnWidth = newWidth
|
||||
}
|
||||
.onChange(of: settingsManager?.landscapeDetailsPanelVisible) { _, newValue in
|
||||
// External writes (preset default visibility, other layout hosts)
|
||||
// must reach the local state too.
|
||||
guard let newValue, newValue != isPanelVisible else { return }
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
isPanelVisible = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Region Panel
|
||||
|
||||
@ViewBuilder
|
||||
private func regionPanel(
|
||||
region: PanelRegion,
|
||||
selectedPage: Binding<PanelPageType>,
|
||||
video: Video
|
||||
) -> some View {
|
||||
PlayerPanelContentView(
|
||||
panelSettings: panelSettings,
|
||||
region: region,
|
||||
selectedPage: selectedPage,
|
||||
context: PanelPageContext(
|
||||
appEnvironment: appEnvironment,
|
||||
video: video,
|
||||
accentColor: accentColor,
|
||||
onChannelTap: onChannelTap
|
||||
),
|
||||
scrollConfig: PanelScrollConfig(
|
||||
topInset: 4,
|
||||
bottomInset: 16
|
||||
),
|
||||
pageBarTopPadding: 8
|
||||
)
|
||||
.background(panelBackgroundColor.ignoresSafeArea(edges: .vertical))
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func togglePanel() {
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
isPanelVisible.toggle()
|
||||
settingsManager?.landscapeDetailsPanelVisible = isPanelVisible
|
||||
settingsManager?.detailsPanelLastVisible = isPanelVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -16,6 +16,11 @@ extension ExpandedPlayerSheet {
|
||||
/// Minimum width required to show widescreen layout with floating panel.
|
||||
static let minWidthForWidescreen: CGFloat = 700
|
||||
|
||||
/// Minimum width required for the corner layout; below this the corner
|
||||
/// preset setting falls back to the stacked widescreen layout (Split View,
|
||||
/// narrow windows).
|
||||
static let minWidthForCornerLayout: CGFloat = 900
|
||||
|
||||
// MARK: - Layout Detection
|
||||
|
||||
/// Check if widescreen layout should be used based on available size.
|
||||
@@ -31,6 +36,21 @@ extension ExpandedPlayerSheet {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
/// Whether the corner layout should be used for the given size: iPad/macOS
|
||||
/// only, opted into via the preset's panel settings, wide enough, and in a
|
||||
/// widescreen orientation. Evaluated per layout pass so window resizes and
|
||||
/// Split View changes fall back automatically.
|
||||
func isCornerLayout(size: CGSize) -> Bool {
|
||||
#if os(iOS)
|
||||
guard UIDevice.current.userInterfaceIdiom == .pad else { return false }
|
||||
#endif
|
||||
return playerControlsLayout.effectivePanelSettings.effectiveLayoutMode == .corner
|
||||
&& size.width >= Self.minWidthForCornerLayout
|
||||
&& isWideScreenLayout(size: size)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Download Action Handler
|
||||
|
||||
#if !os(tvOS)
|
||||
@@ -1276,6 +1296,36 @@ extension ExpandedPlayerSheet {
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Corner Content
|
||||
|
||||
/// Corner layout: video top-left, side column, optional below-video
|
||||
/// region. Reuses `playerAreaForWidescreen` so the MPV hosting path is
|
||||
/// identical to the stacked layout — only the container size differs.
|
||||
@ViewBuilder
|
||||
func cornerContent(video: Video) -> some View {
|
||||
CornerPlayerLayout(
|
||||
playerControlsLayout: playerControlsLayout,
|
||||
playerContent: { onTogglePanel, isPanelVisible, width, height in
|
||||
// isPanelPinned=true keeps the controls visible alongside the
|
||||
// panel regions (corner regions behave like a pinned panel).
|
||||
playerAreaForWidescreen(
|
||||
onTogglePanel: onTogglePanel,
|
||||
isPanelVisible: isPanelVisible,
|
||||
isPanelPinned: true,
|
||||
panelSide: .right,
|
||||
onHidePanel: {},
|
||||
leadingSafeArea: 0,
|
||||
trailingSafeArea: 0,
|
||||
fullWidth: width,
|
||||
fullHeight: height
|
||||
)
|
||||
},
|
||||
onChannelTap: video.author.hasRealChannelInfo ? {
|
||||
navigationCoordinator?.navigateToChannel(for: video, collapsePlayer: true)
|
||||
} : nil
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Widescreen Player Area
|
||||
|
||||
/// Player area for widescreen layout with panel support.
|
||||
|
||||
@@ -294,6 +294,9 @@ struct ExpandedPlayerSheet: View {
|
||||
private var playerContent: some View {
|
||||
GeometryReader { geometry in
|
||||
let wideScreen = isWideScreenLayout(size: geometry.size)
|
||||
#if os(iOS) || os(macOS)
|
||||
let cornerActive = isCornerLayout(size: geometry.size)
|
||||
#endif
|
||||
|
||||
ZStack(alignment: .top) {
|
||||
// Full-screen background - prevents content leaking during aspect ratio animation
|
||||
@@ -310,10 +313,17 @@ struct ExpandedPlayerSheet: View {
|
||||
Color.black.ignoresSafeArea(.all)
|
||||
} else if wideScreen {
|
||||
if let video = playerState?.currentVideo {
|
||||
// Widescreen layout with floating panel
|
||||
// Must ignore safe area to get full screen geometry
|
||||
wideScreenContent(video: video)
|
||||
.ignoresSafeArea(.all)
|
||||
if cornerActive {
|
||||
// Corner layout: video top-left with panel regions.
|
||||
// Laid out within the safe area (black background
|
||||
// extends beneath it).
|
||||
cornerContent(video: video)
|
||||
} else {
|
||||
// Widescreen layout with floating panel
|
||||
// Must ignore safe area to get full screen geometry
|
||||
wideScreenContent(video: video)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
} else {
|
||||
// Widescreen loading state - just show black with spinner
|
||||
Color.black
|
||||
@@ -337,6 +347,11 @@ struct ExpandedPlayerSheet: View {
|
||||
// Disable animations during layout switch to prevent position interpolation
|
||||
transaction.animation = nil
|
||||
}
|
||||
#if os(iOS) || os(macOS)
|
||||
.onChange(of: cornerActive) { _, newValue in
|
||||
navigationCoordinator?.isCornerLayoutActive = newValue
|
||||
}
|
||||
#endif
|
||||
.onChange(of: wideScreen) { _, newValue in
|
||||
isInWideScreenLayout = newValue
|
||||
#if os(iOS)
|
||||
@@ -371,6 +386,9 @@ struct ExpandedPlayerSheet: View {
|
||||
}
|
||||
.onAppear {
|
||||
isInWideScreenLayout = wideScreen
|
||||
#if os(iOS) || os(macOS)
|
||||
navigationCoordinator?.isCornerLayoutActive = cornerActive
|
||||
#endif
|
||||
#if os(iOS)
|
||||
previousIsLandscape = wideScreen
|
||||
if wideScreen {
|
||||
@@ -988,6 +1006,11 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier {
|
||||
|
||||
var playerState: PlayerState? { appEnvironment?.playerService.state }
|
||||
|
||||
/// Corner layout resizes the window freely: no aspect lock, no auto-fit.
|
||||
private var isCornerLayoutActive: Bool {
|
||||
appEnvironment?.navigationCoordinator.isCornerLayoutActive == true
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onChange(of: playerState?.showDebugOverlay) { _, isVisible in
|
||||
@@ -1003,12 +1026,30 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier {
|
||||
.onChange(of: playerState?.currentVideo?.id) { oldValue, newValue in
|
||||
handleVideoChangeForResize(oldValue: oldValue, newValue: newValue)
|
||||
}
|
||||
.onChange(of: appEnvironment?.navigationCoordinator.isCornerLayoutActive) { _, isCorner in
|
||||
handleCornerLayoutChange(isCorner: isCorner == true)
|
||||
}
|
||||
.onAppear {
|
||||
handleMacOSAppear()
|
||||
}
|
||||
}
|
||||
|
||||
/// Entering corner mode drops the window's aspect-ratio constraint so it
|
||||
/// resizes freely; leaving it restores the lock (and auto-fit, when on)
|
||||
/// exactly as the aspect-change handler would have applied it.
|
||||
private func handleCornerLayoutChange(isCorner: Bool) {
|
||||
if isCorner {
|
||||
ExpandedPlayerWindowManager.shared.unlockAspectRatio()
|
||||
} else if let aspectRatio = playerState?.videoAspectRatio, aspectRatio > 0 {
|
||||
ExpandedPlayerWindowManager.shared.lockAspectRatio(aspectRatio)
|
||||
if appEnvironment?.settingsManager.playerSheetAutoResize == true {
|
||||
ExpandedPlayerWindowManager.shared.resizeToFitAspectRatio(aspectRatio, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleAspectRatioChange(oldValue: Double?, newValue: Double?) {
|
||||
guard !isCornerLayoutActive else { return }
|
||||
// nil while a video is loaded means audio-only playback (no video
|
||||
// track reports a size) — fall back to the standard 16:9 instead of
|
||||
// keeping the previous video's ratio.
|
||||
@@ -1039,6 +1080,7 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier {
|
||||
}
|
||||
|
||||
private func handleVideoChangeForResize(oldValue: Video.ID?, newValue: Video.ID?) {
|
||||
guard !isCornerLayoutActive else { return }
|
||||
guard oldValue != nil, newValue != nil else { return }
|
||||
guard appEnvironment?.settingsManager.playerSheetAutoResize == true else { return }
|
||||
|
||||
@@ -1054,6 +1096,7 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier {
|
||||
}
|
||||
|
||||
private func handleMacOSAppear() {
|
||||
guard !isCornerLayoutActive else { return }
|
||||
guard let aspectRatio = playerState?.videoAspectRatio, aspectRatio > 0 else { return }
|
||||
|
||||
// Lock aspect ratio for manual resize unconditionally.
|
||||
|
||||
@@ -176,7 +176,11 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
|
||||
// Lock manual resize to the video aspect ratio. Seeded with the real ratio
|
||||
// when known (else 16:9); updated as soon as the real ratio is known.
|
||||
Self.applyAspectRatioConstraint(seedAspect, to: window)
|
||||
// Corner layout (already active on a window re-mount, e.g. switching
|
||||
// from the inline overlay) keeps the window unconstrained.
|
||||
if appEnvironment.navigationCoordinator.isCornerLayoutActive != true {
|
||||
Self.applyAspectRatioConstraint(seedAspect, to: window)
|
||||
}
|
||||
|
||||
// Force the intended size. Assigning `contentViewController` above resizes
|
||||
// the window to the hosting controller's fitting size — and the lightweight
|
||||
@@ -588,6 +592,17 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
Self.applyAspectRatioConstraint(aspectRatio, to: window)
|
||||
}
|
||||
|
||||
/// Removes the window's aspect-ratio resize constraint so it resizes
|
||||
/// freely. Used by the corner layout, where the window frames the whole
|
||||
/// video-plus-panels arrangement rather than the video alone. The size
|
||||
/// floor stays enforced by `windowWillResize(_:to:)`.
|
||||
func unlockAspectRatio() {
|
||||
guard let window = playerWindow else { return }
|
||||
pendingFullScreenAspectRatio = nil
|
||||
window.contentAspectRatio = .zero
|
||||
LoggingService.shared.debug("unlockAspectRatio (corner layout)", category: .player)
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Sets `contentAspectRatio` and a ratio-consistent minimum content size on
|
||||
@@ -813,6 +828,8 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate {
|
||||
// exactly what the aspect-change handler would have done windowed.
|
||||
if let ratio = pendingFullScreenAspectRatio {
|
||||
pendingFullScreenAspectRatio = nil
|
||||
// Corner layout keeps the window unconstrained.
|
||||
guard appEnvironment?.navigationCoordinator.isCornerLayoutActive != true else { return }
|
||||
Self.applyAspectRatioConstraint(ratio, to: window)
|
||||
if appEnvironment?.settingsManager.playerSheetAutoResize == true {
|
||||
resizeToFitAspectRatio(ratio, animated: false)
|
||||
|
||||
@@ -15,6 +15,7 @@ struct PanelPageEditorView: View {
|
||||
|
||||
// Local state for immediate UI updates
|
||||
@State private var isEnabled: Bool = true
|
||||
@State private var region: PanelRegion = .primary
|
||||
@State private var showDescription: Bool = true
|
||||
@State private var showStatsRow: Bool = true
|
||||
@State private var showChannelRow: Bool = true
|
||||
@@ -32,6 +33,10 @@ struct PanelPageEditorView: View {
|
||||
Form {
|
||||
enabledSection
|
||||
|
||||
if PanelSettingsView.supportsCornerLayout, viewModel.panelLayoutMode == .corner {
|
||||
regionSection
|
||||
}
|
||||
|
||||
if pageType == .info {
|
||||
infoElementsSection
|
||||
}
|
||||
@@ -73,6 +78,28 @@ struct PanelPageEditorView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Region Section (corner layout)
|
||||
|
||||
private var regionSection: some View {
|
||||
Section {
|
||||
Picker(
|
||||
String(localized: "panel.settings.page.region"),
|
||||
selection: $region
|
||||
) {
|
||||
ForEach(PanelRegion.allCases, id: \.self) { region in
|
||||
Text(region.displayName).tag(region)
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
.onChange(of: region) { _, newValue in
|
||||
guard newValue != configuration?.region else { return }
|
||||
viewModel.setPanelPageRegion(newValue, for: pageType)
|
||||
}
|
||||
} footer: {
|
||||
Text(String(localized: "panel.settings.page.region.footer"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Info Elements Section
|
||||
|
||||
private var infoElementsSection: some View {
|
||||
@@ -117,6 +144,7 @@ struct PanelPageEditorView: View {
|
||||
|
||||
private func syncLocalState() {
|
||||
isEnabled = configuration?.isEnabled ?? true
|
||||
region = configuration?.region ?? .primary
|
||||
let info = infoSettings
|
||||
showDescription = info.effectiveShowDescription
|
||||
showStatsRow = info.effectiveShowStatsRow
|
||||
|
||||
@@ -14,9 +14,22 @@ struct PanelSettingsView: View {
|
||||
|
||||
// Local state for immediate UI updates
|
||||
@State private var defaultVisibility: PanelDefaultVisibility = .hidden
|
||||
@State private var layoutMode: PanelLayoutMode = .stacked
|
||||
|
||||
/// Corner layout exists on iPad and macOS only.
|
||||
static var supportsCornerLayout: Bool {
|
||||
#if os(macOS)
|
||||
return true
|
||||
#else
|
||||
return UIDevice.current.userInterfaceIdiom == .pad
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if Self.supportsCornerLayout {
|
||||
layoutSection
|
||||
}
|
||||
visibilitySection
|
||||
pagesSection
|
||||
}
|
||||
@@ -40,6 +53,28 @@ struct PanelSettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout Section
|
||||
|
||||
private var layoutSection: some View {
|
||||
Section {
|
||||
Picker(
|
||||
String(localized: "panel.settings.layoutMode"),
|
||||
selection: $layoutMode
|
||||
) {
|
||||
ForEach(PanelLayoutMode.allCases, id: \.self) { mode in
|
||||
Text(mode.displayName).tag(mode)
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
.onChange(of: layoutMode) { _, newValue in
|
||||
guard newValue != viewModel.panelLayoutMode else { return }
|
||||
viewModel.syncPanelLayoutMode(newValue)
|
||||
}
|
||||
} footer: {
|
||||
Text(String(localized: "panel.settings.layoutMode.footer"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Visibility Section
|
||||
|
||||
private var visibilitySection: some View {
|
||||
@@ -92,10 +127,15 @@ struct PanelSettingsView: View {
|
||||
HStack {
|
||||
Label(page.pageType.displayName, systemImage: page.pageType.systemImage)
|
||||
Spacer()
|
||||
Text(page.isEnabled
|
||||
? String(localized: "common.on")
|
||||
: String(localized: "common.off"))
|
||||
.foregroundStyle(.secondary)
|
||||
if page.isEnabled, layoutMode == .corner, Self.supportsCornerLayout {
|
||||
Text(page.region.displayName)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text(page.isEnabled
|
||||
? String(localized: "common.on")
|
||||
: String(localized: "common.off"))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +144,7 @@ struct PanelSettingsView: View {
|
||||
|
||||
private func syncLocalState() {
|
||||
defaultVisibility = viewModel.panelDefaultVisibility
|
||||
layoutMode = viewModel.panelLayoutMode
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user