diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index afe48934..12d8a4af 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -12,6 +12,7 @@ import TVVLCKit import MediaPlayer import JellyfinAPI import Combine +import Defaults protocol VideoPlayerSettingsDelegate: AnyObject { func selectNew(audioTrack id: Int32) @@ -149,8 +150,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, func fetchVideo() { // Fetch max bitrate from UserDefaults depending on current connection mode - let defaults = UserDefaults.standard - let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth") + let maxBitrate = Defaults[.inNetworkBandwidth] // Build a device profile let builder = DeviceProfileBuilder() @@ -195,7 +195,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, item.videoUrl = streamURL - let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "") + let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -208,7 +208,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") if stream.isDefault == true{ selectedCaptionTrack = Int32(stream.index!) @@ -220,7 +220,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } if stream.type == .audio { - let track = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + let track = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 0b493e32..a1ad7550 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; + 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; }; 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; }; 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; }; @@ -133,6 +134,10 @@ 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; }; 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; + 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; }; + 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; }; + 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; + 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; @@ -285,6 +290,7 @@ 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; + 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; 625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; 625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = ""; }; 625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; @@ -304,6 +310,7 @@ 628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = ""; }; 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; + 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsExtension.swift; sourceTree = ""; }; 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = ""; }; 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; @@ -331,6 +338,7 @@ 53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */, 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, + 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */, 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, @@ -343,6 +351,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */, 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */, 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */, @@ -591,6 +600,8 @@ 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, 6267B3D92671138200A7371D /* ImageExtensions.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */, + 624C21742685CF60007F1390 /* SearchablePickerView.swift */, ); path = Extensions; sourceTree = ""; @@ -662,6 +673,7 @@ 53A431BE266B0FFE0016769F /* JellyfinAPI */, 53ABFDEC26799D7700886593 /* ActivityIndicator */, 536D3D83267BEA550004248C /* ParallaxView */, + 62CB3F472685BB3B003D0A6F /* Defaults */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -693,6 +705,7 @@ 53A431BC266B0FF20016769F /* JellyfinAPI */, 625CB5792678C4A400530A6E /* ActivityIndicator */, 53EC6E24267EB10F006DD26A /* SwiftyJSON */, + 62CB3F452685BAF7003D0A6F /* Defaults */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -761,6 +774,7 @@ 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -944,6 +958,7 @@ 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, + 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, @@ -982,6 +997,7 @@ 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, + 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, @@ -1008,6 +1024,7 @@ 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, + 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, @@ -1450,6 +1467,14 @@ minimumVersion = 1.1.0; }; }; + 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Defaults"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1533,6 +1558,16 @@ package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; + 62CB3F452685BAF7003D0A6F /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; + 62CB3F472685BB3B003D0A6F /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index f56b4837..f98581b0 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,6 +19,15 @@ "version": "0.6.0" } }, + { + "package": "Defaults", + "repositoryURL": "https://github.com/sindresorhus/Defaults", + "state": { + "branch": null, + "revision": "63d93f97ad545c8bceb125a8a36175ea705f7cf5", + "version": "5.0.0" + } + }, { "package": "Gifu", "repositoryURL": "https://github.com/kaishin/Gifu", diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 333bd721..9c15655f 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -7,28 +7,25 @@ import CoreData import SwiftUI +import Defaults struct SettingsView: View { @Environment(\.managedObjectContext) private var viewContext - + @ObservedObject var viewModel: SettingsViewModel - + @Binding var close: Bool - @State private var inNetworkStreamBitrate: Int = 40_000_000 - @State private var outOfNetworkStreamBitrate: Int = 40_000_000 - @State private var autoSelectSubtitles: Bool = false - @State private var autoSelectSubtitlesLangcode: String = "none" + @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() { - let defaults = UserDefaults.standard username = SessionManager.current.user.username ?? "" - inNetworkStreamBitrate = defaults.integer(forKey: "InNetworkBandwidth") - outOfNetworkStreamBitrate = defaults.integer(forKey: "OutOfNetworkBandwidth") - autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles") - autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? "" } - + var body: some View { NavigationView { Form { @@ -37,29 +34,35 @@ struct SettingsView: View { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } - }.onChange(of: inNetworkStreamBitrate) { _ in - let defaults = UserDefaults.standard - defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth") } - + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } - }.onChange(of: outOfNetworkStreamBitrate) { _ in - let defaults = UserDefaults.standard - defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth") } } - + Section(header: Text("Accessibility")) { - Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in - let defaults = UserDefaults.standard - defaults.setValue(autoSelectSubtitles, forKey: "AutoSelectSubtitles") - }) - Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {} + 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 { HStack { Text("Signed in as \(username)").foregroundColor(.primary) @@ -83,7 +86,7 @@ struct SettingsView: View { Button { close = false } label: { - Text("Back").font(.callout) + Image(systemName: "xmark") } } } diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 24035f9d..198dd4cd 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -12,7 +12,7 @@ import MediaPlayer import Combine import GoogleCast import SwiftyJSON - +import Defaults enum PlayerDestination { case remote @@ -417,8 +417,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.drawable = videoContentView // Fetch max bitrate from UserDefaults depending on current connection mode - let defaults = UserDefaults.standard - let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth") + let maxBitrate = Defaults[.inNetworkBandwidth] // Build a device profile let builder = DeviceProfileBuilder() @@ -456,7 +455,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe item.videoType = .transcode item.videoUrl = streamURL! - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -468,7 +467,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -476,7 +475,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -500,7 +499,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe item.videoUrl = streamURL item.videoType = .directPlay - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -512,7 +511,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!) + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -520,7 +519,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -543,7 +542,29 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe .store(in: &cancellables) } } - + + func setupTracksForPreferredDefaults() { + subtitleTrackArray.forEach { subtitle in + if Defaults[.isAutoSelectSubtitles] { + if Defaults[.autoSelectSubtitlesLangCode] == "Auto", + subtitle.languageCode.contains(Locale.current.languageCode ?? "") { + selectedCaptionTrack = subtitle.id + mediaPlayer.currentVideoSubTitleIndex = subtitle.id + } else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) { + selectedCaptionTrack = subtitle.id + mediaPlayer.currentVideoSubTitleIndex = subtitle.id + } + } + } + + audioTrackArray.forEach { audio in + if audio.languageCode.contains(Defaults[.autoSelectAudioLangCode]) { + selectedAudioTrack = audio.id + mediaPlayer.currentAudioTrackIndex = audio.id + } + } + } + func startLocalPlaybackEngine(_ fetchCaptions: Bool) { print("Local playback engine starting.") mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) @@ -595,6 +616,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.pause() mediaPlayer.play() + setupTracksForPreferredDefaults() print("Local engine started.") } diff --git a/Shared/Extensions/DefaultsExtension.swift b/Shared/Extensions/DefaultsExtension.swift new file mode 100644 index 00000000..ba24206d --- /dev/null +++ b/Shared/Extensions/DefaultsExtension.swift @@ -0,0 +1,19 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import Defaults + +extension Defaults.Keys { + static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000) + static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000) + static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false) + static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto") + static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto") +} diff --git a/Shared/Extensions/SearchablePickerView.swift b/Shared/Extensions/SearchablePickerView.swift new file mode 100644 index 00000000..91550271 --- /dev/null +++ b/Shared/Extensions/SearchablePickerView.swift @@ -0,0 +1,72 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +private struct SearchablePickerView: View { + @Environment(\.presentationMode) var presentationMode + + let options: [Selectable] + let optionToString: (Selectable) -> String + let label: String + + @State var text = "" + @Binding var selected: Selectable + + var body: some View { + VStack { + SearchBar(text: $text) + List(options.filter { + guard !text.isEmpty else { return true } + return optionToString($0).lowercased().contains(text.lowercased()) + }, id: \.self) { selectable in + Button(action: { + selected = selectable + presentationMode.wrappedValue.dismiss() + }) { + HStack { + Text(optionToString(selectable)).foregroundColor(Color.primary) + Spacer() + if selected == selectable { + Image(systemName: "checkmark").foregroundColor(.accentColor) + } + } + } + }.listStyle(GroupedListStyle()) + } + } +} + +struct SearchablePicker: View { + let label: String + let options: [Selectable] + let optionToString: (Selectable) -> String + + @Binding var selected: Selectable + + var body: some View { + NavigationLink(destination: searchablePickerView()) { + HStack { + Text(label) + Spacer() + Text(optionToString(selected)) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + } + } + + private func searchablePickerView() -> some View { + SearchablePickerView(options: options, + optionToString: optionToString, + label: label, + selected: $selected) + } +} diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index cf3fe59a..eba576d6 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -1,11 +1,11 @@ // - /* - * 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 - */ +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ import Foundation @@ -23,8 +23,17 @@ struct Bitrates: Codable, Hashable { public var value: Int } +struct TrackLanguage: Hashable { + var name: String + var isoCode: String + + static let auto = TrackLanguage(name: "Auto", isoCode: "Auto") +} + final class SettingsViewModel: ObservableObject { + let currentLocale = Locale.current var bitrates: [Bitrates] = [] + var langs = [TrackLanguage]() init() { let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")! @@ -39,5 +48,11 @@ final class SettingsViewModel: ObservableObject { } catch { print(error) } + + self.langs = Locale.isoLanguageCodes.compactMap { + guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil } + return TrackLanguage(name: name, isoCode: $0) + }.sorted(by: { $0.name < $1.name }) + self.langs.insert(.auto, at: 0) } } diff --git a/Shared/ViewModels/SplashViewModel.swift b/Shared/ViewModels/SplashViewModel.swift index 28da5de6..57776ab6 100644 --- a/Shared/ViewModels/SplashViewModel.swift +++ b/Shared/ViewModels/SplashViewModel.swift @@ -30,14 +30,6 @@ final class SplashViewModel: ViewModel { WidgetCenter.shared.reloadAllTimelines() #endif - let defaults = UserDefaults.standard - if defaults.integer(forKey: "InNetworkBandwidth") == 0 { - defaults.setValue(40_000_000, forKey: "InNetworkBandwidth") - } - if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 { - defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth") - } - let nc = NotificationCenter.default nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil) nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil) diff --git a/Shared/ViewModels/VideoPlayerModel.swift b/Shared/ViewModels/VideoPlayerModel.swift index cb219a0d..117c22bb 100644 --- a/Shared/ViewModels/VideoPlayerModel.swift +++ b/Shared/ViewModels/VideoPlayerModel.swift @@ -16,10 +16,12 @@ struct Subtitle { var url: URL? var delivery: SubtitleDeliveryMethod var codec: String + var languageCode: String } struct AudioTrack { var name: String + var languageCode: String var id: Int32 }