lots of final tvos work
This commit is contained in:
parent
7ab85e453d
commit
2d7cad8cec
|
@ -14,6 +14,15 @@ struct JellyfinPlayer_tvOSApp: App {
|
|||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainCoordinator().view()
|
||||
.onAppear {
|
||||
JellyfinPlayer_tvOSApp.setupAppearance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func setupAppearance() {
|
||||
let scenes = UIApplication.shared.connectedScenes
|
||||
let windowScene = scenes.first as? UIWindowScene
|
||||
windowScene?.windows.first?.overrideUserInterfaceStyle = .dark
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,12 +28,15 @@
|
|||
<true/>
|
||||
</dict>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>UIColorName</key>
|
||||
<string>LaunchScreenBackground</string>
|
||||
</dict>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<string>Dark</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -39,7 +39,7 @@ struct BasicAppSettingsView: View {
|
|||
}
|
||||
.alert(L10n.reset, isPresented: $resetTapped, actions: {
|
||||
Button(role: .destructive) {
|
||||
viewModel.reset()
|
||||
viewModel.resetAppSettings()
|
||||
basicAppSettingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
L10n.reset.text
|
||||
|
|
|
@ -13,24 +13,11 @@ import SwiftUI
|
|||
struct CinematicEpisodeItemView: View {
|
||||
|
||||
@ObservedObject var viewModel: EpisodeItemViewModel
|
||||
@State var verticalScrollViewOffset: CGFloat = 0
|
||||
@State var wrappedScrollView: UIScrollView?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
GeometryReader { overlayGeoReader in
|
||||
Text("")
|
||||
.onAppear {
|
||||
self.verticalScrollViewOffset = overlayGeoReader.frame(in: .global).origin.y + overlayGeoReader.frame(in: .global).height - 200
|
||||
}
|
||||
}
|
||||
.frame(height: 50)
|
||||
}
|
||||
|
||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
|
||||
bh: viewModel.item.getBackdropImageBlurHash())
|
||||
.ignoresSafeArea()
|
||||
|
@ -38,10 +25,9 @@ struct CinematicEpisodeItemView: View {
|
|||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
|
||||
Spacer(minLength: verticalScrollViewOffset)
|
||||
|
||||
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView)
|
||||
.focusSection()
|
||||
.frame(height: UIScreen.main.bounds.height - 10)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ struct CinematicItemAboutView: View {
|
|||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 230))
|
||||
.frame(width: 230, height: 380)
|
||||
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 257))
|
||||
.frame(width: 257, height: 380)
|
||||
.cornerRadius(10)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
|
|
|
@ -25,68 +25,86 @@ struct CinematicItemViewTopRow: View {
|
|||
.ignoresSafeArea()
|
||||
.frame(height: 210)
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .PlayInformationAlignmentGuide) {
|
||||
CinematicItemViewTopRowButton(wrappedScrollView: wrappedScrollView) {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .PlayInformationAlignmentGuide) {
|
||||
|
||||
// MARK: Play
|
||||
Button {
|
||||
itemRouter.route(to: \.videoPlayer, viewModel.itemVideoPlayerViewModel!)
|
||||
} label: {
|
||||
ZStack {
|
||||
Color.white.frame(width: 230, height: 100)
|
||||
|
||||
Text("Play")
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "play.fill")
|
||||
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black)
|
||||
.font(.title3)
|
||||
.foregroundColor(.black)
|
||||
Text(viewModel.playButtonText())
|
||||
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black)
|
||||
// .font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(width: 230, height: 100)
|
||||
.background(viewModel.playButtonItem == nil ? Color.secondarySystemFill : Color.white)
|
||||
.cornerRadius(10)
|
||||
|
||||
// ZStack {
|
||||
// Color.white.frame(width: 230, height: 100)
|
||||
//
|
||||
// Text("Play")
|
||||
// .font(.title3)
|
||||
// .foregroundColor(.black)
|
||||
// }
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.disabled(viewModel.itemVideoPlayerViewModel == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(viewModel.item.name ?? "")
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(viewModel.item.name ?? "")
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
|
||||
if let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.getEpisodeLocator() {
|
||||
Text("\(seriesName) - \(episodeLocator)")
|
||||
if let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.getEpisodeLocator() {
|
||||
Text("\(seriesName) - \(episodeLocator)")
|
||||
}
|
||||
|
||||
HStack(alignment: .PlayInformationAlignmentGuide, spacing: 20) {
|
||||
|
||||
if let runtime = viewModel.item.getItemRuntime() {
|
||||
Text(runtime)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
|
||||
if let productionYear = viewModel.item.productionYear {
|
||||
Text(String(productionYear))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
if let officialRating = viewModel.item.officialRating {
|
||||
Text(officialRating)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.secondary, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
HStack(alignment: .PlayInformationAlignmentGuide, spacing: 20) {
|
||||
|
||||
if let runtime = viewModel.item.getItemRuntime() {
|
||||
Text(runtime)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
|
||||
if let productionYear = viewModel.item.productionYear {
|
||||
Text(String(productionYear))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
if let officialRating = viewModel.item.officialRating {
|
||||
Text(officialRating)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.secondary, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
.padding(.horizontal, 50)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
.padding(.horizontal, 50)
|
||||
.padding(.bottom, 50)
|
||||
|
||||
}
|
||||
.onChange(of: envFocused) { envFocus in
|
||||
if envFocus == true {
|
||||
|
|
|
@ -38,7 +38,10 @@ struct CinematicItemViewTopRowButton<Content: View>: View {
|
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
wrappedScrollView?.scrollToTop()
|
||||
}
|
||||
print("Scroll to top")
|
||||
|
||||
withAnimation(.linear(duration: 0.15)) {
|
||||
self.focused = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,24 +13,11 @@ import SwiftUI
|
|||
struct CinematicMovieItemView: View {
|
||||
|
||||
@ObservedObject var viewModel: MovieItemViewModel
|
||||
@State var verticalScrollViewOffset: CGFloat = 0
|
||||
@State var wrappedScrollView: UIScrollView?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
GeometryReader { overlayGeoReader in
|
||||
Text("")
|
||||
.onAppear {
|
||||
self.verticalScrollViewOffset = overlayGeoReader.frame(in: .global).origin.y + overlayGeoReader.frame(in: .global).height - 200
|
||||
}
|
||||
}
|
||||
.frame(height: 50)
|
||||
}
|
||||
|
||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
|
||||
bh: viewModel.item.getBackdropImageBlurHash())
|
||||
.ignoresSafeArea()
|
||||
|
@ -38,10 +25,9 @@ struct CinematicMovieItemView: View {
|
|||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
|
||||
Spacer(minLength: verticalScrollViewOffset)
|
||||
|
||||
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView)
|
||||
.focusSection()
|
||||
.frame(height: UIScreen.main.bounds.height - 10)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ struct ServerDetailView: View {
|
|||
Text(SessionManager.main.currentLogin.server.name)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.focusable()
|
||||
|
||||
HStack {
|
||||
Text("URI")
|
||||
|
|
|
@ -67,7 +67,7 @@ struct ServerListView: View {
|
|||
Text("Connect to a Jellyfin server to get started")
|
||||
.frame(minWidth: 50, maxWidth: 500)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout)
|
||||
.font(.body)
|
||||
|
||||
Button {
|
||||
serverListRouter.route(to: \.connectToServer)
|
||||
|
@ -75,8 +75,12 @@ struct ServerListView: View {
|
|||
L10n.connect.text
|
||||
.bold()
|
||||
.font(.callout)
|
||||
.padding(.vertical)
|
||||
.padding(.horizontal, 30)
|
||||
.background(Color.jellyfinPurple)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
.buttonStyle(CardButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
/* JellyfinPlayer/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 CoreData
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
|
||||
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate
|
||||
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
|
||||
@Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode
|
||||
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black
|
||||
.ignoresSafeArea()
|
||||
|
||||
GeometryReader { reader in
|
||||
HStack {
|
||||
|
||||
Image(uiImage: UIImage(named: "App Icon")!)
|
||||
.frame(width: reader.size.width / 2)
|
||||
|
||||
Form {
|
||||
Section(header: L10n.playbackSettings.text) {
|
||||
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
Text(bitrate.name).tag(bitrate.value)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
Text(bitrate.name).tag(bitrate.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Section(header: Text(SessionManager.main.currentLogin.server.name)) {
|
||||
HStack {
|
||||
Text(L10n.signedInAsWithString(SessionManager.main.currentLogin.user.username)).foregroundColor(.primary)
|
||||
Spacer()
|
||||
Button {
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||
} label: {
|
||||
L10n.switchUser.text.font(.callout)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
SessionManager.main.logout()
|
||||
} label: {
|
||||
Text("Sign out").font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 90)
|
||||
.padding(.trailing, 90)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView(viewModel: SettingsViewModel())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
/*
|
||||
* 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 SwiftUI
|
||||
|
||||
struct ExperimentalSettingsView: View {
|
||||
|
||||
@Default(.Experimental.syncSubtitleStateWithAdjacent) var syncSubtitleStateWithAdjacent
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
|
||||
Toggle("Sync Subtitles with Adjacent Episodes", isOn: $syncSubtitleStateWithAdjacent)
|
||||
|
||||
} header: {
|
||||
Text("Experimental")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
/*
|
||||
* 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 SwiftUI
|
||||
|
||||
struct OverlaySettingsView: View {
|
||||
|
||||
@Default(.shouldShowPlayPreviousItem) var shouldShowPlayPreviousItem
|
||||
@Default(.shouldShowPlayNextItem) var shouldShowPlayNextItem
|
||||
@Default(.shouldShowAutoPlay) var shouldShowAutoPlay
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Overlay")) {
|
||||
|
||||
Toggle("\(Image(systemName: "chevron.left.circle")) Play Previous Item", isOn: $shouldShowPlayPreviousItem)
|
||||
Toggle("\(Image(systemName: "chevron.right.circle")) Play Next Item", isOn: $shouldShowPlayNextItem)
|
||||
Toggle("\(Image(systemName: "play.circle.fill")) Auto Play", isOn: $shouldShowAutoPlay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/* JellyfinPlayer/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 CoreData
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode
|
||||
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||
@Default(.downActionShowsMenu) var downActionShowsMenu
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { reader in
|
||||
HStack {
|
||||
|
||||
Image(uiImage: UIImage(named: "App Icon")!)
|
||||
.scaleEffect(2)
|
||||
.frame(width: reader.size.width / 2)
|
||||
|
||||
Form {
|
||||
Section(header: EmptyView()) {
|
||||
HStack {
|
||||
Text("User")
|
||||
Spacer()
|
||||
Text(viewModel.user.username)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.serverDetail)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Server")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(viewModel.server.name)
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
SessionManager.main.logout()
|
||||
} label: {
|
||||
Text("Switch User")
|
||||
.foregroundColor(Color.jellyfinPurple)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Video Player")) {
|
||||
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
||||
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
|
||||
Text(length.label).tag(length.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Press Down for Menu", isOn: $downActionShowsMenu)
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.overlaySettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Overlay")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.experimentalSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Experimental")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView(viewModel: SettingsViewModel(server: .sample, user: .sample))
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ protocol PlayerOverlayDelegate {
|
|||
|
||||
func didSelectClose()
|
||||
func didSelectMenu()
|
||||
func didDeselectMenu()
|
||||
|
||||
func didSelectBackward()
|
||||
func didSelectForward()
|
||||
|
|
|
@ -116,6 +116,8 @@ class VLCPlayerViewController: UIViewController {
|
|||
// aren't unnecessarily set more than once
|
||||
vlcMediaPlayer.delegate = self
|
||||
vlcMediaPlayer.drawable = videoContentView
|
||||
|
||||
// TODO: custom font sizes
|
||||
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
|
||||
|
||||
setupMediaPlayer(newViewModel: viewModel)
|
||||
|
@ -188,8 +190,7 @@ class VLCPlayerViewController: UIViewController {
|
|||
guard let buttonPress = presses.first?.type else { return }
|
||||
|
||||
switch(buttonPress) {
|
||||
case .menu:
|
||||
print("Menu")
|
||||
case .menu: () // Captured by other gesture
|
||||
case .playPause:
|
||||
didSelectMain()
|
||||
case .select:
|
||||
|
@ -201,24 +202,22 @@ class VLCPlayerViewController: UIViewController {
|
|||
showOverlay()
|
||||
restartOverlayDismissTimer()
|
||||
}
|
||||
|
||||
print("Up arrow")
|
||||
case .downArrow:
|
||||
if !displayingContentOverlay {
|
||||
stopOverlayDismissTimer()
|
||||
|
||||
hideOverlay()
|
||||
showOverlayContent()
|
||||
if Defaults[.downActionShowsMenu] {
|
||||
if !displayingContentOverlay {
|
||||
didSelectMenu()
|
||||
}
|
||||
}
|
||||
case .leftArrow:
|
||||
didSelectBackward()
|
||||
print("Left arrow")
|
||||
if !displayingContentOverlay {
|
||||
didSelectBackward()
|
||||
}
|
||||
case .rightArrow:
|
||||
didSelectForward()
|
||||
case .pageUp:
|
||||
print("page up")
|
||||
case .pageDown:
|
||||
print("page down")
|
||||
if !displayingContentOverlay {
|
||||
didSelectForward()
|
||||
}
|
||||
case .pageUp: ()
|
||||
case .pageDown: ()
|
||||
@unknown default: ()
|
||||
}
|
||||
}
|
||||
|
@ -235,6 +234,9 @@ class VLCPlayerViewController: UIViewController {
|
|||
hideOverlay()
|
||||
} else if displayingContentOverlay {
|
||||
hideOverlayContent()
|
||||
|
||||
showOverlay()
|
||||
restartOverlayDismissTimer()
|
||||
} else {
|
||||
vlcMediaPlayer.pause()
|
||||
|
||||
|
@ -301,11 +303,23 @@ class VLCPlayerViewController: UIViewController {
|
|||
currentOverlayContentHostingController.removeFromParent()
|
||||
}
|
||||
|
||||
let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel,
|
||||
title: "Subtitles",
|
||||
items: viewModel.subtitleStreams) { selectedMediaStream in
|
||||
self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1)
|
||||
}
|
||||
// let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel,
|
||||
// title: "Subtitles",
|
||||
// items: viewModel.subtitleStreams) { selectedMediaStream in
|
||||
// self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1)
|
||||
// }
|
||||
// let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel,
|
||||
// items: viewModel.subtitleStreams,
|
||||
// selectedIndex: viewModel.selectedSubtitleStreamIndex,
|
||||
// title: "Subtitles") { selectedMediaStream in
|
||||
// DispatchQueue.main.async {
|
||||
// self.viewModel.selectedSubtitleStreamIndex = selectedMediaStream.index ?? -1
|
||||
// self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1)
|
||||
// }
|
||||
// }
|
||||
|
||||
let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel)
|
||||
|
||||
let newOverlayContentHostingController = UIHostingController(rootView: newSmallMenuOverlayView)
|
||||
// let newOverlayContentView = tvOSOverlayContentView(viewModel: viewModel)
|
||||
// let newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView)
|
||||
|
@ -357,6 +371,8 @@ extension VLCPlayerViewController {
|
|||
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||
|
||||
// TODO: Custom buffer/cache amounts
|
||||
|
||||
let media = VLCMedia(url: newViewModel.streamURL)
|
||||
media.addOption("--prefetch-buffer-size=1048576")
|
||||
media.addOption("--network-caching=5000")
|
||||
|
@ -640,14 +656,11 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Implement properly in overlays
|
||||
func didSelectMenu() {
|
||||
stopOverlayDismissTimer()
|
||||
}
|
||||
|
||||
// TODO: Implement properly in overlays
|
||||
func didDeselectMenu() {
|
||||
restartOverlayDismissTimer()
|
||||
hideOverlay()
|
||||
showOverlayContent()
|
||||
}
|
||||
|
||||
func didSelectBackward() {
|
||||
|
|
|
@ -10,47 +10,244 @@
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Needs replacement/reworking
|
||||
struct SmallMediaStreamSelectionView: View {
|
||||
|
||||
enum Layer: Hashable {
|
||||
case subtitles
|
||||
case audio
|
||||
case playbackSpeed
|
||||
}
|
||||
|
||||
enum MediaSection: Hashable {
|
||||
case titles
|
||||
case items
|
||||
}
|
||||
|
||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||
let title: String
|
||||
var items: [MediaStream]
|
||||
var selectedAction: (MediaStream) -> Void
|
||||
|
||||
@State private var updateFocusedLayer: Layer = .subtitles
|
||||
|
||||
@FocusState private var subtitlesFocused: Bool
|
||||
@FocusState private var audioFocused: Bool
|
||||
@FocusState private var playbackSpeedFocused: Bool
|
||||
@FocusState private var focusedSection: MediaSection?
|
||||
@FocusState private var focusedLayer: Layer? {
|
||||
willSet {
|
||||
updateFocusedLayer = newValue!
|
||||
|
||||
if focusedSection == .titles {
|
||||
lastFocusedLayer = newValue!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var lastFocusedLayer: Layer = .subtitles
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.9)]),
|
||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
.ignoresSafeArea()
|
||||
.frame(height: 150)
|
||||
.frame(height: 300)
|
||||
|
||||
VStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Text("Subtitles")
|
||||
|
||||
// MARK: Subtitle Header
|
||||
Button {
|
||||
updateFocusedLayer = .subtitles
|
||||
focusedLayer = .subtitles
|
||||
} label: {
|
||||
if updateFocusedLayer == .subtitles {
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "captions.bubble")
|
||||
Text("Subtitles")
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.foregroundColor(.black)
|
||||
} else {
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "captions.bubble")
|
||||
Text("Subtitles")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
.focused($focusedLayer, equals: .subtitles)
|
||||
.focused($subtitlesFocused)
|
||||
.onChange(of: subtitlesFocused) { isFocused in
|
||||
if isFocused {
|
||||
focusedLayer = .subtitles
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Audio Header
|
||||
Button {
|
||||
updateFocusedLayer = .audio
|
||||
focusedLayer = .audio
|
||||
} label: {
|
||||
if updateFocusedLayer == .audio {
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "speaker.wave.3")
|
||||
Text("Audio")
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.foregroundColor(.black)
|
||||
} else {
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "speaker.wave.3")
|
||||
Text("Audio")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
.focused($focusedLayer, equals: .audio)
|
||||
.focused($audioFocused)
|
||||
.onChange(of: audioFocused) { isFocused in
|
||||
if isFocused {
|
||||
focusedLayer = .audio
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Playback Speed Header
|
||||
Button {
|
||||
updateFocusedLayer = .playbackSpeed
|
||||
focusedLayer = .playbackSpeed
|
||||
} label: {
|
||||
if updateFocusedLayer == .playbackSpeed {
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "speedometer")
|
||||
Text("Playback Speed")
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.foregroundColor(.black)
|
||||
} else {
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "speedometer")
|
||||
Text("Playback Speed")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
.focused($focusedLayer, equals: .playbackSpeed)
|
||||
.focused($playbackSpeedFocused)
|
||||
.onChange(of: playbackSpeedFocused) { isFocused in
|
||||
if isFocused {
|
||||
focusedLayer = .playbackSpeed
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(items, id: \.self) { item in
|
||||
Button {
|
||||
viewModel.playerOverlayDelegate?.didSelectSubtitleStream(index: item.index ?? -1)
|
||||
} label: {
|
||||
if item.index ?? -1 == viewModel.selectedSubtitleStreamIndex {
|
||||
Label(item.displayTitle ?? "No Title", systemImage: "checkmark")
|
||||
} else {
|
||||
Text(item.displayTitle ?? "No Title")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.focusSection()
|
||||
.focused($focusedSection, equals: .titles)
|
||||
.onChange(of: focusedSection) { newSection in
|
||||
if focusedSection == .titles {
|
||||
if lastFocusedLayer == .subtitles {
|
||||
subtitlesFocused = true
|
||||
} else if lastFocusedLayer == .audio {
|
||||
audioFocused = true
|
||||
} else if lastFocusedLayer == .playbackSpeed {
|
||||
playbackSpeedFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 100)
|
||||
|
||||
if updateFocusedLayer == .subtitles && lastFocusedLayer == .subtitles {
|
||||
// MARK: Subtitles
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
if viewModel.subtitleStreams.isEmpty {
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
Text("None")
|
||||
}
|
||||
} else {
|
||||
ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in
|
||||
Button {
|
||||
viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1
|
||||
} label: {
|
||||
if subtitleStream.index == viewModel.selectedSubtitleStreamIndex {
|
||||
Label(subtitleStream.displayTitle ?? "No Title", systemImage: "checkmark")
|
||||
} else {
|
||||
Text(subtitleStream.displayTitle ?? "No Title")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
.focusSection()
|
||||
.focused($focusedSection, equals: .items)
|
||||
}
|
||||
} else if updateFocusedLayer == .audio && lastFocusedLayer == .audio {
|
||||
// MARK: Audio
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
if viewModel.audioStreams.isEmpty {
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
Text("None")
|
||||
}
|
||||
} else {
|
||||
ForEach(viewModel.audioStreams, id: \.self) { audioStream in
|
||||
Button {
|
||||
viewModel.selectedAudioStreamIndex = audioStream.index ?? -1
|
||||
} label: {
|
||||
if audioStream.index == viewModel.selectedAudioStreamIndex {
|
||||
Label(audioStream.displayTitle ?? "No Title", systemImage: "checkmark")
|
||||
} else {
|
||||
Text(audioStream.displayTitle ?? "No Title")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
.focusSection()
|
||||
.focused($focusedSection, equals: .items)
|
||||
}
|
||||
} else if updateFocusedLayer == .playbackSpeed && lastFocusedLayer == .playbackSpeed {
|
||||
// MARK: Rates
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(PlaybackSpeed.allCases, id: \.self) { playbackSpeed in
|
||||
Button {
|
||||
viewModel.playbackSpeed = playbackSpeed
|
||||
} label: {
|
||||
if playbackSpeed == viewModel.playbackSpeed {
|
||||
Label(playbackSpeed.displayTitle, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(playbackSpeed.displayTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
.focusSection()
|
||||
.focused($focusedSection, equals: .items)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +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 JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct tvOSOverlayContentView: View {
|
||||
|
||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
HStack {
|
||||
Button {
|
||||
print("here")
|
||||
} label: {
|
||||
Text("About")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
|
||||
Button {
|
||||
print("here")
|
||||
} label: {
|
||||
Text("Chapters")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
|
||||
Button {
|
||||
print("here")
|
||||
} label: {
|
||||
Text("Subtitles")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
|
||||
Button {
|
||||
print("here")
|
||||
} label: {
|
||||
Text("Audio")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
}
|
||||
.frame(height: 50)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
Color.gray
|
||||
.frame(height: 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct tvOSOverlayContentView_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,
|
||||
shouldShowAutoPlay: true)
|
||||
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
Color.red
|
||||
.ignoresSafeArea()
|
||||
|
||||
tvOSOverlayContentView(viewModel: videoPlayerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,12 +7,14 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct tvOSVLCOverlay: View {
|
||||
|
||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||
@Default(.downActionShowsMenu) var downActionShowsMenu
|
||||
|
||||
@ViewBuilder
|
||||
private var mainButtonView: some View {
|
||||
|
@ -29,7 +31,7 @@ struct tvOSVLCOverlay: View {
|
|||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
|
||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.7), .black]),
|
||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
.ignoresSafeArea()
|
||||
|
@ -101,16 +103,12 @@ struct tvOSVLCOverlay: View {
|
|||
}
|
||||
}
|
||||
|
||||
SFSymbolButton(systemName: "ellipsis.circle") {
|
||||
viewModel.playerOverlayDelegate?.didSelectMenu()
|
||||
}
|
||||
.frame(maxWidth: 30, maxHeight: 30)
|
||||
.contextMenu {
|
||||
SFSymbolButton(systemName: "speedometer") {
|
||||
print("here")
|
||||
if !downActionShowsMenu {
|
||||
SFSymbolButton(systemName: "ellipsis.circle") {
|
||||
viewModel.playerOverlayDelegate?.didSelectMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 30, maxHeight: 30)
|
||||
} }
|
||||
.offset(x: 0, y: 10)
|
||||
|
||||
SliderView(viewModel: viewModel)
|
||||
|
|
|
@ -13,9 +13,11 @@ struct SliderView: UIViewRepresentable {
|
|||
|
||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||
|
||||
// TODO: look at adjusting value dependent on item runtime
|
||||
private let maxValue: Double = 1000
|
||||
|
||||
func updateUIView(_ uiView: TvOSSlider, context: Context) {
|
||||
guard !viewModel.sliderIsScrubbing else { return }
|
||||
uiView.value = Float(maxValue * viewModel.sliderPercentage)
|
||||
}
|
||||
|
||||
|
|
|
@ -457,6 +457,9 @@ public final class TvOSSlider: UIControl {
|
|||
if !isFocused || abs(deceleratingVelocity) < 1 {
|
||||
stopDeceleratingTimer()
|
||||
}
|
||||
|
||||
viewModel.sliderPercentage = Double(percent)
|
||||
viewModel.sliderIsScrubbing = false
|
||||
}
|
||||
|
||||
private func stopDeceleratingTimer() {
|
||||
|
@ -504,7 +507,6 @@ public final class TvOSSlider: UIControl {
|
|||
|
||||
viewModel.sliderPercentage = Double(percent)
|
||||
case .ended, .cancelled:
|
||||
viewModel.sliderIsScrubbing = false
|
||||
|
||||
thumbViewCenterXConstraintConstant = Float(thumbViewCenterXConstraint.constant)
|
||||
|
||||
|
@ -514,6 +516,7 @@ public final class TvOSSlider: UIControl {
|
|||
deceleratingTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(handleDeceleratingTimer(timer:)), userInfo: nil, repeats: true)
|
||||
}
|
||||
else {
|
||||
viewModel.sliderIsScrubbing = false
|
||||
stopDeceleratingTimer()
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -294,7 +294,6 @@
|
|||
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
||||
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */; };
|
||||
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.swift */; };
|
||||
E17885A6278130610094FBCF /* tvOSOverlayContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A5278130610094FBCF /* tvOSOverlayContent.swift */; };
|
||||
E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
||||
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
|
||||
|
@ -384,6 +383,8 @@
|
|||
E1E5D5462783C28100692DFE /* CinematicItemAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5452783C28100692DFE /* CinematicItemAboutView.swift */; };
|
||||
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */; };
|
||||
E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */; };
|
||||
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */; };
|
||||
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5502783E67700692DFE /* ExperimentalSettingsView.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 */; };
|
||||
|
@ -615,7 +616,6 @@
|
|||
E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = "<group>"; };
|
||||
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVLCOverlay.swift; sourceTree = "<group>"; };
|
||||
E17885A3278105170094FBCF /* SFSymbolButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolButton.swift; sourceTree = "<group>"; };
|
||||
E17885A5278130610094FBCF /* tvOSOverlayContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSOverlayContent.swift; sourceTree = "<group>"; };
|
||||
E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = "<group>"; };
|
||||
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = "<group>"; };
|
||||
E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = "<group>"; };
|
||||
|
@ -668,6 +668,8 @@
|
|||
E1E5D5452783C28100692DFE /* CinematicItemAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemAboutView.swift; sourceTree = "<group>"; };
|
||||
E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = "<group>"; };
|
||||
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = "<group>"; };
|
||||
E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = "<group>"; };
|
||||
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.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>"; };
|
||||
|
@ -1255,7 +1257,7 @@
|
|||
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
|
||||
E193D54F2719430400900D82 /* ServerDetailView.swift */,
|
||||
E193D54A271941D300900D82 /* ServerListView.swift */,
|
||||
5398514426B64DA100101B49 /* SettingsView.swift */,
|
||||
E1E5D54D2783E66600692DFE /* SettingsView */,
|
||||
E193D546271941C500900D82 /* UserListView.swift */,
|
||||
E193D548271941CC00900D82 /* UserSignInView.swift */,
|
||||
5310694F2684E7EE00CFFDBA /* VideoPlayer */,
|
||||
|
@ -1331,7 +1333,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */,
|
||||
E17885A5278130610094FBCF /* tvOSOverlayContent.swift */,
|
||||
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */,
|
||||
);
|
||||
path = tvOSOverlay;
|
||||
|
@ -1460,6 +1461,16 @@
|
|||
path = SettingsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1E5D54D2783E66600692DFE /* SettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */,
|
||||
E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */,
|
||||
5398514426B64DA100101B49 /* SettingsView.swift */,
|
||||
);
|
||||
path = SettingsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1FCD08E26C466F3007C8DCF /* Errors */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1916,7 +1927,6 @@
|
|||
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||
E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */,
|
||||
E17885A6278130610094FBCF /* tvOSOverlayContent.swift in Sources */,
|
||||
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
|
||||
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
|
||||
|
@ -1983,6 +1993,7 @@
|
|||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
||||
E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */,
|
||||
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */,
|
||||
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
||||
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
||||
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
|
||||
|
@ -2016,6 +2027,7 @@
|
|||
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
|
||||
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
|
||||
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
|
||||
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
||||
|
|
|
@ -56,4 +56,7 @@ extension Defaults.Keys {
|
|||
struct Experimental {
|
||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
}
|
||||
|
||||
// tvos specific
|
||||
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
}
|
||||
|
|
|
@ -114,6 +114,11 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
// Necessary PassthroughSubject to capture manual scrubbing from sliders
|
||||
let sliderScrubbingSubject = PassthroughSubject<VideoPlayerViewModel, Never>()
|
||||
|
||||
// During scrubbing, many progress reports were spammed
|
||||
// Send only the current report after a delay
|
||||
private var progressReportTimer: Timer?
|
||||
private var lastProgressReport: PlaybackProgressInfo?
|
||||
|
||||
// MARK: init
|
||||
|
||||
init(item: BaseItemDto,
|
||||
|
@ -304,6 +309,15 @@ extension VideoPlayerViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Progress Report Timer
|
||||
extension VideoPlayerViewModel {
|
||||
|
||||
private func sendNewProgressReportWithTimer() {
|
||||
self.progressReportTimer?.invalidate()
|
||||
self.progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7, target: self, selector: #selector(_sendProgressReport), userInfo: nil, repeats: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Updates
|
||||
extension VideoPlayerViewModel {
|
||||
|
||||
|
@ -408,7 +422,15 @@ extension VideoPlayerViewModel {
|
|||
nowPlayingQueue: nil,
|
||||
playlistItemId: "playlistItem0")
|
||||
|
||||
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
||||
self.lastProgressReport = progressInfo
|
||||
|
||||
self.sendNewProgressReportWithTimer()
|
||||
}
|
||||
|
||||
@objc private func _sendProgressReport() {
|
||||
guard let lastProgressReport = lastProgressReport else { return }
|
||||
|
||||
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: lastProgressReport)
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { _ in
|
||||
|
@ -441,3 +463,10 @@ extension VideoPlayerViewModel {
|
|||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Embedded SubtitleStreamViewModel
|
||||
extension VideoPlayerViewModel {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ struct ImageView: View {
|
|||
self.failureInitials = failureInitials
|
||||
}
|
||||
|
||||
// TODO: fix placeholder hash image
|
||||
@ViewBuilder
|
||||
private var placeholderImage: some View {
|
||||
Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 8, height: 8)) ?? UIImage(blurHash: "001fC^", size: CGSize(width: 8, height: 8))!)
|
||||
|
@ -47,7 +48,12 @@ struct ImageView: View {
|
|||
} else if phase.error != nil {
|
||||
failureImage
|
||||
} else {
|
||||
placeholderImage
|
||||
// TODO: remove once placeholder hash image fixed
|
||||
ZStack {
|
||||
Color.gray.ignoresSafeArea()
|
||||
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue