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 {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainCoordinator().view()
|
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/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict/>
|
<dict>
|
||||||
|
<key>UIColorName</key>
|
||||||
|
<string>LaunchScreenBackground</string>
|
||||||
|
</dict>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
<array>
|
<array>
|
||||||
<string>arm64</string>
|
<string>arm64</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIUserInterfaceStyle</key>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<string>Automatic</string>
|
<string>Dark</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -39,7 +39,7 @@ struct BasicAppSettingsView: View {
|
||||||
}
|
}
|
||||||
.alert(L10n.reset, isPresented: $resetTapped, actions: {
|
.alert(L10n.reset, isPresented: $resetTapped, actions: {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
viewModel.reset()
|
viewModel.resetAppSettings()
|
||||||
basicAppSettingsRouter.dismissCoordinator()
|
basicAppSettingsRouter.dismissCoordinator()
|
||||||
} label: {
|
} label: {
|
||||||
L10n.reset.text
|
L10n.reset.text
|
||||||
|
|
|
@ -13,24 +13,11 @@ import SwiftUI
|
||||||
struct CinematicEpisodeItemView: View {
|
struct CinematicEpisodeItemView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: EpisodeItemViewModel
|
@ObservedObject var viewModel: EpisodeItemViewModel
|
||||||
@State var verticalScrollViewOffset: CGFloat = 0
|
|
||||||
@State var wrappedScrollView: UIScrollView?
|
@State var wrappedScrollView: UIScrollView?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
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),
|
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
|
||||||
bh: viewModel.item.getBackdropImageBlurHash())
|
bh: viewModel.item.getBackdropImageBlurHash())
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
@ -38,10 +25,9 @@ struct CinematicEpisodeItemView: View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
Spacer(minLength: verticalScrollViewOffset)
|
|
||||||
|
|
||||||
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView)
|
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
|
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ struct CinematicItemAboutView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top, spacing: 10) {
|
HStack(alignment: .top, spacing: 10) {
|
||||||
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 230))
|
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 257))
|
||||||
.frame(width: 230, height: 380)
|
.frame(width: 257, height: 380)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
|
|
|
@ -25,68 +25,86 @@ struct CinematicItemViewTopRow: View {
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.frame(height: 210)
|
.frame(height: 210)
|
||||||
|
|
||||||
HStack(alignment: .bottom) {
|
VStack {
|
||||||
VStack(alignment: .leading) {
|
Spacer()
|
||||||
HStack(alignment: .PlayInformationAlignmentGuide) {
|
|
||||||
CinematicItemViewTopRowButton(wrappedScrollView: wrappedScrollView) {
|
HStack(alignment: .bottom) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack(alignment: .PlayInformationAlignmentGuide) {
|
||||||
|
|
||||||
|
// MARK: Play
|
||||||
Button {
|
Button {
|
||||||
itemRouter.route(to: \.videoPlayer, viewModel.itemVideoPlayerViewModel!)
|
itemRouter.route(to: \.videoPlayer, viewModel.itemVideoPlayerViewModel!)
|
||||||
} label: {
|
} label: {
|
||||||
ZStack {
|
HStack(spacing: 15) {
|
||||||
Color.white.frame(width: 230, height: 100)
|
Image(systemName: "play.fill")
|
||||||
|
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black)
|
||||||
Text("Play")
|
|
||||||
.font(.title3)
|
.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())
|
.buttonStyle(CardButtonStyle())
|
||||||
.disabled(viewModel.itemVideoPlayerViewModel == nil)
|
.disabled(viewModel.itemVideoPlayerViewModel == nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text(viewModel.item.name ?? "")
|
|
||||||
.font(.title2)
|
|
||||||
.lineLimit(2)
|
|
||||||
|
|
||||||
if let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.getEpisodeLocator() {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("\(seriesName) - \(episodeLocator)")
|
Text(viewModel.item.name ?? "")
|
||||||
|
.font(.title2)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
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) {
|
Spacer()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 50)
|
||||||
Spacer()
|
.padding(.bottom, 50)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 50)
|
|
||||||
.padding(.bottom, 50)
|
|
||||||
}
|
}
|
||||||
.onChange(of: envFocused) { envFocus in
|
.onChange(of: envFocused) { envFocus in
|
||||||
if envFocus == true {
|
if envFocus == true {
|
||||||
|
|
|
@ -38,7 +38,10 @@ struct CinematicItemViewTopRowButton<Content: View>: View {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||||
wrappedScrollView?.scrollToTop()
|
wrappedScrollView?.scrollToTop()
|
||||||
}
|
}
|
||||||
print("Scroll to top")
|
|
||||||
|
withAnimation(.linear(duration: 0.15)) {
|
||||||
|
self.focused = newValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,24 +13,11 @@ import SwiftUI
|
||||||
struct CinematicMovieItemView: View {
|
struct CinematicMovieItemView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: MovieItemViewModel
|
@ObservedObject var viewModel: MovieItemViewModel
|
||||||
@State var verticalScrollViewOffset: CGFloat = 0
|
|
||||||
@State var wrappedScrollView: UIScrollView?
|
@State var wrappedScrollView: UIScrollView?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
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),
|
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
|
||||||
bh: viewModel.item.getBackdropImageBlurHash())
|
bh: viewModel.item.getBackdropImageBlurHash())
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
@ -38,10 +25,9 @@ struct CinematicMovieItemView: View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
Spacer(minLength: verticalScrollViewOffset)
|
|
||||||
|
|
||||||
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView)
|
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
|
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ struct ServerDetailView: View {
|
||||||
Text(SessionManager.main.currentLogin.server.name)
|
Text(SessionManager.main.currentLogin.server.name)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
.focusable()
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("URI")
|
Text("URI")
|
||||||
|
|
|
@ -67,7 +67,7 @@ struct ServerListView: View {
|
||||||
Text("Connect to a Jellyfin server to get started")
|
Text("Connect to a Jellyfin server to get started")
|
||||||
.frame(minWidth: 50, maxWidth: 500)
|
.frame(minWidth: 50, maxWidth: 500)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.font(.callout)
|
.font(.body)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
serverListRouter.route(to: \.connectToServer)
|
serverListRouter.route(to: \.connectToServer)
|
||||||
|
@ -75,8 +75,12 @@ struct ServerListView: View {
|
||||||
L10n.connect.text
|
L10n.connect.text
|
||||||
.bold()
|
.bold()
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
|
.padding(.vertical)
|
||||||
|
.padding(.horizontal, 30)
|
||||||
|
.background(Color.jellyfinPurple)
|
||||||
}
|
}
|
||||||
.padding(.top, 40)
|
.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 didSelectClose()
|
||||||
func didSelectMenu()
|
func didSelectMenu()
|
||||||
func didDeselectMenu()
|
|
||||||
|
|
||||||
func didSelectBackward()
|
func didSelectBackward()
|
||||||
func didSelectForward()
|
func didSelectForward()
|
||||||
|
|
|
@ -116,6 +116,8 @@ class VLCPlayerViewController: UIViewController {
|
||||||
// aren't unnecessarily set more than once
|
// aren't unnecessarily set more than once
|
||||||
vlcMediaPlayer.delegate = self
|
vlcMediaPlayer.delegate = self
|
||||||
vlcMediaPlayer.drawable = videoContentView
|
vlcMediaPlayer.drawable = videoContentView
|
||||||
|
|
||||||
|
// TODO: custom font sizes
|
||||||
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
|
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
|
||||||
|
|
||||||
setupMediaPlayer(newViewModel: viewModel)
|
setupMediaPlayer(newViewModel: viewModel)
|
||||||
|
@ -188,8 +190,7 @@ class VLCPlayerViewController: UIViewController {
|
||||||
guard let buttonPress = presses.first?.type else { return }
|
guard let buttonPress = presses.first?.type else { return }
|
||||||
|
|
||||||
switch(buttonPress) {
|
switch(buttonPress) {
|
||||||
case .menu:
|
case .menu: () // Captured by other gesture
|
||||||
print("Menu")
|
|
||||||
case .playPause:
|
case .playPause:
|
||||||
didSelectMain()
|
didSelectMain()
|
||||||
case .select:
|
case .select:
|
||||||
|
@ -201,24 +202,22 @@ class VLCPlayerViewController: UIViewController {
|
||||||
showOverlay()
|
showOverlay()
|
||||||
restartOverlayDismissTimer()
|
restartOverlayDismissTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Up arrow")
|
|
||||||
case .downArrow:
|
case .downArrow:
|
||||||
if !displayingContentOverlay {
|
if Defaults[.downActionShowsMenu] {
|
||||||
stopOverlayDismissTimer()
|
if !displayingContentOverlay {
|
||||||
|
didSelectMenu()
|
||||||
hideOverlay()
|
}
|
||||||
showOverlayContent()
|
|
||||||
}
|
}
|
||||||
case .leftArrow:
|
case .leftArrow:
|
||||||
didSelectBackward()
|
if !displayingContentOverlay {
|
||||||
print("Left arrow")
|
didSelectBackward()
|
||||||
|
}
|
||||||
case .rightArrow:
|
case .rightArrow:
|
||||||
didSelectForward()
|
if !displayingContentOverlay {
|
||||||
case .pageUp:
|
didSelectForward()
|
||||||
print("page up")
|
}
|
||||||
case .pageDown:
|
case .pageUp: ()
|
||||||
print("page down")
|
case .pageDown: ()
|
||||||
@unknown default: ()
|
@unknown default: ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,6 +234,9 @@ class VLCPlayerViewController: UIViewController {
|
||||||
hideOverlay()
|
hideOverlay()
|
||||||
} else if displayingContentOverlay {
|
} else if displayingContentOverlay {
|
||||||
hideOverlayContent()
|
hideOverlayContent()
|
||||||
|
|
||||||
|
showOverlay()
|
||||||
|
restartOverlayDismissTimer()
|
||||||
} else {
|
} else {
|
||||||
vlcMediaPlayer.pause()
|
vlcMediaPlayer.pause()
|
||||||
|
|
||||||
|
@ -301,11 +303,23 @@ class VLCPlayerViewController: UIViewController {
|
||||||
currentOverlayContentHostingController.removeFromParent()
|
currentOverlayContentHostingController.removeFromParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel,
|
// let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel,
|
||||||
title: "Subtitles",
|
// title: "Subtitles",
|
||||||
items: viewModel.subtitleStreams) { selectedMediaStream in
|
// items: viewModel.subtitleStreams) { selectedMediaStream in
|
||||||
self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1)
|
// 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 newOverlayContentHostingController = UIHostingController(rootView: newSmallMenuOverlayView)
|
||||||
// let newOverlayContentView = tvOSOverlayContentView(viewModel: viewModel)
|
// let newOverlayContentView = tvOSOverlayContentView(viewModel: viewModel)
|
||||||
// let newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView)
|
// let newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView)
|
||||||
|
@ -357,6 +371,8 @@ extension VLCPlayerViewController {
|
||||||
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||||
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||||
|
|
||||||
|
// TODO: Custom buffer/cache amounts
|
||||||
|
|
||||||
let media = VLCMedia(url: newViewModel.streamURL)
|
let media = VLCMedia(url: newViewModel.streamURL)
|
||||||
media.addOption("--prefetch-buffer-size=1048576")
|
media.addOption("--prefetch-buffer-size=1048576")
|
||||||
media.addOption("--network-caching=5000")
|
media.addOption("--network-caching=5000")
|
||||||
|
@ -640,14 +656,11 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement properly in overlays
|
|
||||||
func didSelectMenu() {
|
func didSelectMenu() {
|
||||||
stopOverlayDismissTimer()
|
stopOverlayDismissTimer()
|
||||||
}
|
|
||||||
|
hideOverlay()
|
||||||
// TODO: Implement properly in overlays
|
showOverlayContent()
|
||||||
func didDeselectMenu() {
|
|
||||||
restartOverlayDismissTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func didSelectBackward() {
|
func didSelectBackward() {
|
||||||
|
|
|
@ -10,47 +10,244 @@
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: Needs replacement/reworking
|
||||||
struct SmallMediaStreamSelectionView: View {
|
struct SmallMediaStreamSelectionView: View {
|
||||||
|
|
||||||
|
enum Layer: Hashable {
|
||||||
|
case subtitles
|
||||||
|
case audio
|
||||||
|
case playbackSpeed
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MediaSection: Hashable {
|
||||||
|
case titles
|
||||||
|
case items
|
||||||
|
}
|
||||||
|
|
||||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||||
let title: String
|
|
||||||
var items: [MediaStream]
|
@State private var updateFocusedLayer: Layer = .subtitles
|
||||||
var selectedAction: (MediaStream) -> Void
|
|
||||||
|
@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 {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.9)]),
|
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.frame(height: 150)
|
.frame(height: 300)
|
||||||
|
|
||||||
VStack {
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Subtitles")
|
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
|
||||||
|
// 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()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
ScrollView(.horizontal) {
|
.focusSection()
|
||||||
HStack {
|
.focused($focusedSection, equals: .titles)
|
||||||
ForEach(items, id: \.self) { item in
|
.onChange(of: focusedSection) { newSection in
|
||||||
Button {
|
if focusedSection == .titles {
|
||||||
viewModel.playerOverlayDelegate?.didSelectSubtitleStream(index: item.index ?? -1)
|
if lastFocusedLayer == .subtitles {
|
||||||
} label: {
|
subtitlesFocused = true
|
||||||
if item.index ?? -1 == viewModel.selectedSubtitleStreamIndex {
|
} else if lastFocusedLayer == .audio {
|
||||||
Label(item.displayTitle ?? "No Title", systemImage: "checkmark")
|
audioFocused = true
|
||||||
} else {
|
} else if lastFocusedLayer == .playbackSpeed {
|
||||||
Text(item.displayTitle ?? "No Title")
|
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
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Defaults
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct tvOSVLCOverlay: View {
|
struct tvOSVLCOverlay: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||||
|
@Default(.downActionShowsMenu) var downActionShowsMenu
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainButtonView: some View {
|
private var mainButtonView: some View {
|
||||||
|
@ -29,7 +31,7 @@ struct tvOSVLCOverlay: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
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,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
@ -101,16 +103,12 @@ struct tvOSVLCOverlay: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SFSymbolButton(systemName: "ellipsis.circle") {
|
if !downActionShowsMenu {
|
||||||
viewModel.playerOverlayDelegate?.didSelectMenu()
|
SFSymbolButton(systemName: "ellipsis.circle") {
|
||||||
}
|
viewModel.playerOverlayDelegate?.didSelectMenu()
|
||||||
.frame(maxWidth: 30, maxHeight: 30)
|
|
||||||
.contextMenu {
|
|
||||||
SFSymbolButton(systemName: "speedometer") {
|
|
||||||
print("here")
|
|
||||||
}
|
}
|
||||||
}
|
.frame(maxWidth: 30, maxHeight: 30)
|
||||||
}
|
} }
|
||||||
.offset(x: 0, y: 10)
|
.offset(x: 0, y: 10)
|
||||||
|
|
||||||
SliderView(viewModel: viewModel)
|
SliderView(viewModel: viewModel)
|
||||||
|
|
|
@ -13,9 +13,11 @@ struct SliderView: UIViewRepresentable {
|
||||||
|
|
||||||
@ObservedObject var viewModel: VideoPlayerViewModel
|
@ObservedObject var viewModel: VideoPlayerViewModel
|
||||||
|
|
||||||
|
// TODO: look at adjusting value dependent on item runtime
|
||||||
private let maxValue: Double = 1000
|
private let maxValue: Double = 1000
|
||||||
|
|
||||||
func updateUIView(_ uiView: TvOSSlider, context: Context) {
|
func updateUIView(_ uiView: TvOSSlider, context: Context) {
|
||||||
|
guard !viewModel.sliderIsScrubbing else { return }
|
||||||
uiView.value = Float(maxValue * viewModel.sliderPercentage)
|
uiView.value = Float(maxValue * viewModel.sliderPercentage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -457,6 +457,9 @@ public final class TvOSSlider: UIControl {
|
||||||
if !isFocused || abs(deceleratingVelocity) < 1 {
|
if !isFocused || abs(deceleratingVelocity) < 1 {
|
||||||
stopDeceleratingTimer()
|
stopDeceleratingTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.sliderPercentage = Double(percent)
|
||||||
|
viewModel.sliderIsScrubbing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopDeceleratingTimer() {
|
private func stopDeceleratingTimer() {
|
||||||
|
@ -504,7 +507,6 @@ public final class TvOSSlider: UIControl {
|
||||||
|
|
||||||
viewModel.sliderPercentage = Double(percent)
|
viewModel.sliderPercentage = Double(percent)
|
||||||
case .ended, .cancelled:
|
case .ended, .cancelled:
|
||||||
viewModel.sliderIsScrubbing = false
|
|
||||||
|
|
||||||
thumbViewCenterXConstraintConstant = Float(thumbViewCenterXConstraint.constant)
|
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)
|
deceleratingTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(handleDeceleratingTimer(timer:)), userInfo: nil, repeats: true)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
viewModel.sliderIsScrubbing = false
|
||||||
stopDeceleratingTimer()
|
stopDeceleratingTimer()
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -294,7 +294,6 @@
|
||||||
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
||||||
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */; };
|
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */; };
|
||||||
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.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 */; };
|
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 */; };
|
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.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 */; };
|
E1E5D5462783C28100692DFE /* CinematicItemAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5452783C28100692DFE /* CinematicItemAboutView.swift */; };
|
||||||
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */; };
|
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */; };
|
||||||
E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.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 */; };
|
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||||
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallMenuOverlay.swift; sourceTree = "<group>"; };
|
||||||
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
|
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1255,7 +1257,7 @@
|
||||||
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
|
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
|
||||||
E193D54F2719430400900D82 /* ServerDetailView.swift */,
|
E193D54F2719430400900D82 /* ServerDetailView.swift */,
|
||||||
E193D54A271941D300900D82 /* ServerListView.swift */,
|
E193D54A271941D300900D82 /* ServerListView.swift */,
|
||||||
5398514426B64DA100101B49 /* SettingsView.swift */,
|
E1E5D54D2783E66600692DFE /* SettingsView */,
|
||||||
E193D546271941C500900D82 /* UserListView.swift */,
|
E193D546271941C500900D82 /* UserListView.swift */,
|
||||||
E193D548271941CC00900D82 /* UserSignInView.swift */,
|
E193D548271941CC00900D82 /* UserSignInView.swift */,
|
||||||
5310694F2684E7EE00CFFDBA /* VideoPlayer */,
|
5310694F2684E7EE00CFFDBA /* VideoPlayer */,
|
||||||
|
@ -1331,7 +1333,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */,
|
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */,
|
||||||
E17885A5278130610094FBCF /* tvOSOverlayContent.swift */,
|
|
||||||
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */,
|
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */,
|
||||||
);
|
);
|
||||||
path = tvOSOverlay;
|
path = tvOSOverlay;
|
||||||
|
@ -1460,6 +1461,16 @@
|
||||||
path = SettingsView;
|
path = SettingsView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E1E5D54D2783E66600692DFE /* SettingsView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */,
|
||||||
|
E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */,
|
||||||
|
5398514426B64DA100101B49 /* SettingsView.swift */,
|
||||||
|
);
|
||||||
|
path = SettingsView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E1FCD08E26C466F3007C8DCF /* Errors */ = {
|
E1FCD08E26C466F3007C8DCF /* Errors */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1916,7 +1927,6 @@
|
||||||
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||||
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||||
E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */,
|
E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */,
|
||||||
E17885A6278130610094FBCF /* tvOSOverlayContent.swift in Sources */,
|
|
||||||
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||||
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
|
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
|
||||||
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
|
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
|
||||||
|
@ -1983,6 +1993,7 @@
|
||||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
||||||
E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */,
|
E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||||
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */,
|
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */,
|
||||||
|
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */,
|
||||||
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
||||||
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
||||||
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
|
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
|
||||||
|
@ -2016,6 +2027,7 @@
|
||||||
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
|
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
|
||||||
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
||||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||||
|
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
|
||||||
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
|
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
|
||||||
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||||
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
||||||
|
|
|
@ -56,4 +56,7 @@ extension Defaults.Keys {
|
||||||
struct Experimental {
|
struct Experimental {
|
||||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
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
|
// Necessary PassthroughSubject to capture manual scrubbing from sliders
|
||||||
let sliderScrubbingSubject = PassthroughSubject<VideoPlayerViewModel, Never>()
|
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
|
// MARK: init
|
||||||
|
|
||||||
init(item: BaseItemDto,
|
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
|
// MARK: Updates
|
||||||
extension VideoPlayerViewModel {
|
extension VideoPlayerViewModel {
|
||||||
|
|
||||||
|
@ -408,7 +422,15 @@ extension VideoPlayerViewModel {
|
||||||
nowPlayingQueue: nil,
|
nowPlayingQueue: nil,
|
||||||
playlistItemId: "playlistItem0")
|
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
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
} receiveValue: { _ in
|
} receiveValue: { _ in
|
||||||
|
@ -441,3 +463,10 @@ extension VideoPlayerViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Embedded SubtitleStreamViewModel
|
||||||
|
extension VideoPlayerViewModel {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ struct ImageView: View {
|
||||||
self.failureInitials = failureInitials
|
self.failureInitials = failureInitials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: fix placeholder hash image
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var placeholderImage: some View {
|
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))!)
|
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 {
|
} else if phase.error != nil {
|
||||||
failureImage
|
failureImage
|
||||||
} else {
|
} else {
|
||||||
placeholderImage
|
// TODO: remove once placeholder hash image fixed
|
||||||
|
ZStack {
|
||||||
|
Color.gray.ignoresSafeArea()
|
||||||
|
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue