From fa09b2021ca85c20f7367fc0ba673ab2ae81a18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sun, 19 May 2024 12:39:47 +0200 Subject: [PATCH 01/12] speed up sorting for Stream This should help to start playback a bit faster. --- Model/Player/Backends/PlayerBackend.swift | 23 +++++++++++------------ Model/Player/PlayerStreams.swift | 8 +++++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 980e9ab5..18c6ca3d 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -133,23 +133,22 @@ extension PlayerBackend { } func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? { - // filter out non HLS streams - let nonHLSStreams = streams.filter { $0.kind != .hls } + // filter out non-HLS streams and streams with resolution more than maxResolution + let nonHLSStreams = streams.filter { + $0.kind != .hls && $0.resolution <= maxResolution.value + } - // find max resolution from non HLS streams - let bestResolution = nonHLSStreams - .filter { $0.resolution <= maxResolution.value } - .max { $0.resolution < $1.resolution } + // find max resolution and bitrate from non-HLS streams + let bestResolutionStream = nonHLSStreams.max { $0.resolution < $1.resolution } + let bestBitrateStream = nonHLSStreams.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 } - // finde max bitrate from non HLS streams - let bestBitrate = nonHLSStreams - .filter { $0.resolution <= maxResolution.value } - .max { $0.bitrate ?? 0 < $1.bitrate ?? 0 } + let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value + let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate return streams.map { stream in if stream.kind == .hls { - stream.resolution = bestResolution?.resolution ?? maxResolution.value - stream.bitrate = bestBitrate?.bitrate ?? (bestResolution?.resolution.bitrate ?? maxResolution.value.bitrate) + stream.resolution = bestResolution + stream.bitrate = bestBitrate stream.format = .hls } else if stream.kind == .stream { stream.format = .stream diff --git a/Model/Player/PlayerStreams.swift b/Model/Player/PlayerStreams.swift index abc3a861..4fc91740 100644 --- a/Model/Player/PlayerStreams.swift +++ b/Model/Player/PlayerStreams.swift @@ -196,11 +196,13 @@ extension PlayerModel { } } - func streamsSorter(_ lhs: Stream, _ rhs: Stream) -> Bool { - if lhs.resolution.isNil || rhs.resolution.isNil { + func streamsSorter(lhs: Stream, rhs: Stream) -> Bool { + // Use optional chaining to simplify nil handling + guard let lhsRes = lhs.resolution?.height, let rhsRes = rhs.resolution?.height else { return lhs.kind < rhs.kind } - return lhs.kind == rhs.kind ? (lhs.resolution.height > rhs.resolution.height) : (lhs.kind < rhs.kind) + // Compare either kind or resolution based on conditions + return lhs.kind == rhs.kind ? (lhsRes > rhsRes) : (lhs.kind < rhs.kind) } } From c9fb41c8e85bec148b27c456b65ee5968de8e115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sun, 19 May 2024 17:43:35 +0200 Subject: [PATCH 02/12] faster chapter extraction The extraction of chapters is now faster since it is run in parallel for each pattern. Also a new pattern hast been added: "(start) title" --- Model/Applications/VideosAPI.swift | 112 +++++++++++++++++++---------- 1 file changed, 74 insertions(+), 38 deletions(-) diff --git a/Model/Applications/VideosAPI.swift b/Model/Applications/VideosAPI.swift index 31f8705c..a7614956 100644 --- a/Model/Applications/VideosAPI.swift +++ b/Model/Applications/VideosAPI.swift @@ -152,58 +152,94 @@ extension VideosAPI { /* The following chapter patterns are covered: - start - end - title / start - end: Title / start - end title - start - title / start: title / start title / [start] - title / [start]: title / [start] title - index. title - start / index. title start - title: (start) + 1) "start - end - title" / "start - end: Title" / "start - end title" + 2) "start - title" / "start: title" / "start title" / "[start] - title" / "[start]: title" / "[start] title" + 3) "index. title - start" / "index. title start" + 4) "title: (start)" + 5) "(start) title" - The order is important! + These represent: + + - "start" and "end" are timestamps, defining the start and end of the individual chapter + - "title" is the name of the chapter + - "index" is the chapter's position in a list + + The order of these patterns is important as it determines the priority. The patterns listed first have a higher priority. + In the case of multiple matches, the pattern with the highest priority will be chosen - lower number means higher priority. */ let patterns = [ "(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?(?:[0-9]+:){1,2}[0-9]+)\\]?(?:\\s*-\\s*)?(?(?:[0-9]+:){1,2}[0-9]+)?(?:\\s*-\\s*|\\s*[:]\\s*)?(?.*)(?=\\n|$)", "(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?\\s*[-:]?\\s*(?<title>.+)(?=\\n|$)", "(?<=\\n|^)(?<index>[0-9]+\\.\\s)(?<title>.+?)(?:\\s*-\\s*)?(?<start>(?:[0-9]+:){1,2}[0-9]+)(?=\\n|$)", - "(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)" + "(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)", + "(?<=^|\\n)\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)\\s*(?<title>.+?)(?=\\n|$)" ] - for pattern in patterns { - guard let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { continue } - let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description)) + let extractChaptersGroup = DispatchGroup() + var capturedChapters: [Int: [Chapter]] = [:] + let lock = NSLock() - if !chapterLines.isEmpty { - return chapterLines.compactMap { line in - let titleRange = line.range(withName: "title") - let startRange = line.range(withName: "start") - guard let titleSubstringRange = Range(titleRange, in: description), - let startSubstringRange = Range(startRange, in: description) - else { - return nil - } - let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces) - let startCapture = String(description[startSubstringRange]) - let startComponents = startCapture.components(separatedBy: ":") - guard startComponents.count <= 3 else { return nil } + for (index, pattern) in patterns.enumerated() { + extractChaptersGroup.enter() + DispatchQueue.global().async { + if let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) { + let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description)) + let extractedChapters = chapterLines.compactMap { line -> Chapter? in + let titleRange = line.range(withName: "title") + let startRange = line.range(withName: "start") - var hours: Double? - var minutes: Double? - var seconds: Double? + guard let titleSubstringRange = Range(titleRange, in: description), + let startSubstringRange = Range(startRange, in: description) + else { + return nil + } - if startComponents.count == 3 { - hours = Double(startComponents[0]) - minutes = Double(startComponents[1]) - seconds = Double(startComponents[2]) - } else if startComponents.count == 2 { - minutes = Double(startComponents[0]) - seconds = Double(startComponents[1]) + let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces) + let startCapture = String(description[startSubstringRange]) + let startComponents = startCapture.components(separatedBy: ":") + guard startComponents.count <= 3 else { return nil } + + var hours: Double? + var minutes: Double? + var seconds: Double? + + if startComponents.count == 3 { + hours = Double(startComponents[0]) + minutes = Double(startComponents[1]) + seconds = Double(startComponents[2]) + } else if startComponents.count == 2 { + minutes = Double(startComponents[0]) + seconds = Double(startComponents[1]) + } + + guard var startSeconds = seconds else { return nil } + + startSeconds += (minutes ?? 0) * 60 + startSeconds += (hours ?? 0) * 60 * 60 + + return Chapter(title: titleCapture, start: startSeconds) } - guard var startSeconds = seconds else { return nil } - - startSeconds += (minutes ?? 0) * 60 - startSeconds += (hours ?? 0) * 60 * 60 - - return .init(title: titleCapture, start: startSeconds) + if !extractedChapters.isEmpty { + lock.lock() + capturedChapters[index] = extractedChapters + lock.unlock() + } } + extractChaptersGroup.leave() + } + } + + extractChaptersGroup.wait() + + // Now we sort the keys of the capturedChapters dictionary. + // These keys correspond to the priority of each pattern. + let sortedKeys = Array(capturedChapters.keys).sorted(by: <) + + // Return first non-empty result in the order of patterns + for key in sortedKeys { + if let chapters = capturedChapters[key], !chapters.isEmpty { + return chapters } } return [] From c7908d08ae11e288700c95b24825eb1a9825ee69 Mon Sep 17 00:00:00 2001 From: joaooliva <joaooliva@protonmail.com> Date: Sat, 18 May 2024 20:26:54 +0000 Subject: [PATCH 03/12] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (562 of 562 strings) Translation: Yattee/Localizable.strings Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/ --- Shared/pt-BR.lproj/Localizable.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/pt-BR.lproj/Localizable.strings b/Shared/pt-BR.lproj/Localizable.strings index 731cdfb1..4e1ecb2d 100644 --- a/Shared/pt-BR.lproj/Localizable.strings +++ b/Shared/pt-BR.lproj/Localizable.strings @@ -406,7 +406,7 @@ "Country" = "País"; "Clear All" = "Limpar Tudo"; "Clear All Recents" = "Limpar Todos os Recentes"; -"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).\n"; +"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo)."; "Duration" = "Duração"; "Edit Quality Profile" = "Editar Perfil de Qualidade"; "Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussões acontecem no Discord e no Matrix. É um bom lugar para perguntas gerais."; From 9c5f066e555b2e5858e1336b1298fe1c8e92f962 Mon Sep 17 00:00:00 2001 From: Mohammed Al Otaibi <mopes.03.belle@icloud.com> Date: Sun, 19 May 2024 14:11:53 +0000 Subject: [PATCH 04/12] Translated using Weblate (Arabic) Currently translated at 100.0% (562 of 562 strings) Translation: Yattee/Localizable.strings Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/ --- Shared/ar.lproj/Localizable.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Shared/ar.lproj/Localizable.strings b/Shared/ar.lproj/Localizable.strings index 3d010e2a..019185f9 100644 --- a/Shared/ar.lproj/Localizable.strings +++ b/Shared/ar.lproj/Localizable.strings @@ -387,7 +387,7 @@ "Backend" = "الواجهة الخلفية"; "Badge" = "الشارة"; "Close PiP when starting playing other video" = "غلق الفيديو المصغر عند بدء تشغيل فيديو آخر"; -"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).\n"; +"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو)."; "Filter" = "‏ عامل التصفية"; "Frontend URL" = "عنوان URL للواجهة الأمامية"; "Fullscreen size" = "حجم ملء الشاشة"; @@ -628,4 +628,4 @@ "Password required to import" = "كلمة المرور مطلوبة للإستيراد"; "Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد"; "Export in progress..." = "جارِ التصدير..."; -"In progress..." = "في تَقَدم…"; +"In progress..." = "في طور الأجراء…"; From 4db02b2638cd839788fdd5fedc45b240564b2e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= <toni.foerster@gmail.com> Date: Mon, 20 May 2024 02:49:32 +0200 Subject: [PATCH 05/12] 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<PlayerQueueItem?>("lastPlayed") static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv) + static let captionsAutoShow = Key<Bool>("captionsAutoShow", default: false) static let captionsLanguageCode = Key<String?>("captionsLanguageCode") + static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID") static let lastAccountIsPublic = Key<Bool>("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 = "<group>"; }; E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatus.swift; sourceTree = "<group>"; }; E258F3892BF61BD2005B8C28 /* URLTester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTester.swift; sourceTree = "<group>"; }; + E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageCodes.swift; sourceTree = "<group>"; }; /* 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?= <toni.foerster@gmail.com> Date: Mon, 20 May 2024 14:20:08 +0200 Subject: [PATCH 06/12] 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?= <toni.foerster@gmail.com> Date: Mon, 20 May 2024 14:40:25 +0200 Subject: [PATCH 07/12] 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<Bool>("captionsAutoShow", default: false) static let captionsLanguageCode = Key<String?>("captionsLanguageCode") static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) + static let captionsFallbackLanguageCode = Key<String>("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue) static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID") static let lastAccountIsPublic = Key<Bool>("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?= <toni.foerster@gmail.com> Date: Mon, 20 May 2024 15:34:31 +0200 Subject: [PATCH 08/12] 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<String?>("captionsLanguageCode") static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) static let captionsFallbackLanguageCode = Key<String>("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue) + static let captionsFontScaleSize = Key<Float>("captionsFontScale", default: 1.0) static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID") static let lastAccountIsPublic = Key<Bool>("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?= <toni.foerster@gmail.com> Date: Mon, 20 May 2024 17:17:52 +0200 Subject: [PATCH 09/12] 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<String?>("captionsLanguageCode") static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue) static let captionsFallbackLanguageCode = Key<String>("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue) - static let captionsFontScaleSize = Key<Float>("captionsFontScale", default: 1.0) + static let captionsFontScaleSize = Key<String>("captionsFontScale", default: "1.0") + static let captionsFontColor = Key<String>("captionsFontColor", default: "#FFFFFF") static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID") static let lastAccountIsPublic = Key<Bool>("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 7e3e393c6531776ae525e350bbcd04dd5b447eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= <toni.foerster@gmail.com> Date: Mon, 20 May 2024 20:11:41 +0200 Subject: [PATCH 10/12] Invidious: add images to chapters Invidious, by design, has no images attached to chapters, in contrast to Piped. Since the majority of videos with chapters don't have chapter-specific images and only use the videos' thumbnail, there is no difference here when compared to Piped's native thumbnail support. --- Model/Applications/InvidiousAPI.swift | 18 +++++++++++++++++- Shared/Player/Video Details/ChapterView.swift | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index 6d23a3f4..d0a94b41 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -502,7 +502,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { keywords: json["keywords"].arrayValue.compactMap { $0.string }, streams: extractStreams(from: json), related: extractRelated(from: json), - chapters: extractChapters(from: description), + chapters: createChapters(from: description, thumbnails: json), captions: extractCaptions(from: json) ) } @@ -575,6 +575,22 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { } } + private func createChapters(from description: String, thumbnails: JSON) -> [Chapter] { + var chapters = extractChapters(from: description) + + if !chapters.isEmpty { + let thumbnailsData = extractThumbnails(from: thumbnails) + let thumbnailURL = thumbnailsData.first { $0.quality == .medium }?.url + + for chapter in chapters.indices { + if let url = thumbnailURL { + chapters[chapter].image = url + } + } + } + return chapters + } + private static var contentItemsKeys = ["items", "videos", "latestVideos", "playlists", "relatedChannels"] private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage { diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index 695807d4..f4b9c831 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -65,7 +65,7 @@ import SwiftUI } static var thumbnailHeight: Double { - thumbnailWidth / 1.7777 + thumbnailWidth / (16 / 9) } } From 15ce82a686a0289289d1f91927e7e8676e472361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= <toni.foerster@gmail.com> Date: Mon, 20 May 2024 21:53:48 +0200 Subject: [PATCH 11/12] 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" From 6b5ecbdd8ba912d1ca7483ee7a7ee3632afd7077 Mon Sep 17 00:00:00 2001 From: Hiren Patel <heyhiren@gmail.com> Date: Tue, 21 May 2024 22:58:14 -0400 Subject: [PATCH 12/12] - Fix thumbnails failing to load on tvOS Thumbnails fail to load on tvOS when using SDImageAWebPCoder. Use SDImageWebPCoder on tvOS. --- Shared/YatteeApp.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 4adfbd16..7924f0a4 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -154,7 +154,12 @@ struct YatteeApp: App { #if DEBUG SiestaLog.Category.enabled = .common #endif - SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared) + #if os(tvOS) + SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared) + #else + SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared) + #endif + SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app") if !Defaults[.lastAccountIsPublic] {