mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Merge branch 'main' into add-user-agent-to-header
This commit is contained in:
commit
ae9aa6fac7
@ -502,7 +502,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
keywords: json["keywords"].arrayValue.compactMap { $0.string },
|
keywords: json["keywords"].arrayValue.compactMap { $0.string },
|
||||||
streams: extractStreams(from: json),
|
streams: extractStreams(from: json),
|
||||||
related: extractRelated(from: json),
|
related: extractRelated(from: json),
|
||||||
chapters: extractChapters(from: description),
|
chapters: createChapters(from: description, thumbnails: json),
|
||||||
captions: extractCaptions(from: 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 static var contentItemsKeys = ["items", "videos", "latestVideos", "playlists", "relatedChannels"]
|
||||||
|
|
||||||
private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage {
|
private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage {
|
||||||
|
@ -152,58 +152,94 @@ extension VideosAPI {
|
|||||||
/*
|
/*
|
||||||
The following chapter patterns are covered:
|
The following chapter patterns are covered:
|
||||||
|
|
||||||
start - end - title / start - end: Title / start - end title
|
1) "start - end - title" / "start - end: Title" / "start - end title"
|
||||||
start - title / start: title / start title / [start] - title / [start]: title / [start] title
|
2) "start - title" / "start: title" / "start title" / "[start] - title" / "[start]: title" / "[start] title"
|
||||||
index. title - start / index. title start
|
3) "index. title - start" / "index. title start"
|
||||||
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 = [
|
let patterns = [
|
||||||
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?(?:\\s*-\\s*)?(?<end>(?:[0-9]+:){1,2}[0-9]+)?(?:\\s*-\\s*|\\s*[:]\\s*)?(?<title>.*)(?=\\n|$)",
|
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?(?:\\s*-\\s*)?(?<end>(?:[0-9]+:){1,2}[0-9]+)?(?:\\s*-\\s*|\\s*[:]\\s*)?(?<title>.*)(?=\\n|$)",
|
||||||
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?\\s*[-:]?\\s*(?<title>.+)(?=\\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|^)(?<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 {
|
let extractChaptersGroup = DispatchGroup()
|
||||||
guard let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { continue }
|
var capturedChapters: [Int: [Chapter]] = [:]
|
||||||
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
|
let lock = NSLock()
|
||||||
|
|
||||||
if !chapterLines.isEmpty {
|
for (index, pattern) in patterns.enumerated() {
|
||||||
return chapterLines.compactMap { line in
|
extractChaptersGroup.enter()
|
||||||
let titleRange = line.range(withName: "title")
|
DispatchQueue.global().async {
|
||||||
let startRange = line.range(withName: "start")
|
if let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) {
|
||||||
guard let titleSubstringRange = Range(titleRange, in: description),
|
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
|
||||||
let startSubstringRange = Range(startRange, in: description)
|
let extractedChapters = chapterLines.compactMap { line -> Chapter? in
|
||||||
else {
|
let titleRange = line.range(withName: "title")
|
||||||
return nil
|
let startRange = line.range(withName: "start")
|
||||||
}
|
|
||||||
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?
|
guard let titleSubstringRange = Range(titleRange, in: description),
|
||||||
var minutes: Double?
|
let startSubstringRange = Range(startRange, in: description)
|
||||||
var seconds: Double?
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if startComponents.count == 3 {
|
let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces)
|
||||||
hours = Double(startComponents[0])
|
let startCapture = String(description[startSubstringRange])
|
||||||
minutes = Double(startComponents[1])
|
let startComponents = startCapture.components(separatedBy: ":")
|
||||||
seconds = Double(startComponents[2])
|
guard startComponents.count <= 3 else { return nil }
|
||||||
} else if startComponents.count == 2 {
|
|
||||||
minutes = Double(startComponents[0])
|
var hours: Double?
|
||||||
seconds = Double(startComponents[1])
|
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 }
|
if !extractedChapters.isEmpty {
|
||||||
|
lock.lock()
|
||||||
startSeconds += (minutes ?? 0) * 60
|
capturedChapters[index] = extractedChapters
|
||||||
startSeconds += (hours ?? 0) * 60 * 60
|
lock.unlock()
|
||||||
|
}
|
||||||
return .init(title: titleCapture, start: startSeconds)
|
|
||||||
}
|
}
|
||||||
|
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 []
|
return []
|
||||||
|
@ -217,9 +217,22 @@ final class MPVBackend: PlayerBackend {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
var captions: Captions?
|
var captions: Captions?
|
||||||
if let captionsLanguageCode = Defaults[.captionsLanguageCode] {
|
|
||||||
captions = video.captions.first { $0.code == captionsLanguageCode } ??
|
if Defaults[.captionsAutoShow] == true {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
let updateCurrentStream = {
|
let updateCurrentStream = {
|
||||||
@ -254,9 +267,8 @@ final class MPVBackend: PlayerBackend {
|
|||||||
|
|
||||||
self.startClientUpdates()
|
self.startClientUpdates()
|
||||||
|
|
||||||
// Captions should only be displayed when selected by the user,
|
if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() }
|
||||||
// not when the video starts. So, we remove them.
|
PlayerModel.shared.captions = self.captions
|
||||||
self.client?.removeSubs()
|
|
||||||
|
|
||||||
if !preservingTime,
|
if !preservingTime,
|
||||||
!upgrading,
|
!upgrading,
|
||||||
|
@ -68,6 +68,8 @@ final class MPVClient: ObservableObject {
|
|||||||
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
|
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
|
||||||
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
|
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, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "sub-scale", Defaults[.captionsFontScaleSize]))
|
||||||
|
checkError(mpv_set_option_string(mpv, "sub-color", Defaults[.captionsFontColor]))
|
||||||
checkError(mpv_set_option_string(mpv, "user-agent", UserAgentManager.shared.userAgent))
|
checkError(mpv_set_option_string(mpv, "user-agent", UserAgentManager.shared.userAgent))
|
||||||
|
|
||||||
checkError(mpv_initialize(mpv))
|
checkError(mpv_initialize(mpv))
|
||||||
@ -406,6 +408,22 @@ final class MPVClient: ObservableObject {
|
|||||||
setString("video", "no")
|
setString("video", "no")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSubToAuto() {
|
||||||
|
setString("sub", "auto")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubToNo() {
|
||||||
|
setString("sub", "no")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubFontSize(scaleSize: String) {
|
||||||
|
setString("sub-scale", scaleSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubFontColor(color: String) {
|
||||||
|
setString("sub-color", color)
|
||||||
|
}
|
||||||
|
|
||||||
var tracksCount: Int {
|
var tracksCount: Int {
|
||||||
Int(getString("track-list/count") ?? "-1") ?? -1
|
Int(getString("track-list/count") ?? "-1") ?? -1
|
||||||
}
|
}
|
||||||
|
@ -133,23 +133,22 @@ extension PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
|
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
|
||||||
// filter out non HLS streams
|
// filter out non-HLS streams and streams with resolution more than maxResolution
|
||||||
let nonHLSStreams = streams.filter { $0.kind != .hls }
|
let nonHLSStreams = streams.filter {
|
||||||
|
$0.kind != .hls && $0.resolution <= maxResolution.value
|
||||||
|
}
|
||||||
|
|
||||||
// find max resolution from non HLS streams
|
// find max resolution and bitrate from non-HLS streams
|
||||||
let bestResolution = nonHLSStreams
|
let bestResolutionStream = nonHLSStreams.max { $0.resolution < $1.resolution }
|
||||||
.filter { $0.resolution <= maxResolution.value }
|
let bestBitrateStream = nonHLSStreams.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
|
||||||
.max { $0.resolution < $1.resolution }
|
|
||||||
|
|
||||||
// finde max bitrate from non HLS streams
|
let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value
|
||||||
let bestBitrate = nonHLSStreams
|
let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate
|
||||||
.filter { $0.resolution <= maxResolution.value }
|
|
||||||
.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
|
|
||||||
|
|
||||||
return streams.map { stream in
|
return streams.map { stream in
|
||||||
if stream.kind == .hls {
|
if stream.kind == .hls {
|
||||||
stream.resolution = bestResolution?.resolution ?? maxResolution.value
|
stream.resolution = bestResolution
|
||||||
stream.bitrate = bestBitrate?.bitrate ?? (bestResolution?.resolution.bitrate ?? maxResolution.value.bitrate)
|
stream.bitrate = bestBitrate
|
||||||
stream.format = .hls
|
stream.format = .hls
|
||||||
} else if stream.kind == .stream {
|
} else if stream.kind == .stream {
|
||||||
stream.format = .stream
|
stream.format = .stream
|
||||||
|
@ -196,11 +196,13 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsSorter(_ lhs: Stream, _ rhs: Stream) -> Bool {
|
func streamsSorter(lhs: Stream, rhs: Stream) -> Bool {
|
||||||
if lhs.resolution.isNil || rhs.resolution.isNil {
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,7 +301,12 @@ extension Defaults.Keys {
|
|||||||
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
|
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
|
||||||
|
|
||||||
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
|
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
|
||||||
|
static let captionsAutoShow = Key<Bool>("captionsAutoShow", default: false)
|
||||||
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
|
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<String>("captionsFontScale", default: "1.0")
|
||||||
|
static let captionsFontColor = Key<String>("captionsFontColor", default: "#FFFFFF")
|
||||||
|
|
||||||
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
|
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
|
||||||
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)
|
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)
|
||||||
|
109
Shared/LanguageCodes.swift
Normal file
109
Shared/LanguageCodes.swift
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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 English_GB = "en-GB"
|
||||||
|
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 .English_GB: return "English (United Kingdom)"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
@ -11,16 +11,16 @@ struct ControlsOverlay: View {
|
|||||||
@Default(.qualityProfiles) private var qualityProfiles
|
@Default(.qualityProfiles) private var qualityProfiles
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
enum Field: Hashable {
|
enum Field: Hashable {
|
||||||
case qualityProfile
|
case qualityProfile
|
||||||
case stream
|
case stream
|
||||||
case increaseRate
|
case increaseRate
|
||||||
case decreaseRate
|
case decreaseRate
|
||||||
case captions
|
case captions
|
||||||
}
|
}
|
||||||
|
|
||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
@State private var presentingButtonHintAlert = false
|
@State private var presentingButtonHintAlert = false
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -94,10 +94,10 @@ struct ControlsOverlay: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
Text("Press and hold remote button to open captions and quality menus")
|
Text("Press and hold remote button to open captions and quality menus")
|
||||||
.frame(maxWidth: 400)
|
.frame(maxWidth: 400)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.frame(maxHeight: overlayHeight)
|
.frame(maxHeight: overlayHeight)
|
||||||
@ -117,9 +117,9 @@ struct ControlsOverlay: View {
|
|||||||
|
|
||||||
private var overlayHeight: Double {
|
private var overlayHeight: Double {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
contentSize.height + 80.0
|
contentSize.height + 80.0
|
||||||
#else
|
#else
|
||||||
contentSize.height
|
contentSize.height
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,26 +160,26 @@ struct ControlsOverlay: View {
|
|||||||
|
|
||||||
@ViewBuilder private var rateButton: some View {
|
@ViewBuilder private var rateButton: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
ratePicker
|
ratePicker
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.frame(maxWidth: 100)
|
.frame(maxWidth: 100)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
ratePicker
|
ratePicker
|
||||||
} label: {
|
} 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
|
|
||||||
Text(player.rateLabel(player.currentRate))
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,50 +241,50 @@ struct ControlsOverlay: View {
|
|||||||
|
|
||||||
private var rateButtonsSpacing: Double {
|
private var rateButtonsSpacing: Double {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
10
|
10
|
||||||
#else
|
#else
|
||||||
8
|
8
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var qualityProfileButton: some View {
|
@ViewBuilder private var qualityProfileButton: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
qualityProfilePicker
|
qualityProfilePicker
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.frame(maxWidth: 300)
|
.frame(maxWidth: 300)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
qualityProfilePicker
|
qualityProfilePicker
|
||||||
} label: {
|
} label: {
|
||||||
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
||||||
.frame(maxWidth: 240)
|
.frame(maxWidth: 240)
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.frame(maxWidth: 240)
|
.frame(maxWidth: 240)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
#else
|
#else
|
||||||
ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) {
|
ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) {
|
||||||
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Automatic") { player.qualityProfileSelection = nil }
|
Button("Automatic") { player.qualityProfileSelection = nil }
|
||||||
|
|
||||||
ForEach(qualityProfiles) { qualityProfile in
|
ForEach(qualityProfiles) { qualityProfile in
|
||||||
Button {
|
Button {
|
||||||
player.qualityProfileSelection = qualityProfile
|
player.qualityProfileSelection = qualityProfile
|
||||||
} label: {
|
} label: {
|
||||||
Text(qualityProfile.description)
|
Text(qualityProfile.description)
|
||||||
}
|
|
||||||
|
|
||||||
Button("Cancel", role: .cancel) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,71 +300,91 @@ struct ControlsOverlay: View {
|
|||||||
|
|
||||||
@ViewBuilder private var qualityButton: some View {
|
@ViewBuilder private var qualityButton: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
StreamControl()
|
StreamControl()
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.frame(maxWidth: 300)
|
.frame(maxWidth: 300)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
StreamControl()
|
StreamControl()
|
||||||
} label: {
|
} label: {
|
||||||
Text(player.streamSelection?.resolutionAndFormat ?? "loading")
|
Text(player.streamSelection?.resolutionAndFormat ?? "loading")
|
||||||
.frame(width: 140, height: 40)
|
.frame(width: 140, height: 40)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.frame(width: 240, height: 40)
|
.frame(width: 240, height: 40)
|
||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
#else
|
#else
|
||||||
StreamControl(focusedField: $focusedField)
|
StreamControl(focusedField: $focusedField)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var captionsButton: some View {
|
@ViewBuilder private var captionsButton: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
captionsPicker
|
captionsPicker
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.frame(maxWidth: 300)
|
.frame(maxWidth: 300)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
captionsPicker
|
captionsPicker
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "text.bubble")
|
Image(systemName: "text.bubble")
|
||||||
if let captions = captionsBinding.wrappedValue {
|
if let captions = captionsBinding.wrappedValue,
|
||||||
Text(captions.code)
|
let language = LanguageCodes(rawValue: 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 }
|
|
||||||
|
|
||||||
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
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Combine
|
||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@ -383,23 +384,35 @@ struct PlaybackSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var captionsButton: some View {
|
@ViewBuilder private var captionsButton: some View {
|
||||||
|
let videoCaptions = player.currentVideo?.captions
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
captionsPicker
|
captionsPicker
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.frame(maxWidth: 300)
|
.frame(maxWidth: 300)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
captionsPicker
|
if videoCaptions?.isEmpty == false {
|
||||||
|
captionsPicker
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "text.bubble")
|
Image(systemName: "text.bubble")
|
||||||
if let captions = player.captions {
|
if let captions = player.captions,
|
||||||
Text(captions.code)
|
let language = LanguageCodes(rawValue: captions.code)
|
||||||
|
{
|
||||||
|
Text("\(language.description.capitalized) (\(language.rawValue))")
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
|
} else {
|
||||||
|
if videoCaptions?.isEmpty == true {
|
||||||
|
Text("Not available")
|
||||||
|
} else {
|
||||||
|
Text("Disabled")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(alignment: .trailing)
|
.frame(alignment: .trailing)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
|
.disabled(videoCaptions?.isEmpty == true)
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
@ -65,7 +65,7 @@ import SwiftUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var thumbnailHeight: Double {
|
static var thumbnailHeight: Double {
|
||||||
thumbnailWidth / 1.7777
|
thumbnailWidth / (16 / 9)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,12 +30,19 @@ struct PlayerSettings: View {
|
|||||||
|
|
||||||
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
|
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
|
||||||
|
|
||||||
|
@Default(.showRelated) private var showRelated
|
||||||
@Default(.showInspector) private var showInspector
|
@Default(.showInspector) private var showInspector
|
||||||
|
|
||||||
@Default(.showChapters) private var showChapters
|
@Default(.showChapters) private var showChapters
|
||||||
@Default(.showChapterThumbnails) private var showThumbnails
|
@Default(.showChapterThumbnails) private var showThumbnails
|
||||||
@Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent
|
@Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent
|
||||||
@Default(.expandChapters) private var expandChapters
|
@Default(.expandChapters) private var expandChapters
|
||||||
@Default(.showRelated) private var showRelated
|
|
||||||
|
@Default(.captionsAutoShow) private var captionsAutoShow
|
||||||
|
@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
|
@ObservedObject private var accounts = AccountsModel.shared
|
||||||
|
|
||||||
@ -45,6 +52,11 @@ struct PlayerSettings: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
@State private var isShowingDefaultLanguagePicker = false
|
||||||
|
@State private var isShowingFallbackLanguagePicker = false
|
||||||
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@ -93,7 +105,54 @@ struct PlayerSettings: View {
|
|||||||
inspectorVisibilityPicker
|
inspectorVisibilityPicker
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Section(header: SettingsHeader(text: "Captions".localized())) {
|
||||||
|
#if os(tvOS)
|
||||||
|
Text("Size").font(.subheadline)
|
||||||
|
#endif
|
||||||
|
captionsFontScaleSizePicker
|
||||||
|
#if os(tvOS)
|
||||||
|
Text("Color").font(.subheadline)
|
||||||
|
#endif
|
||||||
|
captionsFontColorPicker
|
||||||
|
showCaptionsAutoShowToggle
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
captionDefaultLanguagePicker
|
||||||
|
captionFallbackLanguagePicker
|
||||||
|
#else
|
||||||
|
Button(action: { isShowingDefaultLanguagePicker = true }) {
|
||||||
|
HStack {
|
||||||
|
Text("Default language")
|
||||||
|
Spacer()
|
||||||
|
Text("\(LanguageCodes(rawValue: captionsDefaultLanguageCode)!.description.capitalized) (\(captionsDefaultLanguageCode))").foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) {
|
||||||
|
defaultLanguagePickerTVOS(
|
||||||
|
selectedLanguage: $captionsDefaultLanguageCode,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
Section(header: SettingsHeader(text: "Chapters".localized())) {
|
Section(header: SettingsHeader(text: "Chapters".localized())) {
|
||||||
showChaptersToggle
|
showChaptersToggle
|
||||||
showThumbnailsToggle
|
showThumbnailsToggle
|
||||||
@ -279,6 +338,103 @@ struct PlayerSettings: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private var showCaptionsAutoShowToggle: some View {
|
||||||
|
Toggle("Always show captions", isOn: $captionsAutoShow)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var captionsFontScaleSizePicker: some View {
|
||||||
|
Picker("Size", selection: $captionsFontScaleSize) {
|
||||||
|
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(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()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
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 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 defaultLanguagePickerTVOS: 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
#if !os(tvOS)
|
||||||
private var inspectorVisibilityPicker: some View {
|
private var inspectorVisibilityPicker: some View {
|
||||||
Picker("Inspector", selection: $showInspector) {
|
Picker("Inspector", selection: $showInspector) {
|
||||||
|
@ -154,7 +154,12 @@ struct YatteeApp: App {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
SiestaLog.Category.enabled = .common
|
SiestaLog.Category.enabled = .common
|
||||||
#endif
|
#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")
|
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
||||||
|
|
||||||
if !Defaults[.lastAccountIsPublic] {
|
if !Defaults[.lastAccountIsPublic] {
|
||||||
|
@ -387,7 +387,7 @@
|
|||||||
"Backend" = "الواجهة الخلفية";
|
"Backend" = "الواجهة الخلفية";
|
||||||
"Badge" = "الشارة";
|
"Badge" = "الشارة";
|
||||||
"Close PiP when starting playing other video" = "غلق الفيديو المصغر عند بدء تشغيل فيديو آخر";
|
"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" = " عامل التصفية";
|
"Filter" = " عامل التصفية";
|
||||||
"Frontend URL" = "عنوان URL للواجهة الأمامية";
|
"Frontend URL" = "عنوان URL للواجهة الأمامية";
|
||||||
"Fullscreen size" = "حجم ملء الشاشة";
|
"Fullscreen size" = "حجم ملء الشاشة";
|
||||||
@ -628,4 +628,4 @@
|
|||||||
"Password required to import" = "كلمة المرور مطلوبة للإستيراد";
|
"Password required to import" = "كلمة المرور مطلوبة للإستيراد";
|
||||||
"Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد";
|
"Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد";
|
||||||
"Export in progress..." = "جارِ التصدير...";
|
"Export in progress..." = "جارِ التصدير...";
|
||||||
"In progress..." = "في تَقَدم…";
|
"In progress..." = "في طور الأجراء…";
|
||||||
|
@ -406,7 +406,7 @@
|
|||||||
"Country" = "País";
|
"Country" = "País";
|
||||||
"Clear All" = "Limpar Tudo";
|
"Clear All" = "Limpar Tudo";
|
||||||
"Clear All Recents" = "Limpar Todos os Recentes";
|
"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";
|
"Duration" = "Duração";
|
||||||
"Edit Quality Profile" = "Editar Perfil de Qualidade";
|
"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.";
|
"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.";
|
||||||
|
@ -1079,6 +1079,9 @@
|
|||||||
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||||
E258F38B2BF61BD2005B8C28 /* 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 */; };
|
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 */; };
|
FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@ -1551,6 +1554,7 @@
|
|||||||
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentManager.swift; sourceTree = "<group>"; };
|
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentManager.swift; sourceTree = "<group>"; };
|
||||||
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatus.swift; 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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -2299,6 +2303,7 @@
|
|||||||
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
|
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
|
||||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
|
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
|
||||||
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */,
|
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */,
|
||||||
|
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */,
|
||||||
375B537828DF6CBB004C1D19 /* Localizable.strings */,
|
375B537828DF6CBB004C1D19 /* Localizable.strings */,
|
||||||
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
|
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
|
||||||
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
|
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
|
||||||
@ -3231,6 +3236,7 @@
|
|||||||
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
||||||
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
|
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
|
||||||
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
||||||
|
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
|
||||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
|
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
|
||||||
@ -3411,6 +3417,7 @@
|
|||||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||||
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */,
|
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */,
|
||||||
|
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
|
||||||
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
|
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
|
||||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
@ -4008,6 +4015,7 @@
|
|||||||
37FB28432721B22200A57617 /* ContentItem.swift in Sources */,
|
37FB28432721B22200A57617 /* ContentItem.swift in Sources */,
|
||||||
37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */,
|
37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */,
|
||||||
37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
|
37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
|
||||||
|
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
|
||||||
37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */,
|
37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */,
|
||||||
37AAF2A226741C97007FC770 /* FeedView.swift in Sources */,
|
37AAF2A226741C97007FC770 /* FeedView.swift in Sources */,
|
||||||
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,
|
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,
|
||||||
|
Loading…
Reference in New Issue
Block a user