begin final work
This commit is contained in:
parent
cd3a244f17
commit
5b451ceaaa
|
@ -122,6 +122,7 @@ struct EpisodeItemView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
MediaPlayButtonRowView(viewModel: viewModel)
|
MediaPlayButtonRowView(viewModel: viewModel)
|
||||||
|
.environmentObject(itemRouter)
|
||||||
}
|
}
|
||||||
}.padding(.top, 50)
|
}.padding(.top, 50)
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct SeasonItemView: View {
|
struct SeasonItemView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
@ObservedObject var viewModel: SeasonItemViewModel
|
@ObservedObject var viewModel: SeasonItemViewModel
|
||||||
@State var wrappedScrollView: UIScrollView?
|
@State var wrappedScrollView: UIScrollView?
|
||||||
|
|
||||||
|
@ -101,10 +103,15 @@ struct SeasonItemView: View {
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
LazyHStack {
|
LazyHStack {
|
||||||
Spacer().frame(width: 45)
|
Spacer().frame(width: 45)
|
||||||
|
|
||||||
ForEach(viewModel.episodes, id: \.id) { episode in
|
ForEach(viewModel.episodes, id: \.id) { episode in
|
||||||
NavigationLink(destination: ItemView(item: episode)) {
|
|
||||||
|
Button {
|
||||||
|
itemRouter.route(to: \.item, episode)
|
||||||
|
} label: {
|
||||||
LandscapeItemElement(item: episode, inSeasonView: true)
|
LandscapeItemElement(item: episode, inSeasonView: true)
|
||||||
}.buttonStyle(PlainNavigationLinkButtonStyle())
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
}
|
}
|
||||||
Spacer().frame(width: 45)
|
Spacer().frame(width: 45)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct SeriesItemView: View {
|
struct SeriesItemView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
@ObservedObject var viewModel: SeriesItemViewModel
|
@ObservedObject var viewModel: SeriesItemViewModel
|
||||||
|
|
||||||
@State var actors: [BaseItemPerson] = []
|
@State var actors: [BaseItemPerson] = []
|
||||||
|
@ -141,10 +143,16 @@ struct SeriesItemView: View {
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
LazyHStack {
|
LazyHStack {
|
||||||
Spacer().frame(width: 45)
|
Spacer().frame(width: 45)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ForEach(viewModel.seasons, id: \.id) { season in
|
ForEach(viewModel.seasons, id: \.id) { season in
|
||||||
NavigationLink(destination: ItemView(item: season)) {
|
Button {
|
||||||
|
itemRouter.route(to: \.item, season)
|
||||||
|
} label: {
|
||||||
PortraitItemElement(item: season)
|
PortraitItemElement(item: season)
|
||||||
}.buttonStyle(PlainNavigationLinkButtonStyle())
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
}
|
}
|
||||||
Spacer().frame(width: 45)
|
Spacer().frame(width: 45)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
//
|
//
|
||||||
// NativePlayerViewController.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 11/20/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import AVKit
|
import AVKit
|
||||||
import Combine
|
import Combine
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
//
|
//
|
||||||
// PlayerOverlayDelegate.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 12/27/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
//
|
//
|
||||||
// PlayerViewController.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 11/12/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import AVKit
|
import AVKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
@ -203,7 +205,10 @@ class VLCPlayerViewController: UIViewController {
|
||||||
case .upArrow:
|
case .upArrow:
|
||||||
print("Up arrow")
|
print("Up arrow")
|
||||||
case .downArrow:
|
case .downArrow:
|
||||||
print("Down arrow")
|
stopOverlayDismissTimer()
|
||||||
|
|
||||||
|
hideOverlay()
|
||||||
|
showOverlayContent()
|
||||||
case .leftArrow:
|
case .leftArrow:
|
||||||
didSelectBackward()
|
didSelectBackward()
|
||||||
print("Left arrow")
|
print("Left arrow")
|
||||||
|
@ -227,6 +232,8 @@ class VLCPlayerViewController: UIViewController {
|
||||||
@objc private func didPressMenu() {
|
@objc private func didPressMenu() {
|
||||||
if displayingOverlay {
|
if displayingOverlay {
|
||||||
hideOverlay()
|
hideOverlay()
|
||||||
|
} else if displayingContentOverlay {
|
||||||
|
hideOverlayContent()
|
||||||
} else {
|
} else {
|
||||||
vlcMediaPlayer.pause()
|
vlcMediaPlayer.pause()
|
||||||
|
|
||||||
|
@ -294,6 +301,11 @@ class VLCPlayerViewController: UIViewController {
|
||||||
currentOverlayContentHostingController.removeFromParent()
|
currentOverlayContentHostingController.removeFromParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let newSmallMenuOverlayView = SmallMediaStreamSelectionView(items: viewModel.subtitleStreams,
|
||||||
|
// selectedItem: viewModel.subtitleStreams.first(where: { $0.index == viewModel.selectedSubtitleStreamIndex })) { selectedMediaStream in
|
||||||
|
// self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1)
|
||||||
|
// }
|
||||||
|
// let newOverlayContentHostingController = UIHostingController(rootView: newSmallMenuOverlayView)
|
||||||
let newOverlayContentView = tvOSOverlayContentView(viewModel: viewModel)
|
let newOverlayContentView = tvOSOverlayContentView(viewModel: viewModel)
|
||||||
let newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView)
|
let newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView)
|
||||||
|
|
||||||
|
@ -452,6 +464,10 @@ extension VLCPlayerViewController {
|
||||||
|
|
||||||
guard currentOverlayContentHostingController.view.alpha != 1 else { return }
|
guard currentOverlayContentHostingController.view.alpha != 1 else { return }
|
||||||
|
|
||||||
|
currentOverlayContentHostingController.view.setNeedsFocusUpdate()
|
||||||
|
currentOverlayContentHostingController.setNeedsFocusUpdate()
|
||||||
|
setNeedsFocusUpdate()
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
currentOverlayContentHostingController.view.alpha = 1
|
currentOverlayContentHostingController.view.alpha = 1
|
||||||
}
|
}
|
||||||
|
@ -462,6 +478,8 @@ extension VLCPlayerViewController {
|
||||||
|
|
||||||
guard currentOverlayContentHostingController.view.alpha != 0 else { return }
|
guard currentOverlayContentHostingController.view.alpha != 0 else { return }
|
||||||
|
|
||||||
|
setNeedsFocusUpdate()
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
currentOverlayContentHostingController.view.alpha = 0
|
currentOverlayContentHostingController.view.alpha = 0
|
||||||
}
|
}
|
||||||
|
@ -629,10 +647,10 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
|
|
||||||
// TODO: Implement properly in overlays
|
// TODO: Implement properly in overlays
|
||||||
func didSelectMenu() {
|
func didSelectMenu() {
|
||||||
// stopOverlayDismissTimer()
|
stopOverlayDismissTimer()
|
||||||
//
|
|
||||||
// hideOverlay()
|
hideOverlay()
|
||||||
// showOverlayContent()
|
showOverlayContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement properly in overlays
|
// TODO: Implement properly in overlays
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
//
|
//
|
||||||
// VideoPlayerView.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 11/12/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SmallMediaStreamSelectionView: View {
|
||||||
|
|
||||||
|
@State var selectedItem: MediaStream?
|
||||||
|
private var items: [MediaStream]
|
||||||
|
private var selectedAction: (MediaStream) -> Void
|
||||||
|
|
||||||
|
init(items: [MediaStream], selectedItem: MediaStream? = nil, selectedAction: @escaping (MediaStream) -> Void) {
|
||||||
|
self.items = items
|
||||||
|
self.selectedItem = selectedItem
|
||||||
|
self.selectedAction = selectedAction
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.7)]),
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.frame(height: 150)
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Subtitles")
|
||||||
|
.font(.title3)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
HStack {
|
||||||
|
ForEach(items, id: \.self) { item in
|
||||||
|
Button {
|
||||||
|
// self.selectedItem = item
|
||||||
|
} label: {
|
||||||
|
if item == selectedItem {
|
||||||
|
Label(item.displayTitle ?? "No Title", systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text(item.displayTitle ?? "No Title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxHeight: 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -232,7 +232,6 @@
|
||||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
||||||
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; };
|
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; };
|
||||||
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; };
|
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; };
|
||||||
E10EAA4A277BB6F5000269ED /* VideoPlayerOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA49277BB6F5000269ED /* VideoPlayerOverlay.swift */; };
|
|
||||||
E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; };
|
E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; };
|
||||||
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; };
|
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; };
|
||||||
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; };
|
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; };
|
||||||
|
@ -329,6 +328,12 @@
|
||||||
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; };
|
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; };
|
||||||
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A99998271A3429008E78C0 /* SwiftUICollection */; };
|
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A99998271A3429008E78C0 /* SwiftUICollection */; };
|
||||||
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A9999A271A343C008E78C0 /* SwiftUICollection */; };
|
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A9999A271A343C008E78C0 /* SwiftUICollection */; };
|
||||||
|
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButtonView.swift */; };
|
||||||
|
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
||||||
|
E1AA33202782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
||||||
|
E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA33212782648000F6439C /* OverlaySliderColor.swift */; };
|
||||||
|
E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA33212782648000F6439C /* OverlaySliderColor.swift */; };
|
||||||
|
E1AA332427829B5200F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
||||||
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
||||||
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
||||||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; };
|
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; };
|
||||||
|
@ -344,10 +349,8 @@
|
||||||
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; };
|
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; };
|
||||||
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */; };
|
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */; };
|
||||||
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */; };
|
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */; };
|
||||||
E1C812BF277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */; };
|
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */; };
|
||||||
E1C812C0277A8E5D00918266 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */; };
|
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */; };
|
||||||
E1C812C1277A8E5D00918266 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */; };
|
|
||||||
E1C812C3277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */; };
|
|
||||||
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */; };
|
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */; };
|
||||||
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */; };
|
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */; };
|
||||||
E1C812CB277AE40900918266 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C7277AE40900918266 /* NativePlayerViewController.swift */; };
|
E1C812CB277AE40900918266 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C7277AE40900918266 /* NativePlayerViewController.swift */; };
|
||||||
|
@ -373,6 +376,7 @@
|
||||||
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; };
|
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; };
|
||||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||||
|
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */; };
|
||||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
|
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
|
||||||
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
|
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
|
||||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
|
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
|
||||||
|
@ -571,7 +575,6 @@
|
||||||
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||||
E10EAA49277BB6F5000269ED /* VideoPlayerOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerOverlay.swift; sourceTree = "<group>"; };
|
|
||||||
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
||||||
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
||||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||||
|
@ -617,6 +620,9 @@
|
||||||
E193D54C2719426600900D82 /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
|
E193D54C2719426600900D82 /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
|
||||||
E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; };
|
E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; };
|
||||||
E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = "<group>"; };
|
E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
E1AA331C2782541500F6439C /* PrimaryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButtonView.swift; sourceTree = "<group>"; };
|
||||||
|
E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = "<group>"; };
|
||||||
|
E1AA33212782648000F6439C /* OverlaySliderColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySliderColor.swift; sourceTree = "<group>"; };
|
||||||
E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = "<group>"; };
|
E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = "<group>"; };
|
||||||
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = "<group>"; };
|
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = "<group>"; };
|
||||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = "<group>"; };
|
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -626,10 +632,8 @@
|
||||||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = "<group>"; };
|
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = "<group>"; };
|
||||||
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = "<group>"; };
|
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = "<group>"; };
|
||||||
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
|
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
|
||||||
E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerOverlayView.swift; sourceTree = "<group>"; };
|
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerView.swift; sourceTree = "<group>"; };
|
||||||
E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerOverlayView.swift; sourceTree = "<group>"; };
|
||||||
E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
|
|
||||||
E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerCompactOverlayView.swift; sourceTree = "<group>"; };
|
|
||||||
E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsExtensions.swift; sourceTree = "<group>"; };
|
E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsExtensions.swift; sourceTree = "<group>"; };
|
||||||
E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = "<group>"; };
|
E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = "<group>"; };
|
||||||
E1C812C7277AE40900918266 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
|
E1C812C7277AE40900918266 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -645,6 +649,7 @@
|
||||||
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
|
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
|
||||||
E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = "<group>"; };
|
E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = "<group>"; };
|
||||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
|
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
|
||||||
|
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallMenuOverlay.swift; sourceTree = "<group>"; };
|
||||||
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
|
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
|
||||||
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
|
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
|
||||||
FDEDADB92FA8523BC8432E45 /* Pods-WidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-WidgetExtension/Pods-WidgetExtension.release.xcconfig"; sourceTree = "<group>"; };
|
FDEDADB92FA8523BC8432E45 /* Pods-WidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-WidgetExtension/Pods-WidgetExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
@ -858,6 +863,8 @@
|
||||||
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
|
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
|
||||||
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
|
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
|
||||||
E19169CD272514760085832A /* HTTPScheme.swift */,
|
E19169CD272514760085832A /* HTTPScheme.swift */,
|
||||||
|
E1AA33212782648000F6439C /* OverlaySliderColor.swift */,
|
||||||
|
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
||||||
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
|
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
|
||||||
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
|
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
|
||||||
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
|
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
|
||||||
|
@ -1081,11 +1088,12 @@
|
||||||
53F866422687A45400DCD1D7 /* Components */ = {
|
53F866422687A45400DCD1D7 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
|
||||||
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
||||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
||||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
|
|
||||||
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
|
|
||||||
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
|
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
|
||||||
|
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
|
||||||
|
E1AA331C2782541500F6439C /* PrimaryButtonView.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1201,16 +1209,6 @@
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
E10EAA48277BB6D7000269ED /* Overlays */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
E10EAA49277BB6F5000269ED /* VideoPlayerOverlay.swift */,
|
|
||||||
E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */,
|
|
||||||
E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */,
|
|
||||||
);
|
|
||||||
path = Overlays;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
E12186DF2718F2030010884C /* App */ = {
|
E12186DF2718F2030010884C /* App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1312,6 +1310,7 @@
|
||||||
E17885A7278130690094FBCF /* tvOSOverlay */ = {
|
E17885A7278130690094FBCF /* tvOSOverlay */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */,
|
||||||
E17885A5278130610094FBCF /* tvOSOverlayContent.swift */,
|
E17885A5278130610094FBCF /* tvOSOverlayContent.swift */,
|
||||||
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */,
|
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1350,11 +1349,10 @@
|
||||||
E193D5452719418B00900D82 /* VideoPlayer */ = {
|
E193D5452719418B00900D82 /* VideoPlayer */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */,
|
|
||||||
E10EAA48277BB6D7000269ED /* Overlays */,
|
|
||||||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
||||||
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
|
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
|
||||||
E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */,
|
E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */,
|
||||||
|
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */,
|
||||||
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */,
|
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */,
|
||||||
);
|
);
|
||||||
path = VideoPlayer;
|
path = VideoPlayer;
|
||||||
|
@ -1911,6 +1909,7 @@
|
||||||
E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||||
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||||
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
|
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
|
||||||
|
E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */,
|
||||||
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||||
5398514726B64E4100101B49 /* SearchBarView.swift in Sources */,
|
5398514726B64E4100101B49 /* SearchBarView.swift in Sources */,
|
||||||
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||||
|
@ -1935,6 +1934,7 @@
|
||||||
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
||||||
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
||||||
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
|
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
|
||||||
|
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */,
|
||||||
E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */,
|
E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */,
|
||||||
E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
||||||
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
|
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
|
||||||
|
@ -1942,6 +1942,7 @@
|
||||||
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
||||||
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */,
|
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */,
|
||||||
|
E1AA33202782639D00F6439C /* OverlayType.swift in Sources */,
|
||||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||||
6264E88D273850380081A12A /* Strings.swift in Sources */,
|
6264E88D273850380081A12A /* Strings.swift in Sources */,
|
||||||
|
@ -1992,7 +1993,7 @@
|
||||||
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||||
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
||||||
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
||||||
E1C812C0277A8E5D00918266 /* VideoPlayerView.swift in Sources */,
|
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
|
||||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||||
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||||
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||||
|
@ -2027,16 +2028,14 @@
|
||||||
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
|
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
|
||||||
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
||||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||||
E1C812BF277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */,
|
|
||||||
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
|
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
E1C812C3277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift in Sources */,
|
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */,
|
||||||
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */,
|
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */,
|
||||||
091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||||
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
|
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
|
||||||
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
||||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||||
53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */,
|
53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */,
|
||||||
E1C812C1277A8E5D00918266 /* NativePlayerViewController.swift in Sources */,
|
|
||||||
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
||||||
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
||||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
|
@ -2045,6 +2044,7 @@
|
||||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
||||||
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
|
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
|
||||||
|
E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */,
|
||||||
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
|
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
|
||||||
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
||||||
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
||||||
|
@ -2055,6 +2055,7 @@
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||||
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
|
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
|
||||||
|
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */,
|
||||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||||
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
||||||
|
@ -2063,6 +2064,7 @@
|
||||||
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */,
|
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */,
|
||||||
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
||||||
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */,
|
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */,
|
||||||
|
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
|
||||||
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
|
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
|
||||||
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
|
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
|
||||||
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
|
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||||
|
@ -2101,7 +2103,6 @@
|
||||||
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
|
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
|
||||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||||
E10EAA4A277BB6F5000269ED /* VideoPlayerOverlay.swift in Sources */,
|
|
||||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
||||||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
|
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
@ -2135,6 +2136,7 @@
|
||||||
62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||||
62EC353226766849000E9F2D /* SessionManager.swift in Sources */,
|
62EC353226766849000E9F2D /* SessionManager.swift in Sources */,
|
||||||
536D3D79267BD5D00004248C /* ViewModel.swift in Sources */,
|
536D3D79267BD5D00004248C /* ViewModel.swift in Sources */,
|
||||||
|
E1AA332427829B5200F6439C /* OverlayType.swift in Sources */,
|
||||||
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */,
|
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -2291,7 +2293,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 66;
|
CURRENT_PROJECT_VERSION = 66;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist";
|
INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist";
|
||||||
|
@ -2300,7 +2302,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin;
|
PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = appletvos;
|
SDKROOT = appletvos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
@ -2321,7 +2323,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 66;
|
CURRENT_PROJECT_VERSION = 66;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist";
|
INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist";
|
||||||
|
@ -2330,7 +2332,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin;
|
PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = appletvos;
|
SDKROOT = appletvos;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 SwiftUI
|
||||||
|
|
||||||
|
struct PrimaryButtonView: View {
|
||||||
|
|
||||||
|
private let title: String
|
||||||
|
private let action: () -> Void
|
||||||
|
|
||||||
|
init(title: String, _ action: @escaping () -> Void) {
|
||||||
|
self.title = title
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
action()
|
||||||
|
} label: {
|
||||||
|
ZStack {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(UIColor.systemPurple))
|
||||||
|
.frame(maxWidth: 400, maxHeight: 50)
|
||||||
|
.frame(height: 50)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.horizontal, 30)
|
||||||
|
.padding([.top, .bottom], 20)
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,8 +20,33 @@ struct HomeView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var innerBody: some View {
|
var innerBody: some View {
|
||||||
if viewModel.isLoading {
|
if let errorMessage = viewModel.errorMessage {
|
||||||
|
VStack(spacing: 5) {
|
||||||
|
if viewModel.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 100, height: 100)
|
||||||
|
.scaleEffect(2)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.font(.system(size: 72))
|
||||||
|
.foregroundColor(Color.red)
|
||||||
|
.frame(width: 100, height: 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("\(errorMessage.code)")
|
||||||
|
Text(errorMessage.displayMessage)
|
||||||
|
.frame(minWidth: 50, maxWidth: 240)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
PrimaryButtonView(title: "Retry") {
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.offset(y: -50)
|
||||||
|
} else if viewModel.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
.frame(width: 100, height: 100)
|
||||||
|
.scaleEffect(2)
|
||||||
} else {
|
} else {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -69,22 +69,8 @@ struct ServerListView: View {
|
||||||
.frame(minWidth: 50, maxWidth: 240)
|
.frame(minWidth: 50, maxWidth: 240)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Button {
|
PrimaryButtonView(title: L10n.connect.stringValue) {
|
||||||
serverListRouter.route(to: \.connectToServer)
|
serverListRouter.route(to: \.connectToServer)
|
||||||
} label: {
|
|
||||||
ZStack {
|
|
||||||
Rectangle()
|
|
||||||
.foregroundColor(Color.jellyfinPurple)
|
|
||||||
.frame(maxWidth: 400, maxHeight: 50)
|
|
||||||
.frame(height: 50)
|
|
||||||
.cornerRadius(10)
|
|
||||||
.padding(.horizontal, 30)
|
|
||||||
.padding([.top, .bottom], 20)
|
|
||||||
|
|
||||||
L10n.connect.text
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ struct SettingsView: View {
|
||||||
@Default(.appAppearance) var appAppearance
|
@Default(.appAppearance) var appAppearance
|
||||||
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
||||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||||
@Default(.nativeVideoPlayer) var nativeVideoPlayer
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
@ -83,8 +82,7 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Playback")) {
|
Section(header: Text("Networking")) {
|
||||||
Toggle("Native Player", isOn: $nativeVideoPlayer)
|
|
||||||
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||||
Text(bitrate.name).tag(bitrate.value)
|
Text(bitrate.name).tag(bitrate.value)
|
||||||
|
@ -96,43 +94,45 @@ struct SettingsView: View {
|
||||||
Text(bitrate.name).tag(bitrate.value)
|
Text(bitrate.name).tag(bitrate.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Video Player")) {
|
||||||
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
||||||
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
|
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||||
Text(length.label).tag(length.rawValue)
|
Text(length.label).tag(length.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
||||||
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
|
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||||
Text(length.label).tag(length.rawValue)
|
Text(length.label).tag(length.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: L10n.accessibility.text) {
|
Section(header: L10n.accessibility.text) {
|
||||||
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
// Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
||||||
SearchablePicker(label: "Preferred subtitle language",
|
// SearchablePicker(label: "Preferred subtitle language",
|
||||||
options: viewModel.langs,
|
// options: viewModel.langs,
|
||||||
optionToString: { $0.name },
|
// optionToString: { $0.name },
|
||||||
selected: Binding<TrackLanguage>(get: {
|
// selected: Binding<TrackLanguage>(get: {
|
||||||
viewModel.langs
|
// viewModel.langs
|
||||||
.first(where: { $0.isoCode == autoSelectSubtitlesLangcode
|
// .first(where: { $0.isoCode == autoSelectSubtitlesLangcode
|
||||||
}) ??
|
// }) ??
|
||||||
.auto
|
// .auto
|
||||||
},
|
// },
|
||||||
set: { autoSelectSubtitlesLangcode = $0.isoCode }))
|
// set: { autoSelectSubtitlesLangcode = $0.isoCode }))
|
||||||
SearchablePicker(label: "Preferred audio language",
|
// SearchablePicker(label: "Preferred audio language",
|
||||||
options: viewModel.langs,
|
// options: viewModel.langs,
|
||||||
optionToString: { $0.name },
|
// optionToString: { $0.name },
|
||||||
selected: Binding<TrackLanguage>(get: {
|
// selected: Binding<TrackLanguage>(get: {
|
||||||
viewModel.langs
|
// viewModel.langs
|
||||||
.first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
|
// .first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
|
||||||
.auto
|
// .auto
|
||||||
},
|
// },
|
||||||
set: { autoSelectAudioLangcode = $0.isoCode }))
|
// set: { autoSelectAudioLangcode = $0.isoCode }))
|
||||||
Picker(L10n.appearance, selection: $appAppearance) {
|
Picker(L10n.appearance, selection: $appAppearance) {
|
||||||
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
||||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
//
|
|
||||||
// NativePlayerViewController.swift
|
|
||||||
// JellyfinVideoPlayerDev
|
|
||||||
//
|
|
||||||
// Created by Ethan Pippin on 11/20/21.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AVKit
|
|
||||||
import Combine
|
|
||||||
import JellyfinAPI
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class NativePlayerViewController: AVPlayerViewController {
|
|
||||||
|
|
||||||
let viewModel: VideoPlayerViewModel
|
|
||||||
|
|
||||||
private var timeObserverToken: Any?
|
|
||||||
|
|
||||||
private var lastProgressTicks: Int64 = 0
|
|
||||||
|
|
||||||
init(viewModel: VideoPlayerViewModel) {
|
|
||||||
|
|
||||||
self.viewModel = viewModel
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
|
|
||||||
let player = AVPlayer(url: viewModel.hlsURL)
|
|
||||||
|
|
||||||
player.appliesMediaSelectionCriteriaAutomatically = false
|
|
||||||
player.currentItem?.externalMetadata = createMetadata()
|
|
||||||
|
|
||||||
let timeScale = CMTimeScale(NSEC_PER_SEC)
|
|
||||||
let time = CMTime(seconds: 5, preferredTimescale: timeScale)
|
|
||||||
|
|
||||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
|
|
||||||
// print("Timer timed: \(time)")
|
|
||||||
|
|
||||||
if time.seconds != 0 {
|
|
||||||
self?.sendProgressReport(seconds: time.seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
self.allowsPictureInPicturePlayback = true
|
|
||||||
self.player?.allowsExternalPlayback = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createMetadata() -> [AVMetadataItem] {
|
|
||||||
let allMetadata: [AVMetadataIdentifier: Any] = [
|
|
||||||
.commonIdentifierTitle: viewModel.title,
|
|
||||||
.iTunesMetadataTrackSubTitle: viewModel.subtitle ?? "",
|
|
||||||
.commonIdentifierArtwork: UIImage(data: try! Data(contentsOf: viewModel.item.getBackdropImage(maxWidth: 200)))?.pngData() as Any,
|
|
||||||
.commonIdentifierDescription: viewModel.item.overview ?? ""
|
|
||||||
]
|
|
||||||
|
|
||||||
return allMetadata.compactMap { createMetadataItem(for:$0, value:$1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createMetadataItem(for identifier: AVMetadataIdentifier,
|
|
||||||
value: Any) -> AVMetadataItem {
|
|
||||||
let item = AVMutableMetadataItem()
|
|
||||||
item.identifier = identifier
|
|
||||||
item.value = value as? NSCopying & NSObjectProtocol
|
|
||||||
// Specify "und" to indicate an undefined language.
|
|
||||||
item.extendedLanguageTag = "und"
|
|
||||||
return item.copy() as! AVMetadataItem
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
|
|
||||||
stop()
|
|
||||||
removePeriodicTimeObserver()
|
|
||||||
}
|
|
||||||
|
|
||||||
func removePeriodicTimeObserver() {
|
|
||||||
if let timeObserverToken = timeObserverToken {
|
|
||||||
player?.removeTimeObserver(timeObserverToken)
|
|
||||||
self.timeObserverToken = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
player?.seek(to: CMTimeMake(value: viewModel.item.userData?.playbackPositionTicks ?? 0, timescale: 10_000_000), toleranceBefore: CMTimeMake(value: 5, timescale: 1), toleranceAfter: CMTimeMake(value: 5, timescale: 1), completionHandler: { _ in
|
|
||||||
self.play()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private func play() {
|
|
||||||
player?.play()
|
|
||||||
viewModel.sendPlayReport()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendProgressReport(seconds: Double) {
|
|
||||||
viewModel.sendProgressReport()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func stop() {
|
|
||||||
viewModel.sendStopReport()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,251 +0,0 @@
|
||||||
//
|
|
||||||
// VLCPlayerOverlayView.swift
|
|
||||||
// JellyfinVideoPlayerDev
|
|
||||||
//
|
|
||||||
// Created by Ethan Pippin on 11/24/21.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import MobileVLCKit
|
|
||||||
import SwiftUI
|
|
||||||
import JellyfinAPI
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct VLCPlayerOverlayView: View {
|
|
||||||
|
|
||||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var mainButtonView: some View {
|
|
||||||
switch viewModel.playerState {
|
|
||||||
case .stopped, .paused:
|
|
||||||
Image(systemName: "play")
|
|
||||||
.font(.system(size: 56))
|
|
||||||
case .playing:
|
|
||||||
Image(systemName: "pause")
|
|
||||||
.font(.system(size: 56))
|
|
||||||
default:
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var mainBody: some View {
|
|
||||||
VStack {
|
|
||||||
|
|
||||||
VStack(alignment: .EpisodeSeriesAlignmentGuide) {
|
|
||||||
|
|
||||||
// MARK: Top Bar
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
HStack {
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectClose()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "chevron.backward")
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(viewModel.title)
|
|
||||||
.font(.system(size: 28, weight: .regular, design: .default))
|
|
||||||
.alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in
|
|
||||||
context[.leading]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
|
|
||||||
if viewModel.shouldShowGoogleCast {
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectGoogleCast()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "rectangle.badge.plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.shouldShowAirplay {
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectAirplay()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "airplayvideo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectSubtitles()
|
|
||||||
} label: {
|
|
||||||
if viewModel.subtitlesEnabled {
|
|
||||||
Image(systemName: "captions.bubble.fill")
|
|
||||||
} else {
|
|
||||||
Image(systemName: "captions.bubble")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Settings Menu
|
|
||||||
Menu {
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
ForEach(viewModel.audioStreams, id: \.self) { audioStream in
|
|
||||||
Button {
|
|
||||||
viewModel.selectedAudioStreamIndex = audioStream.index ?? -1
|
|
||||||
} label: {
|
|
||||||
if audioStream.index == viewModel.selectedAudioStreamIndex {
|
|
||||||
Label.init(audioStream.displayTitle ?? "No Title", systemImage: "checkmark")
|
|
||||||
} else {
|
|
||||||
Text(audioStream.displayTitle ?? "No Title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "speaker.wave.3")
|
|
||||||
Text("Audio")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in
|
|
||||||
Button {
|
|
||||||
viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1
|
|
||||||
} label: {
|
|
||||||
if subtitleStream.index == viewModel.selectedSubtitleStreamIndex {
|
|
||||||
Label.init(subtitleStream.displayTitle ?? "No Title", systemImage: "checkmark")
|
|
||||||
} else {
|
|
||||||
Text(subtitleStream.displayTitle ?? "No Title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "captions.bubble")
|
|
||||||
Text("Subtitles")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
Button {
|
|
||||||
print("third pressed")
|
|
||||||
} label: {
|
|
||||||
Text("TODO")
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "speedometer")
|
|
||||||
Text("Playback Speed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "ellipsis.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.system(size: 24))
|
|
||||||
|
|
||||||
if let seriesTitle = viewModel.subtitle {
|
|
||||||
Text(seriesTitle)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(Color.gray)
|
|
||||||
.alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in
|
|
||||||
context[.leading]
|
|
||||||
}
|
|
||||||
.offset(y: -10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// MARK: Center Buttons
|
|
||||||
HStack(spacing: 80) {
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectBackward()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "gobackward.10")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectMain()
|
|
||||||
} label: {
|
|
||||||
mainButtonView
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectForward()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "goforward.10")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.system(size: 48))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// MARK: Bottom Bar
|
|
||||||
HStack {
|
|
||||||
Text(viewModel.leftLabelText)
|
|
||||||
.font(.system(size: 18, weight: .semibold, design: .default))
|
|
||||||
|
|
||||||
Slider(value: $viewModel.sliderPercentage) { editing in
|
|
||||||
viewModel.sliderIsScrubbing = editing
|
|
||||||
}
|
|
||||||
.foregroundColor(.purple)
|
|
||||||
.tint(.purple)
|
|
||||||
|
|
||||||
Text(viewModel.rightLabelText)
|
|
||||||
.font(.system(size: 18, weight: .semibold, design: .default))
|
|
||||||
}
|
|
||||||
.frame(height: 50)
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
.ignoresSafeArea(edges: .vertical)
|
|
||||||
.tint(Color.white)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
mainBody
|
|
||||||
.background {
|
|
||||||
Color(uiColor: .black.withAlphaComponent(0.2))
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.onTapGesture {
|
|
||||||
viewModel.playerOverlayDelegate?.didGenerallyTap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VLCPlayerOverlayView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ZStack {
|
|
||||||
Color.gray
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
VLCPlayerOverlayView(viewModel: VideoPlayerViewModel(item: BaseItemDto(),
|
|
||||||
title: "Glorious Purpose",
|
|
||||||
subtitle: "Loki - S1E1",
|
|
||||||
streamURL: URL(string: "www.apple.com")!,
|
|
||||||
hlsURL: URL(string: "www.apple.com")!,
|
|
||||||
response: PlaybackInfoResponse(),
|
|
||||||
audioStreams: [MediaStream(displayTitle: "English", index: -1)],
|
|
||||||
subtitleStreams: [MediaStream(displayTitle: "None", index: -1)],
|
|
||||||
defaultAudioStreamIndex: -1,
|
|
||||||
defaultSubtitleStreamIndex: -1,
|
|
||||||
playerState: .playing,
|
|
||||||
shouldShowGoogleCast: false,
|
|
||||||
shouldShowAirplay: false,
|
|
||||||
subtitlesEnabled: true,
|
|
||||||
sliderPercentage: 0.0,
|
|
||||||
selectedAudioStreamIndex: -1,
|
|
||||||
selectedSubtitleStreamIndex: -1,
|
|
||||||
showAdjacentItems: true,
|
|
||||||
shouldShowAutoPlayNextItem: true,
|
|
||||||
autoPlayNextItem: true))
|
|
||||||
}
|
|
||||||
.previewInterfaceOrientation(.landscapeLeft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
//
|
|
||||||
/*
|
|
||||||
* 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 SwiftUI
|
|
||||||
|
|
||||||
protocol VideoPlayerOverlay: View {
|
|
||||||
var viewModel: VideoPlayerViewModel { get set }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HorizontalAlignment {
|
|
||||||
|
|
||||||
private struct EpisodeSeriesTitleAlignment: AlignmentID {
|
|
||||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
|
||||||
context[HorizontalAlignment.leading]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static let EpisodeSeriesAlignmentGuide = HorizontalAlignment(EpisodeSeriesTitleAlignment.self)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,9 +1,11 @@
|
||||||
//
|
//
|
||||||
// PlaybackSpeed.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 12/27/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
//
|
//
|
||||||
// PlayerOverlayDelegate.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 12/27/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol PlayerOverlayDelegate {
|
protocol PlayerOverlayDelegate {
|
||||||
|
|
||||||
func didSelectClose()
|
func didSelectClose()
|
||||||
func didSelectGoogleCast()
|
|
||||||
func didSelectAirplay()
|
|
||||||
func didSelectSubtitles()
|
|
||||||
func didSelectMenu()
|
func didSelectMenu()
|
||||||
func didDeselectMenu()
|
func didDeselectMenu()
|
||||||
|
|
||||||
|
@ -28,6 +27,6 @@ protocol PlayerOverlayDelegate {
|
||||||
func didSelectAudioStream(index: Int)
|
func didSelectAudioStream(index: Int)
|
||||||
func didSelectSubtitleStream(index: Int)
|
func didSelectSubtitleStream(index: Int)
|
||||||
|
|
||||||
func didSelectPreviousItem()
|
func didSelectPlayPreviousItem()
|
||||||
func didSelectNextItem()
|
func didSelectPlayNextItem()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
//
|
/*
|
||||||
// VLCPlayerCompactOverlayView.swift
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
// JellyfinVideoPlayerDev
|
* 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/.
|
||||||
// Created by Ethan Pippin on 12/26/21.
|
*
|
||||||
//
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Defaults
|
import Defaults
|
||||||
|
@ -12,11 +13,9 @@ import MobileVLCKit
|
||||||
import Sliders
|
import Sliders
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
struct VLCPlayerOverlayView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||||
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
|
||||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainButtonView: some View {
|
private var mainButtonView: some View {
|
||||||
|
@ -69,33 +68,19 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
|
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
|
|
||||||
if viewModel.shouldShowGoogleCast {
|
if viewModel.shouldShowPlayPreviousItem {
|
||||||
Button {
|
Button {
|
||||||
viewModel.playerOverlayDelegate?.didSelectGoogleCast()
|
viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem()
|
||||||
} label: {
|
|
||||||
Image(systemName: "rectangle.badge.plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.shouldShowAirplay {
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectAirplay()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "airplayvideo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.showAdjacentItems {
|
|
||||||
Button {
|
|
||||||
viewModel.playerOverlayDelegate?.didSelectPreviousItem()
|
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "chevron.left.circle")
|
Image(systemName: "chevron.left.circle")
|
||||||
}
|
}
|
||||||
.disabled(viewModel.previousItemVideoPlayerViewModel == nil)
|
.disabled(viewModel.previousItemVideoPlayerViewModel == nil)
|
||||||
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
|
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.shouldShowPlayNextItem {
|
||||||
Button {
|
Button {
|
||||||
viewModel.playerOverlayDelegate?.didSelectNextItem()
|
viewModel.playerOverlayDelegate?.didSelectPlayNextItem()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "chevron.right.circle")
|
Image(systemName: "chevron.right.circle")
|
||||||
}
|
}
|
||||||
|
@ -105,9 +90,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
|
|
||||||
if viewModel.shouldShowAutoPlayNextItem {
|
if viewModel.shouldShowAutoPlayNextItem {
|
||||||
Button {
|
Button {
|
||||||
viewModel.autoPlayNextItem.toggle()
|
viewModel.autoplayEnabled.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
if viewModel.autoPlayNextItem {
|
if viewModel.autoplayEnabled {
|
||||||
Image(systemName: "play.circle.fill")
|
Image(systemName: "play.circle.fill")
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "play.circle")
|
Image(systemName: "play.circle")
|
||||||
|
@ -117,7 +102,7 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
|
|
||||||
if !viewModel.subtitleStreams.isEmpty {
|
if !viewModel.subtitleStreams.isEmpty {
|
||||||
Button {
|
Button {
|
||||||
viewModel.playerOverlayDelegate?.didSelectSubtitles()
|
viewModel.subtitlesEnabled.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
if viewModel.subtitlesEnabled {
|
if viewModel.subtitlesEnabled {
|
||||||
Image(systemName: "captions.bubble.fill")
|
Image(systemName: "captions.bubble.fill")
|
||||||
|
@ -192,9 +177,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in
|
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in
|
||||||
Button {
|
Button {
|
||||||
jumpForwardLength = forwardLength
|
viewModel.jumpForwardLength = forwardLength
|
||||||
} label: {
|
} label: {
|
||||||
if forwardLength == jumpForwardLength {
|
if forwardLength == viewModel.jumpForwardLength {
|
||||||
Label(forwardLength.shortLabel, systemImage: "checkmark")
|
Label(forwardLength.shortLabel, systemImage: "checkmark")
|
||||||
} else {
|
} else {
|
||||||
Text(forwardLength.shortLabel)
|
Text(forwardLength.shortLabel)
|
||||||
|
@ -211,9 +196,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { backwardLength in
|
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { backwardLength in
|
||||||
Button {
|
Button {
|
||||||
jumpBackwardLength = backwardLength
|
viewModel.jumpBackwardLength = backwardLength
|
||||||
} label: {
|
} label: {
|
||||||
if backwardLength == jumpBackwardLength {
|
if backwardLength == viewModel.jumpBackwardLength {
|
||||||
Label(backwardLength.shortLabel, systemImage: "checkmark")
|
Label(backwardLength.shortLabel, systemImage: "checkmark")
|
||||||
} else {
|
} else {
|
||||||
Text(backwardLength.shortLabel)
|
Text(backwardLength.shortLabel)
|
||||||
|
@ -247,6 +232,11 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Center
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// MARK: Bottom Bar
|
// MARK: Bottom Bar
|
||||||
|
@ -264,7 +254,7 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
Button {
|
Button {
|
||||||
viewModel.playerOverlayDelegate?.didSelectBackward()
|
viewModel.playerOverlayDelegate?.didSelectBackward()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: jumpBackwardLength.backwardImageLabel)
|
Image(systemName: viewModel.jumpBackwardLength.backwardImageLabel)
|
||||||
.padding(.horizontal, 5)
|
.padding(.horizontal, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,12 +269,11 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
Button {
|
Button {
|
||||||
viewModel.playerOverlayDelegate?.didSelectForward()
|
viewModel.playerOverlayDelegate?.didSelectForward()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: jumpForwardLength.forwardImageLabel)
|
Image(systemName: viewModel.jumpForwardLength.forwardImageLabel)
|
||||||
.padding(.horizontal, 5)
|
.padding(.horizontal, 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.font(.system(size: 24, weight: .semibold, design: .default))
|
.font(.system(size: 24, weight: .semibold, design: .default))
|
||||||
// .padding(.trailing, 10)
|
|
||||||
|
|
||||||
Text(viewModel.leftLabelText)
|
Text(viewModel.leftLabelText)
|
||||||
.font(.system(size: 18, weight: .semibold, design: .default))
|
.font(.system(size: 18, weight: .semibold, design: .default))
|
||||||
|
@ -332,32 +321,43 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(),
|
||||||
|
title: "Glorious Purpose",
|
||||||
|
subtitle: "Loki - S1E1",
|
||||||
|
streamURL: URL(string: "www.apple.com")!,
|
||||||
|
hlsURL: URL(string: "www.apple.com")!,
|
||||||
|
response: PlaybackInfoResponse(),
|
||||||
|
audioStreams: [MediaStream(displayTitle: "English", index: -1)],
|
||||||
|
subtitleStreams: [MediaStream(displayTitle: "None", index: -1)],
|
||||||
|
selectedAudioStreamIndex: -1,
|
||||||
|
selectedSubtitleStreamIndex: -1,
|
||||||
|
subtitlesEnabled: true,
|
||||||
|
autoplayEnabled: false,
|
||||||
|
overlayType: .compact,
|
||||||
|
shouldShowPlayPreviousItem: true,
|
||||||
|
shouldShowPlayNextItem: true,
|
||||||
|
shouldShowAutoPlayNextItem: true)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.red
|
Color.red
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
VLCPlayerCompactOverlayView(viewModel: VideoPlayerViewModel(item: BaseItemDto(runTimeTicks: 720 * 10_000_000),
|
VLCPlayerOverlayView(viewModel: videoPlayerViewModel)
|
||||||
title: "Glorious Purpose",
|
|
||||||
subtitle: "Loki - S1E1",
|
|
||||||
streamURL: URL(string: "www.apple.com")!,
|
|
||||||
hlsURL: URL(string: "www.apple.com")!,
|
|
||||||
response: PlaybackInfoResponse(),
|
|
||||||
audioStreams: [MediaStream(displayTitle: "English", index: -1)],
|
|
||||||
subtitleStreams: [MediaStream(displayTitle: "None", index: -1)],
|
|
||||||
defaultAudioStreamIndex: -1,
|
|
||||||
defaultSubtitleStreamIndex: -1,
|
|
||||||
playerState: .playing,
|
|
||||||
shouldShowGoogleCast: false,
|
|
||||||
shouldShowAirplay: false,
|
|
||||||
subtitlesEnabled: true,
|
|
||||||
sliderPercentage: 0.432,
|
|
||||||
selectedAudioStreamIndex: -1,
|
|
||||||
selectedSubtitleStreamIndex: -1,
|
|
||||||
showAdjacentItems: true,
|
|
||||||
shouldShowAutoPlayNextItem: true,
|
|
||||||
autoPlayNextItem: true))
|
|
||||||
}
|
}
|
||||||
.previewInterfaceOrientation(.landscapeLeft)
|
.previewInterfaceOrientation(.landscapeLeft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: TitleSubtitleAlignment
|
||||||
|
extension HorizontalAlignment {
|
||||||
|
|
||||||
|
private struct TitleSubtitleAlignment: AlignmentID {
|
||||||
|
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||||
|
context[HorizontalAlignment.leading]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let EpisodeSeriesAlignmentGuide = HorizontalAlignment(TitleSubtitleAlignment.self)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct VLCPlayerView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
|
let viewModel: VideoPlayerViewModel
|
||||||
|
|
||||||
|
typealias UIViewControllerType = VLCPlayerViewController
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> VLCPlayerViewController {
|
||||||
|
|
||||||
|
return VLCPlayerViewController(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: VLCPlayerViewController, context: Context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
//
|
//
|
||||||
// PlayerViewController.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 11/12/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import AVKit
|
import AVKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
@ -25,7 +27,7 @@ class VLCPlayerViewController: UIViewController {
|
||||||
private var vlcMediaPlayer = VLCMediaPlayer()
|
private var vlcMediaPlayer = VLCMediaPlayer()
|
||||||
private var lastPlayerTicks: Int64 = 0
|
private var lastPlayerTicks: Int64 = 0
|
||||||
private var lastProgressReportTicks: Int64 = 0
|
private var lastProgressReportTicks: Int64 = 0
|
||||||
private var viewModelReactCancellables = Set<AnyCancellable>()
|
private var viewModelListeners = Set<AnyCancellable>()
|
||||||
private var overlayDismissTimer: Timer?
|
private var overlayDismissTimer: Timer?
|
||||||
|
|
||||||
private var currentPlayerTicks: Int64 {
|
private var currentPlayerTicks: Int64 {
|
||||||
|
@ -36,19 +38,11 @@ class VLCPlayerViewController: UIViewController {
|
||||||
return currentOverlayHostingController?.view.alpha ?? 0 > 0
|
return currentOverlayHostingController?.view.alpha ?? 0 > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var jumpForwardLength: VideoPlayerJumpLength {
|
|
||||||
return Defaults[.videoPlayerJumpForward]
|
|
||||||
}
|
|
||||||
|
|
||||||
private var jumpBackwardLength: VideoPlayerJumpLength {
|
|
||||||
return Defaults[.videoPlayerJumpBackward]
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var videoContentView = makeVideoContentView()
|
private lazy var videoContentView = makeVideoContentView()
|
||||||
private lazy var jumpBackwardOverlayView = makeJumpBackwardOverlayView()
|
private lazy var mainGestureView = makeTapGestureView()
|
||||||
private lazy var jumpForwardOverlayView = makeJumpForwardOverlayView()
|
private var currentOverlayHostingController: UIHostingController<VLCPlayerOverlayView>?
|
||||||
private lazy var tapGestureView = makeTapGestureView()
|
private var currentJumpBackwardOverlayView: UIImageView?
|
||||||
private var currentOverlayHostingController: UIHostingController<VLCPlayerCompactOverlayView>?
|
private var currentJumpForwardOverlayView: UIImageView?
|
||||||
|
|
||||||
// MARK: init
|
// MARK: init
|
||||||
|
|
||||||
|
@ -67,12 +61,7 @@ class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
private func setupSubviews() {
|
private func setupSubviews() {
|
||||||
view.addSubview(videoContentView)
|
view.addSubview(videoContentView)
|
||||||
view.addSubview(jumpForwardOverlayView)
|
view.addSubview(mainGestureView)
|
||||||
view.addSubview(jumpBackwardOverlayView)
|
|
||||||
view.addSubview(tapGestureView)
|
|
||||||
|
|
||||||
jumpBackwardOverlayView.alpha = 0
|
|
||||||
jumpForwardOverlayView.alpha = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupConstraints() {
|
private func setupConstraints() {
|
||||||
|
@ -83,18 +72,10 @@ class VLCPlayerViewController: UIViewController {
|
||||||
videoContentView.rightAnchor.constraint(equalTo: view.rightAnchor)
|
videoContentView.rightAnchor.constraint(equalTo: view.rightAnchor)
|
||||||
])
|
])
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
jumpBackwardOverlayView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 150),
|
mainGestureView.topAnchor.constraint(equalTo: videoContentView.topAnchor),
|
||||||
jumpBackwardOverlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
mainGestureView.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor),
|
||||||
])
|
mainGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
||||||
NSLayoutConstraint.activate([
|
mainGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
||||||
jumpForwardOverlayView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -150),
|
|
||||||
jumpForwardOverlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
|
||||||
])
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
tapGestureView.topAnchor.constraint(equalTo: videoContentView.topAnchor),
|
|
||||||
tapGestureView.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor),
|
|
||||||
tapGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
|
||||||
tapGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +108,9 @@ class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
setupMediaPlayer(newViewModel: viewModel)
|
setupMediaPlayer(newViewModel: viewModel)
|
||||||
|
|
||||||
|
refreshJumpBackwardOverlayView(with: viewModel.jumpBackwardLength)
|
||||||
|
refreshJumpForwardOverlayView(with: viewModel.jumpForwardLength)
|
||||||
|
|
||||||
let defaultNotificationCenter = NotificationCenter.default
|
let defaultNotificationCenter = NotificationCenter.default
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
|
defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
|
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
|
||||||
|
@ -192,26 +176,6 @@ class VLCPlayerViewController: UIViewController {
|
||||||
self.didSelectBackward()
|
self.didSelectBackward()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeJumpBackwardOverlayView() -> UIImageView {
|
|
||||||
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48)
|
|
||||||
let forwardSymbolImage = UIImage(systemName: jumpBackwardLength.backwardImageLabel, withConfiguration: symbolConfig)
|
|
||||||
let imageView = UIImageView(image: forwardSymbolImage)
|
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
imageView.tintColor = .white
|
|
||||||
|
|
||||||
return imageView
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeJumpForwardOverlayView() -> UIImageView {
|
|
||||||
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48)
|
|
||||||
let forwardSymbolImage = UIImage(systemName: jumpForwardLength.forwardImageLabel, withConfiguration: symbolConfig)
|
|
||||||
let imageView = UIImageView(image: forwardSymbolImage)
|
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
imageView.tintColor = .white
|
|
||||||
|
|
||||||
return imageView
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: setupOverlayHostingController
|
// MARK: setupOverlayHostingController
|
||||||
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
|
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
|
||||||
|
|
||||||
|
@ -225,11 +189,10 @@ class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
currentOverlayHostingController.view.removeFromSuperview()
|
currentOverlayHostingController.view.removeFromSuperview()
|
||||||
currentOverlayHostingController.removeFromParent()
|
currentOverlayHostingController.removeFromParent()
|
||||||
// self.currentOverlayHostingController = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let newOverlayView = VLCPlayerCompactOverlayView(viewModel: viewModel)
|
let newOverlayView = VLCPlayerOverlayView(viewModel: viewModel)
|
||||||
let newOverlayHostingController = UIHostingController(rootView: newOverlayView)
|
let newOverlayHostingController = UIHostingController(rootView: newOverlayView)
|
||||||
|
|
||||||
newOverlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
newOverlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -256,10 +219,59 @@ class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
self.currentOverlayHostingController = newOverlayHostingController
|
self.currentOverlayHostingController = newOverlayHostingController
|
||||||
|
|
||||||
// There is a behavior when setting this that the navigation bar
|
// There is a weird behavior when after setting the new overlays that the navigation bar pops up, re-hide it
|
||||||
// on the current navigation controller pops up, re-hide it
|
|
||||||
self.navigationController?.isNavigationBarHidden = true
|
self.navigationController?.isNavigationBarHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func refreshJumpBackwardOverlayView(with jumpBackwardLength: VideoPlayerJumpLength) {
|
||||||
|
|
||||||
|
if let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView {
|
||||||
|
currentJumpBackwardOverlayView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48)
|
||||||
|
let backwardSymbolImage = UIImage(systemName: jumpBackwardLength.backwardImageLabel, withConfiguration: symbolConfig)
|
||||||
|
let newJumpBackwardImageView = UIImageView(image: backwardSymbolImage)
|
||||||
|
|
||||||
|
newJumpBackwardImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
newJumpBackwardImageView.tintColor = .white
|
||||||
|
|
||||||
|
newJumpBackwardImageView.alpha = 0
|
||||||
|
|
||||||
|
view.addSubview(newJumpBackwardImageView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
newJumpBackwardImageView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 150),
|
||||||
|
newJumpBackwardImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
currentJumpBackwardOverlayView = newJumpBackwardImageView
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshJumpForwardOverlayView(with jumpForwardLength: VideoPlayerJumpLength) {
|
||||||
|
|
||||||
|
if let currentJumpForwardOverlayView = currentJumpForwardOverlayView {
|
||||||
|
currentJumpForwardOverlayView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48)
|
||||||
|
let forwardSymbolImage = UIImage(systemName: jumpForwardLength.forwardImageLabel, withConfiguration: symbolConfig)
|
||||||
|
let newJumpForwardImageView = UIImageView(image: forwardSymbolImage)
|
||||||
|
|
||||||
|
newJumpForwardImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
newJumpForwardImageView.tintColor = .white
|
||||||
|
|
||||||
|
newJumpForwardImageView.alpha = 0
|
||||||
|
|
||||||
|
view.addSubview(newJumpForwardImageView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
newJumpForwardImageView.leftAnchor.constraint(equalTo: view.rightAnchor, constant: -150),
|
||||||
|
newJumpForwardImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
currentJumpForwardOverlayView = newJumpForwardImageView
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: setupMediaPlayer
|
// MARK: setupMediaPlayer
|
||||||
|
@ -275,7 +287,7 @@ extension VLCPlayerViewController {
|
||||||
|
|
||||||
// Stop current media if there is one
|
// Stop current media if there is one
|
||||||
if vlcMediaPlayer.media != nil {
|
if vlcMediaPlayer.media != nil {
|
||||||
viewModelReactCancellables.forEach({ $0.cancel() })
|
viewModelListeners.forEach({ $0.cancel() })
|
||||||
|
|
||||||
vlcMediaPlayer.stop()
|
vlcMediaPlayer.stop()
|
||||||
viewModel.sendStopReport()
|
viewModel.sendStopReport()
|
||||||
|
@ -297,7 +309,7 @@ extension VLCPlayerViewController {
|
||||||
newViewModel.getAdjacentEpisodes()
|
newViewModel.getAdjacentEpisodes()
|
||||||
newViewModel.playerOverlayDelegate = self
|
newViewModel.playerOverlayDelegate = self
|
||||||
|
|
||||||
let startPercentage = viewModel.item.userData?.playedPercentage ?? 0
|
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
|
||||||
|
|
||||||
if startPercentage > 0 {
|
if startPercentage > 0 {
|
||||||
newViewModel.sliderPercentage = startPercentage / 100
|
newViewModel.sliderPercentage = startPercentage / 100
|
||||||
|
@ -320,9 +332,10 @@ extension VLCPlayerViewController {
|
||||||
// MARK: setupViewModelListeners
|
// MARK: setupViewModelListeners
|
||||||
|
|
||||||
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
||||||
|
|
||||||
viewModel.$playbackSpeed.sink { newSpeed in
|
viewModel.$playbackSpeed.sink { newSpeed in
|
||||||
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
||||||
}.store(in: &viewModelReactCancellables)
|
}.store(in: &viewModelListeners)
|
||||||
|
|
||||||
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
||||||
if sliderIsScrubbing {
|
if sliderIsScrubbing {
|
||||||
|
@ -330,15 +343,27 @@ extension VLCPlayerViewController {
|
||||||
} else {
|
} else {
|
||||||
self.didEndScrubbing()
|
self.didEndScrubbing()
|
||||||
}
|
}
|
||||||
}.store(in: &viewModelReactCancellables)
|
}.store(in: &viewModelListeners)
|
||||||
|
|
||||||
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
||||||
self.didSelectAudioStream(index: newAudioStreamIndex)
|
self.didSelectAudioStream(index: newAudioStreamIndex)
|
||||||
}.store(in: &viewModelReactCancellables)
|
}.store(in: &viewModelListeners)
|
||||||
|
|
||||||
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
||||||
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
||||||
}.store(in: &viewModelReactCancellables)
|
}.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.$jumpForwardLength.sink { newJumpForwardLength in
|
||||||
|
self.refreshJumpForwardOverlayView(with: newJumpForwardLength)
|
||||||
|
}.store(in: &viewModelListeners)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMediaPlayerTimeAtCurrentSlider() {
|
func setMediaPlayerTimeAtCurrentSlider() {
|
||||||
|
@ -395,34 +420,42 @@ extension VLCPlayerViewController {
|
||||||
extension VLCPlayerViewController {
|
extension VLCPlayerViewController {
|
||||||
|
|
||||||
private func flashJumpBackwardOverlay() {
|
private func flashJumpBackwardOverlay() {
|
||||||
jumpBackwardOverlayView.layer.removeAllAnimations()
|
guard let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView else { return }
|
||||||
|
|
||||||
|
currentJumpBackwardOverlayView.layer.removeAllAnimations()
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.1) {
|
UIView.animate(withDuration: 0.1) {
|
||||||
self.jumpBackwardOverlayView.alpha = 1
|
currentJumpBackwardOverlayView.alpha = 1
|
||||||
} completion: { _ in
|
} completion: { _ in
|
||||||
self.hideJumpBackwardOverlay()
|
self.hideJumpBackwardOverlay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hideJumpBackwardOverlay() {
|
private func hideJumpBackwardOverlay() {
|
||||||
|
guard let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView else { return }
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.jumpBackwardOverlayView.alpha = 0
|
currentJumpBackwardOverlayView.alpha = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func flashJumpFowardOverlay() {
|
private func flashJumpFowardOverlay() {
|
||||||
jumpForwardOverlayView.layer.removeAllAnimations()
|
guard let currentJumpForwardOverlayView = currentJumpForwardOverlayView else { return }
|
||||||
|
|
||||||
|
currentJumpForwardOverlayView.layer.removeAllAnimations()
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.1) {
|
UIView.animate(withDuration: 0.1) {
|
||||||
self.jumpForwardOverlayView.alpha = 1
|
currentJumpForwardOverlayView.alpha = 1
|
||||||
} completion: { _ in
|
} completion: { _ in
|
||||||
self.hideJumpForwardOverlay()
|
self.hideJumpForwardOverlay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hideJumpForwardOverlay() {
|
private func hideJumpForwardOverlay() {
|
||||||
|
guard let currentJumpForwardOverlayView = currentJumpForwardOverlayView else { return }
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.jumpForwardOverlayView.alpha = 0
|
currentJumpForwardOverlayView.alpha = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -459,8 +492,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
|
||||||
viewModel.playerState = vlcMediaPlayer.state
|
viewModel.playerState = vlcMediaPlayer.state
|
||||||
|
|
||||||
if vlcMediaPlayer.state == VLCMediaPlayerState.ended {
|
if vlcMediaPlayer.state == VLCMediaPlayerState.ended {
|
||||||
if viewModel.autoPlayNextItem && viewModel.shouldShowAutoPlayNextItem && viewModel.nextItemVideoPlayerViewModel != nil {
|
if viewModel.autoplayEnabled && viewModel.nextItemVideoPlayerViewModel != nil {
|
||||||
didSelectNextItem()
|
didSelectPlayNextItem()
|
||||||
} else {
|
} else {
|
||||||
didSelectClose()
|
didSelectClose()
|
||||||
}
|
}
|
||||||
|
@ -470,13 +503,10 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
|
||||||
// MARK: mediaPlayerTimeChanged
|
// MARK: mediaPlayerTimeChanged
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
||||||
|
|
||||||
guard !viewModel.sliderIsScrubbing else {
|
if !viewModel.sliderIsScrubbing {
|
||||||
lastPlayerTicks = currentPlayerTicks
|
viewModel.sliderPercentage = Double(vlcMediaPlayer.position)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.sliderPercentage = Double(vlcMediaPlayer.position)
|
|
||||||
|
|
||||||
// Have to manually set playing because VLCMediaPlayer doesn't
|
// Have to manually set playing because VLCMediaPlayer doesn't
|
||||||
// properly set it itself
|
// properly set it itself
|
||||||
if abs(currentPlayerTicks - lastPlayerTicks) >= 10_000 {
|
if abs(currentPlayerTicks - lastPlayerTicks) >= 10_000 {
|
||||||
|
@ -486,6 +516,9 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
|
||||||
// If needing to fix subtitle streams during playback
|
// If needing to fix subtitle streams during playback
|
||||||
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled {
|
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled {
|
||||||
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
|
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vlcMediaPlayer.currentAudioTrackIndex != viewModel.selectedAudioStreamIndex {
|
||||||
didSelectAudioStream(index: viewModel.selectedAudioStreamIndex)
|
didSelectAudioStream(index: viewModel.selectedAudioStreamIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,7 +533,7 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: PlayerOverlayDelegate
|
// MARK: PlayerOverlayDelegate and more
|
||||||
extension VLCPlayerViewController: PlayerOverlayDelegate {
|
extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
|
|
||||||
func didSelectAudioStream(index: Int) {
|
func didSelectAudioStream(index: Int) {
|
||||||
|
@ -511,12 +544,11 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
lastProgressReportTicks = currentPlayerTicks
|
lastProgressReportTicks = currentPlayerTicks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Do not call when setting to index -1
|
||||||
func didSelectSubtitleStream(index: Int) {
|
func didSelectSubtitleStream(index: Int) {
|
||||||
if viewModel.subtitlesEnabled {
|
|
||||||
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index)
|
viewModel.subtitlesEnabled = true
|
||||||
} else {
|
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index)
|
||||||
vlcMediaPlayer.currentVideoSubTitleIndex = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.sendProgressReport()
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
|
@ -531,19 +563,8 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
dismiss(animated: true, completion: nil)
|
dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func didSelectGoogleCast() {
|
func didToggleSubtitles(newValue: Bool) {
|
||||||
print("didSelectCast")
|
if newValue {
|
||||||
}
|
|
||||||
|
|
||||||
func didSelectAirplay() {
|
|
||||||
print("didSelectAirplay")
|
|
||||||
}
|
|
||||||
|
|
||||||
func didSelectSubtitles() {
|
|
||||||
|
|
||||||
viewModel.subtitlesEnabled = !viewModel.subtitlesEnabled
|
|
||||||
|
|
||||||
if viewModel.subtitlesEnabled {
|
|
||||||
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex)
|
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex)
|
||||||
} else {
|
} else {
|
||||||
vlcMediaPlayer.currentVideoSubTitleIndex = -1
|
vlcMediaPlayer.currentVideoSubTitleIndex = -1
|
||||||
|
@ -561,27 +582,33 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func didSelectBackward() {
|
func didSelectBackward() {
|
||||||
|
|
||||||
flashJumpBackwardOverlay()
|
flashJumpBackwardOverlay()
|
||||||
|
|
||||||
vlcMediaPlayer.jumpBackward(jumpBackwardLength.rawValue)
|
vlcMediaPlayer.jumpBackward(viewModel.jumpBackwardLength.rawValue)
|
||||||
|
|
||||||
restartOverlayDismissTimer()
|
if displayingOverlay {
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.sendProgressReport()
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
self.lastProgressReportTicks = currentPlayerTicks
|
lastProgressReportTicks = currentPlayerTicks
|
||||||
}
|
}
|
||||||
|
|
||||||
func didSelectForward() {
|
func didSelectForward() {
|
||||||
|
|
||||||
flashJumpFowardOverlay()
|
flashJumpFowardOverlay()
|
||||||
|
|
||||||
vlcMediaPlayer.jumpForward(jumpForwardLength.rawValue)
|
vlcMediaPlayer.jumpForward(viewModel.jumpForwardLength.rawValue)
|
||||||
|
|
||||||
restartOverlayDismissTimer()
|
if displayingOverlay {
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.sendProgressReport()
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
self.lastProgressReportTicks = currentPlayerTicks
|
lastProgressReportTicks = currentPlayerTicks
|
||||||
}
|
}
|
||||||
|
|
||||||
func didSelectMain() {
|
func didSelectMain() {
|
||||||
|
@ -619,16 +646,20 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
|
|
||||||
viewModel.sendProgressReport()
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
self.lastProgressReportTicks = currentPlayerTicks
|
lastProgressReportTicks = currentPlayerTicks
|
||||||
}
|
}
|
||||||
|
|
||||||
func didSelectPreviousItem() {
|
func didSelectPlayPreviousItem() {
|
||||||
setupMediaPlayer(newViewModel: viewModel.previousItemVideoPlayerViewModel!)
|
if let previousItemVideoPlayerViewModel = viewModel.previousItemVideoPlayerViewModel {
|
||||||
startPlayback()
|
setupMediaPlayer(newViewModel: previousItemVideoPlayerViewModel)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func didSelectNextItem() {
|
func didSelectPlayNextItem() {
|
||||||
setupMediaPlayer(newViewModel: viewModel.nextItemVideoPlayerViewModel!)
|
if let nextItemVideoPlayerViewModel = viewModel.nextItemVideoPlayerViewModel {
|
||||||
startPlayback()
|
setupMediaPlayer(newViewModel: nextItemVideoPlayerViewModel)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
//
|
|
||||||
// VideoPlayerView.swift
|
|
||||||
// JellyfinVideoPlayerDev
|
|
||||||
//
|
|
||||||
// Created by Ethan Pippin on 11/12/21.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct NativePlayerView: UIViewControllerRepresentable {
|
|
||||||
|
|
||||||
let viewModel: VideoPlayerViewModel
|
|
||||||
|
|
||||||
typealias UIViewControllerType = NativePlayerViewController
|
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> NativePlayerViewController {
|
|
||||||
|
|
||||||
return NativePlayerViewController(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VLCPlayerView: UIViewControllerRepresentable {
|
|
||||||
|
|
||||||
let viewModel: VideoPlayerViewModel
|
|
||||||
|
|
||||||
typealias UIViewControllerType = VLCPlayerViewController
|
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> VLCPlayerViewController {
|
|
||||||
|
|
||||||
return VLCPlayerViewController(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: VLCPlayerViewController, context: Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
||||||
@Default(.nativeVideoPlayer) var nativeVideoPlayer
|
|
||||||
let viewModel: VideoPlayerViewModel
|
let viewModel: VideoPlayerViewModel
|
||||||
|
|
||||||
init(viewModel: VideoPlayerViewModel) {
|
init(viewModel: VideoPlayerViewModel) {
|
||||||
|
@ -27,24 +26,13 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
if nativeVideoPlayer {
|
PreferenceUIHostingControllerView {
|
||||||
PreferenceUIHostingControllerView {
|
VLCPlayerView(viewModel: self.viewModel)
|
||||||
NativePlayerView(viewModel: self.viewModel)
|
.navigationBarHidden(true)
|
||||||
.navigationBarHidden(true)
|
.statusBar(hidden: true)
|
||||||
.statusBar(hidden: true)
|
.ignoresSafeArea()
|
||||||
.ignoresSafeArea()
|
.prefersHomeIndicatorAutoHidden(true)
|
||||||
.prefersHomeIndicatorAutoHidden(true)
|
.supportedOrientations(.landscape)
|
||||||
.supportedOrientations(.landscape)
|
}.ignoresSafeArea()
|
||||||
}.ignoresSafeArea()
|
|
||||||
} else {
|
|
||||||
PreferenceUIHostingControllerView {
|
|
||||||
VLCPlayerView(viewModel: self.viewModel)
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
.statusBar(hidden: true)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.prefersHomeIndicatorAutoHidden(true)
|
|
||||||
.supportedOrientations(.landscape)
|
|
||||||
}.ignoresSafeArea()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,8 @@ extension BaseItemDto {
|
||||||
hlsURL.addQueryItem(name: "SubtitleStreamIndex", value: "\(defaultSubtitleStream!.index!)")
|
hlsURL.addQueryItem(name: "SubtitleStreamIndex", value: "\(defaultSubtitleStream!.index!)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: VidoPlayerViewModel Creation
|
||||||
|
|
||||||
var subtitle: String? = nil
|
var subtitle: String? = nil
|
||||||
|
|
||||||
// TODO: other forms of media subtitle
|
// TODO: other forms of media subtitle
|
||||||
|
@ -89,34 +91,32 @@ extension BaseItemDto {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let subtitlesEnabled = Defaults[.subtitlesEnabledIfDefault] && defaultSubtitleStream != nil
|
||||||
|
|
||||||
// MARK: VidoPlayerViewModel Creation
|
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
||||||
|
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
||||||
|
|
||||||
// TODO: show adjacent items
|
let overlayType = Defaults[.overlayType]
|
||||||
|
|
||||||
let shouldShowAutoPlayNextItem = Defaults[.shouldShowAutoPlayNextItem] && itemType == .episode
|
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||||
let autoPlayNextItem = Defaults[.autoPlayNextItem]
|
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(item: self,
|
let videoPlayerViewModel = VideoPlayerViewModel(item: self,
|
||||||
title: self.name!,
|
title: self.name ?? "",
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
streamURL: streamURL.url!,
|
streamURL: streamURL.url!,
|
||||||
hlsURL: hlsURL.url!,
|
hlsURL: hlsURL.url!,
|
||||||
response: response,
|
response: response,
|
||||||
audioStreams: audioStreams,
|
audioStreams: audioStreams,
|
||||||
subtitleStreams: subtitleStreams,
|
subtitleStreams: subtitleStreams,
|
||||||
defaultAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
|
||||||
defaultSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
|
||||||
playerState: .playing,
|
|
||||||
shouldShowGoogleCast: false,
|
|
||||||
shouldShowAirplay: false,
|
|
||||||
subtitlesEnabled: defaultSubtitleStream?.index != nil,
|
|
||||||
sliderPercentage: (self.userData?.playedPercentage ?? 0) / 100,
|
|
||||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||||
showAdjacentItems: true,
|
subtitlesEnabled: subtitlesEnabled,
|
||||||
shouldShowAutoPlayNextItem: shouldShowAutoPlayNextItem,
|
autoplayEnabled: autoplayEnabled,
|
||||||
autoPlayNextItem: autoPlayNextItem)
|
overlayType: overlayType,
|
||||||
|
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
||||||
|
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
||||||
|
shouldShowAutoPlayNextItem: shouldShowAutoPlay)
|
||||||
|
|
||||||
return videoPlayerViewModel
|
return videoPlayerViewModel
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum OverlaySliderColor: String, CaseIterable, DefaultsSerializable {
|
||||||
|
case white
|
||||||
|
case jellyfinPurple
|
||||||
|
|
||||||
|
var displayLabel: String {
|
||||||
|
switch self {
|
||||||
|
case .white:
|
||||||
|
return "White"
|
||||||
|
case .jellyfinPurple:
|
||||||
|
return "Jellyfin Purple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum OverlayType: String, CaseIterable, Defaults.Serializable {
|
||||||
|
case normal
|
||||||
|
case compact
|
||||||
|
case bottom
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ final class SessionManager {
|
||||||
|
|
||||||
// MARK: init
|
// MARK: init
|
||||||
private init() {
|
private init() {
|
||||||
if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID],
|
if let lastUserID = Defaults[.lastServerUserID],
|
||||||
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
||||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]) {
|
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]) {
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ final class SessionManager {
|
||||||
var uriComponents = URLComponents(string: uri) ?? URLComponents()
|
var uriComponents = URLComponents(string: uri) ?? URLComponents()
|
||||||
|
|
||||||
if uriComponents.scheme == nil {
|
if uriComponents.scheme == nil {
|
||||||
uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue
|
uriComponents.scheme = Defaults[.defaultHTTPScheme].rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
var uri = uriComponents.string ?? ""
|
var uri = uriComponents.string ?? ""
|
||||||
|
@ -216,7 +216,7 @@ final class SessionManager {
|
||||||
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
||||||
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
||||||
|
|
||||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
Defaults[.lastServerUserID] = user.id
|
||||||
|
|
||||||
currentLogin = (server: currentServer.state, user: currentUser.state)
|
currentLogin = (server: currentServer.state, user: currentUser.state)
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||||
|
@ -230,7 +230,7 @@ final class SessionManager {
|
||||||
// MARK: loginUser
|
// MARK: loginUser
|
||||||
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
||||||
JellyfinAPI.basePath = server.currentURI
|
JellyfinAPI.basePath = server.currentURI
|
||||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
Defaults[.lastServerUserID] = user.id
|
||||||
setAuthHeader(with: user.accessToken)
|
setAuthHeader(with: user.accessToken)
|
||||||
currentLogin = (server: server, user: user)
|
currentLogin = (server: server, user: user)
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||||
|
@ -241,7 +241,7 @@ final class SessionManager {
|
||||||
currentLogin = nil
|
currentLogin = nil
|
||||||
JellyfinAPI.basePath = ""
|
JellyfinAPI.basePath = ""
|
||||||
setAuthHeader(with: "")
|
setAuthHeader(with: "")
|
||||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = nil
|
Defaults[.lastServerUserID] = nil
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,8 +254,8 @@ final class SessionManager {
|
||||||
delete(server: server)
|
delete(server: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete UserDefaults
|
// Delete general UserDefaults
|
||||||
SwiftfinStore.Defaults.suite.removeAll()
|
SwiftfinStore.Defaults.generalSuite.removeAll()
|
||||||
|
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,25 +14,44 @@ extension SwiftfinStore {
|
||||||
|
|
||||||
enum Defaults {
|
enum Defaults {
|
||||||
|
|
||||||
static let suite: UserDefaults = {
|
static let generalSuite: UserDefaults = {
|
||||||
return UserDefaults(suiteName: "swiftfinstore-defaults")!
|
return UserDefaults(suiteName: "swiftfinstore-general-defaults")!
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let universalSuite: UserDefaults = {
|
||||||
|
return UserDefaults(suiteName: "swiftfinstore-universal-defaults")!
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.suite)
|
|
||||||
|
// Universal settings
|
||||||
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite)
|
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
|
||||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
|
||||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
|
||||||
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.suite)
|
// General settings
|
||||||
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite)
|
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite)
|
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.suite)
|
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.suite)
|
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.suite)
|
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let nativeVideoPlayer = Key<Bool>("nativeVideoPlayer", default: false, suite: SwiftfinStore.Defaults.suite)
|
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let shouldShowAutoPlayNextItem = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.suite)
|
|
||||||
static let autoPlayNextItem = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.suite)
|
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
static let gesturesEnabled = 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 subtitlesEnabledIfDefault = Key<Bool>("subtitlesEnabledIfDefault", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", 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)
|
||||||
|
|
||||||
|
// Experimental settings
|
||||||
|
struct Experimental {
|
||||||
|
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,9 @@ final class HomeViewModel: ViewModel {
|
||||||
case .finished: ()
|
case .finished: ()
|
||||||
case .failure:
|
case .failure:
|
||||||
self.libraries = []
|
self.libraries = []
|
||||||
self.handleAPIRequestError(completion: completion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
|
||||||
var newLibraries: [BaseItemDto] = []
|
var newLibraries: [BaseItemDto] = []
|
||||||
|
|
|
@ -12,13 +12,13 @@ import SwiftUI
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
||||||
final class SettingsViewModel: ObservableObject {
|
final class SettingsViewModel: ObservableObject {
|
||||||
let currentLocale = Locale.current
|
|
||||||
var bitrates: [Bitrates] = []
|
var bitrates: [Bitrates] = []
|
||||||
var langs = [TrackLanguage]()
|
var langs: [TrackLanguage] = []
|
||||||
let appearances = AppAppearance.allCases
|
|
||||||
let videoPlayerJumpLengths = VideoPlayerJumpLength.allCases
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
||||||
|
// Bitrates
|
||||||
let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
|
let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -32,8 +32,9 @@ final class SettingsViewModel: ObservableObject {
|
||||||
LogManager.shared.log.error("Error processing JSON file `bitrates.json`")
|
LogManager.shared.log.error("Error processing JSON file `bitrates.json`")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track languages
|
||||||
self.langs = Locale.isoLanguageCodes.compactMap {
|
self.langs = Locale.isoLanguageCodes.compactMap {
|
||||||
guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil }
|
guard let name = Locale.current.localizedString(forLanguageCode: $0) else { return nil }
|
||||||
return TrackLanguage(name: name, isoCode: $0)
|
return TrackLanguage(name: name, isoCode: $0)
|
||||||
}.sorted(by: { $0.name < $1.name })
|
}.sorted(by: { $0.name < $1.name })
|
||||||
self.langs.insert(.auto, at: 0)
|
self.langs.insert(.auto, at: 0)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
//
|
//
|
||||||
// VideoPlayerViewModel.swift
|
/*
|
||||||
// JellyfinVideoPlayerDev
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
//
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
// Created by Ethan Pippin on 11/12/21.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
//
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Defaults
|
import Defaults
|
||||||
|
@ -19,71 +21,59 @@ import MobileVLCKit
|
||||||
|
|
||||||
final class VideoPlayerViewModel: ViewModel {
|
final class VideoPlayerViewModel: ViewModel {
|
||||||
|
|
||||||
|
// MARK: Published
|
||||||
|
|
||||||
// Manually kept state because VLCKit doesn't properly set "played"
|
// Manually kept state because VLCKit doesn't properly set "played"
|
||||||
// on the VLCMediaPlayer object
|
// on the VLCMediaPlayer object
|
||||||
@Published var playerState: VLCMediaPlayerState
|
@Published var playerState: VLCMediaPlayerState = .buffering
|
||||||
@Published var shouldShowGoogleCast: Bool
|
|
||||||
@Published var shouldShowAirplay: Bool
|
|
||||||
@Published var subtitlesEnabled: Bool {
|
|
||||||
didSet {
|
|
||||||
if subtitlesEnabled != oldValue {
|
|
||||||
previousItemVideoPlayerViewModel?.matchSubtitlesEnabled(with: self)
|
|
||||||
nextItemVideoPlayerViewModel?.matchSubtitlesEnabled(with: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Published var leftLabelText: String = "--:--"
|
@Published var leftLabelText: String = "--:--"
|
||||||
@Published var rightLabelText: String = "--:--"
|
@Published var rightLabelText: String = "--:--"
|
||||||
@Published var playbackSpeed: PlaybackSpeed = .one
|
@Published var playbackSpeed: PlaybackSpeed = .one
|
||||||
@Published var sliderPercentage: Double {
|
@Published var subtitlesEnabled: Bool
|
||||||
|
@Published var selectedAudioStreamIndex: Int
|
||||||
|
@Published var selectedSubtitleStreamIndex: Int
|
||||||
|
@Published var previousItemVideoPlayerViewModel: VideoPlayerViewModel?
|
||||||
|
@Published var nextItemVideoPlayerViewModel: VideoPlayerViewModel?
|
||||||
|
@Published var jumpBackwardLength: VideoPlayerJumpLength
|
||||||
|
@Published var jumpForwardLength: VideoPlayerJumpLength
|
||||||
|
@Published var sliderIsScrubbing: Bool = false
|
||||||
|
@Published var sliderPercentage: Double = 0 {
|
||||||
willSet {
|
willSet {
|
||||||
sliderScrubbingSubject.send(self)
|
sliderScrubbingSubject.send(self)
|
||||||
sliderPercentageChanged(newValue: newValue)
|
sliderPercentageChanged(newValue: newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published var sliderIsScrubbing: Bool = false
|
@Published var autoplayEnabled: Bool {
|
||||||
@Published var selectedAudioStreamIndex: Int {
|
|
||||||
didSet {
|
|
||||||
previousItemVideoPlayerViewModel?.matchAudioStream(with: self)
|
|
||||||
nextItemVideoPlayerViewModel?.matchAudioStream(with: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Published var selectedSubtitleStreamIndex: Int {
|
|
||||||
didSet {
|
|
||||||
previousItemVideoPlayerViewModel?.matchSubtitleStream(with: self)
|
|
||||||
nextItemVideoPlayerViewModel?.matchSubtitleStream(with: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Published var showAdjacentItems: Bool
|
|
||||||
@Published var shouldShowAutoPlayNextItem: Bool {
|
|
||||||
willSet {
|
willSet {
|
||||||
Defaults[.shouldShowAutoPlayNextItem] = newValue
|
Defaults[.autoplayEnabled] = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published var autoPlayNextItem: Bool {
|
|
||||||
willSet {
|
|
||||||
Defaults[.autoPlayNextItem] = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Published var previousItemVideoPlayerViewModel: VideoPlayerViewModel?
|
|
||||||
@Published var nextItemVideoPlayerViewModel: VideoPlayerViewModel?
|
|
||||||
|
|
||||||
|
// MARK: ShouldShowItems
|
||||||
|
|
||||||
|
let shouldShowPlayPreviousItem: Bool
|
||||||
|
let shouldShowPlayNextItem: Bool
|
||||||
|
let shouldShowAutoPlayNextItem: Bool
|
||||||
|
|
||||||
|
// MARK: General
|
||||||
let item: BaseItemDto
|
let item: BaseItemDto
|
||||||
let title: String
|
let title: String
|
||||||
let subtitle: String?
|
let subtitle: String?
|
||||||
let streamURL: URL
|
let streamURL: URL
|
||||||
let hlsURL: URL
|
let hlsURL: URL
|
||||||
// Full response kept for convenience
|
|
||||||
let response: PlaybackInfoResponse
|
|
||||||
let audioStreams: [MediaStream]
|
let audioStreams: [MediaStream]
|
||||||
let subtitleStreams: [MediaStream]
|
let subtitleStreams: [MediaStream]
|
||||||
let defaultAudioStreamIndex: Int
|
let overlayType: OverlayType
|
||||||
let defaultSubtitleStreamIndex: Int
|
|
||||||
|
// Full response kept for convenience
|
||||||
|
let response: PlaybackInfoResponse
|
||||||
|
|
||||||
var playerOverlayDelegate: PlayerOverlayDelegate?
|
var playerOverlayDelegate: PlayerOverlayDelegate?
|
||||||
|
|
||||||
// Ticks of the time the media has begun
|
// Ticks of the time the media began playing
|
||||||
var startTimeTicks: Int64?
|
private var startTimeTicks: Int64 = 0
|
||||||
|
|
||||||
|
// MARK: Current Time
|
||||||
|
|
||||||
var currentSeconds: Double {
|
var currentSeconds: Double {
|
||||||
let videoDuration = Double(item.runTimeTicks! / 10_000_000)
|
let videoDuration = Double(item.runTimeTicks! / 10_000_000)
|
||||||
|
@ -107,18 +97,16 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
response: PlaybackInfoResponse,
|
response: PlaybackInfoResponse,
|
||||||
audioStreams: [MediaStream],
|
audioStreams: [MediaStream],
|
||||||
subtitleStreams: [MediaStream],
|
subtitleStreams: [MediaStream],
|
||||||
defaultAudioStreamIndex: Int,
|
|
||||||
defaultSubtitleStreamIndex: Int,
|
|
||||||
playerState: VLCMediaPlayerState,
|
|
||||||
shouldShowGoogleCast: Bool,
|
|
||||||
shouldShowAirplay: Bool,
|
|
||||||
subtitlesEnabled: Bool,
|
|
||||||
sliderPercentage: Double,
|
|
||||||
selectedAudioStreamIndex: Int,
|
selectedAudioStreamIndex: Int,
|
||||||
selectedSubtitleStreamIndex: Int,
|
selectedSubtitleStreamIndex: Int,
|
||||||
showAdjacentItems: Bool,
|
subtitlesEnabled: Bool,
|
||||||
shouldShowAutoPlayNextItem: Bool,
|
autoplayEnabled: Bool,
|
||||||
autoPlayNextItem: Bool) {
|
overlayType: OverlayType,
|
||||||
|
shouldShowPlayPreviousItem: Bool,
|
||||||
|
shouldShowPlayNextItem: Bool,
|
||||||
|
shouldShowAutoPlayNextItem: Bool
|
||||||
|
|
||||||
|
) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.title = title
|
self.title = title
|
||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
|
@ -127,26 +115,21 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
self.response = response
|
self.response = response
|
||||||
self.audioStreams = audioStreams
|
self.audioStreams = audioStreams
|
||||||
self.subtitleStreams = subtitleStreams
|
self.subtitleStreams = subtitleStreams
|
||||||
self.defaultAudioStreamIndex = defaultAudioStreamIndex
|
|
||||||
self.defaultSubtitleStreamIndex = defaultSubtitleStreamIndex
|
|
||||||
self.playerState = playerState
|
|
||||||
self.shouldShowGoogleCast = shouldShowGoogleCast
|
|
||||||
self.shouldShowAirplay = shouldShowAirplay
|
|
||||||
self.subtitlesEnabled = subtitlesEnabled
|
|
||||||
self.sliderPercentage = sliderPercentage
|
|
||||||
self.selectedAudioStreamIndex = selectedAudioStreamIndex
|
self.selectedAudioStreamIndex = selectedAudioStreamIndex
|
||||||
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
|
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
|
||||||
self.showAdjacentItems = showAdjacentItems
|
self.subtitlesEnabled = subtitlesEnabled
|
||||||
|
self.autoplayEnabled = autoplayEnabled
|
||||||
|
self.overlayType = overlayType
|
||||||
|
self.shouldShowPlayPreviousItem = shouldShowPlayPreviousItem
|
||||||
|
self.shouldShowPlayNextItem = shouldShowPlayNextItem
|
||||||
self.shouldShowAutoPlayNextItem = shouldShowAutoPlayNextItem
|
self.shouldShowAutoPlayNextItem = shouldShowAutoPlayNextItem
|
||||||
self.autoPlayNextItem = autoPlayNextItem
|
|
||||||
|
self.jumpBackwardLength = Defaults[.videoPlayerJumpBackward]
|
||||||
|
self.jumpForwardLength = Defaults[.videoPlayerJumpForward]
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.sliderPercentageChanged(newValue: (item.userData?.playedPercentage ?? 0) / 100)
|
self.sliderPercentage = (item.userData?.playedPercentage ?? 0) / 100
|
||||||
|
|
||||||
if item.itemType != .episode {
|
|
||||||
self.showAdjacentItems = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sliderPercentageChanged(newValue: Double) {
|
private func sliderPercentageChanged(newValue: Double) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ class ViewModel: ObservableObject {
|
||||||
func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function, file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>) {
|
func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function, file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>) {
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
self.errorMessage = nil
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)
|
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue