From 93a25eb9c43eddd03e09df87722c086fb6cb6da4 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Sat, 31 Jul 2021 23:52:31 -0400 Subject: [PATCH] add tvOS settings page; fix transcoded video playback always starting at 0 ticks, also add front row image. --- JellyfinPlayer tvOS/HomeView.swift | 13 --- JellyfinPlayer tvOS/MainTabView.swift | 9 ++ JellyfinPlayer tvOS/SettingsView.swift | 93 +++++++++++++++++++ .../VideoPlayerViewController.swift | 7 +- JellyfinPlayer.xcodeproj/project.pbxproj | 10 +- Shared/Extensions/APIExtensions.swift | 10 +- .../Extensions}/SearchBarView.swift | 2 + 7 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 JellyfinPlayer tvOS/SettingsView.swift rename {JellyfinPlayer => Shared/Extensions}/SearchBarView.swift (95%) diff --git a/JellyfinPlayer tvOS/HomeView.swift b/JellyfinPlayer tvOS/HomeView.swift index c6e2003f..8642a1a6 100644 --- a/JellyfinPlayer tvOS/HomeView.swift +++ b/JellyfinPlayer tvOS/HomeView.swift @@ -21,19 +21,6 @@ struct HomeView: View { ProgressView() } else { LazyVStack(alignment: .leading) { - Button { - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - } label: { - HStack { - ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(SessionManager.current.user.user_id!)/Images/Primary?width=500")!) - .frame(width: 50, height: 50) - .cornerRadius(25.0) - Text(SessionManager.current.user.username ?? "") - .font(.headline) - .fontWeight(.semibold) - } - }.padding(.leading, 90) if !viewModel.resumeItems.isEmpty { ContinueWatchingView(items: viewModel.resumeItems) } diff --git a/JellyfinPlayer tvOS/MainTabView.swift b/JellyfinPlayer tvOS/MainTabView.swift index e3e99199..e4eb8c95 100644 --- a/JellyfinPlayer tvOS/MainTabView.swift +++ b/JellyfinPlayer tvOS/MainTabView.swift @@ -53,6 +53,14 @@ struct MainTabView: View { Image(systemName: "folder") } .tag(Tab.allMedia) + + SettingsView(viewModel: SettingsViewModel()) + .offset(y: -1) // don't remove this. it breaks tabview on 4K displays. + .tabItem { + Text("Settings") + Image(systemName: "gear") + } + .tag(Tab.settings) } } } @@ -62,6 +70,7 @@ extension MainTabView { enum Tab: String { case home case allMedia + case settings } } diff --git a/JellyfinPlayer tvOS/SettingsView.swift b/JellyfinPlayer tvOS/SettingsView.swift new file mode 100644 index 00000000..9db18f91 --- /dev/null +++ b/JellyfinPlayer tvOS/SettingsView.swift @@ -0,0 +1,93 @@ +/* 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 + +struct SettingsView: View { + @Environment(\.managedObjectContext) private var viewContext + + @ObservedObject var viewModel: SettingsViewModel + + @Default(.inNetworkBandwidth) var inNetworkStreamBitrate + @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate + @Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles + @Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode + @Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode + @State private var username: String = "" + + func onAppear() { + username = SessionManager.current.user?.username ?? "" + } + + var body: some View { + Form { + Section(header: Text("Playback settings")) { + Picker("Default local quality", selection: $inNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) + }.padding(.leading, 90) + .padding(.trailing, 90) + } + + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) + }.padding(.leading, 90) + .padding(.trailing, 90) + } + } + + Section(header: Text("Accessibility")) { + Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) + SearchablePicker(label: "Preferred subtitle language", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding( + 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( + get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto }, + set: { autoSelectAudioLangcode = $0.isoCode} + ) + ) + } + + Section(header: Text(ServerEnvironment.current.server.name ?? "")) { + HStack { + Text("Signed in as \(username)").foregroundColor(.primary) + Spacer() + Button { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let nc = NotificationCenter.default + nc.post(name: Notification.Name("didSignOut"), object: nil) + } + } label: { + Text("Switch user").font(.callout) + } + } + Button { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + SessionManager.current.logout(); + let nc = NotificationCenter.default + nc.post(name: Notification.Name("didSignOut"), object: nil) + } + } label: { + Text("Sign out").font(.callout) + } + } + }.onAppear(perform: onAppear) + .padding(.leading, 90) + .padding(.trailing, 90) + } +} diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 96cfe7dd..1050f03a 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -582,7 +582,12 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, updateNowPlayingCenter(time: nil, playing: mediaPlayer.state == .playing) if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { - let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (!playing), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") + var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)); + if(ticks == 0) { + ticks = manifest.userData?.playbackPositionTicks ?? 0 + } + + let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (!playing), isMuted: false, positionTicks: ticks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 02e8557d..b12ec6e0 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -98,6 +98,9 @@ 53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; }; 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; }; 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; + 5398514526B64DA100101B49 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5398514426B64DA100101B49 /* SettingsView.swift */; }; + 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; + 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */; }; 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A089CF264DA9DA00D57806 /* MovieItemView.swift */; }; 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BC266B0FF20016769F /* JellyfinAPI */; }; @@ -307,6 +310,7 @@ 5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; 53892771263C8C6F0035E14B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; + 5398514426B64DA100101B49 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 53987CA326572C1300E7EA70 /* SeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemView.swift; sourceTree = ""; }; 53987CA526572F0700E7EA70 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; @@ -535,6 +539,7 @@ 53116A16268B919A003024C9 /* SeriesItemView.swift */, 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */, 53272538268C20100035FBF1 /* EpisodeItemView.swift */, + 5398514426B64DA100101B49 /* SettingsView.swift */, ); path = "JellyfinPlayer tvOS"; sourceTree = ""; @@ -634,7 +639,6 @@ 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */, 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, - 53DE4BD1267098F300739748 /* SearchBarView.swift */, 625CB5672678B6FB00530A6E /* SplashView.swift */, 625CB56B2678C0FD00530A6E /* MainTabView.swift */, 625CB56E2678C23300530A6E /* HomeView.swift */, @@ -692,6 +696,7 @@ 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( + 53DE4BD1267098F300739748 /* SearchBarView.swift */, 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, 5364F454266CA0DC0026ECBA /* APIExtensions.swift */, @@ -1072,6 +1077,7 @@ 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */, 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */, 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, + 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, @@ -1086,8 +1092,10 @@ 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */, 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */, + 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, + 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index 0d40acd2..7ebbbdd1 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -70,7 +70,7 @@ extension BaseItemDto { } let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)&format=webp" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -86,7 +86,7 @@ extension BaseItemDto { let imageTag = (self.parentBackdropImageTags ?? [""])[0] let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)&format=webp" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -94,7 +94,7 @@ extension BaseItemDto { let imageType = "Primary" let imageTag = self.seriesPrimaryImageTag ?? "" let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)&format=webp" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -110,7 +110,7 @@ extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)&format=webp" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" // print(urlString) return URL(string: urlString)! } @@ -156,7 +156,7 @@ extension BaseItemPerson { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)&format=webp" + let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" return URL(string: urlString)! } diff --git a/JellyfinPlayer/SearchBarView.swift b/Shared/Extensions/SearchBarView.swift similarity index 95% rename from JellyfinPlayer/SearchBarView.swift rename to Shared/Extensions/SearchBarView.swift index 3a5dd783..ac4b8f32 100644 --- a/JellyfinPlayer/SearchBarView.swift +++ b/Shared/Extensions/SearchBarView.swift @@ -19,7 +19,9 @@ struct SearchBar: View { TextField(NSLocalizedString("Search...", comment: ""), text: $text) .padding(8) .padding(.horizontal, 16) + #if os(iOS) .background(Color(.systemGray6)) + #endif .cornerRadius(8) if !text.isEmpty { Button(action: {