364 lines
18 KiB
Swift
364 lines
18 KiB
Swift
//
|
|
// VLCPlayerCompactOverlayView.swift
|
|
// JellyfinVideoPlayerDev
|
|
//
|
|
// Created by Ethan Pippin on 12/26/21.
|
|
//
|
|
|
|
import Combine
|
|
import Defaults
|
|
import JellyfinAPI
|
|
import MobileVLCKit
|
|
import Sliders
|
|
import SwiftUI
|
|
|
|
struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
|
|
|
@ObservedObject var viewModel: VideoPlayerViewModel
|
|
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
|
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
|
|
|
@ViewBuilder
|
|
private var mainButtonView: some View {
|
|
switch viewModel.playerState {
|
|
case .stopped, .paused:
|
|
Image(systemName: "play.fill")
|
|
.font(.system(size: 28, weight: .heavy, design: .default))
|
|
case .playing:
|
|
Image(systemName: "pause")
|
|
.font(.system(size: 28, weight: .heavy, design: .default))
|
|
default:
|
|
ProgressView()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var mainBody: some View {
|
|
VStack {
|
|
|
|
// MARK: Top Bar
|
|
ZStack {
|
|
|
|
LinearGradient(gradient: Gradient(colors: [.black, .clear]),
|
|
startPoint: .top,
|
|
endPoint: .bottom)
|
|
.ignoresSafeArea()
|
|
.frame(height: 80)
|
|
|
|
VStack(alignment: .EpisodeSeriesAlignmentGuide) {
|
|
|
|
HStack(alignment: .center) {
|
|
|
|
HStack {
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectClose()
|
|
} label: {
|
|
Image(systemName: "chevron.backward")
|
|
.padding()
|
|
.padding(.trailing, -10)
|
|
}
|
|
|
|
Text(viewModel.title)
|
|
.font(.system(size: 28, weight: .regular, design: .default))
|
|
.alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in
|
|
context[.leading]
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
HStack(spacing: 20) {
|
|
|
|
if viewModel.showAdjacentItems {
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectPreviousItem()
|
|
} label: {
|
|
Image(systemName: "chevron.left.circle")
|
|
}
|
|
.disabled(viewModel.previousItemVideoPlayerViewModel == nil)
|
|
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
|
|
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectNextItem()
|
|
} label: {
|
|
Image(systemName: "chevron.right.circle")
|
|
}
|
|
.disabled(viewModel.nextItemVideoPlayerViewModel == nil)
|
|
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
|
|
}
|
|
|
|
if viewModel.shouldShowGoogleCast {
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectGoogleCast()
|
|
} label: {
|
|
Image(systemName: "rectangle.badge.plus")
|
|
}
|
|
}
|
|
|
|
if viewModel.shouldShowAirplay {
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectAirplay()
|
|
} label: {
|
|
Image(systemName: "airplayvideo")
|
|
}
|
|
}
|
|
|
|
if !viewModel.subtitleStreams.isEmpty {
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectCaptions()
|
|
} label: {
|
|
if viewModel.subtitlesEnabled {
|
|
Image(systemName: "captions.bubble.fill")
|
|
} else {
|
|
Image(systemName: "captions.bubble")
|
|
}
|
|
}
|
|
.disabled(viewModel.selectedSubtitleStreamIndex == -1)
|
|
.foregroundColor(viewModel.selectedSubtitleStreamIndex == -1 ? .gray : .white)
|
|
}
|
|
|
|
if viewModel.shouldShowAutoPlayNextItem {
|
|
Button {
|
|
viewModel.autoPlayNextItem.toggle()
|
|
} label: {
|
|
if viewModel.autoPlayNextItem {
|
|
Image(systemName: "play.circle.fill")
|
|
} else {
|
|
Image(systemName: "play.circle")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Settings Menu
|
|
Menu {
|
|
|
|
Menu {
|
|
ForEach(viewModel.audioStreams, id: \.self) { audioStream in
|
|
Button {
|
|
viewModel.selectedAudioStreamIndex = audioStream.index ?? -1
|
|
} label: {
|
|
if audioStream.index == viewModel.selectedAudioStreamIndex {
|
|
Label.init(audioStream.displayTitle ?? "No Title", systemImage: "checkmark")
|
|
} else {
|
|
Text(audioStream.displayTitle ?? "No Title")
|
|
}
|
|
}
|
|
}
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "speaker.wave.3")
|
|
Text("Audio")
|
|
}
|
|
}
|
|
|
|
Menu {
|
|
ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in
|
|
Button {
|
|
viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1
|
|
} label: {
|
|
if subtitleStream.index == viewModel.selectedSubtitleStreamIndex {
|
|
Label.init(subtitleStream.displayTitle ?? "No Title", systemImage: "checkmark")
|
|
} else {
|
|
Text(subtitleStream.displayTitle ?? "No Title")
|
|
}
|
|
}
|
|
}
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "captions.bubble")
|
|
Text("Subtitles")
|
|
}
|
|
}
|
|
|
|
Menu {
|
|
ForEach(PlaybackSpeed.allCases, id: \.self) { speed in
|
|
Button {
|
|
viewModel.playbackSpeed = speed
|
|
} label: {
|
|
if speed == viewModel.playbackSpeed {
|
|
Label(speed.displayTitle, systemImage: "checkmark")
|
|
} else {
|
|
Text(speed.displayTitle)
|
|
}
|
|
}
|
|
}
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "speedometer")
|
|
Text("Playback Speed")
|
|
}
|
|
}
|
|
|
|
Menu {
|
|
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in
|
|
Button {
|
|
jumpForwardLength = forwardLength
|
|
} label: {
|
|
if forwardLength == jumpForwardLength {
|
|
Label(forwardLength.shortLabel, systemImage: "checkmark")
|
|
} else {
|
|
Text(forwardLength.shortLabel)
|
|
}
|
|
}
|
|
}
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "goforward")
|
|
Text("Jump Forward Length")
|
|
}
|
|
}
|
|
|
|
Menu {
|
|
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { backwardLength in
|
|
Button {
|
|
jumpBackwardLength = backwardLength
|
|
} label: {
|
|
if backwardLength == jumpBackwardLength {
|
|
Label(backwardLength.shortLabel, systemImage: "checkmark")
|
|
} else {
|
|
Text(backwardLength.shortLabel)
|
|
}
|
|
}
|
|
}
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "gobackward")
|
|
Text("Jump Backward Length")
|
|
}
|
|
}
|
|
|
|
} label: {
|
|
Image(systemName: "ellipsis.circle")
|
|
}
|
|
}
|
|
}
|
|
.font(.system(size: 24))
|
|
.frame(height: 50)
|
|
|
|
if let seriesTitle = viewModel.subtitle {
|
|
Text(seriesTitle)
|
|
.font(.subheadline)
|
|
.foregroundColor(Color.gray)
|
|
.alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in
|
|
context[.leading]
|
|
}
|
|
.offset(y: -10)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// MARK: Bottom Bar
|
|
ZStack {
|
|
|
|
LinearGradient(gradient: Gradient(colors: [.clear, .black]),
|
|
startPoint: .top,
|
|
endPoint: .bottom)
|
|
.ignoresSafeArea()
|
|
.frame(height: 70)
|
|
|
|
HStack {
|
|
|
|
HStack {
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectBackward()
|
|
} label: {
|
|
Image(systemName: jumpBackwardLength.backwardImageLabel)
|
|
.padding(.horizontal, 5)
|
|
}
|
|
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectMain()
|
|
} label: {
|
|
mainButtonView
|
|
.frame(minWidth: 30, maxWidth: 30)
|
|
.padding(.horizontal, 10)
|
|
}
|
|
|
|
Button {
|
|
viewModel.playerOverlayDelegate?.didSelectForward()
|
|
} label: {
|
|
Image(systemName: jumpForwardLength.forwardImageLabel)
|
|
.padding(.horizontal, 5)
|
|
}
|
|
}
|
|
.font(.system(size: 24, weight: .semibold, design: .default))
|
|
// .padding(.trailing, 10)
|
|
|
|
Text(viewModel.leftLabelText)
|
|
.font(.system(size: 18, weight: .semibold, design: .default))
|
|
.frame(minWidth: 70, maxWidth: 70)
|
|
|
|
ValueSlider(value: $viewModel.sliderPercentage, onEditingChanged: { editing in
|
|
viewModel.sliderIsScrubbing = editing
|
|
})
|
|
.valueSliderStyle(
|
|
HorizontalValueSliderStyle(track:
|
|
HorizontalValueTrack(view:
|
|
Capsule().foregroundColor(.purple))
|
|
.background(Capsule().foregroundColor(Color.gray.opacity(0.25)))
|
|
.frame(height: 4),
|
|
thumb: Circle().foregroundColor(.purple)
|
|
.onLongPressGesture(perform: {
|
|
print("got it here")
|
|
}),
|
|
thumbSize: CGSize.Circle(radius: viewModel.sliderIsScrubbing ? 25 : 20),
|
|
thumbInteractiveSize: CGSize.Circle(radius: 40),
|
|
options: .defaultOptions)
|
|
)
|
|
|
|
Text(viewModel.rightLabelText)
|
|
.font(.system(size: 18, weight: .semibold, design: .default))
|
|
.frame(minWidth: 70, maxWidth: 70)
|
|
}
|
|
.padding(.horizontal)
|
|
// .frame(maxWidth: 800, maxHeight: 50)
|
|
}
|
|
.frame(maxHeight: 50)
|
|
}
|
|
.ignoresSafeArea(edges: .top)
|
|
.tint(Color.white)
|
|
.foregroundColor(Color.white)
|
|
}
|
|
|
|
var body: some View {
|
|
mainBody
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
viewModel.playerOverlayDelegate?.didGenerallyTap()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
ZStack {
|
|
Color.red
|
|
.ignoresSafeArea()
|
|
|
|
VLCPlayerCompactOverlayView(viewModel: VideoPlayerViewModel(item: BaseItemDto(runTimeTicks: 720 * 10_000_000),
|
|
title: "Glorious Purpose",
|
|
subtitle: "Loki - S1E1",
|
|
streamURL: URL(string: "www.apple.com")!,
|
|
hlsURL: URL(string: "www.apple.com")!,
|
|
response: PlaybackInfoResponse(),
|
|
audioStreams: [MediaStream(displayTitle: "English", index: -1)],
|
|
subtitleStreams: [MediaStream(displayTitle: "None", index: -1)],
|
|
defaultAudioStreamIndex: -1,
|
|
defaultSubtitleStreamIndex: -1,
|
|
playerState: .playing,
|
|
shouldShowGoogleCast: false,
|
|
shouldShowAirplay: false,
|
|
subtitlesEnabled: true,
|
|
sliderPercentage: 0.432,
|
|
selectedAudioStreamIndex: -1,
|
|
selectedSubtitleStreamIndex: -1,
|
|
showAdjacentItems: true,
|
|
shouldShowAutoPlayNextItem: true,
|
|
autoPlayNextItem: true))
|
|
}
|
|
.previewInterfaceOrientation(.landscapeLeft)
|
|
}
|
|
}
|