diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index 0dad2320..49c995c6 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -12,7 +12,7 @@ import JellyfinAPI import UIKit extension BaseItemDto { - func createVideoPlayerViewModel() -> AnyPublisher { + 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 + 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 = 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") - } - - // 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() } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 7b787d15..a043de60 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -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 } diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index f137dd28..3842116f 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -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 diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index 70143ce2..0b493a24 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -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) } diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 59cf368a..25b48078 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -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) + } +} diff --git a/Swiftfin tvOS/Components/ItemDetailsView.swift b/Swiftfin tvOS/Components/ItemDetailsView.swift index 5ab61edb..394bab32 100644 --- a/Swiftfin tvOS/Components/ItemDetailsView.swift +++ b/Swiftfin tvOS/Components/ItemDetailsView.swift @@ -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) + } } } diff --git a/Swiftfin tvOS/Components/MediaPlayButtonRowView.swift b/Swiftfin tvOS/Components/MediaPlayButtonRowView.swift index fc3fc5d1..36378e36 100644 --- a/Swiftfin tvOS/Components/MediaPlayButtonRowView.swift +++ b/Swiftfin tvOS/Components/MediaPlayButtonRowView.swift @@ -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) } diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift index c8c4bf9a..f3c3da8f 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift @@ -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") } diff --git a/Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/tvOSVLCOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/tvOSVLCOverlay.swift index 21e427d2..0aad1deb 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/tvOSVLCOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/tvOSVLCOverlay.swift @@ -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 { diff --git a/Swiftfin/Views/ItemView/ItemViewDetailsView.swift b/Swiftfin/Views/ItemView/ItemViewDetailsView.swift index d9e80bf0..d82df68a 100644 --- a/Swiftfin/Views/ItemView/ItemViewDetailsView.swift +++ b/Swiftfin/Views/ItemView/ItemViewDetailsView.swift @@ -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) } } } diff --git a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift index c8c783aa..c4290d05 100644 --- a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift +++ b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift @@ -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") } diff --git a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift index 88120eaa..7a58de68 100644 --- a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift +++ b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift @@ -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) + } } } } diff --git a/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift b/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift index b7480f12..2ae40695 100644 --- a/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift +++ b/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift @@ -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") } diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerOverlayView.swift index be9389e1..66c94fa9 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerOverlayView.swift @@ -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 { diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 2b050861..77dcbd36 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ