Merge branch 'main' into swiftformat-third-times-the-charm
This commit is contained in:
commit
071d07d5ff
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
#if os(tvOS)
|
||||
import TVVLCKit
|
||||
#else
|
||||
import MobileVLCKit
|
||||
#endif
|
||||
|
||||
extension VLCMediaPlayer {
|
||||
/// Applies font size to the player
|
||||
///
|
||||
/// This is pretty hacky until VLCKit 4 has a public API to support this
|
||||
func setSubtitleSize(_ size: SubtitleSize) {
|
||||
perform(
|
||||
Selector(("setTextRendererFontSize:")),
|
||||
with: size.textRendererFontSize
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Defaults
|
||||
|
||||
enum SubtitleSize: Int32, CaseIterable, Defaults.Serializable {
|
||||
case smallest
|
||||
case smaller
|
||||
case regular
|
||||
case larger
|
||||
case largest
|
||||
}
|
||||
|
||||
// MARK: - appearance
|
||||
|
||||
extension SubtitleSize {
|
||||
var label: String {
|
||||
switch self {
|
||||
case .smallest:
|
||||
return "Smallest"
|
||||
case .smaller:
|
||||
return "Smaller"
|
||||
case .regular:
|
||||
return "Regular"
|
||||
case .larger:
|
||||
return "Larger"
|
||||
case .largest:
|
||||
return "Largest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - sizing for VLC
|
||||
|
||||
extension SubtitleSize {
|
||||
/// Value to be passed to VLCKit (via hacky internal property, until VLCKit 4)
|
||||
///
|
||||
/// note that it doesn't correspond to actual font sizes; a smaller int creates bigger text
|
||||
var textRendererFontSize: Int {
|
||||
switch self {
|
||||
case .smallest:
|
||||
return 24
|
||||
case .smaller:
|
||||
return 20
|
||||
case .regular:
|
||||
return 16
|
||||
case .larger:
|
||||
return 12
|
||||
case .largest:
|
||||
return 8
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,51 +25,47 @@ extension SwiftfinStore {
|
|||
|
||||
extension Defaults.Keys {
|
||||
|
||||
// Universal settings
|
||||
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
|
||||
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
|
||||
// Universal settings
|
||||
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
|
||||
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
|
||||
|
||||
// General settings
|
||||
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto",
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
// General settings
|
||||
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
|
||||
// Customize settings
|
||||
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
// Customize settings
|
||||
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
|
||||
// Video player / overlay settings
|
||||
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
// Video player / overlay settings
|
||||
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
|
||||
// Should show video player items
|
||||
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
// Should show video player items
|
||||
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
|
||||
// Should show video player items in overlay menu
|
||||
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
// Should show video player items in overlay menu
|
||||
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
|
||||
// Experimental settings
|
||||
enum Experimental {
|
||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
}
|
||||
// Experimental settings
|
||||
struct Experimental {
|
||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
}
|
||||
|
||||
// tvos specific
|
||||
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
// tvos specific
|
||||
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
}
|
||||
|
|
|
@ -13,126 +13,127 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var settingsRouter: SettingsCoordinator.Router
|
||||
@ObservedObject
|
||||
var viewModel: SettingsViewModel
|
||||
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
@Default(.autoSelectAudioLangCode)
|
||||
var autoSelectAudioLangcode
|
||||
@Default(.videoPlayerJumpForward)
|
||||
var jumpForwardLength
|
||||
@Default(.videoPlayerJumpBackward)
|
||||
var jumpBackwardLength
|
||||
@Default(.downActionShowsMenu)
|
||||
var downActionShowsMenu
|
||||
@Default(.confirmClose)
|
||||
var confirmClose
|
||||
@Default(.tvOSCinematicViews)
|
||||
var tvOSCinematicViews
|
||||
@Default(.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.resumeOffset)
|
||||
var resumeOffset
|
||||
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode
|
||||
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||
@Default(.downActionShowsMenu) var downActionShowsMenu
|
||||
@Default(.confirmClose) var confirmClose
|
||||
@Default(.tvOSCinematicViews) var tvOSCinematicViews
|
||||
@Default(.showPosterLabels) var showPosterLabels
|
||||
@Default(.resumeOffset) var resumeOffset
|
||||
@Default(.subtitleSize) var subtitleSize
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { reader in
|
||||
HStack {
|
||||
var body: some View {
|
||||
GeometryReader { reader in
|
||||
HStack {
|
||||
|
||||
Image(uiImage: UIImage(named: "App Icon")!)
|
||||
.cornerRadius(30)
|
||||
.scaleEffect(2)
|
||||
.frame(width: reader.size.width / 2)
|
||||
Image(uiImage: UIImage(named: "App Icon")!)
|
||||
.cornerRadius(30)
|
||||
.scaleEffect(2)
|
||||
.frame(width: reader.size.width / 2)
|
||||
|
||||
Form {
|
||||
Section(header: EmptyView()) {
|
||||
Form {
|
||||
Section(header: EmptyView()) {
|
||||
|
||||
Button {} label: {
|
||||
HStack {
|
||||
Text("User")
|
||||
Spacer()
|
||||
Text(viewModel.user.username)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.serverDetail)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Server")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(viewModel.server.name)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("User")
|
||||
Spacer()
|
||||
Text(viewModel.user.username)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
}
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
settingsRouter.route(to: \.serverDetail)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Server")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(viewModel.server.name)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
|
||||
Button {
|
||||
SessionManager.main.logout()
|
||||
} label: {
|
||||
Text("Switch User")
|
||||
.foregroundColor(Color.jellyfinPurple)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Video Player")) {
|
||||
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
SessionManager.main.logout()
|
||||
} label: {
|
||||
Text("Switch User")
|
||||
.foregroundColor(Color.jellyfinPurple)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Video Player")) {
|
||||
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Resume 5 Second Offset", isOn: $resumeOffset)
|
||||
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Press Down for Menu", isOn: $downActionShowsMenu)
|
||||
Toggle("Resume 5 Second Offset", isOn: $resumeOffset)
|
||||
|
||||
Toggle("Confirm Close", isOn: $confirmClose)
|
||||
Toggle("Press Down for Menu", isOn: $downActionShowsMenu)
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.overlaySettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Overlay")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
Toggle("Confirm Close", isOn: $confirmClose)
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.experimentalSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Experimental")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
settingsRouter.route(to: \.overlaySettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Overlay")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Cinematic Views", isOn: $tvOSCinematicViews)
|
||||
Toggle("Show Poster Labels", isOn: $showPosterLabels)
|
||||
Button {
|
||||
settingsRouter.route(to: \.experimentalSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Experimental")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} header: {
|
||||
Text("Appearance")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
Toggle("Cinematic Views", isOn: $tvOSCinematicViews)
|
||||
} header: {
|
||||
Text("Appearance")
|
||||
}
|
||||
|
||||
Section(header: L10n.accessibility.text) {
|
||||
Toggle("Show Poster Labels", isOn: $showPosterLabels)
|
||||
|
||||
Picker("Subtitle size", selection: $subtitleSize) {
|
||||
ForEach(SubtitleSize.allCases, id: \.self) { size in
|
||||
Text(size.label).tag(size.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
|
|
|
@ -382,140 +382,138 @@ class VLCPlayerViewController: UIViewController {
|
|||
|
||||
extension VLCPlayerViewController {
|
||||
|
||||
/// Main function that handles setting up the media player with the current VideoPlayerViewModel
|
||||
/// and also takes the role of setting the 'viewModel' property with the given viewModel
|
||||
///
|
||||
/// Use case for this is setting new media within the same VLCPlayerViewController
|
||||
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
|
||||
/// Main function that handles setting up the media player with the current VideoPlayerViewModel
|
||||
/// and also takes the role of setting the 'viewModel' property with the given viewModel
|
||||
///
|
||||
/// Use case for this is setting new media within the same VLCPlayerViewController
|
||||
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
|
||||
|
||||
// remove old player
|
||||
// remove old player
|
||||
|
||||
if vlcMediaPlayer.media != nil {
|
||||
viewModelListeners.forEach { $0.cancel() }
|
||||
if vlcMediaPlayer.media != nil {
|
||||
viewModelListeners.forEach({ $0.cancel() })
|
||||
|
||||
vlcMediaPlayer.stop()
|
||||
viewModel.sendStopReport()
|
||||
viewModel.playerOverlayDelegate = nil
|
||||
}
|
||||
vlcMediaPlayer.stop()
|
||||
viewModel.sendStopReport()
|
||||
viewModel.playerOverlayDelegate = nil
|
||||
}
|
||||
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
|
||||
// setup with new player and view model
|
||||
// setup with new player and view model
|
||||
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
|
||||
vlcMediaPlayer.delegate = self
|
||||
vlcMediaPlayer.drawable = videoContentView
|
||||
vlcMediaPlayer.delegate = self
|
||||
vlcMediaPlayer.drawable = videoContentView
|
||||
|
||||
// TODO: Custom subtitle sizes
|
||||
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
|
||||
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
|
||||
|
||||
stopOverlayDismissTimer()
|
||||
stopOverlayDismissTimer()
|
||||
|
||||
// Stop current media if there is one
|
||||
if vlcMediaPlayer.media != nil {
|
||||
viewModelListeners.forEach { $0.cancel() }
|
||||
// Stop current media if there is one
|
||||
if vlcMediaPlayer.media != nil {
|
||||
viewModelListeners.forEach({ $0.cancel() })
|
||||
|
||||
vlcMediaPlayer.stop()
|
||||
viewModel.sendStopReport()
|
||||
viewModel.playerOverlayDelegate = nil
|
||||
}
|
||||
vlcMediaPlayer.stop()
|
||||
viewModel.sendStopReport()
|
||||
viewModel.playerOverlayDelegate = nil
|
||||
}
|
||||
|
||||
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
|
||||
// TODO: Custom buffer/cache amounts
|
||||
// TODO: Custom buffer/cache amounts
|
||||
|
||||
let media = VLCMedia(url: newViewModel.streamURL)
|
||||
media.addOption("--prefetch-buffer-size=1048576")
|
||||
media.addOption("--network-caching=5000")
|
||||
let media = VLCMedia(url: newViewModel.streamURL)
|
||||
media.addOption("--prefetch-buffer-size=1048576")
|
||||
media.addOption("--network-caching=5000")
|
||||
|
||||
vlcMediaPlayer.media = media
|
||||
vlcMediaPlayer.media = media
|
||||
|
||||
setupOverlayHostingController(viewModel: newViewModel)
|
||||
setupViewModelListeners(viewModel: newViewModel)
|
||||
setupOverlayHostingController(viewModel: newViewModel)
|
||||
setupViewModelListeners(viewModel: newViewModel)
|
||||
|
||||
newViewModel.getAdjacentEpisodes()
|
||||
newViewModel.playerOverlayDelegate = self
|
||||
newViewModel.getAdjacentEpisodes()
|
||||
newViewModel.playerOverlayDelegate = self
|
||||
|
||||
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
|
||||
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
|
||||
|
||||
if startPercentage > 0 {
|
||||
if viewModel.resumeOffset {
|
||||
let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
var startSeconds = round((startPercentage / 100) * videoDurationSeconds)
|
||||
startSeconds = startSeconds.subtract(5, floor: 0)
|
||||
let newStartPercentage = startSeconds / videoDurationSeconds
|
||||
newViewModel.sliderPercentage = newStartPercentage
|
||||
} else {
|
||||
newViewModel.sliderPercentage = startPercentage / 100
|
||||
}
|
||||
}
|
||||
if startPercentage > 0 {
|
||||
if viewModel.resumeOffset {
|
||||
let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
var startSeconds = round((startPercentage / 100) * videoDurationSeconds)
|
||||
startSeconds = startSeconds.subtract(5, floor: 0)
|
||||
let newStartPercentage = startSeconds / videoDurationSeconds
|
||||
newViewModel.sliderPercentage = newStartPercentage
|
||||
} else {
|
||||
newViewModel.sliderPercentage = startPercentage / 100
|
||||
}
|
||||
}
|
||||
|
||||
viewModel = newViewModel
|
||||
}
|
||||
viewModel = newViewModel
|
||||
}
|
||||
|
||||
// MARK: startPlayback
|
||||
// MARK: startPlayback
|
||||
func startPlayback() {
|
||||
vlcMediaPlayer.play()
|
||||
|
||||
func startPlayback() {
|
||||
vlcMediaPlayer.play()
|
||||
// Setup external subtitles
|
||||
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
|
||||
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
|
||||
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup external subtitles
|
||||
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
|
||||
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
|
||||
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
|
||||
}
|
||||
}
|
||||
setMediaPlayerTimeAtCurrentSlider()
|
||||
|
||||
setMediaPlayerTimeAtCurrentSlider()
|
||||
viewModel.sendPlayReport()
|
||||
|
||||
viewModel.sendPlayReport()
|
||||
restartOverlayDismissTimer(interval: 5)
|
||||
}
|
||||
|
||||
restartOverlayDismissTimer(interval: 5)
|
||||
}
|
||||
// MARK: setupViewModelListeners
|
||||
|
||||
// MARK: setupViewModelListeners
|
||||
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
||||
viewModel.$playbackSpeed.sink { newSpeed in
|
||||
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
||||
viewModel.$playbackSpeed.sink { newSpeed in
|
||||
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
||||
if sliderIsScrubbing {
|
||||
self.didBeginScrubbing()
|
||||
} else {
|
||||
self.didEndScrubbing()
|
||||
}
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
||||
if sliderIsScrubbing {
|
||||
self.didBeginScrubbing()
|
||||
} else {
|
||||
self.didEndScrubbing()
|
||||
}
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
||||
self.didSelectAudioStream(index: newAudioStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
||||
self.didSelectAudioStream(index: newAudioStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
||||
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
||||
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
|
||||
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
|
||||
}.store(in: &viewModelListeners)
|
||||
}
|
||||
|
||||
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
|
||||
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
|
||||
}.store(in: &viewModelListeners)
|
||||
}
|
||||
func setMediaPlayerTimeAtCurrentSlider() {
|
||||
// Necessary math as VLCMediaPlayer doesn't work well
|
||||
// by just setting the position
|
||||
let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000)
|
||||
let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration)
|
||||
let newPositionOffset = secondsScrubbedTo - videoPosition
|
||||
|
||||
func setMediaPlayerTimeAtCurrentSlider() {
|
||||
// Necessary math as VLCMediaPlayer doesn't work well
|
||||
// by just setting the position
|
||||
let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000)
|
||||
let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration)
|
||||
let newPositionOffset = secondsScrubbedTo - videoPosition
|
||||
|
||||
if newPositionOffset > 0 {
|
||||
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
|
||||
} else {
|
||||
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
|
||||
}
|
||||
}
|
||||
if newPositionOffset > 0 {
|
||||
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
|
||||
} else {
|
||||
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Show/Hide Overlay
|
||||
|
|
|
@ -138,6 +138,11 @@
|
|||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */; };
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
|
||||
5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
|
||||
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
|
||||
5D1603FE278A40DC00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
|
||||
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; };
|
||||
5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; };
|
||||
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */; };
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; };
|
||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||
|
@ -572,6 +577,8 @@
|
|||
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemView.swift; sourceTree = "<group>"; };
|
||||
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
|
||||
5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSize.swift; sourceTree = "<group>"; };
|
||||
5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VLCPlayer+subtitles.swift"; sourceTree = "<group>"; };
|
||||
5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingSwizzling.swift; sourceTree = "<group>"; };
|
||||
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
|
||||
|
@ -977,6 +984,7 @@
|
|||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
||||
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
|
||||
E10D87DD278510E300BD264C /* PosterSize.swift */,
|
||||
5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */,
|
||||
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
|
||||
535870AC2669D8DD00D05A09 /* Typings.swift */,
|
||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
|
||||
|
@ -1035,6 +1043,7 @@
|
|||
62ECA01926FA6D6900E8EBB7 /* AppURLHandler */,
|
||||
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
||||
53F866422687A45400DCD1D7 /* Components */,
|
||||
5D160401278A41BA00D22B99 /* Extensions */,
|
||||
5377CC02263B596B003A4E83 /* Info.plist */,
|
||||
E13D02842788B634000FCB04 /* Swiftfin.entitlements */,
|
||||
5377CBFA263B596B003A4E83 /* Preview Content */,
|
||||
|
@ -1215,6 +1224,13 @@
|
|||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5D160401278A41BA00D22B99 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5D64683B277B15E4009E09AE /* PreferenceUIHosting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1238,6 +1254,7 @@
|
|||
E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */,
|
||||
E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */,
|
||||
62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */,
|
||||
5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */,
|
||||
6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
|
@ -1665,6 +1682,7 @@
|
|||
buildPhases = (
|
||||
3D0F2756C71CDF6B9EEBD4E0 /* [CP] Check Pods Manifest.lock */,
|
||||
6286F0A3271C0ABA00C40ED5 /* R.swift */,
|
||||
C6EE6AB295A273FF14E6EF56 /* [CP] Prepare Artifacts */,
|
||||
5358705C2669D21600D05A09 /* Sources */,
|
||||
5358705D2669D21600D05A09 /* Frameworks */,
|
||||
5358705E2669D21600D05A09 /* Resources */,
|
||||
|
@ -2023,6 +2041,23 @@
|
|||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Swiftfin iOS/Pods-Swiftfin iOS-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C6EE6AB295A273FF14E6EF56 /* [CP] Prepare Artifacts */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Swiftfin tvOS/Pods-Swiftfin tvOS-artifacts-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Prepare Artifacts";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Swiftfin tvOS/Pods-Swiftfin tvOS-artifacts-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Swiftfin tvOS/Pods-Swiftfin tvOS-artifacts.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
D4D3981ADF75BCD341D590C0 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -2086,6 +2121,7 @@
|
|||
E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
|
||||
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */,
|
||||
E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
||||
5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */,
|
||||
E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
|
||||
E1E5D53B2783A80900692DFE /* CinematicItemViewTopRow.swift in Sources */,
|
||||
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
||||
|
@ -2163,6 +2199,7 @@
|
|||
E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
|
||||
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */,
|
||||
E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */,
|
||||
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */,
|
||||
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||
|
@ -2272,6 +2309,7 @@
|
|||
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
||||
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
|
||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||
C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
|
||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||
|
@ -2312,6 +2350,7 @@
|
|||
091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
|
||||
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
||||
5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */,
|
||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||
53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */,
|
||||
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
||||
|
@ -2426,6 +2465,7 @@
|
|||
62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||
62EC353226766849000E9F2D /* SessionManager.swift in Sources */,
|
||||
536D3D79267BD5D00004248C /* ViewModel.swift in Sources */,
|
||||
5D1603FE278A40DC00D22B99 /* SubtitleSize.swift in Sources */,
|
||||
E1AA332427829B5200F6439C /* OverlayType.swift in Sources */,
|
||||
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -13,148 +13,139 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var settingsRouter: SettingsCoordinator.Router
|
||||
@ObservedObject
|
||||
var viewModel: SettingsViewModel
|
||||
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
@Default(.inNetworkBandwidth)
|
||||
var inNetworkStreamBitrate
|
||||
@Default(.outOfNetworkBandwidth)
|
||||
var outOfNetworkStreamBitrate
|
||||
@Default(.isAutoSelectSubtitles)
|
||||
var isAutoSelectSubtitles
|
||||
@Default(.autoSelectSubtitlesLangCode)
|
||||
var autoSelectSubtitlesLangcode
|
||||
@Default(.autoSelectAudioLangCode)
|
||||
var autoSelectAudioLangcode
|
||||
@Default(.appAppearance)
|
||||
var appAppearance
|
||||
@Default(.overlayType)
|
||||
var overlayType
|
||||
@Default(.videoPlayerJumpForward)
|
||||
var jumpForwardLength
|
||||
@Default(.videoPlayerJumpBackward)
|
||||
var jumpBackwardLength
|
||||
@Default(.jumpGesturesEnabled)
|
||||
var jumpGesturesEnabled
|
||||
@Default(.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.showCastAndCrew)
|
||||
var showCastAndCrew
|
||||
@Default(.resumeOffset)
|
||||
var resumeOffset
|
||||
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
|
||||
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate
|
||||
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
|
||||
@Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode
|
||||
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode
|
||||
@Default(.appAppearance) var appAppearance
|
||||
@Default(.overlayType) var overlayType
|
||||
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||
@Default(.jumpGesturesEnabled) var jumpGesturesEnabled
|
||||
@Default(.showPosterLabels) var showPosterLabels
|
||||
@Default(.showCastAndCrew) var showCastAndCrew
|
||||
@Default(.resumeOffset) var resumeOffset
|
||||
@Default(.subtitleSize) var subtitleSize
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: EmptyView()) {
|
||||
HStack {
|
||||
Text("User")
|
||||
Spacer()
|
||||
Text(viewModel.user.username)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: EmptyView()) {
|
||||
HStack {
|
||||
Text("User")
|
||||
Spacer()
|
||||
Text(viewModel.user.username)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.serverDetail)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Server")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(viewModel.server.name)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
Button {
|
||||
settingsRouter.route(to: \.serverDetail)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Server")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(viewModel.server.name)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.dismissCoordinator {
|
||||
SessionManager.main.logout()
|
||||
}
|
||||
} label: {
|
||||
Text("Switch User")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
settingsRouter.dismissCoordinator {
|
||||
SessionManager.main.logout()
|
||||
}
|
||||
} label: {
|
||||
Text("Switch User")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement these for playback
|
||||
// Section(header: Text("Networking")) {
|
||||
// Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
// Text(bitrate.name).tag(bitrate.value)
|
||||
// }
|
||||
// }
|
||||
// TODO: Implement these for playback
|
||||
// Section(header: Text("Networking")) {
|
||||
// Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
// Text(bitrate.name).tag(bitrate.value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
||||
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
// Text(bitrate.name).tag(bitrate.value)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
||||
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
// Text(bitrate.name).tag(bitrate.value)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Section(header: Text("Video Player")) {
|
||||
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Video Player")) {
|
||||
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Jump Gestures Enabled", isOn: $jumpGesturesEnabled)
|
||||
Toggle("Jump Gestures Enabled", isOn: $jumpGesturesEnabled)
|
||||
|
||||
Toggle("Resume 5 Second Offset", isOn: $resumeOffset)
|
||||
Toggle("Resume 5 Second Offset", isOn: $resumeOffset)
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.overlaySettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Overlay")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(overlayType.label)
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
settingsRouter.route(to: \.overlaySettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Overlay")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(overlayType.label)
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.experimentalSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Experimental")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
settingsRouter.route(to: \.experimentalSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Experimental")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: L10n.accessibility.text) {
|
||||
Toggle("Show Poster Labels", isOn: $showPosterLabels)
|
||||
Toggle("Show Cast and Crew", isOn: $showCastAndCrew)
|
||||
Section(header: L10n.accessibility.text) {
|
||||
Toggle("Show Poster Labels", isOn: $showPosterLabels)
|
||||
Toggle("Show Cast and Crew", isOn: $showCastAndCrew)
|
||||
|
||||
Picker(L10n.appearance, selection: $appAppearance) {
|
||||
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Settings", displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
settingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker(L10n.appearance, selection: $appAppearance) {
|
||||
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
}
|
||||
}
|
||||
Picker("Subtitle size", selection: $subtitleSize) {
|
||||
ForEach(SubtitleSize.allCases, id: \.self) { size in
|
||||
Text(size.label).tag(size.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Settings", displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
settingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -284,138 +284,136 @@ class VLCPlayerViewController: UIViewController {
|
|||
|
||||
extension VLCPlayerViewController {
|
||||
|
||||
/// Main function that handles setting up the media player with the current VideoPlayerViewModel
|
||||
/// and also takes the role of setting the 'viewModel' property with the given viewModel
|
||||
///
|
||||
/// Use case for this is setting new media within the same VLCPlayerViewController
|
||||
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
|
||||
/// Main function that handles setting up the media player with the current VideoPlayerViewModel
|
||||
/// and also takes the role of setting the 'viewModel' property with the given viewModel
|
||||
///
|
||||
/// Use case for this is setting new media within the same VLCPlayerViewController
|
||||
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
|
||||
|
||||
// remove old player
|
||||
// remove old player
|
||||
|
||||
if vlcMediaPlayer.media != nil {
|
||||
viewModelListeners.forEach { $0.cancel() }
|
||||
if vlcMediaPlayer.media != nil {
|
||||
viewModelListeners.forEach({ $0.cancel() })
|
||||
|
||||
vlcMediaPlayer.stop()
|
||||
viewModel.sendStopReport()
|
||||
viewModel.playerOverlayDelegate = nil
|
||||
}
|
||||
vlcMediaPlayer.stop()
|
||||
viewModel.sendStopReport()
|
||||
viewModel.playerOverlayDelegate = nil
|
||||
}
|
||||
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
|
||||
// setup with new player and view model
|
||||
// setup with new player and view model
|
||||
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
vlcMediaPlayer = VLCMediaPlayer()
|
||||
|
||||
vlcMediaPlayer.delegate = self
|
||||
vlcMediaPlayer.drawable = videoContentView
|
||||
vlcMediaPlayer.delegate = self
|
||||
vlcMediaPlayer.drawable = videoContentView
|
||||
|
||||
// TODO: Custom subtitle sizes
|
||||
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
|
||||
|
||||
stopOverlayDismissTimer()
|
||||
stopOverlayDismissTimer()
|
||||
|
||||
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
|
||||
let media = VLCMedia(url: newViewModel.streamURL)
|
||||
media.addOption("--prefetch-buffer-size=1048576")
|
||||
media.addOption("--network-caching=5000")
|
||||
let media = VLCMedia(url: newViewModel.streamURL)
|
||||
media.addOption("--prefetch-buffer-size=1048576")
|
||||
media.addOption("--network-caching=5000")
|
||||
|
||||
vlcMediaPlayer.media = media
|
||||
vlcMediaPlayer.media = media
|
||||
|
||||
setupOverlayHostingController(viewModel: newViewModel)
|
||||
setupViewModelListeners(viewModel: newViewModel)
|
||||
setupOverlayHostingController(viewModel: newViewModel)
|
||||
setupViewModelListeners(viewModel: newViewModel)
|
||||
|
||||
newViewModel.getAdjacentEpisodes()
|
||||
newViewModel.playerOverlayDelegate = self
|
||||
newViewModel.getAdjacentEpisodes()
|
||||
newViewModel.playerOverlayDelegate = self
|
||||
|
||||
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
|
||||
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
|
||||
|
||||
if startPercentage > 0 {
|
||||
if viewModel.resumeOffset {
|
||||
let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
var startSeconds = round((startPercentage / 100) * videoDurationSeconds)
|
||||
startSeconds = startSeconds.subtract(5, floor: 0)
|
||||
let newStartPercentage = startSeconds / videoDurationSeconds
|
||||
newViewModel.sliderPercentage = newStartPercentage
|
||||
} else {
|
||||
newViewModel.sliderPercentage = startPercentage / 100
|
||||
}
|
||||
}
|
||||
if startPercentage > 0 {
|
||||
if viewModel.resumeOffset {
|
||||
let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
var startSeconds = round((startPercentage / 100) * videoDurationSeconds)
|
||||
startSeconds = startSeconds.subtract(5, floor: 0)
|
||||
let newStartPercentage = startSeconds / videoDurationSeconds
|
||||
newViewModel.sliderPercentage = newStartPercentage
|
||||
} else {
|
||||
newViewModel.sliderPercentage = startPercentage / 100
|
||||
}
|
||||
}
|
||||
|
||||
viewModel = newViewModel
|
||||
}
|
||||
viewModel = newViewModel
|
||||
}
|
||||
|
||||
// MARK: startPlayback
|
||||
// MARK: startPlayback
|
||||
func startPlayback() {
|
||||
vlcMediaPlayer.play()
|
||||
|
||||
func startPlayback() {
|
||||
vlcMediaPlayer.play()
|
||||
// Setup external subtitles
|
||||
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
|
||||
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
|
||||
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup external subtitles
|
||||
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
|
||||
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
|
||||
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
|
||||
}
|
||||
}
|
||||
setMediaPlayerTimeAtCurrentSlider()
|
||||
|
||||
setMediaPlayerTimeAtCurrentSlider()
|
||||
viewModel.sendPlayReport()
|
||||
|
||||
viewModel.sendPlayReport()
|
||||
restartOverlayDismissTimer()
|
||||
}
|
||||
|
||||
restartOverlayDismissTimer()
|
||||
}
|
||||
// MARK: setupViewModelListeners
|
||||
|
||||
// MARK: setupViewModelListeners
|
||||
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
||||
|
||||
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
||||
viewModel.$playbackSpeed.sink { newSpeed in
|
||||
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$playbackSpeed.sink { newSpeed in
|
||||
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
||||
if sliderIsScrubbing {
|
||||
self.didBeginScrubbing()
|
||||
} else {
|
||||
self.didEndScrubbing()
|
||||
}
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
||||
if sliderIsScrubbing {
|
||||
self.didBeginScrubbing()
|
||||
} else {
|
||||
self.didEndScrubbing()
|
||||
}
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
||||
self.didSelectAudioStream(index: newAudioStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
||||
self.didSelectAudioStream(index: newAudioStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
||||
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
||||
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
|
||||
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
|
||||
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in
|
||||
self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength)
|
||||
}.store(in: &viewModelListeners)
|
||||
|
||||
viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in
|
||||
self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength)
|
||||
}.store(in: &viewModelListeners)
|
||||
viewModel.$jumpForwardLength.sink { newJumpForwardLength in
|
||||
self.refreshJumpForwardOverlayView(with: newJumpForwardLength)
|
||||
}.store(in: &viewModelListeners)
|
||||
}
|
||||
|
||||
viewModel.$jumpForwardLength.sink { newJumpForwardLength in
|
||||
self.refreshJumpForwardOverlayView(with: newJumpForwardLength)
|
||||
}.store(in: &viewModelListeners)
|
||||
}
|
||||
func setMediaPlayerTimeAtCurrentSlider() {
|
||||
// Necessary math as VLCMediaPlayer doesn't work well
|
||||
// by just setting the position
|
||||
let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000)
|
||||
let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration)
|
||||
let newPositionOffset = secondsScrubbedTo - videoPosition
|
||||
|
||||
func setMediaPlayerTimeAtCurrentSlider() {
|
||||
// Necessary math as VLCMediaPlayer doesn't work well
|
||||
// by just setting the position
|
||||
let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000)
|
||||
let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||
let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration)
|
||||
let newPositionOffset = secondsScrubbedTo - videoPosition
|
||||
|
||||
if newPositionOffset > 0 {
|
||||
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
|
||||
} else {
|
||||
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
|
||||
}
|
||||
}
|
||||
if newPositionOffset > 0 {
|
||||
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
|
||||
} else {
|
||||
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Show/Hide Overlay
|
||||
|
|
Loading…
Reference in New Issue