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 var state: VLCVideoPlayer.State = .opening
@Published @Published
var subtitleTrackIndex: Int = -1 var subtitleTrackIndex: Int = -1
@Published
var playbackSpeed: PlaybackSpeed = PlaybackSpeed.one
// MARK: ViewModel // MARK: ViewModel

View File

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

View File

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

View File

@ -30,72 +30,73 @@ extension VideoPlayer {
@StateObject @StateObject
private var overlayTimer: TimerProxy = .init() private var overlayTimer: TimerProxy = .init()
var body: some View { @ViewBuilder
ZStack { private var currentOverlay: some View {
switch currentOverlayType {
MainOverlay() case .chapters:
.visible(currentOverlayType == .main)
ConfirmCloseOverlay()
.visible(currentOverlayType == .confirmClose)
SmallMenuOverlay()
.visible(currentOverlayType == .smallMenu)
ChapterOverlay() 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) var body: some View {
.environmentObject(overlayTimer) currentOverlay
.onChange(of: currentOverlayType) { newValue in .visible(isPresentingOverlay)
if [.smallMenu, .chapters].contains(newValue) { .animation(.linear(duration: 0.1), value: currentOverlayType)
overlayTimer.pause() .environment(\.currentOverlayType, $currentOverlayType)
} else if isPresentingOverlay { .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) overlayTimer.start(5)
} }
} .onMenuPressed {
.onChange(of: overlayTimer.isActive) { isActive in
guard !isActive else { return }
withAnimation(.linear(duration: 0.3)) { overlayTimer.start(5)
isPresentingOverlay = false confirmCloseWorkItem?.cancel()
}
}
.onSelectPressed {
currentOverlayType = .main
isPresentingOverlay = true
overlayTimer.start(5)
}
.onMenuPressed {
overlayTimer.start(5) if isPresentingOverlay && currentOverlayType == .confirmClose {
confirmCloseWorkItem?.cancel() proxy.stop()
router.dismissCoordinator()
if isPresentingOverlay && currentOverlayType == .confirmClose { } else if isPresentingOverlay && currentOverlayType == .smallMenu {
proxy.stop() currentOverlayType = .main
router.dismissCoordinator() } else {
} else if isPresentingOverlay && currentOverlayType == .smallMenu {
currentOverlayType = .main
} else {
withAnimation {
currentOverlayType = .confirmClose
isPresentingOverlay = true
}
let task = DispatchWorkItem {
withAnimation { withAnimation {
isPresentingOverlay = false currentOverlayType = .confirmClose
overlayTimer.stop() 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 { enum MenuSection: String, Displayable {
case audio case audio
case chapters
case playbackSpeed case playbackSpeed
case subtitles case subtitles
@ -22,8 +21,6 @@ extension VideoPlayer {
switch self { switch self {
case .audio: case .audio:
return "Audio" return "Audio"
case .chapters:
return "Chapters"
case .playbackSpeed: case .playbackSpeed:
return "Playback Speed" return "Playback Speed"
case .subtitles: case .subtitles:
@ -41,7 +38,7 @@ extension VideoPlayer {
private var focusedSection: MenuSection? private var focusedSection: MenuSection?
@State @State
private var lastFocusedSection: MenuSection? private var lastFocusedSection: MenuSection = .subtitles
@StateObject @StateObject
private var focusGuide: FocusGuide = .init() private var focusGuide: FocusGuide = .init()
@ -50,24 +47,54 @@ extension VideoPlayer {
private var subtitleMenu: some View { private var subtitleMenu: some View {
HStack { HStack {
ForEach(viewModel.subtitleStreams, id: \.self) { mediaStream in ForEach(viewModel.subtitleStreams, id: \.self) { mediaStream in
Button {} label: { Button {
if videoPlayerManager.subtitleTrackIndex == mediaStream.index { videoPlayerManager.subtitleTrackIndex = mediaStream.index ?? -1
Label(mediaStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") videoPlayerManager.proxy.setSubtitleTrack(.absolute(mediaStream.index ?? -1))
} else { } label: {
Text(mediaStream.displayTitle ?? L10n.noTitle) Label(
} mediaStream.displayTitle ?? L10n.noTitle,
systemImage: videoPlayerManager.subtitleTrackIndex == mediaStream.index ? "checkmark.circle.fill" : "circle"
)
} }
} }
} }
.frame(height: 80) .modifier(MenuStyle(focusGuide: focusGuide))
.padding(.horizontal, 50) }
.padding(.top)
.padding(.bottom, 45) @ViewBuilder
.focusGuide( private var audioMenu: some View {
focusGuide, HStack {
tag: "contents", ForEach(viewModel.audioStreams, id: \.self) { mediaStream in
top: "sections" 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 { var body: some View {
@ -98,14 +125,6 @@ extension VideoPlayer {
focused: $focusedSection, focused: $focusedSection,
lastFocused: $lastFocusedSection lastFocused: $lastFocusedSection
) )
if !viewModel.chapters.isEmpty {
SectionButton(
section: .chapters,
focused: $focusedSection,
lastFocused: $lastFocusedSection
)
}
} }
.focusGuide( .focusGuide(
focusGuide, focusGuide,
@ -123,10 +142,10 @@ extension VideoPlayer {
switch lastFocusedSection { switch lastFocusedSection {
case .subtitles: case .subtitles:
subtitleMenu subtitleMenu
default: case .audio:
Button { audioMenu
Text("None") case .playbackSpeed:
} playbackSpeedMenu
} }
} }
} }
@ -151,7 +170,7 @@ extension VideoPlayer {
let section: MenuSection let section: MenuSection
let focused: FocusState<MenuSection?>.Binding let focused: FocusState<MenuSection?>.Binding
let lastFocused: Binding<MenuSection?> let lastFocused: Binding<MenuSection>
var body: some View { var body: some View {
Button { Button {
@ -170,5 +189,22 @@ extension VideoPlayer {
.focused(focused, equals: section) .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 { struct PlaybackSpeedMenu: View {
@Environment(\.playbackSpeed)
@Binding
private var playbackSpeed
@EnvironmentObject @EnvironmentObject
private var overlayTimer: TimerProxy private var overlayTimer: TimerProxy
@EnvironmentObject @EnvironmentObject
@ -30,20 +26,16 @@ extension VideoPlayer.Overlay.ActionButtons {
Menu { Menu {
ForEach(PlaybackSpeed.allCases, id: \.self) { speed in ForEach(PlaybackSpeed.allCases, id: \.self) { speed in
Button { Button {
playbackSpeed = Float(speed.rawValue) videoPlayerManager.playbackSpeed = speed
videoPlayerProxy.setRate(.absolute(Float(speed.rawValue))) videoPlayerProxy.setRate(.absolute(Float(speed.rawValue)))
} label: { } label: {
if Float(speed.rawValue) == playbackSpeed { if speed == videoPlayerManager.playbackSpeed {
Label(speed.displayTitle, systemImage: "checkmark") Label(speed.displayTitle, systemImage: "checkmark")
} else { } else {
Text(speed.displayTitle) Text(speed.displayTitle)
} }
} }
} }
if !PlaybackSpeed.allCases.map(\.rawValue).contains(where: { $0 == Double(playbackSpeed) }) {
Label(String(format: "%.2f", playbackSpeed).appending("x"), systemImage: "checkmark")
}
} label: { } label: {
content().eraseToAnyView() content().eraseToAnyView()
} }