begin final work

This commit is contained in:
Ethan Pippin 2022-01-02 21:20:20 -07:00
parent cd3a244f17
commit 5b451ceaaa
32 changed files with 675 additions and 856 deletions

View File

@ -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)

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}
}

View File

@ -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;

View File

@ -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()
}
}
}
}

View File

@ -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) {

View File

@ -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()
}
} }
} }
} }

View File

@ -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)
} }
} }

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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()
} }

View File

@ -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)
}

View File

@ -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) {
}
}

View File

@ -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()
}
} }
} }

View File

@ -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) {
}
}

View File

@ -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()
}
} }
} }

View File

@ -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
}) })

View File

@ -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"
}
}
}

View File

@ -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
}

View File

@ -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)
} }

View File

@ -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)
}
} }

View File

@ -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] = []

View File

@ -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)

View File

@ -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) {

View File

@ -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)