Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions BookPlayer/Player/Views/ArtworkView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ struct ArtworkView: View {
var title: String
var author: String
var imagePath: String?

/// Where the square sits when it's narrower than the space it's given
var alignment: Alignment = .center

var body: some View {
ZStack(alignment: .top) {
ZStack {
Expand Down Expand Up @@ -103,7 +105,7 @@ struct ArtworkView: View {
.cornerRadius(12)
.clipped()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment)
}
}

Expand Down
11 changes: 10 additions & 1 deletion BookPlayer/Player/Views/MediaActionRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ struct MediaActionRow<TimerPopover: View>: View {
@Binding var showsTimerPopover: Bool

let currentAlertOrigin: MediaAction?
/// A fixed gap that keeps the buttons together in the middle; without one they spread
/// evenly across the row
var buttonSpacing: CGFloat? = nil
let onActionTapped: (MediaAction) -> Void
@ViewBuilder let timerPopover: () -> TimerPopover

Expand Down Expand Up @@ -55,7 +58,7 @@ struct MediaActionRow<TimerPopover: View>: View {
}

var body: some View {
HStack {
HStack(spacing: buttonSpacing) {
Spacer()
ForEach(MediaAction.allCases) { ma in
BubbleButton(
Expand All @@ -77,6 +80,12 @@ struct MediaActionRow<TimerPopover: View>: View {
content: timerPopover
)

if buttonSpacing == nil {
Spacer()
}
}

if buttonSpacing != nil {
Spacer()
}
}
Expand Down
8 changes: 6 additions & 2 deletions BookPlayer/Player/Views/NavigationRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ struct NavigationRowView: View {
var onNextTap: (() -> Void)?
var onTitleToggle: (() -> Void)?
var onPreviousTap: (() -> Void)?

/// Keeps the row as tall for a one-line title as for two. Off beside the artwork, where the
/// row lines up with the artwork's top and the extra height would sit as empty space above
/// a one-line title
var keepsTitleHeight = true

var body: some View {
HStack(spacing: 4) {
Button {
Expand All @@ -41,7 +45,7 @@ struct NavigationRowView: View {
.bpFont(.playerTitle)
.foregroundColor(theme.primaryColor)
.multilineTextAlignment(.center)
.frame(height: 56)
.frame(height: keepsTitleHeight ? 56 : nil)
}
.layoutPriority(1)

Expand Down
4 changes: 3 additions & 1 deletion BookPlayer/Player/Views/PlayerJumpView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ struct PlayerJumpView: View {
}
.buttonStyle(.plain)
.background(Color.clear)
.frame(width: 54, height: 48)
/// As tall as the icon: a shorter frame lets it spill past, and layouts around it
/// (centering, spacing) go by the frame
.frame(width: 54, height: 56)
}
}

Expand Down
160 changes: 88 additions & 72 deletions BookPlayer/Player/Views/PlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,29 @@ struct PlayerView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.displayScale) private var displayScale

@Environment(\.verticalSizeClass) private var verticalSizeClass

@StateObject private var viewModel: PlayerViewModel
@StateObject private var theme = ThemeViewModel()
@State private var noteText: String = ""
@State private var dragOffset: CGSize = .zero
@State private var dragThresholdReached = false
private var dismissThreshold: CGFloat { 44.0 * displayScale }

/// Side by side, the screen is about half as tall, so half the distance closes the player
private var dismissThreshold: CGFloat { (isSideBySide ? 22.0 : 44.0) * displayScale }
/// The artwork beside the controls on a short screen, like iPhone in landscape
private var isSideBySide: Bool { verticalSizeClass == .compact }
/// Side by side, the artwork sits against the controls; it fills less than its half when
/// the height is what limits it
private var artworkAlignment: Alignment { isSideBySide ? .trailing : .center }

init(initModel: @escaping () -> PlayerViewModel) {
self._viewModel = .init(wrappedValue: initModel())
}

var body: some View {

var body: some View {
/// Switching the layout keeps its views, so rotating moves them instead of rebuilding them
let layout = isSideBySide ? AnyLayout(HStackLayout(spacing: 40)) : AnyLayout(VStackLayout())

VStack {
DismissableRegionView()
.simultaneousGesture(
Expand All @@ -38,88 +48,95 @@ struct PlayerView: View {
handleDragEnded(gesture)
}
)

VStack {
Group {
if viewModel.isVideoItem {
VideoArtworkView()
} else {
ArtworkView(
title: viewModel.title,
author: viewModel.author,
imagePath: viewModel.relativePath
)
}
}
.simultaneousGesture(
DragGesture(minimumDistance: 15)
.onChanged { gesture in
handleDragChanged(gesture)
}
.onEnded { gesture in
handleDragEnded(gesture)
}
)

Spacer()
}
.frame(maxWidth: .infinity)
layout {
VStack {
ArtworkView(
title: viewModel.title,
author: viewModel.author,
imagePath: viewModel.relativePath,
alignment: artworkAlignment
)
.simultaneousGesture(
DragGesture(minimumDistance: 15)
.onChanged { gesture in
handleDragChanged(gesture)
}
.onEnded { gesture in
handleDragEnded(gesture)
}
)

VStack(spacing: 4) {
NavigationRowView(
playerTitle: viewModel.progressData.chapterTitle,
hasNextChapter: viewModel.hasNextChapter,
hasPreviousChapter: viewModel.hasPreviousChapter,
onNextTap: viewModel.handleNextTap,
onTitleToggle: viewModel.processToggleProgressState,
onPreviousTap: viewModel.handlePreviousTap
)
/// Side by side, the artwork spans the controls' full height
Spacer(minLength: isSideBySide ? 0 : nil)
}
.frame(maxWidth: .infinity, alignment: artworkAlignment)
/// VoiceOver reads by position, which side by side would mix the artwork's elements
/// into the controls
.accessibilityElement(children: .contain)

ListeningProgressView(
progress: $viewModel.progressData.sliderValue,
remainingTime: viewModel.progressData.formattedMaxTime ?? "00:00",
remainingTimeAccessLabel: viewModel.remainingTimeAccessLabel,
currentTime: viewModel.progressData.formattedCurrentTime,
currentTimeAccessLabel: viewModel.currentTimeAccessLabel,
progressLabel: viewModel.progressData.progress ?? "",
onSliderDragChanged: { value in
viewModel.handleSliderDragChanged(value: value)
},
onSliderChange: { progress in
viewModel.handleSliderUpEvent(with: Float(progress))
},
onProgressToggle: {
viewModel.processToggleProgressState()
},
onRemainingToggle: {
viewModel.processToggleMaxTime()
}
)
VStack(spacing: 4) {
NavigationRowView(
playerTitle: viewModel.progressData.chapterTitle,
hasNextChapter: viewModel.hasNextChapter,
hasPreviousChapter: viewModel.hasPreviousChapter,
onNextTap: viewModel.handleNextTap,
onTitleToggle: viewModel.processToggleProgressState,
onPreviousTap: viewModel.handlePreviousTap,
keepsTitleHeight: !isSideBySide
)

Spacer()
ListeningProgressView(
progress: $viewModel.progressData.sliderValue,
remainingTime: viewModel.progressData.formattedMaxTime ?? "00:00",
remainingTimeAccessLabel: viewModel.remainingTimeAccessLabel,
currentTime: viewModel.progressData.formattedCurrentTime,
currentTimeAccessLabel: viewModel.currentTimeAccessLabel,
progressLabel: viewModel.progressData.progress ?? "",
onSliderDragChanged: { value in
viewModel.handleSliderDragChanged(value: value)
},
onSliderChange: { progress in
viewModel.handleSliderUpEvent(with: Float(progress))
},
onProgressToggle: {
viewModel.processToggleProgressState()
},
onRemainingToggle: {
viewModel.processToggleMaxTime()
}
)

Spacer()

PlayControlsRowView(isPlaying: viewModel.isPlaying)
PlayControlsRowView(isPlaying: viewModel.isPlaying)

Spacer()
Spacer()
}
.contentShape(Rectangle())
.simultaneousGesture(
DragGesture(minimumDistance: 15)
.onChanged(handleDragChanged)
.onEnded(handleDragEnded)
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityElement(children: .contain)
}
.contentShape(Rectangle())
.simultaneousGesture(
DragGesture(minimumDistance: 15)
.onChanged(handleDragChanged)
.onEnded(handleDragEnded)
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(.horizontal, 8)
.safeAreaPadding([.top, .horizontal])
.safeAreaInset(edge: .bottom) {
/// Side by side, the artwork runs down to the row, so it gets some room
.safeAreaInset(edge: .bottom, spacing: isSideBySide ? 16 : nil) {
MediaActionRow(
speedText: viewModel.formattedSpeed(),
sleepText: viewModel.sleepText,
sleepAccessibilityLabel: viewModel.sleepAccessibilityLabel,
currentAlert: $viewModel.currentAlert,
showsTimerPopover: $viewModel.showsCustomSleepTimer,
currentAlertOrigin: viewModel.currentAlertOrigin,
/// Side by side, the buttons stay together in the middle instead of spreading across
/// the screen's full width
buttonSpacing: isSideBySide ? 24 : nil,
onActionTapped: { viewModel.handleButtonTap(media: $0) }
) {
DurationPickerPopover(
Expand All @@ -145,7 +162,6 @@ struct PlayerView: View {
viewModel.bindBookObservers()
viewModel.handleAutolockStatus(forceDisable: false)
viewModel.recalculateProgress()
viewModel.refreshVideoState()
}
.onDisappear {
viewModel.handleAutolockStatus(forceDisable: true)
Expand Down Expand Up @@ -211,7 +227,7 @@ struct PlayerView: View {
.environmentObject(theme)
}
}

private func handleDragChanged(_ gesture: DragGesture.Value) {
if gesture.translation.height > 0 {
dragOffset = gesture.translation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct SettingsPlayerControlsView: View {
ListOptionsSectionView()
ProgressLabelsSectionView()
StartupPlayerSectionView()
VideoPlaybackSectionView()
// VideoPlaybackSectionView()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WARN — Commenting out VideoPlaybackSectionView() has nothing to do with the landscape layout, and the PR description doesn't mention it. It hides the "Show Video" and "Picture in Picture" toggles. Anyone who already turned them on can't turn them off, and VideoPiPCoordinator.isEnabled still reads videoPictureInPictureEnabled. It looks like leftover debugging. Restore the line, or if hiding video is intended, remove the feature in a separate PR instead of leaving commented-out code.

}
.environmentObject(theme)
.scrollContentBackground(.hidden)
Expand Down
Loading