From 4db02b2638cd839788fdd5fedc45b240564b2e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 20 May 2024 02:49:32 +0200 Subject: [PATCH 1/6] Improved Captions handling New options for captions in `Settings-Player`: - Always show captions - Default language User can now select whether they want to show captions automatically when the video starts, and select the language. Captions selector now shows proper name -> `English (en)` instead of only `en` --- Model/Player/Backends/MPVBackend.swift | 17 ++-- Model/Player/Backends/MPVClient.swift | 8 ++ Shared/Defaults.swift | 2 + Shared/LanguageCodes.swift | 107 +++++++++++++++++++++++++ Shared/Player/PlaybackSettings.swift | 7 +- Shared/Settings/PlayerSettings.swift | 28 ++++++- Yattee.xcodeproj/project.pbxproj | 8 ++ 7 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 Shared/LanguageCodes.swift diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 01a9adb7..a77806c6 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -217,9 +217,15 @@ final class MPVBackend: PlayerBackend { #endif var captions: Captions? - if let captionsLanguageCode = Defaults[.captionsLanguageCode] { - captions = video.captions.first { $0.code == captionsLanguageCode } ?? - video.captions.first { $0.code.contains(captionsLanguageCode) } + + if Defaults[.captionsAutoShow] == true { + let captionsLanguageCode = Defaults[.captionsDefaultLanguageCode] + if !captionsLanguageCode.isEmpty { + captions = video.captions.first { $0.code == captionsLanguageCode } ?? + video.captions.first { $0.code.contains(captionsLanguageCode) } + } + } else { + captions = nil } let updateCurrentStream = { @@ -254,9 +260,8 @@ final class MPVBackend: PlayerBackend { self.startClientUpdates() - // Captions should only be displayed when selected by the user, - // not when the video starts. So, we remove them. - self.client?.removeSubs() + if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() } + PlayerModel.shared.captions = self.captions if !preservingTime, !upgrading, diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index 75f9128c..11066345 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -405,6 +405,14 @@ final class MPVClient: ObservableObject { setString("video", "no") } + func setSubToAuto() { + setString("sub", "auto") + } + + func setSubToNo() { + setString("sub", "no") + } + var tracksCount: Int { Int(getString("track-list/count") ?? "-1") ?? -1 } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index c2cc9d23..0529ad27 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -301,7 +301,9 @@ extension Defaults.Keys { static let lastPlayed = Key("lastPlayed") static let activeBackend = Key("activeBackend", default: .mpv) + static let captionsAutoShow = Key("captionsAutoShow", default: false) static let captionsLanguageCode = Key("captionsLanguageCode") + static let captionsDefaultLanguageCode = Key("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) static let lastUsedPlaylistID = Key("lastPlaylistID") static let lastAccountIsPublic = Key("lastAccountIsPublic", default: false) diff --git a/Shared/LanguageCodes.swift b/Shared/LanguageCodes.swift new file mode 100644 index 00000000..2a271995 --- /dev/null +++ b/Shared/LanguageCodes.swift @@ -0,0 +1,107 @@ +enum LanguageCodes: String, CaseIterable { + case Afrikaans = "af" + case Arabic = "ar" + case Azerbaijani = "az" + case Bengali = "bn" + case Catalan = "ca" + case Czech = "cs" + case Welsh = "cy" + case Danish = "da" + case German = "de" + case Greek = "el" + case English = "en" + case Spanish = "es" + case Persian = "fa" + case Finnish = "fi" + case Filipino = "fil" + case French = "fr" + case Irish = "ga" + case Hebrew = "he" + case Hindi = "hi" + case Hungarian = "hu" + case Indonesian = "id" + case Italian = "it" + case Japanese = "ja" + case Javanese = "jv" + case Korean = "ko" + case Lithuanian = "lt" + case Malay = "ms" + case Maltese = "mt" + case Dutch = "nl" + case Norwegian = "no" + case Polish = "pl" + case Portuguese = "pt" + case Romanian = "ro" + case Russian = "ru" + case Slovak = "sk" + case Slovene = "sl" + case Swedish = "sv" + case Swahili = "sw" + case Thai = "th" + case Tagalog = "tl" + case Turkish = "tr" + case Ukrainian = "uk" + case Urdu = "ur" + case Uzbek = "uz" + case Vietnamese = "vi" + case Xhosa = "xh" + case Chinese = "zh" + case Zulu = "zu" + + var description: String { + switch self { + case .Afrikaans: return "Afrikaans" + case .Arabic: return "Arabic" + case .Azerbaijani: return "Azerbaijani" + case .Bengali: return "Bengali" + case .Catalan: return "Catalan" + case .Czech: return "Czech" + case .Welsh: return "Welsh" + case .Danish: return "Danish" + case .German: return "German" + case .Greek: return "Greek" + case .English: return "English" + case .Spanish: return "Spanish" + case .Persian: return "Persian" + case .Finnish: return "Finnish" + case .Filipino: return "Filipino" + case .French: return "French" + case .Irish: return "Irish" + case .Hebrew: return "Hebrew" + case .Hindi: return "Hindi" + case .Hungarian: return "Hungarian" + case .Indonesian: return "Indonesian" + case .Italian: return "Italian" + case .Japanese: return "Japanese" + case .Javanese: return "Javanese" + case .Korean: return "Korean" + case .Lithuanian: return "Lithuanian" + case .Malay: return "Malay" + case .Maltese: return "Maltese" + case .Dutch: return "Dutch" + case .Norwegian: return "Norwegian" + case .Polish: return "Polish" + case .Portuguese: return "Portuguese" + case .Romanian: return "Romanian" + case .Russian: return "Russian" + case .Slovak: return "Slovak" + case .Slovene: return "Slovene" + case .Swedish: return "Swedish" + case .Swahili: return "Swahili" + case .Thai: return "Thai" + case .Tagalog: return "Tagalog" + case .Turkish: return "Turkish" + case .Ukrainian: return "Ukrainian" + case .Urdu: return "Urdu" + case .Uzbek: return "Uzbek" + case .Vietnamese: return "Vietnamese" + case .Xhosa: return "Xhosa" + case .Chinese: return "Chinese" + case .Zulu: return "Zulu" + } + } + + static func languageName(for code: String) -> String { + return LanguageCodes(rawValue: code)?.description ?? "Unknown" + } +} diff --git a/Shared/Player/PlaybackSettings.swift b/Shared/Player/PlaybackSettings.swift index 66638b11..765314f8 100644 --- a/Shared/Player/PlaybackSettings.swift +++ b/Shared/Player/PlaybackSettings.swift @@ -1,3 +1,4 @@ +import Combine import Defaults import SwiftUI @@ -393,8 +394,10 @@ struct PlaybackSettings: View { } label: { HStack(spacing: 4) { Image(systemName: "text.bubble") - if let captions = player.captions { - Text(captions.code) + if let captions = player.captions, + let language = LanguageCodes(rawValue: captions.code) + { + Text("\(language.description.capitalized) (\(language.rawValue))") .foregroundColor(.accentColor) } } diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index eaa890f9..9c5e6c9f 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -30,12 +30,16 @@ struct PlayerSettings: View { @Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike + @Default(.showRelated) private var showRelated @Default(.showInspector) private var showInspector + @Default(.showChapters) private var showChapters @Default(.showChapterThumbnails) private var showThumbnails @Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent @Default(.expandChapters) private var expandChapters - @Default(.showRelated) private var showRelated + + @Default(.captionsAutoShow) private var captionsAutoShow + @Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode @ObservedObject private var accounts = AccountsModel.shared @@ -93,7 +97,14 @@ struct PlayerSettings: View { inspectorVisibilityPicker #endif } + #endif + Section(header: SettingsHeader(text: "Captions".localized())) { + showCaptionsAutoShowToggle + captionDefaultLanguagePicker + } + + #if !os(tvOS) Section(header: SettingsHeader(text: "Chapters".localized())) { showChaptersToggle showThumbnailsToggle @@ -290,6 +301,21 @@ struct PlayerSettings: View { #endif } + private var showCaptionsAutoShowToggle: some View { + Toggle("Always show captions", isOn: $captionsAutoShow) + } + + private var captionDefaultLanguagePicker: some View { + Picker("Default language", selection: $captionsDefaultLanguageCode) { + ForEach(LanguageCodes.allCases, id: \.self) { language in + Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue) + } + } + #if os(macOS) + .labelsHidden() + #endif + } + private var showChaptersToggle: some View { Toggle("Show chapters", isOn: $showChapters) } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 5691e4bb..dfcdc1d1 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -1076,6 +1076,9 @@ E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; }; E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; }; E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; }; + E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; }; + E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; }; + E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; }; FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; }; /* End PBXBuildFile section */ @@ -1547,6 +1550,7 @@ 3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatus.swift; sourceTree = ""; }; E258F3892BF61BD2005B8C28 /* URLTester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTester.swift; sourceTree = ""; }; + E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageCodes.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2295,6 +2299,7 @@ 37D2E0D328B67EFC00F64D52 /* Delay.swift */, 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */, + E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */, 375B537828DF6CBB004C1D19 /* Localizable.strings */, 3729037D2739E47400EA99F6 /* MenuCommands.swift */, 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */, @@ -3225,6 +3230,7 @@ 37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */, 37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */, + E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */, 377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */, 37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */, 377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */, @@ -3405,6 +3411,7 @@ 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */, 3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */, 375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */, + E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */, 37001564271B1F250049C794 /* AccountsModel.swift in Sources */, 378FFBC528660172009E3FBE /* URLParser.swift in Sources */, 3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, @@ -4000,6 +4007,7 @@ 37FB28432721B22200A57617 /* ContentItem.swift in Sources */, 37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */, 37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */, + E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */, 37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */, 37AAF2A226741C97007FC770 /* FeedView.swift in Sources */, 37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */, From c9125644ed166e5fa9b51b75020001b77b92b8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 20 May 2024 14:20:08 +0200 Subject: [PATCH 2/6] improvements to captions on tvOS --- Shared/Player/Controls/ControlsOverlay.swift | 260 ++++++++++--------- Shared/Player/PlaybackSettings.swift | 12 +- Shared/Settings/PlayerSettings.swift | 71 +++-- 3 files changed, 207 insertions(+), 136 deletions(-) diff --git a/Shared/Player/Controls/ControlsOverlay.swift b/Shared/Player/Controls/ControlsOverlay.swift index 6de44e8a..df0e00f6 100644 --- a/Shared/Player/Controls/ControlsOverlay.swift +++ b/Shared/Player/Controls/ControlsOverlay.swift @@ -11,16 +11,16 @@ struct ControlsOverlay: View { @Default(.qualityProfiles) private var qualityProfiles #if os(tvOS) - enum Field: Hashable { - case qualityProfile - case stream - case increaseRate - case decreaseRate - case captions - } + enum Field: Hashable { + case qualityProfile + case stream + case increaseRate + case decreaseRate + case captions + } - @FocusState private var focusedField: Field? - @State private var presentingButtonHintAlert = false + @FocusState private var focusedField: Field? + @State private var presentingButtonHintAlert = false #endif var body: some View { @@ -94,10 +94,10 @@ struct ControlsOverlay: View { #endif #if os(tvOS) - Text("Press and hold remote button to open captions and quality menus") - .frame(maxWidth: 400) - .font(.caption) - .foregroundColor(.secondary) + Text("Press and hold remote button to open captions and quality menus") + .frame(maxWidth: 400) + .font(.caption) + .foregroundColor(.secondary) #endif } .frame(maxHeight: overlayHeight) @@ -117,9 +117,9 @@ struct ControlsOverlay: View { private var overlayHeight: Double { #if os(tvOS) - contentSize.height + 80.0 + contentSize.height + 80.0 #else - contentSize.height + contentSize.height #endif } @@ -160,26 +160,26 @@ struct ControlsOverlay: View { @ViewBuilder private var rateButton: some View { #if os(macOS) - ratePicker - .labelsHidden() - .frame(maxWidth: 100) + ratePicker + .labelsHidden() + .frame(maxWidth: 100) #elseif os(iOS) - Menu { - ratePicker - } label: { - Text(player.rateLabel(player.currentRate)) - .foregroundColor(.primary) - .frame(width: 123) - } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.primary) - .frame(width: 123, height: 40) - .modifier(ControlBackgroundModifier()) - .mask(RoundedRectangle(cornerRadius: 3)) - #else + Menu { + ratePicker + } label: { Text(player.rateLabel(player.currentRate)) - .frame(minWidth: 120) + .foregroundColor(.primary) + .frame(width: 123) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .foregroundColor(.primary) + .frame(width: 123, height: 40) + .modifier(ControlBackgroundModifier()) + .mask(RoundedRectangle(cornerRadius: 3)) + #else + Text(player.rateLabel(player.currentRate)) + .frame(minWidth: 120) #endif } @@ -241,50 +241,50 @@ struct ControlsOverlay: View { private var rateButtonsSpacing: Double { #if os(tvOS) - 10 + 10 #else - 8 + 8 #endif } @ViewBuilder private var qualityProfileButton: some View { #if os(macOS) - qualityProfilePicker - .labelsHidden() - .frame(maxWidth: 300) + qualityProfilePicker + .labelsHidden() + .frame(maxWidth: 300) #elseif os(iOS) - Menu { - qualityProfilePicker - } label: { - Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) - .frame(maxWidth: 240) - } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.primary) - .frame(maxWidth: 240) - .frame(height: 40) - .modifier(ControlBackgroundModifier()) - .mask(RoundedRectangle(cornerRadius: 3)) + Menu { + qualityProfilePicker + } label: { + Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) + .frame(maxWidth: 240) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .foregroundColor(.primary) + .frame(maxWidth: 240) + .frame(height: 40) + .modifier(ControlBackgroundModifier()) + .mask(RoundedRectangle(cornerRadius: 3)) #else - ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) { - Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) - .lineLimit(1) - .frame(maxWidth: 320) - } - .contextMenu { - Button("Automatic") { player.qualityProfileSelection = nil } + ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) { + Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) + .lineLimit(1) + .frame(maxWidth: 320) + } + .contextMenu { + Button("Automatic") { player.qualityProfileSelection = nil } - ForEach(qualityProfiles) { qualityProfile in - Button { - player.qualityProfileSelection = qualityProfile - } label: { - Text(qualityProfile.description) - } - - Button("Cancel", role: .cancel) {} + ForEach(qualityProfiles) { qualityProfile in + Button { + player.qualityProfileSelection = qualityProfile + } label: { + Text(qualityProfile.description) } + + Button("Cancel", role: .cancel) {} } + } #endif } @@ -300,71 +300,91 @@ struct ControlsOverlay: View { @ViewBuilder private var qualityButton: some View { #if os(macOS) - StreamControl() - .labelsHidden() - .frame(maxWidth: 300) + StreamControl() + .labelsHidden() + .frame(maxWidth: 300) #elseif os(iOS) - Menu { - StreamControl() - } label: { - Text(player.streamSelection?.resolutionAndFormat ?? "loading") - .frame(width: 140, height: 40) - .foregroundColor(.primary) - } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.primary) - .frame(width: 240, height: 40) - .modifier(ControlBackgroundModifier()) - .mask(RoundedRectangle(cornerRadius: 3)) + Menu { + StreamControl() + } label: { + Text(player.streamSelection?.resolutionAndFormat ?? "loading") + .frame(width: 140, height: 40) + .foregroundColor(.primary) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .foregroundColor(.primary) + .frame(width: 240, height: 40) + .modifier(ControlBackgroundModifier()) + .mask(RoundedRectangle(cornerRadius: 3)) #else - StreamControl(focusedField: $focusedField) + StreamControl(focusedField: $focusedField) #endif } @ViewBuilder private var captionsButton: some View { #if os(macOS) - captionsPicker - .labelsHidden() - .frame(maxWidth: 300) + captionsPicker + .labelsHidden() + .frame(maxWidth: 300) #elseif os(iOS) - Menu { - captionsPicker - } label: { - HStack(spacing: 4) { - Image(systemName: "text.bubble") - if let captions = captionsBinding.wrappedValue { - Text(captions.code) - .foregroundColor(.primary) - } - } - .frame(width: 240) - .frame(height: 40) - } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.primary) - .frame(width: 240) - .modifier(ControlBackgroundModifier()) - .mask(RoundedRectangle(cornerRadius: 3)) - #else - ControlsOverlayButton(focusedField: $focusedField, field: .captions) { - HStack(spacing: 8) { - Image(systemName: "text.bubble") - if let captions = captionsBinding.wrappedValue { - Text(captions.code) - } - } - .frame(maxWidth: 320) - } - .contextMenu { - Button("Disabled") { captionsBinding.wrappedValue = nil } + Menu { + captionsPicker + } label: { + HStack(spacing: 4) { + Image(systemName: "text.bubble") + if let captions = captionsBinding.wrappedValue, + let language = LanguageCodes(rawValue: captions.code) - ForEach(player.currentVideo?.captions ?? []) { caption in - Button(caption.description) { captionsBinding.wrappedValue = caption } + { + Text("\(language.description.capitalized) (\(language.rawValue))") + .foregroundColor(.accentColor) + } else { + if captionsBinding.wrappedValue == nil { + Text("Not available") + } else { + Text("Disabled") + .foregroundColor(.accentColor) + } } - Button("Cancel", role: .cancel) {} } + .frame(width: 240) + .frame(height: 40) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .foregroundColor(.primary) + .frame(width: 240) + .modifier(ControlBackgroundModifier()) + .mask(RoundedRectangle(cornerRadius: 3)) + #else + ControlsOverlayButton(focusedField: $focusedField, field: .captions) { + HStack(spacing: 8) { + Image(systemName: "text.bubble") + if let captions = captionsBinding.wrappedValue, + let language = LanguageCodes(rawValue: captions.code) + { + Text("\(language.description.capitalized) (\(language.rawValue))") + .foregroundColor(.accentColor) + } else { + if captionsBinding.wrappedValue == nil { + Text("Not available") + } else { + Text("Disabled") + .foregroundColor(.accentColor) + } + } + } + .frame(maxWidth: 320) + } + .contextMenu { + Button("Disabled") { captionsBinding.wrappedValue = nil } + + ForEach(player.currentVideo?.captions ?? []) { caption in + Button(caption.description) { captionsBinding.wrappedValue = caption } + } + Button("Cancel", role: .cancel) {} + } #endif } diff --git a/Shared/Player/PlaybackSettings.swift b/Shared/Player/PlaybackSettings.swift index 765314f8..c9861235 100644 --- a/Shared/Player/PlaybackSettings.swift +++ b/Shared/Player/PlaybackSettings.swift @@ -384,13 +384,16 @@ struct PlaybackSettings: View { } @ViewBuilder private var captionsButton: some View { + let videoCaptions = player.currentVideo?.captions #if os(macOS) captionsPicker .labelsHidden() .frame(maxWidth: 300) #elseif os(iOS) Menu { - captionsPicker + if videoCaptions?.isEmpty == false { + captionsPicker + } } label: { HStack(spacing: 4) { Image(systemName: "text.bubble") @@ -399,10 +402,17 @@ struct PlaybackSettings: View { { Text("\(language.description.capitalized) (\(language.rawValue))") .foregroundColor(.accentColor) + } else { + if videoCaptions?.isEmpty == true { + Text("Not available") + } else { + Text("Disabled") + } } } .frame(alignment: .trailing) .frame(height: 40) + .disabled(videoCaptions?.isEmpty == true) } .transaction { t in t.animation = .none } .buttonStyle(.plain) diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 9c5e6c9f..4329a862 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -49,6 +49,10 @@ struct PlayerSettings: View { } #endif + #if os(tvOS) + @State private var isShowingLanguagePicker = false + #endif + var body: some View { Group { #if os(macOS) @@ -101,7 +105,23 @@ struct PlayerSettings: View { Section(header: SettingsHeader(text: "Captions".localized())) { showCaptionsAutoShowToggle - captionDefaultLanguagePicker + #if !os(tvOS) + captionDefaultLanguagePicker + #else + Button(action: { isShowingLanguagePicker = true }) { + HStack { + Text("Default language") + Spacer() + Text("\(LanguageCodes(rawValue: captionsDefaultLanguageCode)!.description.capitalized) (\(captionsDefaultLanguageCode))").foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity).sheet(isPresented: $isShowingLanguagePicker) { + LanguagePickerTVOS( + selectedLanguage: $captionsDefaultLanguageCode, + isShowing: $isShowingLanguagePicker + ) + } + #endif } #if !os(tvOS) @@ -290,21 +310,11 @@ struct PlayerSettings: View { } #endif + private var showCaptionsAutoShowToggle: some View { + Toggle("Always show captions", isOn: $captionsAutoShow) + } + #if !os(tvOS) - private var inspectorVisibilityPicker: some View { - Picker("Inspector", selection: $showInspector) { - Text("Always").tag(ShowInspectorSetting.always) - Text("Only for local files and URLs").tag(ShowInspectorSetting.onlyLocal) - } - #if os(macOS) - .labelsHidden() - #endif - } - - private var showCaptionsAutoShowToggle: some View { - Toggle("Always show captions", isOn: $captionsAutoShow) - } - private var captionDefaultLanguagePicker: some View { Picker("Default language", selection: $captionsDefaultLanguageCode) { ForEach(LanguageCodes.allCases, id: \.self) { language in @@ -315,6 +325,37 @@ struct PlayerSettings: View { .labelsHidden() #endif } + #else + struct LanguagePickerTVOS: View { + @Binding var selectedLanguage: String + @Binding var isShowing: Bool + + var body: some View { + NavigationView { + List(LanguageCodes.allCases, id: \.self) { language in + Button(action: { + selectedLanguage = language.rawValue + isShowing = false + }) { + Text("\(language.description.capitalized) (\(language.rawValue))") + } + } + .navigationTitle("Select Default Language") + } + } + } + #endif + + #if !os(tvOS) + private var inspectorVisibilityPicker: some View { + Picker("Inspector", selection: $showInspector) { + Text("Always").tag(ShowInspectorSetting.always) + Text("Only for local files and URLs").tag(ShowInspectorSetting.onlyLocal) + } + #if os(macOS) + .labelsHidden() + #endif + } private var showChaptersToggle: some View { Toggle("Show chapters", isOn: $showChapters) From 4fa5a15ad457fe138d2c6d588ee64b6fc724187a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 20 May 2024 14:40:25 +0200 Subject: [PATCH 3/6] fallback language for captions --- Model/Player/Backends/MPVBackend.swift | 15 +++++-- Shared/Defaults.swift | 1 + Shared/Settings/PlayerSettings.swift | 59 +++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index a77806c6..3fd47092 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -219,10 +219,17 @@ final class MPVBackend: PlayerBackend { var captions: Captions? if Defaults[.captionsAutoShow] == true { - let captionsLanguageCode = Defaults[.captionsDefaultLanguageCode] - if !captionsLanguageCode.isEmpty { - captions = video.captions.first { $0.code == captionsLanguageCode } ?? - video.captions.first { $0.code.contains(captionsLanguageCode) } + let captionsDefaultLanguageCode = Defaults[.captionsDefaultLanguageCode], + captionsFallbackLanguageCode = Defaults[.captionsFallbackLanguageCode] + + // Try to get captions with the default language code first + captions = video.captions.first { $0.code == captionsDefaultLanguageCode } ?? + video.captions.first { $0.code.contains(captionsDefaultLanguageCode) } + + // If there are still no captions, try to get captions with the fallback language code + if captions.isNil && !captionsFallbackLanguageCode.isEmpty { + captions = video.captions.first { $0.code == captionsFallbackLanguageCode } ?? + video.captions.first { $0.code.contains(captionsFallbackLanguageCode) } } } else { captions = nil diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 0529ad27..821c67d4 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -304,6 +304,7 @@ extension Defaults.Keys { static let captionsAutoShow = Key("captionsAutoShow", default: false) static let captionsLanguageCode = Key("captionsLanguageCode") static let captionsDefaultLanguageCode = Key("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) + static let captionsFallbackLanguageCode = Key("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue) static let lastUsedPlaylistID = Key("lastPlaylistID") static let lastAccountIsPublic = Key("lastAccountIsPublic", default: false) diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 4329a862..55e3cc5e 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -40,6 +40,7 @@ struct PlayerSettings: View { @Default(.captionsAutoShow) private var captionsAutoShow @Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode + @Default(.captionsFallbackLanguageCode) private var captionsFallbackLanguageCode @ObservedObject private var accounts = AccountsModel.shared @@ -50,7 +51,8 @@ struct PlayerSettings: View { #endif #if os(tvOS) - @State private var isShowingLanguagePicker = false + @State private var isShowingDefaultLanguagePicker = false + @State private var isShowingFallbackLanguagePicker = false #endif var body: some View { @@ -107,18 +109,33 @@ struct PlayerSettings: View { showCaptionsAutoShowToggle #if !os(tvOS) captionDefaultLanguagePicker + captionFallbackLanguagePicker #else - Button(action: { isShowingLanguagePicker = true }) { + Button(action: { isShowingDefaultLanguagePicker = true }) { HStack { Text("Default language") Spacer() Text("\(LanguageCodes(rawValue: captionsDefaultLanguageCode)!.description.capitalized) (\(captionsDefaultLanguageCode))").foregroundColor(.secondary) } } - .frame(maxWidth: .infinity).sheet(isPresented: $isShowingLanguagePicker) { - LanguagePickerTVOS( + .frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) { + defaultLanguagePickerTVOS( selectedLanguage: $captionsDefaultLanguageCode, - isShowing: $isShowingLanguagePicker + isShowing: $isShowingDefaultLanguagePicker + ) + } + + Button(action: { isShowingFallbackLanguagePicker = true }) { + HStack { + Text("Fallback language") + Spacer() + Text("\(LanguageCodes(rawValue: captionsFallbackLanguageCode)!.description.capitalized) (\(captionsFallbackLanguageCode))").foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) { + fallbackLanguagePickerTVOS( + selectedLanguage: $captionsFallbackLanguageCode, + isShowing: $isShowingFallbackLanguagePicker ) } #endif @@ -325,8 +342,19 @@ struct PlayerSettings: View { .labelsHidden() #endif } + + private var captionFallbackLanguagePicker: some View { + Picker("Fallback language", selection: $captionsFallbackLanguageCode) { + ForEach(LanguageCodes.allCases, id: \.self) { language in + Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue) + } + } + #if os(macOS) + .labelsHidden() + #endif + } #else - struct LanguagePickerTVOS: View { + struct defaultLanguagePickerTVOS: View { @Binding var selectedLanguage: String @Binding var isShowing: Bool @@ -344,6 +372,25 @@ struct PlayerSettings: View { } } } + + struct fallbackLanguagePickerTVOS: View { + @Binding var selectedLanguage: String + @Binding var isShowing: Bool + + var body: some View { + NavigationView { + List(LanguageCodes.allCases, id: \.self) { language in + Button(action: { + selectedLanguage = language.rawValue + isShowing = false + }) { + Text("\(language.description.capitalized) (\(language.rawValue))") + } + } + .navigationTitle("Select Fallback Language") + } + } + } #endif #if !os(tvOS) From 96df7fdec5179439429cfd6a98d68c2e3959ea24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 20 May 2024 15:34:31 +0200 Subject: [PATCH 4/6] let the user select caption size --- Model/Player/Backends/MPVClient.swift | 5 +++++ Shared/Defaults.swift | 1 + Shared/Settings/PlayerSettings.swift | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index 11066345..f7cbeffc 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -68,6 +68,7 @@ final class MPVClient: ObservableObject { checkError(mpv_set_option_string(mpv, "vo", "libmpv")) checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1")) checkError(mpv_set_option_string(mpv, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no")) + checkError(mpv_set_option_string(mpv, "sub-scale", "\(Defaults[.captionsFontScaleSize])")) checkError(mpv_initialize(mpv)) @@ -413,6 +414,10 @@ final class MPVClient: ObservableObject { setString("sub", "no") } + func setSubFontSize() { + setString("sub-scale", "\(Defaults[.captionsFontScaleSize])") + } + var tracksCount: Int { Int(getString("track-list/count") ?? "-1") ?? -1 } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 821c67d4..90b6f000 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -305,6 +305,7 @@ extension Defaults.Keys { static let captionsLanguageCode = Key("captionsLanguageCode") static let captionsDefaultLanguageCode = Key("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) static let captionsFallbackLanguageCode = Key("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue) + static let captionsFontScaleSize = Key("captionsFontScale", default: 1.0) static let lastUsedPlaylistID = Key("lastPlaylistID") static let lastAccountIsPublic = Key("lastAccountIsPublic", default: false) diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 55e3cc5e..4a1f9910 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -41,6 +41,7 @@ struct PlayerSettings: View { @Default(.captionsAutoShow) private var captionsAutoShow @Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode @Default(.captionsFallbackLanguageCode) private var captionsFallbackLanguageCode + @Default(.captionsFontScaleSize) private var captionsFontScaleSize @ObservedObject private var accounts = AccountsModel.shared @@ -106,7 +107,12 @@ struct PlayerSettings: View { #endif Section(header: SettingsHeader(text: "Captions".localized())) { + #if os(tvOS) + Text("Size").font(.subheadline) + #endif + captionsFontScaleSizePicker showCaptionsAutoShowToggle + #if !os(tvOS) captionDefaultLanguagePicker captionFallbackLanguagePicker @@ -331,6 +337,20 @@ struct PlayerSettings: View { Toggle("Always show captions", isOn: $captionsAutoShow) } + private var captionsFontScaleSizePicker: some View { + Picker("Size", selection: $captionsFontScaleSize) { + Text("Small").tag(Float(0.5)) + Text("Medium").tag(Float(1.0)) + Text("Large").tag(Float(2.0)) + } + .onChange(of: captionsFontScaleSize) { _ in + PlayerModel.shared.mpvBackend.client.setSubFontSize() + } + #if os(macOS) + .labelsHidden() + #endif + } + #if !os(tvOS) private var captionDefaultLanguagePicker: some View { Picker("Default language", selection: $captionsDefaultLanguageCode) { From 108b4de4830dc9d2910720cd07cdb1f9b0898c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 20 May 2024 17:17:52 +0200 Subject: [PATCH 5/6] allow user to choose captions color --- Model/Player/Backends/MPVClient.swift | 11 +++++++--- Shared/Defaults.swift | 3 ++- Shared/Settings/PlayerSettings.swift | 30 +++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index f7cbeffc..dd63590d 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -68,7 +68,8 @@ final class MPVClient: ObservableObject { checkError(mpv_set_option_string(mpv, "vo", "libmpv")) checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1")) checkError(mpv_set_option_string(mpv, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no")) - checkError(mpv_set_option_string(mpv, "sub-scale", "\(Defaults[.captionsFontScaleSize])")) + checkError(mpv_set_option_string(mpv, "sub-scale", Defaults[.captionsFontScaleSize])) + checkError(mpv_set_option_string(mpv, "sub-color", Defaults[.captionsFontColor])) checkError(mpv_initialize(mpv)) @@ -414,8 +415,12 @@ final class MPVClient: ObservableObject { setString("sub", "no") } - func setSubFontSize() { - setString("sub-scale", "\(Defaults[.captionsFontScaleSize])") + func setSubFontSize(scaleSize: String) { + setString("sub-scale", scaleSize) + } + + func setSubFontColor(color: String) { + setString("sub-color", color) } var tracksCount: Int { diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 90b6f000..971ac876 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -305,7 +305,8 @@ extension Defaults.Keys { static let captionsLanguageCode = Key("captionsLanguageCode") static let captionsDefaultLanguageCode = Key("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) static let captionsFallbackLanguageCode = Key("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue) - static let captionsFontScaleSize = Key("captionsFontScale", default: 1.0) + static let captionsFontScaleSize = Key("captionsFontScale", default: "1.0") + static let captionsFontColor = Key("captionsFontColor", default: "#FFFFFF") static let lastUsedPlaylistID = Key("lastPlaylistID") static let lastAccountIsPublic = Key("lastAccountIsPublic", default: false) diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 4a1f9910..9c1aefdc 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -42,6 +42,7 @@ struct PlayerSettings: View { @Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode @Default(.captionsFallbackLanguageCode) private var captionsFallbackLanguageCode @Default(.captionsFontScaleSize) private var captionsFontScaleSize + @Default(.captionsFontColor) private var captionsFontColor @ObservedObject private var accounts = AccountsModel.shared @@ -111,6 +112,10 @@ struct PlayerSettings: View { Text("Size").font(.subheadline) #endif captionsFontScaleSizePicker + #if os(tvOS) + Text("Color").font(.subheadline) + #endif + captionsFontColorPicker showCaptionsAutoShowToggle #if !os(tvOS) @@ -339,12 +344,29 @@ struct PlayerSettings: View { private var captionsFontScaleSizePicker: some View { Picker("Size", selection: $captionsFontScaleSize) { - Text("Small").tag(Float(0.5)) - Text("Medium").tag(Float(1.0)) - Text("Large").tag(Float(2.0)) + Text("Small").tag(String("0.5")) + Text("Medium").tag(String("1.0")) + Text("Large").tag(String("2.0")) } .onChange(of: captionsFontScaleSize) { _ in - PlayerModel.shared.mpvBackend.client.setSubFontSize() + PlayerModel.shared.mpvBackend.client.setSubFontSize(scaleSize: captionsFontScaleSize) + } + #if os(macOS) + .labelsHidden() + #endif + } + + private var captionsFontColorPicker: some View { + Picker("Color", selection: $captionsFontColor) { + Text("White").tag(String("#FFFFFF")) + Text("Yellow").tag(String("#FFFF00")) + Text("Red").tag(String("#FF0000")) + Text("Orange").tag(String("#FFA500")) + Text("Green").tag(String("#008000")) + Text("Blue").tag(String("#0000FF")) + } + .onChange(of: captionsFontColor) { _ in + PlayerModel.shared.mpvBackend.client.setSubFontColor(color: captionsFontColor) } #if os(macOS) .labelsHidden() From 15ce82a686a0289289d1f91927e7e8676e472361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 20 May 2024 21:53:48 +0200 Subject: [PATCH 6/6] added en-GB to LanguageCodes --- Shared/LanguageCodes.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Shared/LanguageCodes.swift b/Shared/LanguageCodes.swift index 2a271995..74fd00e5 100644 --- a/Shared/LanguageCodes.swift +++ b/Shared/LanguageCodes.swift @@ -10,6 +10,7 @@ enum LanguageCodes: String, CaseIterable { case German = "de" case Greek = "el" case English = "en" + case English_GB = "en-GB" case Spanish = "es" case Persian = "fa" case Finnish = "fi" @@ -61,6 +62,7 @@ enum LanguageCodes: String, CaseIterable { case .German: return "German" case .Greek: return "Greek" case .English: return "English" + case .English_GB: return "English (United Kingdom)" case .Spanish: return "Spanish" case .Persian: return "Persian" case .Finnish: return "Finnish"