Merge branch 'main' into add-m1-to-device-builder
This commit is contained in:
commit
b087adf0e6
|
@ -12,7 +12,7 @@ import JellyfinAPI
|
|||
import UIKit
|
||||
|
||||
extension BaseItemDto {
|
||||
func createVideoPlayerViewModel() -> AnyPublisher<VideoPlayerViewModel, Error> {
|
||||
func createVideoPlayerViewModel() -> AnyPublisher<[VideoPlayerViewModel], Error> {
|
||||
|
||||
LogManager.shared.log.debug("Creating video player view model for item: \(id ?? "")")
|
||||
|
||||
|
@ -33,79 +33,95 @@ extension BaseItemDto {
|
|||
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
||||
autoOpenLiveStream: true,
|
||||
playbackInfoDto: playbackInfo)
|
||||
.map { response -> VideoPlayerViewModel in
|
||||
let mediaSource = response.mediaSources!.first!
|
||||
.map { response -> [VideoPlayerViewModel] in
|
||||
let mediaSources = response.mediaSources!
|
||||
|
||||
let audioStreams = mediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
||||
let subtitleStreams = mediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
||||
var viewModels: [VideoPlayerViewModel] = []
|
||||
|
||||
let defaultAudioStream = audioStreams.first(where: { $0.index! == mediaSource.defaultAudioStreamIndex! })
|
||||
for currentMediaSource in mediaSources {
|
||||
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
||||
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
||||
|
||||
let defaultSubtitleStream = subtitleStreams.first(where: { $0.index! == mediaSource.defaultSubtitleStreamIndex ?? -1 })
|
||||
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
||||
|
||||
// MARK: Stream
|
||||
let defaultSubtitleStream = subtitleStreams
|
||||
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
||||
|
||||
var streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)!
|
||||
var streamURL: URLComponents
|
||||
let streamType: ServerStreamType
|
||||
|
||||
let streamType: ServerStreamType
|
||||
if let transcodeURL = currentMediaSource.transcodingUrl {
|
||||
streamType = .transcode
|
||||
streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI.appending(transcodeURL))!
|
||||
} else {
|
||||
streamType = .direct
|
||||
streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)!
|
||||
streamURL.path = "/Videos/\(self.id!)/stream"
|
||||
streamURL.addQueryItem(name: "Static", value: "true")
|
||||
streamURL.addQueryItem(name: "MediaSourceId", value: self.id!)
|
||||
streamURL.addQueryItem(name: "Tag", value: self.etag)
|
||||
streamURL.addQueryItem(name: "MinSegments", value: "6")
|
||||
|
||||
if let transcodeURL = mediaSource.transcodingUrl {
|
||||
streamType = .transcode
|
||||
streamURL.path = transcodeURL
|
||||
} else {
|
||||
streamType = .direct
|
||||
streamURL.path = "/Videos/\(self.id!)/stream"
|
||||
}
|
||||
|
||||
streamURL.addQueryItem(name: "Static", value: "true")
|
||||
streamURL.addQueryItem(name: "MediaSourceId", value: self.id!)
|
||||
streamURL.addQueryItem(name: "Tag", value: self.etag)
|
||||
streamURL.addQueryItem(name: "MinSegments", value: "6")
|
||||
|
||||
// MARK: VidoPlayerViewModel Creation
|
||||
|
||||
var subtitle: String?
|
||||
|
||||
// MARK: Attach media content to self
|
||||
|
||||
var modifiedSelfItem = self
|
||||
modifiedSelfItem.mediaStreams = mediaSource.mediaStreams
|
||||
|
||||
// TODO: other forms of media subtitle
|
||||
if self.itemType == .episode {
|
||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||
subtitle = "\(seriesName) - \(episodeLocator)"
|
||||
if mediaSources.count > 1 {
|
||||
streamURL.addQueryItem(name: "MediaSourceId", value: currentMediaSource.id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: VidoPlayerViewModel Creation
|
||||
|
||||
var subtitle: String?
|
||||
|
||||
// MARK: Attach media content to self
|
||||
|
||||
var modifiedSelfItem = self
|
||||
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
||||
|
||||
// TODO: other forms of media subtitle
|
||||
if self.itemType == .episode {
|
||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||
subtitle = "\(seriesName) - \(episodeLocator)"
|
||||
}
|
||||
}
|
||||
|
||||
let subtitlesEnabled = defaultSubtitleStream != nil
|
||||
|
||||
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
||||
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
||||
|
||||
let overlayType = Defaults[.overlayType]
|
||||
|
||||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||
|
||||
var fileName: String?
|
||||
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
||||
fileName = String(lastInPath)
|
||||
}
|
||||
|
||||
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem,
|
||||
title: modifiedSelfItem.name ?? "",
|
||||
subtitle: subtitle,
|
||||
streamURL: streamURL.url!,
|
||||
streamType: streamType,
|
||||
response: response,
|
||||
audioStreams: audioStreams,
|
||||
subtitleStreams: subtitleStreams,
|
||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||
subtitlesEnabled: subtitlesEnabled,
|
||||
autoplayEnabled: autoplayEnabled,
|
||||
overlayType: overlayType,
|
||||
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
||||
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
||||
shouldShowAutoPlay: shouldShowAutoPlay,
|
||||
container: currentMediaSource.container ?? "",
|
||||
filename: fileName,
|
||||
versionName: currentMediaSource.name)
|
||||
|
||||
viewModels.append(videoPlayerViewModel)
|
||||
}
|
||||
|
||||
let subtitlesEnabled = defaultSubtitleStream != nil
|
||||
|
||||
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
||||
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
||||
|
||||
let overlayType = Defaults[.overlayType]
|
||||
|
||||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||
|
||||
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem,
|
||||
title: modifiedSelfItem.name ?? "",
|
||||
subtitle: subtitle,
|
||||
streamURL: streamURL.url!,
|
||||
streamType: streamType,
|
||||
response: response,
|
||||
audioStreams: audioStreams,
|
||||
subtitleStreams: subtitleStreams,
|
||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||
subtitlesEnabled: subtitlesEnabled,
|
||||
autoplayEnabled: autoplayEnabled,
|
||||
overlayType: overlayType,
|
||||
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
||||
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
||||
shouldShowAutoPlay: shouldShowAutoPlay)
|
||||
|
||||
return videoPlayerViewModel
|
||||
return viewModels
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
|
|
@ -265,11 +265,6 @@ public extension BaseItemDto {
|
|||
func createMediaItems() -> [ItemDetail] {
|
||||
var mediaItems: [ItemDetail] = []
|
||||
|
||||
if let container = container {
|
||||
let containerList = container.split(separator: ",").joined(separator: ", ")
|
||||
mediaItems.append(ItemDetail(title: L10n.containers, content: containerList))
|
||||
}
|
||||
|
||||
if let mediaStreams = mediaStreams {
|
||||
let audioStreams = mediaStreams.filter { $0.type == .audio }
|
||||
let subtitleStreams = mediaStreams.filter { $0.type == .subtitle }
|
||||
|
|
|
@ -102,6 +102,8 @@ internal enum L10n {
|
|||
internal static let experimental = L10n.tr("Localizable", "experimental")
|
||||
/// Favorites
|
||||
internal static let favorites = L10n.tr("Localizable", "favorites")
|
||||
/// File
|
||||
internal static let file = L10n.tr("Localizable", "file")
|
||||
/// Filter Results
|
||||
internal static let filterResults = L10n.tr("Localizable", "filterResults")
|
||||
/// Filters
|
||||
|
|
|
@ -33,8 +33,8 @@ class ItemViewModel: ViewModel {
|
|||
@Published
|
||||
var informationItems: [BaseItemDto.ItemDetail]
|
||||
@Published
|
||||
var mediaItems: [BaseItemDto.ItemDetail]
|
||||
var itemVideoPlayerViewModel: VideoPlayerViewModel?
|
||||
var selectedVideoPlayerViewModel: VideoPlayerViewModel?
|
||||
var videoPlayerViewModels: [VideoPlayerViewModel] = []
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
|
@ -48,7 +48,6 @@ class ItemViewModel: ViewModel {
|
|||
}
|
||||
|
||||
informationItems = item.createInformationItems()
|
||||
mediaItems = item.createMediaItems()
|
||||
|
||||
isFavorited = item.userData?.isFavorite ?? false
|
||||
isWatched = item.userData?.played ?? false
|
||||
|
@ -84,9 +83,9 @@ class ItemViewModel: ViewModel {
|
|||
item.createVideoPlayerViewModel()
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { videoPlayerViewModel in
|
||||
self.itemVideoPlayerViewModel = videoPlayerViewModel
|
||||
self.mediaItems = videoPlayerViewModel.item.createMediaItems()
|
||||
} receiveValue: { viewModels in
|
||||
self.videoPlayerViewModels = viewModels
|
||||
self.selectedVideoPlayerViewModel = viewModels.first
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
|
|
@ -91,6 +91,9 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
@Published
|
||||
var mediaItems: [BaseItemDto.ItemDetail]
|
||||
|
||||
// MARK: ShouldShowItems
|
||||
|
||||
let shouldShowPlayPreviousItem: Bool
|
||||
|
@ -110,6 +113,9 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
let jumpGesturesEnabled: Bool
|
||||
let resumeOffset: Bool
|
||||
let streamType: ServerStreamType
|
||||
let container: String
|
||||
let filename: String?
|
||||
let versionName: String?
|
||||
|
||||
// MARK: Experimental
|
||||
|
||||
|
@ -173,7 +179,10 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
overlayType: OverlayType,
|
||||
shouldShowPlayPreviousItem: Bool,
|
||||
shouldShowPlayNextItem: Bool,
|
||||
shouldShowAutoPlay: Bool)
|
||||
shouldShowAutoPlay: Bool,
|
||||
container: String,
|
||||
filename: String?,
|
||||
versionName: String?)
|
||||
{
|
||||
self.item = item
|
||||
self.title = title
|
||||
|
@ -191,6 +200,9 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
self.shouldShowPlayPreviousItem = shouldShowPlayPreviousItem
|
||||
self.shouldShowPlayNextItem = shouldShowPlayNextItem
|
||||
self.shouldShowAutoPlay = shouldShowAutoPlay
|
||||
self.container = container
|
||||
self.filename = filename
|
||||
self.versionName = versionName
|
||||
|
||||
self.jumpBackwardLength = Defaults[.videoPlayerJumpBackward]
|
||||
self.jumpForwardLength = Defaults[.videoPlayerJumpForward]
|
||||
|
@ -203,6 +215,8 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
|
||||
self.confirmClose = Defaults[.confirmClose]
|
||||
|
||||
self.mediaItems = item.createMediaItems()
|
||||
|
||||
super.init()
|
||||
|
||||
self.sliderPercentage = (item.userData?.playedPercentage ?? 0) / 100
|
||||
|
@ -283,11 +297,13 @@ extension VideoPlayerViewModel {
|
|||
nextItem.createVideoPlayerViewModel()
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { videoPlayerViewModel in
|
||||
videoPlayerViewModel.matchSubtitleStream(with: self)
|
||||
videoPlayerViewModel.matchAudioStream(with: self)
|
||||
} receiveValue: { viewModels in
|
||||
for viewModel in viewModels {
|
||||
viewModel.matchSubtitleStream(with: self)
|
||||
viewModel.matchAudioStream(with: self)
|
||||
}
|
||||
|
||||
self.nextItemVideoPlayerViewModel = videoPlayerViewModel
|
||||
self.nextItemVideoPlayerViewModel = viewModels.first
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
} else {
|
||||
|
@ -297,11 +313,13 @@ extension VideoPlayerViewModel {
|
|||
previousItem.createVideoPlayerViewModel()
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { videoPlayerViewModel in
|
||||
videoPlayerViewModel.matchSubtitleStream(with: self)
|
||||
videoPlayerViewModel.matchAudioStream(with: self)
|
||||
} receiveValue: { viewModels in
|
||||
for viewModel in viewModels {
|
||||
viewModel.matchSubtitleStream(with: self)
|
||||
viewModel.matchAudioStream(with: self)
|
||||
}
|
||||
|
||||
self.previousItemVideoPlayerViewModel = videoPlayerViewModel
|
||||
self.previousItemVideoPlayerViewModel = viewModels.first
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
|
@ -314,22 +332,26 @@ extension VideoPlayerViewModel {
|
|||
previousItem.createVideoPlayerViewModel()
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { videoPlayerViewModel in
|
||||
videoPlayerViewModel.matchSubtitleStream(with: self)
|
||||
videoPlayerViewModel.matchAudioStream(with: self)
|
||||
} receiveValue: { viewModels in
|
||||
for viewModel in viewModels {
|
||||
viewModel.matchSubtitleStream(with: self)
|
||||
viewModel.matchAudioStream(with: self)
|
||||
}
|
||||
|
||||
self.previousItemVideoPlayerViewModel = videoPlayerViewModel
|
||||
self.previousItemVideoPlayerViewModel = viewModels.first
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
nextItem.createVideoPlayerViewModel()
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { videoPlayerViewModel in
|
||||
videoPlayerViewModel.matchSubtitleStream(with: self)
|
||||
videoPlayerViewModel.matchAudioStream(with: self)
|
||||
} receiveValue: { viewModels in
|
||||
for viewModel in viewModels {
|
||||
viewModel.matchSubtitleStream(with: self)
|
||||
viewModel.matchAudioStream(with: self)
|
||||
}
|
||||
|
||||
self.nextItemVideoPlayerViewModel = videoPlayerViewModel
|
||||
self.nextItemVideoPlayerViewModel = viewModels.first
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
|
@ -564,3 +586,15 @@ extension VideoPlayerViewModel: Equatable {
|
|||
lhs.item.userData?.playbackPositionTicks == rhs.item.userData?.playbackPositionTicks
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Hashable
|
||||
|
||||
extension VideoPlayerViewModel: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(item)
|
||||
hasher.combine(streamURL)
|
||||
hasher.combine(filename)
|
||||
hasher.combine(versionName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,13 +35,15 @@ struct ItemDetailsView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
L10n.media.text
|
||||
.font(.title3)
|
||||
.padding(.bottom, 5)
|
||||
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
L10n.media.text
|
||||
.font(.title3)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
ForEach(viewModel.mediaItems, id: \.self.title) { mediaItem in
|
||||
ItemDetail(title: mediaItem.title, content: mediaItem.content)
|
||||
ForEach(selectedVideoPlayerViewModel.mediaItems, id: \.self.title) { mediaItem in
|
||||
ItemDetail(title: mediaItem.title, content: mediaItem.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ struct MediaPlayButtonRowView: View {
|
|||
HStack {
|
||||
VStack {
|
||||
Button {
|
||||
itemRouter.route(to: \.videoPlayer, viewModel.itemVideoPlayerViewModel!)
|
||||
itemRouter.route(to: \.videoPlayer, viewModel.selectedVideoPlayerViewModel!)
|
||||
} label: {
|
||||
MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView)
|
||||
}
|
||||
|
|
|
@ -59,8 +59,8 @@ struct CinematicItemViewTopRow: View {
|
|||
// MARK: Play
|
||||
|
||||
Button {
|
||||
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
|
||||
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
|
||||
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
|
||||
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
|
||||
} else {
|
||||
LogManager.shared.log.error("Attempted to play item but no playback information available")
|
||||
}
|
||||
|
@ -81,9 +81,9 @@ struct CinematicItemViewTopRow: View {
|
|||
.contextMenu {
|
||||
if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 {
|
||||
Button {
|
||||
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
|
||||
itemVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
|
||||
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
|
||||
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
|
||||
selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
|
||||
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
|
||||
} else {
|
||||
LogManager.shared.log.error("Attempted to play item but no playback information available")
|
||||
}
|
||||
|
|
|
@ -153,7 +153,10 @@ struct tvOSVLCOverlay_Previews: PreviewProvider {
|
|||
overlayType: .compact,
|
||||
shouldShowPlayPreviousItem: true,
|
||||
shouldShowPlayNextItem: true,
|
||||
shouldShowAutoPlay: true)
|
||||
shouldShowAutoPlay: true,
|
||||
container: "",
|
||||
filename: nil,
|
||||
versionName: nil)
|
||||
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
|
|
|
@ -36,20 +36,34 @@ struct ItemViewDetailsView: View {
|
|||
.padding(.bottom, 20)
|
||||
}
|
||||
|
||||
if !viewModel.mediaItems.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
L10n.media.text
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
L10n.media.text
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
|
||||
ForEach(viewModel.mediaItems, id: \.self.title) { mediaItem in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(mediaItem.title)
|
||||
.font(.subheadline)
|
||||
Text(mediaItem.content)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
L10n.file.text
|
||||
.font(.subheadline)
|
||||
Text(viewModel.selectedVideoPlayerViewModel?.filename ?? "--")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
L10n.containers.text
|
||||
.font(.subheadline)
|
||||
Text(viewModel.selectedVideoPlayerViewModel?.container ?? "--")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
|
||||
ForEach(viewModel.selectedVideoPlayerViewModel?.mediaItems ?? [], id: \.self.title) { mediaItem in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(mediaItem.title)
|
||||
.font(.subheadline)
|
||||
Text(mediaItem.content)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ struct ItemLandscapeMainView: View {
|
|||
// MARK: Play
|
||||
|
||||
Button {
|
||||
self.itemRouter.route(to: \.videoPlayer, viewModel.itemVideoPlayerViewModel!)
|
||||
self.itemRouter.route(to: \.videoPlayer, viewModel.selectedVideoPlayerViewModel!)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "play.fill")
|
||||
|
@ -49,13 +49,13 @@ struct ItemLandscapeMainView: View {
|
|||
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.disabled(viewModel.playButtonItem == nil || viewModel.itemVideoPlayerViewModel == nil)
|
||||
.disabled(viewModel.playButtonItem == nil || viewModel.selectedVideoPlayerViewModel == nil)
|
||||
.contextMenu {
|
||||
if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 {
|
||||
Button {
|
||||
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
|
||||
itemVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
|
||||
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
|
||||
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
|
||||
selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
|
||||
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
|
||||
} else {
|
||||
LogManager.shared.log.error("Attempted to play item but no playback information available")
|
||||
}
|
||||
|
|
|
@ -99,7 +99,31 @@ struct ItemLandscapeTopBarView: View {
|
|||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
.padding(.leading)
|
||||
|
||||
if viewModel.videoPlayerViewModels.count > 1 {
|
||||
Menu {
|
||||
ForEach(viewModel.videoPlayerViewModels, id: \.versionName) { viewModelOption in
|
||||
Button {
|
||||
viewModel.selectedVideoPlayerViewModel = viewModelOption
|
||||
} label: {
|
||||
if viewModelOption.versionName == viewModel.selectedVideoPlayerViewModel?.versionName {
|
||||
Label(viewModelOption.versionName ?? L10n.noTitle, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(viewModelOption.versionName ?? L10n.noTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 5) {
|
||||
Text(viewModel.selectedVideoPlayerViewModel?.versionName ?? L10n.noTitle)
|
||||
.fontWeight(.semibold)
|
||||
.fixedSize()
|
||||
Image(systemName: "chevron.down")
|
||||
}
|
||||
}
|
||||
.padding(.leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,29 @@ struct PortraitHeaderOverlayView: View {
|
|||
.stroke(Color.secondary, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.videoPlayerViewModels.count > 1 {
|
||||
Menu {
|
||||
ForEach(viewModel.videoPlayerViewModels, id: \.versionName) { viewModelOption in
|
||||
Button {
|
||||
viewModel.selectedVideoPlayerViewModel = viewModelOption
|
||||
} label: {
|
||||
if viewModelOption.versionName == viewModel.selectedVideoPlayerViewModel?.versionName {
|
||||
Label(viewModelOption.versionName ?? L10n.noTitle, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(viewModelOption.versionName ?? L10n.noTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 5) {
|
||||
Text(viewModel.selectedVideoPlayerViewModel?.versionName ?? L10n.noTitle)
|
||||
.fontWeight(.semibold)
|
||||
.fixedSize()
|
||||
Image(systemName: "chevron.down")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30)
|
||||
}
|
||||
|
@ -90,8 +113,8 @@ struct PortraitHeaderOverlayView: View {
|
|||
// MARK: Play
|
||||
|
||||
Button {
|
||||
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
|
||||
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
|
||||
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
|
||||
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
|
||||
} else {
|
||||
LogManager.shared.log.error("Attempted to play item but no playback information available")
|
||||
}
|
||||
|
@ -109,13 +132,13 @@ struct PortraitHeaderOverlayView: View {
|
|||
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.disabled(viewModel.playButtonItem == nil)
|
||||
.disabled(viewModel.playButtonItem == nil || viewModel.selectedVideoPlayerViewModel == nil)
|
||||
.contextMenu {
|
||||
if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 {
|
||||
Button {
|
||||
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
|
||||
itemVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
|
||||
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
|
||||
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
|
||||
selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
|
||||
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
|
||||
} else {
|
||||
LogManager.shared.log.error("Attempted to play item but no playback information available")
|
||||
}
|
||||
|
|
|
@ -395,7 +395,10 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
|||
overlayType: .compact,
|
||||
shouldShowPlayPreviousItem: true,
|
||||
shouldShowPlayNextItem: true,
|
||||
shouldShowAutoPlay: true)
|
||||
shouldShowAutoPlay: true,
|
||||
container: "",
|
||||
filename: nil,
|
||||
versionName: nil)
|
||||
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
|
|
|
@ -343,6 +343,13 @@ extension VLCPlayerViewController {
|
|||
}
|
||||
|
||||
viewModel = newViewModel
|
||||
|
||||
switch viewModel.streamType {
|
||||
case .transcode:
|
||||
LogManager.shared.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")")
|
||||
case .direct:
|
||||
LogManager.shared.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: startPlayback
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue