Add audio, subtitle, playback select for tvOS (#859)

This commit is contained in:
Tony 2023-10-12 18:34:57 -07:00 committed by GitHub
parent 667d48b0e9
commit 289868e71a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 110 deletions

View File

@ -40,6 +40,8 @@ class VideoPlayerManager: ViewModel {
var state: VLCVideoPlayer.State = .opening
@Published
var subtitleTrackIndex: Int = -1
@Published
var playbackSpeed: PlaybackSpeed = PlaybackSpeed.one
// MARK: ViewModel

View File

@ -16,16 +16,7 @@ struct SFSymbolButton: UIViewRepresentable {
private let systemName: String
private let systemNameFocused: String?
func makeUIView(context: Context) -> some UIButton {
var configuration = UIButton.Configuration.plain()
configuration.cornerStyle = .capsule
let buttonAction = UIAction(title: "") { _ in
self.onSelect()
}
let button = UIButton(configuration: configuration, primaryAction: buttonAction)
private func makeButtonConfig(_ button: UIButton) {
let symbolImageConfig = UIImage.SymbolConfiguration(pointSize: pointSize)
let symbolImage = UIImage(systemName: systemName, withConfiguration: symbolImageConfig)
@ -36,11 +27,26 @@ struct SFSymbolButton: UIViewRepresentable {
button.setImage(focusedSymbolImage, for: .focused)
}
}
func makeUIView(context: Context) -> some UIButton {
var configuration = UIButton.Configuration.plain()
configuration.cornerStyle = .capsule
let buttonAction = UIAction(title: "") { _ in
self.onSelect()
}
let button = UIButton(configuration: configuration, primaryAction: buttonAction)
makeButtonConfig(button)
return button
}
func updateUIView(_ uiView: UIViewType, context: Context) {}
func updateUIView(_ uiView: UIViewType, context: Context) {
makeButtonConfig(uiView)
}
}
extension SFSymbolButton {

View File

@ -28,7 +28,6 @@ extension VideoPlayer.Overlay.ActionButtons {
overlayTimer.start(5)
}
.frame(maxWidth: 30, maxHeight: 30)
.id(autoPlayEnabled)
}
}
}

View File

@ -30,72 +30,73 @@ extension VideoPlayer {
@StateObject
private var overlayTimer: TimerProxy = .init()
var body: some View {
ZStack {
MainOverlay()
.visible(currentOverlayType == .main)
ConfirmCloseOverlay()
.visible(currentOverlayType == .confirmClose)
SmallMenuOverlay()
.visible(currentOverlayType == .smallMenu)
@ViewBuilder
private var currentOverlay: some View {
switch currentOverlayType {
case .chapters:
ChapterOverlay()
.visible(currentOverlayType == .chapters)
case .confirmClose:
ConfirmCloseOverlay()
case .main:
MainOverlay()
case .smallMenu:
SmallMenuOverlay()
}
.visible(isPresentingOverlay)
.animation(.linear(duration: 0.1), value: currentOverlayType)
.environment(\.currentOverlayType, $currentOverlayType)
.environmentObject(overlayTimer)
.onChange(of: currentOverlayType) { newValue in
if [.smallMenu, .chapters].contains(newValue) {
overlayTimer.pause()
} else if isPresentingOverlay {
}
var body: some View {
currentOverlay
.visible(isPresentingOverlay)
.animation(.linear(duration: 0.1), value: currentOverlayType)
.environment(\.currentOverlayType, $currentOverlayType)
.environmentObject(overlayTimer)
.onChange(of: currentOverlayType) { newValue in
if [.smallMenu, .chapters].contains(newValue) {
overlayTimer.pause()
} else if isPresentingOverlay {
overlayTimer.start(5)
}
}
.onChange(of: overlayTimer.isActive) { isActive in
guard !isActive else { return }
withAnimation(.linear(duration: 0.3)) {
isPresentingOverlay = false
}
}
.onSelectPressed {
currentOverlayType = .main
isPresentingOverlay = true
overlayTimer.start(5)
}
}
.onChange(of: overlayTimer.isActive) { isActive in
guard !isActive else { return }
.onMenuPressed {
withAnimation(.linear(duration: 0.3)) {
isPresentingOverlay = false
}
}
.onSelectPressed {
currentOverlayType = .main
isPresentingOverlay = true
overlayTimer.start(5)
}
.onMenuPressed {
overlayTimer.start(5)
confirmCloseWorkItem?.cancel()
overlayTimer.start(5)
confirmCloseWorkItem?.cancel()
if isPresentingOverlay && currentOverlayType == .confirmClose {
proxy.stop()
router.dismissCoordinator()
} else if isPresentingOverlay && currentOverlayType == .smallMenu {
currentOverlayType = .main
} else {
withAnimation {
currentOverlayType = .confirmClose
isPresentingOverlay = true
}
let task = DispatchWorkItem {
if isPresentingOverlay && currentOverlayType == .confirmClose {
proxy.stop()
router.dismissCoordinator()
} else if isPresentingOverlay && currentOverlayType == .smallMenu {
currentOverlayType = .main
} else {
withAnimation {
isPresentingOverlay = false
overlayTimer.stop()
currentOverlayType = .confirmClose
isPresentingOverlay = true
}
let task = DispatchWorkItem {
withAnimation {
isPresentingOverlay = false
overlayTimer.stop()
}
}
confirmCloseWorkItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task)
}
confirmCloseWorkItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task)
}
}
}
}
}

View File

@ -14,7 +14,6 @@ extension VideoPlayer {
enum MenuSection: String, Displayable {
case audio
case chapters
case playbackSpeed
case subtitles
@ -22,8 +21,6 @@ extension VideoPlayer {
switch self {
case .audio:
return "Audio"
case .chapters:
return "Chapters"
case .playbackSpeed:
return "Playback Speed"
case .subtitles:
@ -41,7 +38,7 @@ extension VideoPlayer {
private var focusedSection: MenuSection?
@State
private var lastFocusedSection: MenuSection?
private var lastFocusedSection: MenuSection = .subtitles
@StateObject
private var focusGuide: FocusGuide = .init()
@ -50,24 +47,54 @@ extension VideoPlayer {
private var subtitleMenu: some View {
HStack {
ForEach(viewModel.subtitleStreams, id: \.self) { mediaStream in
Button {} label: {
if videoPlayerManager.subtitleTrackIndex == mediaStream.index {
Label(mediaStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark")
} else {
Text(mediaStream.displayTitle ?? L10n.noTitle)
}
Button {
videoPlayerManager.subtitleTrackIndex = mediaStream.index ?? -1
videoPlayerManager.proxy.setSubtitleTrack(.absolute(mediaStream.index ?? -1))
} label: {
Label(
mediaStream.displayTitle ?? L10n.noTitle,
systemImage: videoPlayerManager.subtitleTrackIndex == mediaStream.index ? "checkmark.circle.fill" : "circle"
)
}
}
}
.frame(height: 80)
.padding(.horizontal, 50)
.padding(.top)
.padding(.bottom, 45)
.focusGuide(
focusGuide,
tag: "contents",
top: "sections"
)
.modifier(MenuStyle(focusGuide: focusGuide))
}
@ViewBuilder
private var audioMenu: some View {
HStack {
ForEach(viewModel.audioStreams, id: \.self) { mediaStream in
Button {
videoPlayerManager.audioTrackIndex = mediaStream.index ?? -1
videoPlayerManager.proxy.setAudioTrack(.absolute(mediaStream.index ?? -1))
} label: {
Label(
mediaStream.displayTitle ?? L10n.noTitle,
systemImage: videoPlayerManager.audioTrackIndex == mediaStream.index ? "checkmark.circle.fill" : "circle"
)
}
}
}
.modifier(MenuStyle(focusGuide: focusGuide))
}
@ViewBuilder
private var playbackSpeedMenu: some View {
HStack {
ForEach(PlaybackSpeed.allCases, id: \.self) { speed in
Button {
videoPlayerManager.playbackSpeed = speed
videoPlayerManager.proxy.setRate(.absolute(Float(speed.rawValue)))
} label: {
Label(
speed.displayTitle,
systemImage: speed == videoPlayerManager.playbackSpeed ? "checkmark.circle.fill" : "circle"
)
}
}
}
.modifier(MenuStyle(focusGuide: focusGuide))
}
var body: some View {
@ -98,14 +125,6 @@ extension VideoPlayer {
focused: $focusedSection,
lastFocused: $lastFocusedSection
)
if !viewModel.chapters.isEmpty {
SectionButton(
section: .chapters,
focused: $focusedSection,
lastFocused: $lastFocusedSection
)
}
}
.focusGuide(
focusGuide,
@ -123,10 +142,10 @@ extension VideoPlayer {
switch lastFocusedSection {
case .subtitles:
subtitleMenu
default:
Button {
Text("None")
}
case .audio:
audioMenu
case .playbackSpeed:
playbackSpeedMenu
}
}
}
@ -151,7 +170,7 @@ extension VideoPlayer {
let section: MenuSection
let focused: FocusState<MenuSection?>.Binding
let lastFocused: Binding<MenuSection?>
let lastFocused: Binding<MenuSection>
var body: some View {
Button {
@ -170,5 +189,22 @@ extension VideoPlayer {
.focused(focused, equals: section)
}
}
struct MenuStyle: ViewModifier {
var focusGuide: FocusGuide
func body(content: Content) -> some View {
content
.focusGuide(
focusGuide,
tag: "contents",
top: "sections"
)
.frame(height: 80)
.padding(.horizontal, 50)
.padding(.top)
.padding(.bottom, 45)
}
}
}
}

View File

@ -13,10 +13,6 @@ extension VideoPlayer.Overlay.ActionButtons {
struct PlaybackSpeedMenu: View {
@Environment(\.playbackSpeed)
@Binding
private var playbackSpeed
@EnvironmentObject
private var overlayTimer: TimerProxy
@EnvironmentObject
@ -30,20 +26,16 @@ extension VideoPlayer.Overlay.ActionButtons {
Menu {
ForEach(PlaybackSpeed.allCases, id: \.self) { speed in
Button {
playbackSpeed = Float(speed.rawValue)
videoPlayerManager.playbackSpeed = speed
videoPlayerProxy.setRate(.absolute(Float(speed.rawValue)))
} label: {
if Float(speed.rawValue) == playbackSpeed {
if speed == videoPlayerManager.playbackSpeed {
Label(speed.displayTitle, systemImage: "checkmark")
} else {
Text(speed.displayTitle)
}
}
}
if !PlaybackSpeed.allCases.map(\.rawValue).contains(where: { $0 == Double(playbackSpeed) }) {
Label(String(format: "%.2f", playbackSpeed).appending("x"), systemImage: "checkmark")
}
} label: {
content().eraseToAnyView()
}