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)
MediaPlayButtonRowView(viewModel: viewModel)
.environmentObject(itemRouter)
}
}.padding(.top, 50)

View File

@ -11,6 +11,8 @@ import SwiftUI
import JellyfinAPI
struct SeasonItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: SeasonItemViewModel
@State var wrappedScrollView: UIScrollView?
@ -101,10 +103,15 @@ struct SeasonItemView: View {
ScrollView(.horizontal) {
LazyHStack {
Spacer().frame(width: 45)
ForEach(viewModel.episodes, id: \.id) { episode in
NavigationLink(destination: ItemView(item: episode)) {
Button {
itemRouter.route(to: \.item, episode)
} label: {
LandscapeItemElement(item: episode, inSeasonView: true)
}.buttonStyle(PlainNavigationLinkButtonStyle())
}
.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}

View File

@ -11,6 +11,8 @@ import SwiftUI
import JellyfinAPI
struct SeriesItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: SeriesItemViewModel
@State var actors: [BaseItemPerson] = []
@ -141,10 +143,16 @@ struct SeriesItemView: View {
ScrollView(.horizontal) {
LazyHStack {
Spacer().frame(width: 45)
ForEach(viewModel.seasons, id: \.id) { season in
NavigationLink(destination: ItemView(item: season)) {
Button {
itemRouter.route(to: \.item, season)
} label: {
PortraitItemElement(item: season)
}.buttonStyle(PlainNavigationLinkButtonStyle())
}
.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}

View File

@ -1,9 +1,11 @@
//
// NativePlayerViewController.swift
// JellyfinVideoPlayerDev
//
// Created by Ethan Pippin on 11/20/21.
//
/*
* 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 AVKit
import Combine

View File

@ -1,9 +1,11 @@
//
// PlayerOverlayDelegate.swift
// JellyfinVideoPlayerDev
//
// Created by Ethan Pippin on 12/27/21.
//
/*
* 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 Foundation

View File

@ -1,9 +1,11 @@
//
// PlayerViewController.swift
// JellyfinVideoPlayerDev
//
// Created by Ethan Pippin on 11/12/21.
//
/*
* 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 AVKit
import AVFoundation
@ -203,7 +205,10 @@ class VLCPlayerViewController: UIViewController {
case .upArrow:
print("Up arrow")
case .downArrow:
print("Down arrow")
stopOverlayDismissTimer()
hideOverlay()
showOverlayContent()
case .leftArrow:
didSelectBackward()
print("Left arrow")
@ -227,6 +232,8 @@ class VLCPlayerViewController: UIViewController {
@objc private func didPressMenu() {
if displayingOverlay {
hideOverlay()
} else if displayingContentOverlay {
hideOverlayContent()
} else {
vlcMediaPlayer.pause()
@ -294,6 +301,11 @@ class VLCPlayerViewController: UIViewController {
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 newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView)
@ -452,6 +464,10 @@ extension VLCPlayerViewController {
guard currentOverlayContentHostingController.view.alpha != 1 else { return }
currentOverlayContentHostingController.view.setNeedsFocusUpdate()
currentOverlayContentHostingController.setNeedsFocusUpdate()
setNeedsFocusUpdate()
UIView.animate(withDuration: 0.2) {
currentOverlayContentHostingController.view.alpha = 1
}
@ -462,6 +478,8 @@ extension VLCPlayerViewController {
guard currentOverlayContentHostingController.view.alpha != 0 else { return }
setNeedsFocusUpdate()
UIView.animate(withDuration: 0.2) {
currentOverlayContentHostingController.view.alpha = 0
}
@ -629,10 +647,10 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
// TODO: Implement properly in overlays
func didSelectMenu() {
// stopOverlayDismissTimer()
//
// hideOverlay()
// showOverlayContent()
stopOverlayDismissTimer()
hideOverlay()
showOverlayContent()
}
// TODO: Implement properly in overlays

View File

@ -1,9 +1,11 @@
//
// VideoPlayerView.swift
// JellyfinVideoPlayerDev
//
// Created by Ethan Pippin on 11/12/21.
//
/*
* 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

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 */; };
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* 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 */; };
E10EAA4F277BBCC4000269ED /* 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 */; };
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A99998271A3429008E78C0 /* 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 */; };
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.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 */; };
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */; };
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */; };
E1C812BF277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */; };
E1C812C0277A8E5D00918266 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */; };
E1C812C1277A8E5D00918266 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */; };
E1C812C3277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */; };
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */; };
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */; };
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */; };
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C6277AE40900918266 /* PlayerOverlayDelegate.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 */; };
E1F0204E26CCCA74001C1C3B /* 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 */; };
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.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>"; };
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>"; };
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>"; };
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>"; };
@ -617,6 +620,9 @@
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>"; };
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>"; };
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>"; };
@ -626,10 +632,8 @@
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>"; };
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 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.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>"; };
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerView.swift; sourceTree = "<group>"; };
E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerOverlayView.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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -858,6 +863,8 @@
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
E19169CD272514760085832A /* HTTPScheme.swift */,
E1AA33212782648000F6439C /* OverlaySliderColor.swift */,
E1AA331E2782639D00F6439C /* OverlayType.swift */,
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
@ -1081,11 +1088,12 @@
53F866422687A45400DCD1D7 /* Components */ = {
isa = PBXGroup;
children = (
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
E1AA331C2782541500F6439C /* PrimaryButtonView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -1201,16 +1209,6 @@
path = Pods;
sourceTree = "<group>";
};
E10EAA48277BB6D7000269ED /* Overlays */ = {
isa = PBXGroup;
children = (
E10EAA49277BB6F5000269ED /* VideoPlayerOverlay.swift */,
E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */,
E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */,
);
path = Overlays;
sourceTree = "<group>";
};
E12186DF2718F2030010884C /* App */ = {
isa = PBXGroup;
children = (
@ -1312,6 +1310,7 @@
E17885A7278130690094FBCF /* tvOSOverlay */ = {
isa = PBXGroup;
children = (
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */,
E17885A5278130610094FBCF /* tvOSOverlayContent.swift */,
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */,
);
@ -1350,11 +1349,10 @@
E193D5452719418B00900D82 /* VideoPlayer */ = {
isa = PBXGroup;
children = (
E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */,
E10EAA48277BB6D7000269ED /* Overlays */,
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */,
E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */,
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */,
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */,
);
path = VideoPlayer;
@ -1911,6 +1909,7 @@
E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */,
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
5398514726B64E4100101B49 /* SearchBarView.swift in Sources */,
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
@ -1935,6 +1934,7 @@
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */,
E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */,
E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
@ -1942,6 +1942,7 @@
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */,
E1AA33202782639D00F6439C /* OverlayType.swift in Sources */,
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
6264E88D273850380081A12A /* Strings.swift in Sources */,
@ -1992,7 +1993,7 @@
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
E1C812C0277A8E5D00918266 /* VideoPlayerView.swift in Sources */,
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
@ -2027,16 +2028,14 @@
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
E1C812BF277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */,
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
E1C812C3277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift in Sources */,
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */,
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */,
091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */,
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */,
E1C812C1277A8E5D00918266 /* NativePlayerViewController.swift in Sources */,
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
@ -2045,6 +2044,7 @@
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */,
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
@ -2055,6 +2055,7 @@
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */,
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
@ -2063,6 +2064,7 @@
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */,
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */,
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
@ -2101,7 +2103,6 @@
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
E10EAA4A277BB6F5000269ED /* VideoPlayerOverlay.swift in Sources */,
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
);
@ -2135,6 +2136,7 @@
62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */,
62EC353226766849000E9F2D /* SessionManager.swift in Sources */,
536D3D79267BD5D00004248C /* ViewModel.swift in Sources */,
E1AA332427829B5200F6439C /* OverlayType.swift in Sources */,
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -2291,7 +2293,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = TY84JMYEFE;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist";
@ -2300,7 +2302,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin;
PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@ -2321,7 +2323,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = TY84JMYEFE;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist";
@ -2330,7 +2332,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin;
PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
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
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()
.frame(width: 100, height: 100)
.scaleEffect(2)
} else {
ScrollView {
VStack(alignment: .leading) {

View File

@ -69,22 +69,8 @@ struct ServerListView: View {
.frame(minWidth: 50, maxWidth: 240)
.multilineTextAlignment(.center)
Button {
PrimaryButtonView(title: L10n.connect.stringValue) {
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(.videoPlayerJumpForward) var jumpForwardLength
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
@Default(.nativeVideoPlayer) var nativeVideoPlayer
var body: some View {
Form {
@ -83,8 +82,7 @@ struct SettingsView: View {
}
}
Section(header: Text("Playback")) {
Toggle("Native Player", isOn: $nativeVideoPlayer)
Section(header: Text("Networking")) {
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
@ -96,43 +94,45 @@ struct SettingsView: View {
Text(bitrate.name).tag(bitrate.value)
}
}
}
Section(header: Text("Video Player")) {
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)
}
}
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)
}
}
}
Section(header: L10n.accessibility.text) {
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
SearchablePicker(label: "Preferred subtitle language",
options: viewModel.langs,
optionToString: { $0.name },
selected: Binding<TrackLanguage>(get: {
viewModel.langs
.first(where: { $0.isoCode == autoSelectSubtitlesLangcode
}) ??
.auto
},
set: { autoSelectSubtitlesLangcode = $0.isoCode }))
SearchablePicker(label: "Preferred audio language",
options: viewModel.langs,
optionToString: { $0.name },
selected: Binding<TrackLanguage>(get: {
viewModel.langs
.first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
.auto
},
set: { autoSelectAudioLangcode = $0.isoCode }))
// Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
// SearchablePicker(label: "Preferred subtitle language",
// options: viewModel.langs,
// optionToString: { $0.name },
// selected: Binding<TrackLanguage>(get: {
// viewModel.langs
// .first(where: { $0.isoCode == autoSelectSubtitlesLangcode
// }) ??
// .auto
// },
// set: { autoSelectSubtitlesLangcode = $0.isoCode }))
// SearchablePicker(label: "Preferred audio language",
// options: viewModel.langs,
// optionToString: { $0.name },
// selected: Binding<TrackLanguage>(get: {
// viewModel.langs
// .first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
// .auto
// },
// set: { autoSelectAudioLangcode = $0.isoCode }))
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)
}
}

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
//
// Created by Ethan Pippin on 12/27/21.
//
/*
* 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 Foundation

View File

@ -1,18 +1,17 @@
//
// PlayerOverlayDelegate.swift
// JellyfinVideoPlayerDev
//
// Created by Ethan Pippin on 12/27/21.
//
/*
* 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 Foundation
protocol PlayerOverlayDelegate {
func didSelectClose()
func didSelectGoogleCast()
func didSelectAirplay()
func didSelectSubtitles()
func didSelectMenu()
func didDeselectMenu()
@ -28,6 +27,6 @@ protocol PlayerOverlayDelegate {
func didSelectAudioStream(index: Int)
func didSelectSubtitleStream(index: Int)
func didSelectPreviousItem()
func didSelectNextItem()
func didSelectPlayPreviousItem()
func didSelectPlayNextItem()
}

View File

@ -1,9 +1,10 @@
//
// VLCPlayerCompactOverlayView.swift
// JellyfinVideoPlayerDev
//
// Created by Ethan Pippin on 12/26/21.
//
/*
* 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 Combine
import Defaults
@ -12,11 +13,9 @@ import MobileVLCKit
import Sliders
import SwiftUI
struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
struct VLCPlayerOverlayView: View {
@ObservedObject var viewModel: VideoPlayerViewModel
@Default(.videoPlayerJumpForward) var jumpForwardLength
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
@ViewBuilder
private var mainButtonView: some View {
@ -69,33 +68,19 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
HStack(spacing: 20) {
if viewModel.shouldShowGoogleCast {
if viewModel.shouldShowPlayPreviousItem {
Button {
viewModel.playerOverlayDelegate?.didSelectGoogleCast()
} label: {
Image(systemName: "rectangle.badge.plus")
}
}
if viewModel.shouldShowAirplay {
Button {
viewModel.playerOverlayDelegate?.didSelectAirplay()
} label: {
Image(systemName: "airplayvideo")
}
}
if viewModel.showAdjacentItems {
Button {
viewModel.playerOverlayDelegate?.didSelectPreviousItem()
viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem()
} label: {
Image(systemName: "chevron.left.circle")
}
.disabled(viewModel.previousItemVideoPlayerViewModel == nil)
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
}
if viewModel.shouldShowPlayNextItem {
Button {
viewModel.playerOverlayDelegate?.didSelectNextItem()
viewModel.playerOverlayDelegate?.didSelectPlayNextItem()
} label: {
Image(systemName: "chevron.right.circle")
}
@ -105,9 +90,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
if viewModel.shouldShowAutoPlayNextItem {
Button {
viewModel.autoPlayNextItem.toggle()
viewModel.autoplayEnabled.toggle()
} label: {
if viewModel.autoPlayNextItem {
if viewModel.autoplayEnabled {
Image(systemName: "play.circle.fill")
} else {
Image(systemName: "play.circle")
@ -117,7 +102,7 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
if !viewModel.subtitleStreams.isEmpty {
Button {
viewModel.playerOverlayDelegate?.didSelectSubtitles()
viewModel.subtitlesEnabled.toggle()
} label: {
if viewModel.subtitlesEnabled {
Image(systemName: "captions.bubble.fill")
@ -192,9 +177,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
Menu {
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in
Button {
jumpForwardLength = forwardLength
viewModel.jumpForwardLength = forwardLength
} label: {
if forwardLength == jumpForwardLength {
if forwardLength == viewModel.jumpForwardLength {
Label(forwardLength.shortLabel, systemImage: "checkmark")
} else {
Text(forwardLength.shortLabel)
@ -211,9 +196,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
Menu {
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { backwardLength in
Button {
jumpBackwardLength = backwardLength
viewModel.jumpBackwardLength = backwardLength
} label: {
if backwardLength == jumpBackwardLength {
if backwardLength == viewModel.jumpBackwardLength {
Label(backwardLength.shortLabel, systemImage: "checkmark")
} else {
Text(backwardLength.shortLabel)
@ -247,6 +232,11 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
}
}
// MARK: Center
Spacer()
Spacer()
// MARK: Bottom Bar
@ -264,7 +254,7 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
Button {
viewModel.playerOverlayDelegate?.didSelectBackward()
} label: {
Image(systemName: jumpBackwardLength.backwardImageLabel)
Image(systemName: viewModel.jumpBackwardLength.backwardImageLabel)
.padding(.horizontal, 5)
}
@ -279,12 +269,11 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
Button {
viewModel.playerOverlayDelegate?.didSelectForward()
} label: {
Image(systemName: jumpForwardLength.forwardImageLabel)
Image(systemName: viewModel.jumpForwardLength.forwardImageLabel)
.padding(.horizontal, 5)
}
}
.font(.system(size: 24, weight: .semibold, design: .default))
// .padding(.trailing, 10)
Text(viewModel.leftLabelText)
.font(.system(size: 18, weight: .semibold, design: .default))
@ -332,32 +321,43 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
}
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 {
ZStack {
Color.red
.ignoresSafeArea()
VLCPlayerCompactOverlayView(viewModel: VideoPlayerViewModel(item: BaseItemDto(runTimeTicks: 720 * 10_000_000),
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))
VLCPlayerOverlayView(viewModel: videoPlayerViewModel)
}
.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
//
// Created by Ethan Pippin on 11/12/21.
//
/*
* 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 AVKit
import AVFoundation
@ -25,7 +27,7 @@ class VLCPlayerViewController: UIViewController {
private var vlcMediaPlayer = VLCMediaPlayer()
private var lastPlayerTicks: Int64 = 0
private var lastProgressReportTicks: Int64 = 0
private var viewModelReactCancellables = Set<AnyCancellable>()
private var viewModelListeners = Set<AnyCancellable>()
private var overlayDismissTimer: Timer?
private var currentPlayerTicks: Int64 {
@ -36,19 +38,11 @@ class VLCPlayerViewController: UIViewController {
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 jumpBackwardOverlayView = makeJumpBackwardOverlayView()
private lazy var jumpForwardOverlayView = makeJumpForwardOverlayView()
private lazy var tapGestureView = makeTapGestureView()
private var currentOverlayHostingController: UIHostingController<VLCPlayerCompactOverlayView>?
private lazy var mainGestureView = makeTapGestureView()
private var currentOverlayHostingController: UIHostingController<VLCPlayerOverlayView>?
private var currentJumpBackwardOverlayView: UIImageView?
private var currentJumpForwardOverlayView: UIImageView?
// MARK: init
@ -67,12 +61,7 @@ class VLCPlayerViewController: UIViewController {
private func setupSubviews() {
view.addSubview(videoContentView)
view.addSubview(jumpForwardOverlayView)
view.addSubview(jumpBackwardOverlayView)
view.addSubview(tapGestureView)
jumpBackwardOverlayView.alpha = 0
jumpForwardOverlayView.alpha = 0
view.addSubview(mainGestureView)
}
private func setupConstraints() {
@ -83,18 +72,10 @@ class VLCPlayerViewController: UIViewController {
videoContentView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
NSLayoutConstraint.activate([
jumpBackwardOverlayView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 150),
jumpBackwardOverlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
NSLayoutConstraint.activate([
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)
mainGestureView.topAnchor.constraint(equalTo: videoContentView.topAnchor),
mainGestureView.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor),
mainGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
mainGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
])
}
@ -127,6 +108,9 @@ class VLCPlayerViewController: UIViewController {
setupMediaPlayer(newViewModel: viewModel)
refreshJumpBackwardOverlayView(with: viewModel.jumpBackwardLength)
refreshJumpForwardOverlayView(with: viewModel.jumpForwardLength)
let defaultNotificationCenter = NotificationCenter.default
defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
@ -192,26 +176,6 @@ class VLCPlayerViewController: UIViewController {
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
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
@ -225,11 +189,10 @@ class VLCPlayerViewController: UIViewController {
currentOverlayHostingController.view.removeFromSuperview()
currentOverlayHostingController.removeFromParent()
// self.currentOverlayHostingController = nil
}
}
let newOverlayView = VLCPlayerCompactOverlayView(viewModel: viewModel)
let newOverlayView = VLCPlayerOverlayView(viewModel: viewModel)
let newOverlayHostingController = UIHostingController(rootView: newOverlayView)
newOverlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false
@ -256,10 +219,59 @@ class VLCPlayerViewController: UIViewController {
self.currentOverlayHostingController = newOverlayHostingController
// There is a behavior when setting this that the navigation bar
// on the current navigation controller pops up, re-hide it
// There is a weird behavior when after setting the new overlays that the navigation bar pops up, re-hide it
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
@ -275,7 +287,7 @@ extension VLCPlayerViewController {
// Stop current media if there is one
if vlcMediaPlayer.media != nil {
viewModelReactCancellables.forEach({ $0.cancel() })
viewModelListeners.forEach({ $0.cancel() })
vlcMediaPlayer.stop()
viewModel.sendStopReport()
@ -297,7 +309,7 @@ extension VLCPlayerViewController {
newViewModel.getAdjacentEpisodes()
newViewModel.playerOverlayDelegate = self
let startPercentage = viewModel.item.userData?.playedPercentage ?? 0
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
if startPercentage > 0 {
newViewModel.sliderPercentage = startPercentage / 100
@ -320,9 +332,10 @@ extension VLCPlayerViewController {
// MARK: setupViewModelListeners
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
viewModel.$playbackSpeed.sink { newSpeed in
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &viewModelReactCancellables)
}.store(in: &viewModelListeners)
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
if sliderIsScrubbing {
@ -330,15 +343,27 @@ extension VLCPlayerViewController {
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelReactCancellables)
}.store(in: &viewModelListeners)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &viewModelReactCancellables)
}.store(in: &viewModelListeners)
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
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() {
@ -395,34 +420,42 @@ extension VLCPlayerViewController {
extension VLCPlayerViewController {
private func flashJumpBackwardOverlay() {
jumpBackwardOverlayView.layer.removeAllAnimations()
guard let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView else { return }
currentJumpBackwardOverlayView.layer.removeAllAnimations()
UIView.animate(withDuration: 0.1) {
self.jumpBackwardOverlayView.alpha = 1
currentJumpBackwardOverlayView.alpha = 1
} completion: { _ in
self.hideJumpBackwardOverlay()
}
}
private func hideJumpBackwardOverlay() {
guard let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView else { return }
UIView.animate(withDuration: 0.3) {
self.jumpBackwardOverlayView.alpha = 0
currentJumpBackwardOverlayView.alpha = 0
}
}
private func flashJumpFowardOverlay() {
jumpForwardOverlayView.layer.removeAllAnimations()
guard let currentJumpForwardOverlayView = currentJumpForwardOverlayView else { return }
currentJumpForwardOverlayView.layer.removeAllAnimations()
UIView.animate(withDuration: 0.1) {
self.jumpForwardOverlayView.alpha = 1
currentJumpForwardOverlayView.alpha = 1
} completion: { _ in
self.hideJumpForwardOverlay()
}
}
private func hideJumpForwardOverlay() {
guard let currentJumpForwardOverlayView = currentJumpForwardOverlayView else { return }
UIView.animate(withDuration: 0.3) {
self.jumpForwardOverlayView.alpha = 0
currentJumpForwardOverlayView.alpha = 0
}
}
}
@ -459,8 +492,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
viewModel.playerState = vlcMediaPlayer.state
if vlcMediaPlayer.state == VLCMediaPlayerState.ended {
if viewModel.autoPlayNextItem && viewModel.shouldShowAutoPlayNextItem && viewModel.nextItemVideoPlayerViewModel != nil {
didSelectNextItem()
if viewModel.autoplayEnabled && viewModel.nextItemVideoPlayerViewModel != nil {
didSelectPlayNextItem()
} else {
didSelectClose()
}
@ -470,13 +503,10 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
// MARK: mediaPlayerTimeChanged
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
guard !viewModel.sliderIsScrubbing else {
lastPlayerTicks = currentPlayerTicks
return
if !viewModel.sliderIsScrubbing {
viewModel.sliderPercentage = Double(vlcMediaPlayer.position)
}
viewModel.sliderPercentage = Double(vlcMediaPlayer.position)
// Have to manually set playing because VLCMediaPlayer doesn't
// properly set it itself
if abs(currentPlayerTicks - lastPlayerTicks) >= 10_000 {
@ -486,6 +516,9 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
// If needing to fix subtitle streams during playback
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled {
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
}
if vlcMediaPlayer.currentAudioTrackIndex != viewModel.selectedAudioStreamIndex {
didSelectAudioStream(index: viewModel.selectedAudioStreamIndex)
}
@ -500,7 +533,7 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
}
}
// MARK: PlayerOverlayDelegate
// MARK: PlayerOverlayDelegate and more
extension VLCPlayerViewController: PlayerOverlayDelegate {
func didSelectAudioStream(index: Int) {
@ -511,12 +544,11 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
lastProgressReportTicks = currentPlayerTicks
}
/// Do not call when setting to index -1
func didSelectSubtitleStream(index: Int) {
if viewModel.subtitlesEnabled {
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index)
} else {
vlcMediaPlayer.currentVideoSubTitleIndex = -1
}
viewModel.subtitlesEnabled = true
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index)
viewModel.sendProgressReport()
@ -531,19 +563,8 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
dismiss(animated: true, completion: nil)
}
func didSelectGoogleCast() {
print("didSelectCast")
}
func didSelectAirplay() {
print("didSelectAirplay")
}
func didSelectSubtitles() {
viewModel.subtitlesEnabled = !viewModel.subtitlesEnabled
if viewModel.subtitlesEnabled {
func didToggleSubtitles(newValue: Bool) {
if newValue {
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex)
} else {
vlcMediaPlayer.currentVideoSubTitleIndex = -1
@ -561,27 +582,33 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
}
func didSelectBackward() {
flashJumpBackwardOverlay()
vlcMediaPlayer.jumpBackward(jumpBackwardLength.rawValue)
vlcMediaPlayer.jumpBackward(viewModel.jumpBackwardLength.rawValue)
restartOverlayDismissTimer()
if displayingOverlay {
restartOverlayDismissTimer()
}
viewModel.sendProgressReport()
self.lastProgressReportTicks = currentPlayerTicks
lastProgressReportTicks = currentPlayerTicks
}
func didSelectForward() {
flashJumpFowardOverlay()
vlcMediaPlayer.jumpForward(jumpForwardLength.rawValue)
vlcMediaPlayer.jumpForward(viewModel.jumpForwardLength.rawValue)
restartOverlayDismissTimer()
if displayingOverlay {
restartOverlayDismissTimer()
}
viewModel.sendProgressReport()
self.lastProgressReportTicks = currentPlayerTicks
lastProgressReportTicks = currentPlayerTicks
}
func didSelectMain() {
@ -619,16 +646,20 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
viewModel.sendProgressReport()
self.lastProgressReportTicks = currentPlayerTicks
lastProgressReportTicks = currentPlayerTicks
}
func didSelectPreviousItem() {
setupMediaPlayer(newViewModel: viewModel.previousItemVideoPlayerViewModel!)
startPlayback()
func didSelectPlayPreviousItem() {
if let previousItemVideoPlayerViewModel = viewModel.previousItemVideoPlayerViewModel {
setupMediaPlayer(newViewModel: previousItemVideoPlayerViewModel)
startPlayback()
}
}
func didSelectNextItem() {
setupMediaPlayer(newViewModel: viewModel.nextItemVideoPlayerViewModel!)
startPlayback()
func didSelectPlayNextItem() {
if let nextItemVideoPlayerViewModel = viewModel.nextItemVideoPlayerViewModel {
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
@Default(.nativeVideoPlayer) var nativeVideoPlayer
let viewModel: VideoPlayerViewModel
init(viewModel: VideoPlayerViewModel) {
@ -27,24 +26,13 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
}
@ViewBuilder func makeStart() -> some View {
if nativeVideoPlayer {
PreferenceUIHostingControllerView {
NativePlayerView(viewModel: self.viewModel)
.navigationBarHidden(true)
.statusBar(hidden: true)
.ignoresSafeArea()
.prefersHomeIndicatorAutoHidden(true)
.supportedOrientations(.landscape)
}.ignoresSafeArea()
} else {
PreferenceUIHostingControllerView {
VLCPlayerView(viewModel: self.viewModel)
.navigationBarHidden(true)
.statusBar(hidden: true)
.ignoresSafeArea()
.prefersHomeIndicatorAutoHidden(true)
.supportedOrientations(.landscape)
}.ignoresSafeArea()
}
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!)")
}
// MARK: VidoPlayerViewModel Creation
var subtitle: String? = nil
// 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 autoPlayNextItem = Defaults[.autoPlayNextItem]
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
let videoPlayerViewModel = VideoPlayerViewModel(item: self,
title: self.name!,
title: self.name ?? "",
subtitle: subtitle,
streamURL: streamURL.url!,
hlsURL: hlsURL.url!,
response: response,
audioStreams: audioStreams,
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,
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
showAdjacentItems: true,
shouldShowAutoPlayNextItem: shouldShowAutoPlayNextItem,
autoPlayNextItem: autoPlayNextItem)
subtitlesEnabled: subtitlesEnabled,
autoplayEnabled: autoplayEnabled,
overlayType: overlayType,
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
shouldShowPlayNextItem: shouldShowPlayNextItem,
shouldShowAutoPlayNextItem: shouldShowAutoPlay)
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
private init() {
if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID],
if let lastUserID = Defaults[.lastServerUserID],
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]) {
@ -64,7 +64,7 @@ final class SessionManager {
var uriComponents = URLComponents(string: uri) ?? URLComponents()
if uriComponents.scheme == nil {
uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue
uriComponents.scheme = Defaults[.defaultHTTPScheme].rawValue
}
var uri = uriComponents.string ?? ""
@ -216,7 +216,7 @@ final class SessionManager {
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
Defaults[.lastServerUserID] = user.id
currentLogin = (server: currentServer.state, user: currentUser.state)
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
@ -230,7 +230,7 @@ final class SessionManager {
// MARK: loginUser
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
JellyfinAPI.basePath = server.currentURI
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
Defaults[.lastServerUserID] = user.id
setAuthHeader(with: user.accessToken)
currentLogin = (server: server, user: user)
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
@ -241,7 +241,7 @@ final class SessionManager {
currentLogin = nil
JellyfinAPI.basePath = ""
setAuthHeader(with: "")
SwiftfinStore.Defaults.suite[.lastServerUserID] = nil
Defaults[.lastServerUserID] = nil
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
}
@ -254,8 +254,8 @@ final class SessionManager {
delete(server: server)
}
// Delete UserDefaults
SwiftfinStore.Defaults.suite.removeAll()
// Delete general UserDefaults
SwiftfinStore.Defaults.generalSuite.removeAll()
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
}

View File

@ -14,25 +14,44 @@ extension SwiftfinStore {
enum Defaults {
static let suite: UserDefaults = {
return UserDefaults(suiteName: "swiftfinstore-defaults")!
static let generalSuite: UserDefaults = {
return UserDefaults(suiteName: "swiftfinstore-general-defaults")!
}()
static let universalSuite: UserDefaults = {
return UserDefaults(suiteName: "swiftfinstore-universal-defaults")!
}()
}
}
extension Defaults.Keys {
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.suite)
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite)
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
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)
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.suite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.suite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.suite)
static let nativeVideoPlayer = Key<Bool>("nativeVideoPlayer", default: false, suite: SwiftfinStore.Defaults.suite)
static let shouldShowAutoPlayNextItem = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.suite)
static let autoPlayNextItem = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.suite)
// Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
// General settings
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite)
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
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 .failure:
self.libraries = []
self.handleAPIRequestError(completion: completion)
}
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
var newLibraries: [BaseItemDto] = []

View File

@ -12,13 +12,13 @@ import SwiftUI
import Defaults
final class SettingsViewModel: ObservableObject {
let currentLocale = Locale.current
var bitrates: [Bitrates] = []
var langs = [TrackLanguage]()
let appearances = AppAppearance.allCases
let videoPlayerJumpLengths = VideoPlayerJumpLength.allCases
var langs: [TrackLanguage] = []
init() {
// Bitrates
let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
do {
@ -32,8 +32,9 @@ final class SettingsViewModel: ObservableObject {
LogManager.shared.log.error("Error processing JSON file `bitrates.json`")
}
// Track languages
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)
}.sorted(by: { $0.name < $1.name })
self.langs.insert(.auto, at: 0)

View File

@ -1,9 +1,11 @@
//
// VideoPlayerViewModel.swift
// JellyfinVideoPlayerDev
//
// Created by Ethan Pippin on 11/12/21.
//
/*
* 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 Combine
import Defaults
@ -19,71 +21,59 @@ import MobileVLCKit
final class VideoPlayerViewModel: ViewModel {
// MARK: Published
// Manually kept state because VLCKit doesn't properly set "played"
// on the VLCMediaPlayer object
@Published var playerState: VLCMediaPlayerState
@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 playerState: VLCMediaPlayerState = .buffering
@Published var leftLabelText: String = "--:--"
@Published var rightLabelText: String = "--:--"
@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 {
sliderScrubbingSubject.send(self)
sliderPercentageChanged(newValue: newValue)
}
}
@Published var sliderIsScrubbing: Bool = false
@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 {
@Published var autoplayEnabled: Bool {
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 title: String
let subtitle: String?
let streamURL: URL
let hlsURL: URL
// Full response kept for convenience
let response: PlaybackInfoResponse
let audioStreams: [MediaStream]
let subtitleStreams: [MediaStream]
let defaultAudioStreamIndex: Int
let defaultSubtitleStreamIndex: Int
let overlayType: OverlayType
// Full response kept for convenience
let response: PlaybackInfoResponse
var playerOverlayDelegate: PlayerOverlayDelegate?
// Ticks of the time the media has begun
var startTimeTicks: Int64?
// Ticks of the time the media began playing
private var startTimeTicks: Int64 = 0
// MARK: Current Time
var currentSeconds: Double {
let videoDuration = Double(item.runTimeTicks! / 10_000_000)
@ -107,18 +97,16 @@ final class VideoPlayerViewModel: ViewModel {
response: PlaybackInfoResponse,
audioStreams: [MediaStream],
subtitleStreams: [MediaStream],
defaultAudioStreamIndex: Int,
defaultSubtitleStreamIndex: Int,
playerState: VLCMediaPlayerState,
shouldShowGoogleCast: Bool,
shouldShowAirplay: Bool,
subtitlesEnabled: Bool,
sliderPercentage: Double,
selectedAudioStreamIndex: Int,
selectedSubtitleStreamIndex: Int,
showAdjacentItems: Bool,
shouldShowAutoPlayNextItem: Bool,
autoPlayNextItem: Bool) {
subtitlesEnabled: Bool,
autoplayEnabled: Bool,
overlayType: OverlayType,
shouldShowPlayPreviousItem: Bool,
shouldShowPlayNextItem: Bool,
shouldShowAutoPlayNextItem: Bool
) {
self.item = item
self.title = title
self.subtitle = subtitle
@ -127,26 +115,21 @@ final class VideoPlayerViewModel: ViewModel {
self.response = response
self.audioStreams = audioStreams
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.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
self.showAdjacentItems = showAdjacentItems
self.subtitlesEnabled = subtitlesEnabled
self.autoplayEnabled = autoplayEnabled
self.overlayType = overlayType
self.shouldShowPlayPreviousItem = shouldShowPlayPreviousItem
self.shouldShowPlayNextItem = shouldShowPlayNextItem
self.shouldShowAutoPlayNextItem = shouldShowAutoPlayNextItem
self.autoPlayNextItem = autoPlayNextItem
self.jumpBackwardLength = Defaults[.videoPlayerJumpBackward]
self.jumpForwardLength = Defaults[.videoPlayerJumpForward]
super.init()
self.sliderPercentageChanged(newValue: (item.userData?.playedPercentage ?? 0) / 100)
if item.itemType != .episode {
self.showAdjacentItems = false
}
self.sliderPercentage = (item.userData?.playedPercentage ?? 0) / 100
}
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>) {
switch completion {
case .finished:
break
self.errorMessage = nil
case .failure(let error):
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)