mirror of
https://github.com/yattee/yattee.git
synced 2025-12-16 13:08:14 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6cfadab9a | ||
|
|
5b917ef91d | ||
|
|
34cb7860b3 | ||
|
|
934bd65752 | ||
|
|
e53985534e | ||
|
|
03e4c6d4e6 | ||
|
|
335e99cb7b | ||
|
|
ae9aa6fac7 | ||
|
|
2f4fb9fc67 | ||
|
|
f6bea6e045 | ||
|
|
fa712d8177 | ||
|
|
03d24fbc42 | ||
|
|
4fd3a37705 | ||
|
|
a66857b1fb | ||
|
|
e44c7f84c8 | ||
|
|
6b5ecbdd8b | ||
|
|
15ce82a686 | ||
|
|
7e3e393c65 | ||
|
|
108b4de483 | ||
|
|
7c9810ddf0 | ||
|
|
96df7fdec5 | ||
|
|
4fa5a15ad4 | ||
|
|
c9125644ed | ||
|
|
4db02b2638 | ||
|
|
9c5f066e55 | ||
|
|
c7908d08ae | ||
|
|
c9fb41c8e8 | ||
|
|
2e9cceafa5 | ||
|
|
fa09b2021c | ||
|
|
90777d91f6 | ||
|
|
6959778775 | ||
|
|
0f43efef6f | ||
|
|
959fb0d1fc | ||
|
|
81be57904b | ||
|
|
a42345896d | ||
|
|
43fc9e20c0 | ||
|
|
1a1bd1ba5b | ||
|
|
99aca8e23c | ||
|
|
ddee3b74f0 | ||
|
|
b271aed52b | ||
|
|
1c608c78a1 | ||
|
|
0ec227ba80 | ||
|
|
2a93ff52a3 | ||
|
|
896d46d0cf | ||
|
|
ad79180530 | ||
|
|
101f20c538 | ||
|
|
f28cec79ba | ||
|
|
a12755ec4b | ||
|
|
38c4ddbe43 | ||
|
|
e35f8b7892 | ||
|
|
c3e4c074d6 | ||
|
|
6eba2a45c8 |
48
CHANGELOG.md
48
CHANGELOG.md
@@ -1,4 +1,26 @@
|
|||||||
## Build 181
|
## Build 185
|
||||||
|
* Fix thumbnails failing to load on tvOS by @patelhiren in https://github.com/yattee/yattee/pull/688
|
||||||
|
* speed up sorting for Stream by @stonerl in https://github.com/yattee/yattee/pull/681
|
||||||
|
* faster chapter extraction by @stonerl in https://github.com/yattee/yattee/pull/682
|
||||||
|
* Invidious: add images to chapters by @stonerl in https://github.com/yattee/yattee/pull/685
|
||||||
|
* Improved Captions handling by @stonerl in https://github.com/yattee/yattee/pull/684
|
||||||
|
* Add User-Agent to request by @stonerl in https://github.com/yattee/yattee/pull/680
|
||||||
|
* MPV: speed up playback start by @stonerl in https://github.com/yattee/yattee/pull/689
|
||||||
|
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/683
|
||||||
|
|
||||||
|
## Previous builds
|
||||||
|
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
||||||
|
* Added Settings Import/Export
|
||||||
|
* Export all settings, instances and accounts
|
||||||
|
* Import selected elements from the file
|
||||||
|
* Include unencrypted passwords in the export or provide them during the import
|
||||||
|
* Import via URL for tvOS
|
||||||
|
* Added Controls setting "Action button labels" icon or icon and text
|
||||||
|
* Added Advanced setting for MPV: "deinterlace"
|
||||||
|
* Add help text to all header buttons (by @rickykresslein)
|
||||||
|
* History Setting: hide the recent activity in the sidebar or limit the number of items shown (by @rickykresslein)
|
||||||
|
* Fix issues with empty comments (by @stonerl)
|
||||||
|
* Improved Invidious comments (by @stonerl)
|
||||||
* Don't show related in sidebar when disabled in settings by @stonerl in https://github.com/yattee/yattee/pull/635
|
* Don't show related in sidebar when disabled in settings by @stonerl in https://github.com/yattee/yattee/pull/635
|
||||||
* Handle audio session interrupts by other media by @stonerl in https://github.com/yattee/yattee/pull/640
|
* Handle audio session interrupts by other media by @stonerl in https://github.com/yattee/yattee/pull/640
|
||||||
* Only show Queue header in sidebar view by @stonerl in https://github.com/yattee/yattee/pull/642
|
* Only show Queue header in sidebar view by @stonerl in https://github.com/yattee/yattee/pull/642
|
||||||
@@ -18,26 +40,16 @@
|
|||||||
* HLS: set target bitrate / AVPlayer: higher resolution by @stonerl in https://github.com/yattee/yattee/pull/667
|
* HLS: set target bitrate / AVPlayer: higher resolution by @stonerl in https://github.com/yattee/yattee/pull/667
|
||||||
* Fix #619: Remove ports from shared YouTube links by @0x000C in https://github.com/yattee/yattee/pull/627
|
* Fix #619: Remove ports from shared YouTube links by @0x000C in https://github.com/yattee/yattee/pull/627
|
||||||
* XCode enable IDEPreferLogStreaming by @stonerl in https://github.com/yattee/yattee/pull/638
|
* XCode enable IDEPreferLogStreaming by @stonerl in https://github.com/yattee/yattee/pull/638
|
||||||
* Updated localizations
|
* Conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/662
|
||||||
* Upgraded dependencies
|
* HomeView: Changes to Favourites and History Widget by @stonerl in https://github.com/yattee/yattee/pull/672
|
||||||
|
* Snappy UI - Offloading non UI task to background threads by @stonerl in https://github.com/yattee/yattee/pull/671
|
||||||
## Previous builds
|
* Fix PiP Mode Not Working Using MPV by @stonerl in https://github.com/yattee/yattee/pull/676
|
||||||
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
* Advanced Settings: cache-pause-initial by @stonerl in https://github.com/yattee/yattee/pull/679
|
||||||
* Added Settings Import/Export
|
* Changed description for Format reordering by @stonerl in https://github.com/yattee/yattee/pull/677
|
||||||
* Export all settings, instances and accounts
|
|
||||||
* Import selected elements from the file
|
|
||||||
* Include unencrypted passwords in the export or provide them during the import
|
|
||||||
* Import via URL for tvOS
|
|
||||||
* Added Controls setting "Action button labels" icon or icon and text
|
|
||||||
* Added Advanced setting for MPV: "deinterlace"
|
|
||||||
* Add help text to all header buttons (by @rickykresslein)
|
|
||||||
* History Setting: hide the recent activity in the sidebar or limit the number of items shown (by @rickykresslein)
|
|
||||||
* Fix issues with empty comments (by @stonerl)
|
|
||||||
* Improved Invidious comments (by @stonerl)
|
|
||||||
* Downgrade MPVKit to 0.36.0-1 due to issues with WebVTT subtitles
|
|
||||||
* Add Chinese (Traditional) localization (by @rexcsk)
|
* Add Chinese (Traditional) localization (by @rexcsk)
|
||||||
* Localization fixes
|
* Localization fixes
|
||||||
* Updated localizations
|
* Updated localizations
|
||||||
|
* Upgraded dependencies
|
||||||
* Fixed reported crash
|
* Fixed reported crash
|
||||||
* Other minor changes and improvements
|
* Other minor changes and improvements
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ GEM
|
|||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.929.0)
|
aws-partitions (1.933.0)
|
||||||
aws-sdk-core (3.196.1)
|
aws-sdk-core (3.196.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.81.0)
|
aws-sdk-kms (1.82.0)
|
||||||
aws-sdk-core (~> 3, >= 3.193.0)
|
aws-sdk-core (~> 3, >= 3.193.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.151.0)
|
aws-sdk-s3 (1.151.0)
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -655,7 +671,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
kind: .adaptive,
|
kind: .adaptive,
|
||||||
encoding: videoStream["encoding"].string,
|
encoding: videoStream["encoding"].string,
|
||||||
videoFormat: videoStream["type"].string,
|
videoFormat: videoStream["type"].string,
|
||||||
bitrate: videoStream["bitrate"].int
|
bitrate: videoStream["bitrate"].int,
|
||||||
|
requestRange: videoStream["init"].string ?? videoStream["index"].string
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -491,6 +491,35 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func nonProxiedAsset(asset: AVURLAsset, completion: @escaping (AVURLAsset?) -> Void) {
|
||||||
|
guard var urlComponents = URLComponents(url: asset.url, resolvingAgainstBaseURL: false) else {
|
||||||
|
completion(asset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let hostItem = urlComponents.queryItems?.first(where: { $0.name == "host" }),
|
||||||
|
let hostValue = hostItem.value
|
||||||
|
else {
|
||||||
|
completion(asset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urlComponents.host = hostValue
|
||||||
|
|
||||||
|
guard let newUrl = urlComponents.url else {
|
||||||
|
completion(asset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(AVURLAsset(url: newUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload used for hlsURLS
|
||||||
|
static func nonProxiedAsset(url: URL, completion: @escaping (AVURLAsset?) -> Void) {
|
||||||
|
let asset = AVURLAsset(url: url)
|
||||||
|
nonProxiedAsset(asset: asset, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
private func extractVideo(from content: JSON) -> Video? {
|
private func extractVideo(from content: JSON) -> Video? {
|
||||||
let details = content.dictionaryValue
|
let details = content.dictionaryValue
|
||||||
|
|
||||||
@@ -579,10 +608,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return URL(string: thumbnailURL
|
return URL(
|
||||||
.absoluteString
|
string: thumbnailURL
|
||||||
.replacingOccurrences(of: "hqdefault", with: quality.filename)
|
.absoluteString
|
||||||
.replacingOccurrences(of: "maxresdefault", with: quality.filename)
|
.replacingOccurrences(of: "hqdefault", with: quality.filename)
|
||||||
|
.replacingOccurrences(of: "maxresdefault", with: quality.filename)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,6 +718,19 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
let resolution = Stream.Resolution.from(resolution: quality, fps: fps)
|
let resolution = Stream.Resolution.from(resolution: quality, fps: fps)
|
||||||
let videoFormat = videoStream.dictionaryValue["format"]?.string
|
let videoFormat = videoStream.dictionaryValue["format"]?.string
|
||||||
let bitrate = videoStream.dictionaryValue["bitrate"]?.int
|
let bitrate = videoStream.dictionaryValue["bitrate"]?.int
|
||||||
|
var requestRange: String?
|
||||||
|
|
||||||
|
if let initStart = videoStream.dictionaryValue["initStart"]?.int,
|
||||||
|
let initEnd = videoStream.dictionaryValue["initEnd"]?.int
|
||||||
|
{
|
||||||
|
requestRange = "\(initStart)-\(initEnd)"
|
||||||
|
} else if let indexStart = videoStream.dictionaryValue["indexStart"]?.int,
|
||||||
|
let indexEnd = videoStream.dictionaryValue["indexEnd"]?.int
|
||||||
|
{
|
||||||
|
requestRange = "\(indexStart)-\(indexEnd)"
|
||||||
|
} else {
|
||||||
|
requestRange = nil
|
||||||
|
}
|
||||||
|
|
||||||
if videoOnly {
|
if videoOnly {
|
||||||
streams.append(
|
streams.append(
|
||||||
@@ -698,7 +741,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
resolution: resolution,
|
resolution: resolution,
|
||||||
kind: .adaptive,
|
kind: .adaptive,
|
||||||
videoFormat: videoFormat,
|
videoFormat: videoFormat,
|
||||||
bitrate: bitrate
|
bitrate: bitrate,
|
||||||
|
requestRange: requestRange
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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 []
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ enum VideosApp: String, CaseIterable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allowsDisablingVidoesProxying: Bool {
|
var allowsDisablingVidoesProxying: Bool {
|
||||||
self == .invidious
|
self == .invidious || self == .piped
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportsOpeningVideosByID: Bool {
|
var supportsOpeningVideosByID: Bool {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final class NetworkStateModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bufferingStateText: String? {
|
var bufferingStateText: String? {
|
||||||
guard detailsAvailable else { return nil }
|
guard detailsAvailable && player.hasStarted else { return nil }
|
||||||
return String(format: "%.0f%%", bufferingState)
|
return String(format: "%.0f%%", bufferingState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
|
|
||||||
var isLoadingVideo = false
|
var isLoadingVideo = false
|
||||||
|
|
||||||
|
var hasStarted = false
|
||||||
|
var isPaused: Bool {
|
||||||
|
avPlayer.timeControlStatus == .paused
|
||||||
|
}
|
||||||
|
|
||||||
var isPlaying: Bool {
|
var isPlaying: Bool {
|
||||||
avPlayer.timeControlStatus == .playing
|
avPlayer.timeControlStatus == .playing
|
||||||
}
|
}
|
||||||
@@ -158,6 +163,12 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
avPlayer.play()
|
avPlayer.play()
|
||||||
|
|
||||||
|
// Setting hasStarted to true the first time player started
|
||||||
|
if !hasStarted {
|
||||||
|
hasStarted = true
|
||||||
|
}
|
||||||
|
|
||||||
model.objectWillChange.send()
|
model.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +191,7 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
avPlayer.replaceCurrentItem(with: nil)
|
avPlayer.replaceCurrentItem(with: nil)
|
||||||
|
hasStarted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelLoads() {
|
func cancelLoads() {
|
||||||
@@ -220,7 +232,10 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
upgrading: Bool = false
|
upgrading: Bool = false
|
||||||
) {
|
) {
|
||||||
asset?.cancelLoading()
|
asset?.cancelLoading()
|
||||||
asset = AVURLAsset(url: url)
|
asset = AVURLAsset(
|
||||||
|
url: url,
|
||||||
|
options: ["AVURLAssetHTTPHeaderFieldsKey": ["User-Agent": "\(UserAgentManager.shared.userAgent)"]]
|
||||||
|
)
|
||||||
asset?.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
|
asset?.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
|
||||||
var error: NSError?
|
var error: NSError?
|
||||||
switch self?.asset?.statusOfValue(forKey: "duration", error: &error) {
|
switch self?.asset?.statusOfValue(forKey: "duration", error: &error) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SwiftUI
|
|||||||
|
|
||||||
final class MPVBackend: PlayerBackend {
|
final class MPVBackend: PlayerBackend {
|
||||||
static var timeUpdateInterval = 0.5
|
static var timeUpdateInterval = 0.5
|
||||||
static var networkStateUpdateInterval = 1.0
|
static var networkStateUpdateInterval = 0.1
|
||||||
|
|
||||||
private var logger = Logger(label: "mpv-backend")
|
private var logger = Logger(label: "mpv-backend")
|
||||||
|
|
||||||
@@ -44,6 +44,8 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
var hasStarted = false
|
||||||
|
var isPaused = false
|
||||||
var isPlaying = true { didSet {
|
var isPlaying = true { didSet {
|
||||||
networkStateTimer.start()
|
networkStateTimer.start()
|
||||||
|
|
||||||
@@ -215,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 = {
|
||||||
@@ -252,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,
|
||||||
@@ -337,7 +351,6 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func play() {
|
func play() {
|
||||||
isPlaying = true
|
|
||||||
startClientUpdates()
|
startClientUpdates()
|
||||||
|
|
||||||
if controls.presentingControls {
|
if controls.presentingControls {
|
||||||
@@ -354,13 +367,22 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client?.play()
|
client?.play()
|
||||||
|
|
||||||
|
isPlaying = true
|
||||||
|
isPaused = false
|
||||||
|
|
||||||
|
// Setting hasStarted to true the first time player started
|
||||||
|
if !hasStarted {
|
||||||
|
hasStarted = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pause() {
|
func pause() {
|
||||||
isPlaying = false
|
|
||||||
stopClientUpdates()
|
stopClientUpdates()
|
||||||
|
|
||||||
client?.pause()
|
client?.pause()
|
||||||
|
isPaused = true
|
||||||
|
isPlaying = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func togglePlay() {
|
func togglePlay() {
|
||||||
@@ -377,6 +399,9 @@ final class MPVBackend: PlayerBackend {
|
|||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
client?.stop()
|
client?.stop()
|
||||||
|
isPlaying = false
|
||||||
|
isPaused = false
|
||||||
|
hasStarted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) {
|
func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) {
|
||||||
@@ -392,8 +417,8 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeItem() {
|
func closeItem() {
|
||||||
client?.pause()
|
pause()
|
||||||
client?.stop()
|
stop()
|
||||||
self.video = nil
|
self.video = nil
|
||||||
self.stream = nil
|
self.stream = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,14 +60,43 @@ final class MPVClient: ObservableObject {
|
|||||||
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
|
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
checkError(mpv_set_option_string(mpv, "cache-pause-initial", "yes"))
|
// CACHING //
|
||||||
|
|
||||||
|
checkError(mpv_set_option_string(mpv, "cache-pause-initial", Defaults[.mpvCachePauseInital] ? "yes" : "no"))
|
||||||
checkError(mpv_set_option_string(mpv, "cache-secs", Defaults[.mpvCacheSecs]))
|
checkError(mpv_set_option_string(mpv, "cache-secs", Defaults[.mpvCacheSecs]))
|
||||||
checkError(mpv_set_option_string(mpv, "cache-pause-wait", Defaults[.mpvCachePauseWait]))
|
checkError(mpv_set_option_string(mpv, "cache-pause-wait", Defaults[.mpvCachePauseWait]))
|
||||||
|
|
||||||
|
// PLAYBACK //
|
||||||
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
|
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
|
||||||
checkError(mpv_set_option_string(mpv, "hwdec", machine == "x86_64" ? "no" : "auto-safe"))
|
|
||||||
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, "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))
|
||||||
|
|
||||||
|
// GPU //
|
||||||
|
|
||||||
|
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
|
||||||
|
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
|
||||||
|
|
||||||
|
// We set set everything to OpenGL so MPV doesn't have to probe for other APIs.
|
||||||
|
checkError(mpv_set_option_string(mpv, "gpu-api", "opengl"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "opengl-swapinterval", "0"))
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
checkError(mpv_set_option_string(mpv, "opengl-es", "yes"))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// We set this to ordered since we use OpenGL and Apple's implementation is ancient.
|
||||||
|
checkError(mpv_set_option_string(mpv, "dither", "ordered"))
|
||||||
|
|
||||||
|
// DEMUXER //
|
||||||
|
|
||||||
|
// We request to test for lavf first and skip probing other demuxer.
|
||||||
|
checkError(mpv_set_option_string(mpv, "demuxer", "lavf"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "audio-demuxer", "lavf"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "sub-demuxer", "lavf"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
|
||||||
|
|
||||||
checkError(mpv_initialize(mpv))
|
checkError(mpv_initialize(mpv))
|
||||||
|
|
||||||
@@ -405,6 +434,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ protocol PlayerBackend {
|
|||||||
var loadedVideo: Bool { get }
|
var loadedVideo: Bool { get }
|
||||||
var isLoadingVideo: Bool { get }
|
var isLoadingVideo: Bool { get }
|
||||||
|
|
||||||
|
var hasStarted: Bool { get }
|
||||||
|
var isPaused: Bool { get }
|
||||||
var isPlaying: Bool { get }
|
var isPlaying: Bool { get }
|
||||||
var isSeeking: Bool { get }
|
var isSeeking: Bool { get }
|
||||||
var playerItemDuration: CMTime? { get }
|
var playerItemDuration: CMTime? { get }
|
||||||
@@ -131,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
|
||||||
|
|||||||
@@ -298,6 +298,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
backend.isPlaying
|
backend.isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPaused: Bool {
|
||||||
|
backend.isPaused
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasStarted: Bool {
|
||||||
|
backend.hasStarted
|
||||||
|
}
|
||||||
|
|
||||||
var playerItemDuration: CMTime? {
|
var playerItemDuration: CMTime? {
|
||||||
guard !currentItem.isNil else {
|
guard !currentItem.isNil else {
|
||||||
return nil
|
return nil
|
||||||
@@ -675,10 +683,11 @@ final class PlayerModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let video = currentVideo else { return }
|
// First, we need to create an array with supported formats.
|
||||||
guard let stream = backend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: qualityProfile!.formats) else { return }
|
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
|
||||||
|
|
||||||
exitFullScreen()
|
guard let video = currentVideo else { return }
|
||||||
|
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
|
||||||
|
|
||||||
if avPlayerBackend.video == video {
|
if avPlayerBackend.video == video {
|
||||||
if activeBackend != .appleAVPlayer {
|
if activeBackend != .appleAVPlayer {
|
||||||
@@ -690,7 +699,19 @@ final class PlayerModel: ObservableObject {
|
|||||||
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
|
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
|
||||||
}
|
}
|
||||||
|
|
||||||
controls.objectWillChange.send()
|
var retryCount = 0
|
||||||
|
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||||
|
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
|
||||||
|
self?.exitFullScreen()
|
||||||
|
self?.controls.objectWillChange.send()
|
||||||
|
timer.invalidate()
|
||||||
|
} else if retryCount < 3, self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
||||||
|
// If PiP didn't start, try starting it again up to 3 times,
|
||||||
|
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
||||||
|
self?.avPlayerBackend.tryStartingPictureInPicture()
|
||||||
|
retryCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var transitioningToPiP: Bool {
|
var transitioningToPiP: Bool {
|
||||||
@@ -718,12 +739,19 @@ final class PlayerModel: ObservableObject {
|
|||||||
show()
|
show()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
backend.closePiP()
|
|
||||||
if previousActiveBackend == .mpv {
|
if previousActiveBackend == .mpv {
|
||||||
saveTime {
|
saveTime {
|
||||||
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
|
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
|
||||||
self.controls.resetTimer()
|
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||||
|
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
|
||||||
|
self?.backend.closePiP()
|
||||||
|
self?.controls.resetTimer()
|
||||||
|
timer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
backend.closePiP()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ extension PlayerModel {
|
|||||||
preservedTime = currentItem.playbackTime
|
preservedTime = currentItem.playbackTime
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self else { return }
|
guard let self = self else { return }
|
||||||
guard let video = item.video else {
|
guard let video = item.video else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.videoBeingOpened = nil
|
self.videoBeingOpened = nil
|
||||||
self.availableStreams = self.streamsWithInstance(instance: playerInstance, streams: video.streams)
|
self.streamsWithInstance(instance: playerInstance, streams: video.streams) { processedStreams in
|
||||||
|
self.availableStreams = processedStreams
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AVFoundation
|
||||||
import Foundation
|
import Foundation
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@@ -41,7 +42,9 @@ extension PlayerModel {
|
|||||||
self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed")
|
self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.availableStreams = self.streamsWithInstance(instance: instance, streams: video.streams)
|
self.streamsWithInstance(instance: instance, streams: video.streams) { processedStreams in
|
||||||
|
self.availableStreams = processedStreams
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.logger.critical("no streams available from \(instance.description)")
|
self.logger.critical("no streams available from \(instance.description)")
|
||||||
}
|
}
|
||||||
@@ -53,28 +56,153 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsWithInstance(instance: Instance, streams: [Stream]) -> [Stream] {
|
func streamsWithInstance(instance _: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) {
|
||||||
streams.map { stream in
|
// Queue for stream processing
|
||||||
stream.instance = instance
|
let streamProcessingQueue = DispatchQueue(label: "stream.yattee.streamProcessing.Queue", qos: .userInitiated)
|
||||||
|
// Queue for accessing the processedStreams array
|
||||||
|
let processedStreamsQueue = DispatchQueue(label: "stream.yattee.processedStreams.Queue")
|
||||||
|
// DispatchGroup for managing multiple tasks
|
||||||
|
let streamProcessingGroup = DispatchGroup()
|
||||||
|
|
||||||
if instance.app == .invidious, instance.proxiesVideos {
|
var processedStreams = [Stream]()
|
||||||
if let audio = stream.audioAsset {
|
|
||||||
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio)
|
for stream in streams {
|
||||||
|
streamProcessingQueue.async(group: streamProcessingGroup) {
|
||||||
|
let forbiddenAssetTestGroup = DispatchGroup()
|
||||||
|
var hasForbiddenAsset = false
|
||||||
|
|
||||||
|
let (nonHLSAssets, hlsURLs) = self.getAssets(from: [stream])
|
||||||
|
|
||||||
|
if let randomStream = nonHLSAssets.randomElement() {
|
||||||
|
let instance = randomStream.0
|
||||||
|
let asset = randomStream.1
|
||||||
|
let url = randomStream.2
|
||||||
|
let requestRange = randomStream.3
|
||||||
|
|
||||||
|
// swiftlint:disable:next shorthand_optional_binding
|
||||||
|
if let asset = asset, let instance = instance, !instance.proxiesVideos {
|
||||||
|
if instance.app == .invidious {
|
||||||
|
self.testAsset(url: url, range: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in
|
||||||
|
hasForbiddenAsset = isForbidden
|
||||||
|
}
|
||||||
|
} else if instance.app == .piped {
|
||||||
|
self.testPipedAssets(asset: asset, requestRange: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in
|
||||||
|
hasForbiddenAsset = isForbidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let randomHLS = hlsURLs.randomElement() {
|
||||||
|
let instance = randomHLS.0
|
||||||
|
let asset = AVURLAsset(url: randomHLS.1)
|
||||||
|
|
||||||
|
if instance?.app == .piped {
|
||||||
|
self.testPipedAssets(asset: asset, requestRange: nil, isHLS: true, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in
|
||||||
|
hasForbiddenAsset = isForbidden
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let video = stream.videoAsset {
|
|
||||||
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video)
|
forbiddenAssetTestGroup.wait()
|
||||||
|
|
||||||
|
// Post-processing code
|
||||||
|
if let instance = stream.instance {
|
||||||
|
if instance.app == .invidious {
|
||||||
|
if hasForbiddenAsset || instance.proxiesVideos {
|
||||||
|
if let audio = stream.audioAsset {
|
||||||
|
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio)
|
||||||
|
}
|
||||||
|
if let video = stream.videoAsset {
|
||||||
|
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if instance.app == .piped, !instance.proxiesVideos, !hasForbiddenAsset {
|
||||||
|
if let hlsURL = stream.hlsURL {
|
||||||
|
PipedAPI.nonProxiedAsset(url: hlsURL) { possibleNonProxiedURL in
|
||||||
|
if let nonProxiedURL = possibleNonProxiedURL {
|
||||||
|
stream.hlsURL = nonProxiedURL.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let audio = stream.audioAsset {
|
||||||
|
PipedAPI.nonProxiedAsset(asset: audio) { nonProxiedAudioAsset in
|
||||||
|
stream.audioAsset = nonProxiedAudioAsset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let video = stream.videoAsset {
|
||||||
|
PipedAPI.nonProxiedAsset(asset: video) { nonProxiedVideoAsset in
|
||||||
|
stream.videoAsset = nonProxiedVideoAsset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to processedStreams within the processedStreamsQueue
|
||||||
|
processedStreamsQueue.sync {
|
||||||
|
processedStreams.append(stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return stream
|
streamProcessingGroup.notify(queue: .main) {
|
||||||
|
// Access and pass processedStreams within the processedStreamsQueue block
|
||||||
|
processedStreamsQueue.sync {
|
||||||
|
completion(processedStreams)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsSorter(_ lhs: Stream, _ rhs: Stream) -> Bool {
|
private func getAssets(from streams: [Stream]) -> (nonHLSAssets: [(Instance?, AVURLAsset?, URL, String?)], hlsURLs: [(Instance?, URL)]) {
|
||||||
if lhs.resolution.isNil || rhs.resolution.isNil {
|
var nonHLSAssets = [(Instance?, AVURLAsset?, URL, String?)]()
|
||||||
|
var hlsURLs = [(Instance?, URL)]()
|
||||||
|
|
||||||
|
for stream in streams {
|
||||||
|
if stream.isHLS {
|
||||||
|
if let url = stream.hlsURL?.url {
|
||||||
|
hlsURLs.append((stream.instance, url))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let asset = stream.audioAsset {
|
||||||
|
nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange))
|
||||||
|
}
|
||||||
|
if let asset = stream.videoAsset {
|
||||||
|
nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (nonHLSAssets, hlsURLs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func testAsset(url: URL, range: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) {
|
||||||
|
// In case the range is nil, generate a random one.
|
||||||
|
let randomEnd = Int.random(in: 200 ... 800)
|
||||||
|
let requestRange = range ?? "0-\(randomEnd)"
|
||||||
|
|
||||||
|
forbiddenAssetTestGroup.enter()
|
||||||
|
URLTester.testURLResponse(url: url, range: requestRange, isHLS: isHLS) { statusCode in
|
||||||
|
completion(statusCode == HTTPStatus.Forbidden)
|
||||||
|
forbiddenAssetTestGroup.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func testPipedAssets(asset: AVURLAsset, requestRange: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) {
|
||||||
|
PipedAPI.nonProxiedAsset(asset: asset) { possibleNonProxiedAsset in
|
||||||
|
if let nonProxiedAsset = possibleNonProxiedAsset {
|
||||||
|
self.testAsset(url: nonProxiedAsset.url, range: requestRange, isHLS: isHLS, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: completion)
|
||||||
|
} else {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class Stream: Equatable, Hashable, Identifiable {
|
|||||||
var encoding: String?
|
var encoding: String?
|
||||||
var videoFormat: String?
|
var videoFormat: String?
|
||||||
var bitrate: Int?
|
var bitrate: Int?
|
||||||
|
var requestRange: String?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
instance: Instance? = nil,
|
instance: Instance? = nil,
|
||||||
@@ -181,7 +182,8 @@ class Stream: Equatable, Hashable, Identifiable {
|
|||||||
kind: Kind = .hls,
|
kind: Kind = .hls,
|
||||||
encoding: String? = nil,
|
encoding: String? = nil,
|
||||||
videoFormat: String? = nil,
|
videoFormat: String? = nil,
|
||||||
bitrate: Int? = nil
|
bitrate: Int? = nil,
|
||||||
|
requestRange: String? = nil
|
||||||
) {
|
) {
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.audioAsset = audioAsset
|
self.audioAsset = audioAsset
|
||||||
@@ -193,6 +195,7 @@ class Stream: Equatable, Hashable, Identifiable {
|
|||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
format = .from(videoFormat ?? "")
|
format = .from(videoFormat ?? "")
|
||||||
self.bitrate = bitrate
|
self.bitrate = bitrate
|
||||||
|
self.requestRange = requestRange
|
||||||
}
|
}
|
||||||
|
|
||||||
var isLocal: Bool {
|
var isLocal: Bool {
|
||||||
|
|||||||
@@ -103,9 +103,8 @@ enum Constants {
|
|||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
if isIPad {
|
if isIPad {
|
||||||
return .sidebar
|
return .sidebar
|
||||||
} else {
|
|
||||||
return .tab
|
|
||||||
}
|
}
|
||||||
|
return .tab
|
||||||
#else
|
#else
|
||||||
return .tab
|
return .tab
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ extension Defaults.Keys {
|
|||||||
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||||
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||||
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p60, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||||
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@@ -266,7 +266,10 @@ extension Defaults.Keys {
|
|||||||
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
|
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
|
||||||
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "120")
|
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "120")
|
||||||
static let mpvCachePauseWait = Key<String>("mpvCachePauseWait", default: "3")
|
static let mpvCachePauseWait = Key<String>("mpvCachePauseWait", default: "3")
|
||||||
|
static let mpvCachePauseInital = Key<Bool>("mpvCachePauseInitial", default: false)
|
||||||
static let mpvDeinterlace = Key<Bool>("mpvDeinterlace", default: false)
|
static let mpvDeinterlace = Key<Bool>("mpvDeinterlace", default: false)
|
||||||
|
static let mpvHWdec = Key<String>("hwdec", default: "auto-safe")
|
||||||
|
static let mpvDemuxerLavfProbeInfo = Key<String>("mpvDemuxerLavfProbeInfo", default: "no")
|
||||||
|
|
||||||
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
|
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
|
||||||
static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
|
static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
|
||||||
@@ -300,7 +303,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)
|
||||||
|
|||||||
81
Shared/HTTPStatus.swift
Normal file
81
Shared/HTTPStatus.swift
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// HTTP response status codes
|
||||||
|
|
||||||
|
enum HTTPStatus {
|
||||||
|
// Informational responses (100 - 199)
|
||||||
|
|
||||||
|
static let Continue = 100
|
||||||
|
static let SwitchingProtocols = 101
|
||||||
|
static let Processing = 102
|
||||||
|
static let EarlyHints = 103
|
||||||
|
|
||||||
|
// Successful responses (200 - 299)
|
||||||
|
|
||||||
|
static let OK = 200
|
||||||
|
static let Created = 201
|
||||||
|
static let Accepted = 202
|
||||||
|
static let NonAuthoritativeInformation = 203
|
||||||
|
static let NoContent = 204
|
||||||
|
static let ResetContent = 205
|
||||||
|
static let PartialContent = 206
|
||||||
|
static let MultiStatus = 207
|
||||||
|
static let AlreadyReported = 208
|
||||||
|
static let IMUsed = 226
|
||||||
|
|
||||||
|
// Redirection messages (300 - 399)
|
||||||
|
|
||||||
|
static let MultipleChoices = 300
|
||||||
|
static let MovedPermanently = 301
|
||||||
|
static let Found = 302
|
||||||
|
static let SeeOther = 303
|
||||||
|
static let NotModified = 304
|
||||||
|
static let UseProxy = 305
|
||||||
|
static let SwitchProxy = 306
|
||||||
|
static let TemporaryRedirect = 307
|
||||||
|
static let PermanentRedirect = 308
|
||||||
|
|
||||||
|
// Client error responses (400 - 499)
|
||||||
|
|
||||||
|
static let BadRequest = 400
|
||||||
|
static let Unauthorized = 401
|
||||||
|
static let PaymentRequired = 402
|
||||||
|
static let Forbidden = 403
|
||||||
|
static let NotFound = 404
|
||||||
|
static let MethodNotAllowed = 405
|
||||||
|
static let NotAcceptable = 406
|
||||||
|
static let ProxyAuthenticationRequired = 407
|
||||||
|
static let RequestTimeout = 408
|
||||||
|
static let Conflict = 409
|
||||||
|
static let Gone = 410
|
||||||
|
static let LengthRequired = 411
|
||||||
|
static let PreconditionFailed = 412
|
||||||
|
static let PayloadTooLarge = 413
|
||||||
|
static let URITooLong = 414
|
||||||
|
static let UnsupportedMediaType = 415
|
||||||
|
static let RangeNotSatisfiable = 416
|
||||||
|
static let ExpectationFailed = 417
|
||||||
|
static let IAmATeapot = 418
|
||||||
|
static let MisdirectedRequest = 421
|
||||||
|
static let UnprocessableEntity = 422
|
||||||
|
static let Locked = 423
|
||||||
|
static let FailedDependency = 424
|
||||||
|
static let TooEarly = 425
|
||||||
|
static let UpgradeRequired = 426
|
||||||
|
static let PreconditionRequired = 428
|
||||||
|
static let TooManyRequests = 429
|
||||||
|
static let RequestHeaderFieldsTooLarge = 431
|
||||||
|
static let UnavailableForLegalReasons = 451
|
||||||
|
|
||||||
|
// Server error responses (500 - 599)
|
||||||
|
|
||||||
|
static let InternalServerError = 500
|
||||||
|
static let NotImplemented = 501
|
||||||
|
static let BadGateway = 502
|
||||||
|
static let ServiceUnavailable = 503
|
||||||
|
static let GatewayTimeout = 504
|
||||||
|
static let HTTPVersionNotSupported = 505
|
||||||
|
static let VariantAlsoNegotiates = 506
|
||||||
|
static let InsufficientStorage = 507
|
||||||
|
static let LoopDetected = 508
|
||||||
|
static let NotExtended = 510
|
||||||
|
static let NetworkAuthenticationRequired = 511
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import UniformTypeIdentifiers
|
|||||||
|
|
||||||
struct FavoriteItemView: View {
|
struct FavoriteItemView: View {
|
||||||
var item: FavoriteItem
|
var item: FavoriteItem
|
||||||
|
@Binding var favoritesChanged: Bool
|
||||||
|
|
||||||
@Environment(\.navigationStyle) private var navigationStyle
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
@StateObject private var store = FavoriteResourceObserver()
|
@StateObject private var store = FavoriteResourceObserver()
|
||||||
@@ -25,8 +26,9 @@ struct FavoriteItemView: View {
|
|||||||
@Default(.widgetsSettings) private var widgetsSettings
|
@Default(.widgetsSettings) private var widgetsSettings
|
||||||
@Default(.visibleSections) private var visibleSections
|
@Default(.visibleSections) private var visibleSections
|
||||||
|
|
||||||
init(item: FavoriteItem) {
|
init(item: FavoriteItem, favoritesChanged: Binding<Bool>) {
|
||||||
self.item = item
|
self.item = item
|
||||||
|
_favoritesChanged = favoritesChanged
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -89,20 +91,23 @@ struct FavoriteItemView: View {
|
|||||||
loadCacheAndResource()
|
loadCacheAndResource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onDisappear {
|
||||||
|
resource?.removeObservers(ownedBy: store)
|
||||||
|
}
|
||||||
.onChange(of: player.currentVideo) { _ in reloadVisibleWatches() }
|
.onChange(of: player.currentVideo) { _ in reloadVisibleWatches() }
|
||||||
.onChange(of: hideShorts) { _ in reloadVisibleWatches() }
|
.onChange(of: hideShorts) { _ in reloadVisibleWatches() }
|
||||||
.onChange(of: hideWatched) { _ in reloadVisibleWatches() }
|
.onChange(of: hideWatched) { _ in reloadVisibleWatches() }
|
||||||
|
.onChange(of: favoritesChanged) { _ in reloadVisibleWatches() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.id(watchModel.historyToken)
|
.id(watchModel.historyToken)
|
||||||
.onChange(of: accounts.current) { _ in
|
.onChange(of: accounts.current) { _ in
|
||||||
|
resource?.removeObservers(ownedBy: store)
|
||||||
resource?.addObserver(store)
|
resource?.addObserver(store)
|
||||||
loadCacheAndResource(force: true)
|
loadCacheAndResource(force: true)
|
||||||
}
|
}
|
||||||
.onChange(of: watchModel.historyToken) { _ in
|
.onChange(of: watchModel.historyToken) { _ in
|
||||||
Delay.by(0.5) {
|
reloadVisibleWatches()
|
||||||
reloadVisibleWatches()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,12 +169,15 @@ struct FavoriteItemView: View {
|
|||||||
.prefix(favoritesModel.limit(item))
|
.prefix(favoritesModel.limit(item))
|
||||||
)
|
)
|
||||||
let last = watches.last
|
let last = watches.last
|
||||||
|
|
||||||
for watch in watches {
|
for watch in watches {
|
||||||
player.loadHistoryVideoDetails(watch) {
|
player.loadHistoryVideoDetails(watch) {
|
||||||
guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return }
|
guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return }
|
||||||
visibleWatches.append(watch)
|
visibleWatches.append(watch)
|
||||||
guard watch == last else { return }
|
|
||||||
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() }
|
if watch == last {
|
||||||
|
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,14 +494,22 @@ struct FavoriteItemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct FavoriteItemView_Previews: PreviewProvider {
|
struct FavoriteItemView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
struct PreviewWrapper: View {
|
||||||
NavigationView {
|
@State private var favoritesChanged = false
|
||||||
VStack {
|
|
||||||
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Search: resistance body upper band workout")))
|
var body: some View {
|
||||||
.environment(\.navigationStyle, .tab)
|
NavigationView {
|
||||||
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Marques")))
|
VStack {
|
||||||
.environment(\.navigationStyle, .sidebar)
|
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Search: resistance body upper band workout")), favoritesChanged: $favoritesChanged)
|
||||||
|
.environment(\.navigationStyle, .tab)
|
||||||
|
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Marques")), favoritesChanged: $favoritesChanged)
|
||||||
|
.environment(\.navigationStyle, .sidebar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
PreviewWrapper()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,52 @@ final class FavoriteResourceObserver: ObservableObject, ResourceObserver {
|
|||||||
@Published var contentItems = [ContentItem]()
|
@Published var contentItems = [ContentItem]()
|
||||||
|
|
||||||
func resourceChanged(_ resource: Resource, event _: ResourceEvent) {
|
func resourceChanged(_ resource: Resource, event _: ResourceEvent) {
|
||||||
|
// swiftlint:disable discouraged_optional_collection
|
||||||
|
var newVideos: [Video]?
|
||||||
|
var newItems: [ContentItem]?
|
||||||
|
// swiftlint:enable discouraged_optional_collection
|
||||||
|
|
||||||
|
var newChannel: Channel?
|
||||||
|
var newChannelPlaylist: ChannelPlaylist?
|
||||||
|
var newPlaylist: Playlist?
|
||||||
|
var newPage: SearchPage?
|
||||||
|
|
||||||
if let videos: [Video] = resource.typedContent() {
|
if let videos: [Video] = resource.typedContent() {
|
||||||
contentItems = videos.map { ContentItem(video: $0) }
|
newVideos = videos
|
||||||
} else if let channel: Channel = resource.typedContent() {
|
} else if let channel: Channel = resource.typedContent() {
|
||||||
contentItems = channel.videos.map { ContentItem(video: $0) }
|
newChannel = channel
|
||||||
} else if let playlist: ChannelPlaylist = resource.typedContent() {
|
} else if let playlist: ChannelPlaylist = resource.typedContent() {
|
||||||
contentItems = playlist.videos.map { ContentItem(video: $0) }
|
newChannelPlaylist = playlist
|
||||||
} else if let playlist: Playlist = resource.typedContent() {
|
} else if let playlist: Playlist = resource.typedContent() {
|
||||||
contentItems = playlist.videos.map { ContentItem(video: $0) }
|
newPlaylist = playlist
|
||||||
} else if let page: SearchPage = resource.typedContent() {
|
} else if let page: SearchPage = resource.typedContent() {
|
||||||
contentItems = page.results
|
newPage = page
|
||||||
} else if let items: [ContentItem] = resource.typedContent() {
|
} else if let items: [ContentItem] = resource.typedContent() {
|
||||||
contentItems = items
|
newItems = items
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
var newContentItems: [ContentItem] = []
|
||||||
|
|
||||||
|
if let videos = newVideos {
|
||||||
|
newContentItems = videos.map { ContentItem(video: $0) }
|
||||||
|
} else if let channel = newChannel {
|
||||||
|
newContentItems = channel.videos.map { ContentItem(video: $0) }
|
||||||
|
} else if let playlist = newChannelPlaylist {
|
||||||
|
newContentItems = playlist.videos.map { ContentItem(video: $0) }
|
||||||
|
} else if let playlist = newPlaylist {
|
||||||
|
newContentItems = playlist.videos.map { ContentItem(video: $0) }
|
||||||
|
} else if let page = newPage {
|
||||||
|
newContentItems = page.results
|
||||||
|
} else if let items = newItems {
|
||||||
|
newContentItems = items
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if !newContentItems.isEmpty {
|
||||||
|
self.contentItems = newContentItems
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HistoryView: View {
|
struct HistoryView: View {
|
||||||
var limit = 10
|
var limit: Int
|
||||||
|
|
||||||
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
||||||
var watches: FetchedResults<Watch>
|
var watches: FetchedResults<Watch>
|
||||||
|
|
||||||
@ObservedObject private var player = PlayerModel.shared
|
@ObservedObject private var player = PlayerModel.shared
|
||||||
|
|
||||||
@State private var visibleWatches = [Watch]()
|
@State private var visibleWatches = [Watch]()
|
||||||
|
|
||||||
init(limit: Int = 10) {
|
|
||||||
self.limit = limit
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
if visibleWatches.isEmpty {
|
if visibleWatches.isEmpty {
|
||||||
@@ -38,10 +33,14 @@ struct HistoryView: View {
|
|||||||
func reloadVisibleWatches() {
|
func reloadVisibleWatches() {
|
||||||
visibleWatches = Array(watches.filter { $0.videoID != player.currentVideo?.videoID }.prefix(limit))
|
visibleWatches = Array(watches.filter { $0.videoID != player.currentVideo?.videoID }.prefix(limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(limit: Int = 10) {
|
||||||
|
self.limit = limit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HistoryView_Previews: PreviewProvider {
|
struct HistoryView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HistoryView()
|
HistoryView(limit: 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ struct HomeView: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
ForEach(Defaults[.favorites]) { item in
|
ForEach(Defaults[.favorites]) { item in
|
||||||
FavoriteItemView(item: item)
|
FavoriteItemView(item: item, favoritesChanged: $favoritesChanged)
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
ForEach(favorites) { item in
|
ForEach(favorites) { item in
|
||||||
FavoriteItemView(item: item)
|
FavoriteItemView(item: item, favoritesChanged: $favoritesChanged)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.workaroundForVerticalScrollingBug()
|
.workaroundForVerticalScrollingBug()
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -333,9 +333,19 @@ struct ControlsOverlay: View {
|
|||||||
} 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)
|
|
||||||
|
{
|
||||||
|
Text("\(language.description.capitalized) (\(language.rawValue))")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
} else {
|
||||||
|
if captionsBinding.wrappedValue == nil {
|
||||||
|
Text("Not available")
|
||||||
|
} else {
|
||||||
|
Text("Disabled")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 240)
|
.frame(width: 240)
|
||||||
@@ -351,8 +361,18 @@ struct ControlsOverlay: View {
|
|||||||
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
|
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
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)
|
||||||
|
{
|
||||||
|
Text("\(language.description.capitalized) (\(language.rawValue))")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
} else {
|
||||||
|
if captionsBinding.wrappedValue == nil {
|
||||||
|
Text("Not available")
|
||||||
|
} else {
|
||||||
|
Text("Disabled")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
|
|||||||
@@ -10,26 +10,28 @@ struct OpeningStream: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var visible: Bool {
|
var visible: Bool {
|
||||||
(!player.currentItem.isNil && !player.videoBeingOpened.isNil) || (player.isLoadingVideo && !model.pausedForCache && !player.isSeeking)
|
(!player.currentItem.isNil && !player.videoBeingOpened.isNil) ||
|
||||||
|
(player.isLoadingVideo && !model.pausedForCache && !player.isSeeking) ||
|
||||||
|
!player.hasStarted
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason: String {
|
var reason: String {
|
||||||
guard player.videoBeingOpened == nil else {
|
guard player.videoBeingOpened == nil else {
|
||||||
return "Loading streams...".localized()
|
return "Loading streams…".localized()
|
||||||
}
|
}
|
||||||
|
|
||||||
if player.musicMode {
|
if player.musicMode {
|
||||||
return "Opening audio stream...".localized()
|
return "Opening audio stream…".localized()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let selection = player.streamSelection {
|
if let selection = player.streamSelection {
|
||||||
if selection.isLocal {
|
if selection.isLocal {
|
||||||
return "Opening file...".localized()
|
return "Opening file…".localized()
|
||||||
}
|
}
|
||||||
return String(format: "Opening %@ stream...".localized(), selection.shortQuality)
|
return String(format: "Opening %@ stream…".localized(), selection.shortQuality)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Loading streams...".localized()
|
return "Loading streams…".localized()
|
||||||
}
|
}
|
||||||
|
|
||||||
var state: String? {
|
var state: String? {
|
||||||
|
|||||||
@@ -75,16 +75,20 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
|
Spacer()
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack(spacing: 0) {
|
GeometryReader { geometry in
|
||||||
ZStack {
|
VStack(spacing: 0) {
|
||||||
OpeningStream()
|
ZStack {
|
||||||
NetworkState()
|
OpeningStream()
|
||||||
|
NetworkState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.position(
|
||||||
Spacer()
|
x: geometry.size.width / 2,
|
||||||
|
y: geometry.size.height / 2
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.offset(y: playerControlsLayout.osdVerticalOffset + 5)
|
|
||||||
|
|
||||||
if showControls {
|
if showControls {
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
@@ -278,10 +278,6 @@ enum PlayerControlsLayout: String, CaseIterable, Defaults.Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var osdVerticalOffset: Double {
|
|
||||||
buttonSize
|
|
||||||
}
|
|
||||||
|
|
||||||
var osdProgressBarHeight: Double {
|
var osdProgressBarHeight: Double {
|
||||||
switch self {
|
switch self {
|
||||||
case .tvRegular:
|
case .tvRegular:
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ struct AdvancedSettings: View {
|
|||||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||||
@Default(.mpvCacheSecs) private var mpvCacheSecs
|
@Default(.mpvCacheSecs) private var mpvCacheSecs
|
||||||
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
|
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
|
||||||
|
@Default(.mpvCachePauseInital) private var mpvCachePauseInital
|
||||||
@Default(.mpvDeinterlace) private var mpvDeinterlace
|
@Default(.mpvDeinterlace) private var mpvDeinterlace
|
||||||
@Default(.mpvEnableLogging) private var mpvEnableLogging
|
@Default(.mpvEnableLogging) private var mpvEnableLogging
|
||||||
|
@Default(.mpvHWdec) private var mpvHWdec
|
||||||
|
@Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo
|
||||||
@Default(.showCacheStatus) private var showCacheStatus
|
@Default(.showCacheStatus) private var showCacheStatus
|
||||||
@Default(.feedCacheSize) private var feedCacheSize
|
@Default(.feedCacheSize) private var feedCacheSize
|
||||||
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
|
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
|
||||||
@@ -68,9 +71,39 @@ struct AdvancedSettings: View {
|
|||||||
mpvEnableLoggingToggle
|
mpvEnableLoggingToggle
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Toggle(isOn: $mpvCachePauseInital) {
|
||||||
|
HStack {
|
||||||
|
Text("cache-pause-initial")
|
||||||
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "link")
|
||||||
|
.accessibilityAddTraits([.isButton, .isLink])
|
||||||
|
.font(.footnote)
|
||||||
|
#if os(iOS)
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-initial")!)
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
.onHover(perform: onHover(_:))
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("cache-secs")
|
Text("cache-secs")
|
||||||
.frame(minWidth: 140, alignment: .leading)
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "link")
|
||||||
|
.accessibilityAddTraits([.isButton, .isLink])
|
||||||
|
.font(.footnote)
|
||||||
|
#if os(iOS)
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-secs")!)
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
.onHover(perform: onHover(_:))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
TextField("cache-secs", text: $mpvCacheSecs)
|
TextField("cache-secs", text: $mpvCacheSecs)
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
@@ -79,8 +112,22 @@ struct AdvancedSettings: View {
|
|||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("cache-pause-wait")
|
Group {
|
||||||
.frame(minWidth: 140, alignment: .leading)
|
Text("cache-pause-wait")
|
||||||
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "link")
|
||||||
|
.accessibilityAddTraits([.isButton, .isLink])
|
||||||
|
.font(.footnote)
|
||||||
|
#if os(iOS)
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-wait")!)
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
.onHover(perform: onHover(_:))
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}.frame(minWidth: 140, alignment: .leading)
|
||||||
|
|
||||||
TextField("cache-pause-wait", text: $mpvCachePauseWait)
|
TextField("cache-pause-wait", text: $mpvCachePauseWait)
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
@@ -88,7 +135,79 @@ struct AdvancedSettings: View {
|
|||||||
}
|
}
|
||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
|
|
||||||
Toggle("deinterlace", isOn: $mpvDeinterlace)
|
Toggle(isOn: $mpvDeinterlace) {
|
||||||
|
HStack {
|
||||||
|
Text("deinterlace")
|
||||||
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "link")
|
||||||
|
.accessibilityAddTraits([.isButton, .isLink])
|
||||||
|
.font(.footnote)
|
||||||
|
#if os(iOS)
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-deinterlace")!)
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
.onHover(perform: onHover(_:))
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("hwdec")
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "link")
|
||||||
|
.accessibilityAddTraits([.isButton, .isLink])
|
||||||
|
.font(.footnote)
|
||||||
|
#if os(iOS)
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-hwdec")!)
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
.onHover(perform: onHover(_:))
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Picker("", selection: $mpvHWdec) {
|
||||||
|
ForEach(["auto", "auto-safe", "auto-copy"], id: \.self) {
|
||||||
|
Text($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.pickerStyle(MenuPickerStyle())
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if mpvEnableLogging {
|
||||||
|
logButton
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("demuxer-lavf-probe-info")
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "link")
|
||||||
|
.accessibilityAddTraits([.isButton, .isLink])
|
||||||
|
.font(.footnote)
|
||||||
|
#if os(iOS)
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-demuxer-lavf-probe-info")!)
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
.onHover(perform: onHover(_:))
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Picker("", selection: $mpvDemuxerLavfProbeInfo) {
|
||||||
|
ForEach(["yes", "no", "auto", "nostreams"], id: \.self) {
|
||||||
|
Text($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.pickerStyle(MenuPickerStyle())
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
if mpvEnableLogging {
|
if mpvEnableLogging {
|
||||||
logButton
|
logButton
|
||||||
@@ -103,20 +222,19 @@ struct AdvancedSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder var mpvFooter: some View {
|
@ViewBuilder var mpvFooter: some View {
|
||||||
let url = "https://mpv.io/manual/master"
|
let url = "https://mpv.io/manual/stable/"
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Restart the app to apply the settings above.")
|
Text("Restart the app to apply the settings above.")
|
||||||
|
.padding(.bottom, 1)
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
Text("More info can be found in MPV Documentation:")
|
Text("More info can be found in MPV reference manual:")
|
||||||
Text(url)
|
Text(url)
|
||||||
#else
|
#else
|
||||||
Text("More info can be found in:")
|
Text("Further information can be found in the ")
|
||||||
Link("MPV Documentation", destination: URL(string: url)!)
|
+ Text("MPV reference manual").underline().bold()
|
||||||
#if os(macOS)
|
+ Text(" by clicking on the link icon next to the option.")
|
||||||
.onHover(perform: onHover(_:))
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -136,9 +136,20 @@ struct QualityProfileForm: View {
|
|||||||
|
|
||||||
var formatsFooter: some View {
|
var formatsFooter: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Formats can be reordered and will be selected in this order.")
|
if #available(iOS 16.0, *) {
|
||||||
.foregroundColor(.secondary)
|
Text("Formats can be reordered and will be selected in this order.")
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.foregroundColor(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
} else if #available(iOS 14.0, *) {
|
||||||
|
Text("Formats will be selected in the order they are listed.")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
} else {
|
||||||
|
Text("Formats will be selected in the order they are listed.")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
Text("**Note:** HLS is an adaptive format where specific resolution settings don't apply.")
|
Text("**Note:** HLS is an adaptive format where specific resolution settings don't apply.")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
@@ -304,7 +315,7 @@ struct QualityProfileForm: View {
|
|||||||
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
|
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
|
||||||
guard backend == .appleAVPlayer else { return false }
|
guard backend == .appleAVPlayer else { return false }
|
||||||
|
|
||||||
return resolution.value > .hd1080p60
|
return resolution.value > .hd720p30
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeForm() {
|
func initializeForm() {
|
||||||
|
|||||||
110
Shared/URLTester.swift
Normal file
110
Shared/URLTester.swift
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
|
||||||
|
enum URLTester {
|
||||||
|
private static let hlsMediaPrefix = "#EXT-X-MEDIA:"
|
||||||
|
private static let hlsInfPrefix = "#EXTINF:"
|
||||||
|
private static let uriRegex = "(?<=URI=\")(.*?)(?=\")"
|
||||||
|
|
||||||
|
static func testURLResponse(url: URL, range: String, isHLS: Bool, completion: @escaping (Int) -> Void) {
|
||||||
|
if isHLS {
|
||||||
|
parseAndTestHLSManifest(manifestUrl: url, range: range, completion: completion)
|
||||||
|
} else {
|
||||||
|
httpRequest(url: url, range: range) { statusCode, _ in
|
||||||
|
completion(statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func httpRequest(url: URL, range: String, completion: @escaping (Int, URLSessionDataTask?) -> Void) {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "HEAD"
|
||||||
|
request.setValue("bytes=\(range)", forHTTPHeaderField: "Range")
|
||||||
|
request.setValue(UserAgentManager.shared.userAgent, forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
var dataTask: URLSessionDataTask?
|
||||||
|
dataTask = URLSession.shared.dataTask(with: request) { _, response, _ in
|
||||||
|
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? HTTPStatus.Forbidden
|
||||||
|
Logger(label: "stream.yattee.httpRequest").info("URL: \(url) | Status Code: \(statusCode)")
|
||||||
|
completion(statusCode, dataTask)
|
||||||
|
}
|
||||||
|
dataTask?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func parseAndTestHLSManifest(manifestUrl: URL, range: String, completion: @escaping (Int) -> Void) {
|
||||||
|
recursivelyParseManifest(manifestUrl: manifestUrl) { allURLs in
|
||||||
|
if let url = allURLs.randomElement() {
|
||||||
|
httpRequest(url: url, range: range) { statusCode, _ in
|
||||||
|
completion(statusCode)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(HTTPStatus.NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func recursivelyParseManifest(manifestUrl: URL, fullyParsed: @escaping ([URL]) -> Void) {
|
||||||
|
parseHLSManifest(manifestUrl: manifestUrl) { urls in
|
||||||
|
var allURLs = [URL]()
|
||||||
|
let group = DispatchGroup()
|
||||||
|
for url in urls {
|
||||||
|
if url.pathExtension == "m3u8" {
|
||||||
|
group.enter()
|
||||||
|
recursivelyParseManifest(manifestUrl: url) { subUrls in
|
||||||
|
allURLs += subUrls
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allURLs.append(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
fullyParsed(allURLs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func parseHLSManifest(manifestUrl: URL, completion: @escaping ([URL]) -> Void) {
|
||||||
|
URLSession.shared.dataTask(with: manifestUrl) { data, _, _ in
|
||||||
|
// swiftlint:disable:next shorthand_optional_binding
|
||||||
|
guard let data = data else {
|
||||||
|
Logger(label: "stream.yattee.httpRequest").error("Data is nil")
|
||||||
|
completion([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next non_optional_string_data_conversion
|
||||||
|
guard let manifest = String(data: data, encoding: .utf8), !manifest.isEmpty else {
|
||||||
|
Logger(label: "stream.yattee.httpRequest").error("Cannot read or empty HLS manifest")
|
||||||
|
completion([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let lines = manifest.split(separator: "\n")
|
||||||
|
var mediaURLs: [URL] = []
|
||||||
|
|
||||||
|
for index in 0 ..< lines.count {
|
||||||
|
let lineString = String(lines[index])
|
||||||
|
|
||||||
|
if lineString.hasPrefix(hlsMediaPrefix),
|
||||||
|
let uriRange = lineString.range(of: uriRegex, options: .regularExpression)
|
||||||
|
{
|
||||||
|
let uri = lineString[uriRange]
|
||||||
|
if let url = URL(string: String(uri)) {
|
||||||
|
mediaURLs.append(url)
|
||||||
|
}
|
||||||
|
} else if lineString.hasPrefix(hlsInfPrefix), index < lines.count - 1 {
|
||||||
|
let possibleURL = String(lines[index + 1])
|
||||||
|
let baseURL = manifestUrl.deletingLastPathComponent()
|
||||||
|
if let relativeURL = URL(string: possibleURL, relativeTo: baseURL),
|
||||||
|
relativeURL.scheme != nil
|
||||||
|
{
|
||||||
|
mediaURLs.append(relativeURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completion(mediaURLs)
|
||||||
|
}
|
||||||
|
.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Shared/UserAgentManager.swift
Normal file
37
Shared/UserAgentManager.swift
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Logging
|
||||||
|
#if !os(tvOS)
|
||||||
|
import WebKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
final class UserAgentManager {
|
||||||
|
static let shared = UserAgentManager()
|
||||||
|
|
||||||
|
private(set) var userAgent: String
|
||||||
|
#if !os(tvOS)
|
||||||
|
private var webView: WKWebView?
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
/*
|
||||||
|
In case an error occurs while retrieving the actual User-Agent, and on tvOS,
|
||||||
|
we set a default User-Agent value that represents a commonly used User-Agent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
userAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
|
||||||
|
#if !os(tvOS)
|
||||||
|
webView = WKWebView()
|
||||||
|
webView?.evaluateJavaScript("navigator.userAgent") { [weak self] result, _ in
|
||||||
|
if let userAgent = result as? String {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.userAgent = userAgent
|
||||||
|
Logger(label: "stream.yattee.userAgentManager").info("User-Agent: \(userAgent)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger(label: "stream.yattee.userAgentManager").warning("Failed to update User-Agent.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
Logger(label: "stream.yattee.userAgentManager.tvOS").info("User-Agent: \(userAgent)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ListView: View {
|
struct ListView: View {
|
||||||
var items: [ContentItem]
|
var items: [ContentItem]
|
||||||
var limit: Int? = 10
|
var limit: Int?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVStack(alignment: .leading) {
|
LazyVStack(alignment: .leading) {
|
||||||
@@ -16,16 +16,12 @@ struct ListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var limitedItems: [ContentItem] {
|
var limitedItems: [ContentItem] {
|
||||||
if let limit, limit >= 0 {
|
Array(items.prefix(limit ?? items.count))
|
||||||
return Array(items.prefix(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ListView_Previews: PreviewProvider {
|
struct ListView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ListView(items: [.init(video: .fixture)])
|
ListView(items: [.init(video: .fixture)], limit: 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,61 +150,82 @@ struct YatteeApp: App {
|
|||||||
}
|
}
|
||||||
configured = true
|
configured = true
|
||||||
|
|
||||||
#if DEBUG
|
DispatchQueue.main.async {
|
||||||
SiestaLog.Category.enabled = .common
|
#if DEBUG
|
||||||
#endif
|
SiestaLog.Category.enabled = .common
|
||||||
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
|
#endif
|
||||||
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
#if os(tvOS)
|
||||||
|
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
|
||||||
|
#else
|
||||||
|
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
|
||||||
|
#endif
|
||||||
|
|
||||||
if !Defaults[.lastAccountIsPublic] {
|
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
||||||
AccountsModel.shared.configureAccount()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let countryOfPublicInstances = Defaults[.countryOfPublicInstances] {
|
if !Defaults[.lastAccountIsPublic] {
|
||||||
InstancesManifest.shared.setPublicAccount(countryOfPublicInstances, asCurrent: AccountsModel.shared.current.isNil)
|
AccountsModel.shared.configureAccount()
|
||||||
}
|
|
||||||
|
|
||||||
if !AccountsModel.shared.current.isNil {
|
|
||||||
player.restoreQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !Defaults[.saveRecents] {
|
|
||||||
recents.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
let startupSection = Defaults[.startupSection]
|
|
||||||
var section: TabSelection? = startupSection.tabSelection
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
if section == .playlists {
|
|
||||||
section = .search
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
NavigationModel.shared.tabSelection = section ?? .search
|
if let countryOfPublicInstances = Defaults[.countryOfPublicInstances] {
|
||||||
|
InstancesManifest.shared.setPublicAccount(countryOfPublicInstances, asCurrent: AccountsModel.shared.current.isNil)
|
||||||
|
}
|
||||||
|
|
||||||
playlists.load()
|
if !AccountsModel.shared.current.isNil {
|
||||||
|
player.restoreQueue()
|
||||||
|
}
|
||||||
|
|
||||||
#if !os(macOS)
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
player.updateRemoteCommandCenter()
|
if !Defaults[.saveRecents] {
|
||||||
#endif
|
recents.clear()
|
||||||
|
|
||||||
if player.presentingPlayer {
|
|
||||||
player.presentingPlayer = false
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
||||||
if Defaults[.lockPortraitWhenBrowsing] {
|
|
||||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
URLBookmarkModel.shared.refreshAll()
|
let startupSection = Defaults[.startupSection]
|
||||||
|
var section: TabSelection? = startupSection.tabSelection
|
||||||
|
|
||||||
migrateHomeHistoryItems()
|
#if os(macOS)
|
||||||
migrateQualityProfiles()
|
if section == .playlists {
|
||||||
|
section = .search
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
NavigationModel.shared.tabSelection = section ?? .search
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
playlists.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
player.updateRemoteCommandCenter()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if player.presentingPlayer {
|
||||||
|
player.presentingPlayer = false
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
if Defaults[.lockPortraitWhenBrowsing] {
|
||||||
|
Orientation.lockOrientation(.all, andRotateTo: .portrait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Initialize UserAgentManager
|
||||||
|
_ = UserAgentManager.shared
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
URLBookmarkModel.shared.refreshAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
self.migrateHomeHistoryItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
self.migrateQualityProfiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateHomeHistoryItems() {
|
func migrateHomeHistoryItems() {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
"LIVE" = "مباشر";
|
"LIVE" = "مباشر";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "تحميل بثوث...";
|
"Loading streams…" = "تحميل بثوث…";
|
||||||
"Loading..." = "تحميل...";
|
"Loading..." = "تحميل...";
|
||||||
|
|
||||||
/* Video duration filter in search */
|
/* Video duration filter in search */
|
||||||
@@ -163,8 +163,8 @@
|
|||||||
"Open Settings" = "فتح الإعدادات";
|
"Open Settings" = "فتح الإعدادات";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "فتح بث %@ ...";
|
"Opening %@ stream…" = "فتح بث %@ …";
|
||||||
"Opening audio stream..." = "فتح بث صوتي...";
|
"Opening audio stream…" = "فتح بث صوتي…";
|
||||||
"Orientation" = "اتجاه";
|
"Orientation" = "اتجاه";
|
||||||
"Play in PiP" = "تشغيل في الفيديو المصغر";
|
"Play in PiP" = "تشغيل في الفيديو المصغر";
|
||||||
"Play Last" = "تشغيل الأخير";
|
"Play Last" = "تشغيل الأخير";
|
||||||
@@ -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" = "حجم ملء الشاشة";
|
||||||
@@ -558,7 +558,7 @@
|
|||||||
"Are you sure you want to clear cache?" = "هل أنت متأكد من أنك تريد مسح ذاكرة التخزين المؤقت؟";
|
"Are you sure you want to clear cache?" = "هل أنت متأكد من أنك تريد مسح ذاكرة التخزين المؤقت؟";
|
||||||
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لأزرار الأسهم عن بعد (للجيل الثاني من Siri Remote أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
|
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لأزرار الأسهم عن بعد (للجيل الثاني من Siri Remote أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
|
||||||
"Opened File" = "ملف مفتوح";
|
"Opened File" = "ملف مفتوح";
|
||||||
"Opening file..." = "فتح الملف...";
|
"Opening file…" = "فتح الملف…";
|
||||||
"Short videos: hidden" = "مقاطع الفيديو القصيرة: مخفية";
|
"Short videos: hidden" = "مقاطع الفيديو القصيرة: مخفية";
|
||||||
"Mark channel feed as watched" = "وضع علامة تمت المشاهدة على محتوى القناة";
|
"Mark channel feed as watched" = "وضع علامة تمت المشاهدة على محتوى القناة";
|
||||||
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
|
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
|
||||||
@@ -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..." = "في طور الأجراء…";
|
||||||
|
|||||||
@@ -277,11 +277,11 @@
|
|||||||
"Large" = "Böyük";
|
"Large" = "Böyük";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Yayımlar yüklənilir...";
|
"Loading streams…" = "Yayımlar yüklənilir…";
|
||||||
"Only when signed in" = "Yalnız daxil olduqda";
|
"Only when signed in" = "Yalnız daxil olduqda";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "%@ yayımı açılır...";
|
"Opening %@ stream…" = "%@ yayımı açılır…";
|
||||||
"Matrix Chat" = "Matrix Söhbət";
|
"Matrix Chat" = "Matrix Söhbət";
|
||||||
|
|
||||||
/* Player controls layout size */
|
/* Player controls layout size */
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
"Open Settings" = "Tənzimləmələri Aç";
|
"Open Settings" = "Tənzimləmələri Aç";
|
||||||
"Movies" = "Filmlər";
|
"Movies" = "Filmlər";
|
||||||
"No description" = "Açıqlama yoxdur";
|
"No description" = "Açıqlama yoxdur";
|
||||||
"Opening audio stream..." = "Səs yayımı açılır...";
|
"Opening audio stream…" = "Səs yayımı açılır…";
|
||||||
"Password" = "Şifrə";
|
"Password" = "Şifrə";
|
||||||
"Preferred Formats" = "Üstünlük Verilən Formatlar";
|
"Preferred Formats" = "Üstünlük Verilən Formatlar";
|
||||||
"Quality Profile" = "Profil Keyfiyyəti";
|
"Quality Profile" = "Profil Keyfiyyəti";
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
"LIVE" = "EN VIU";
|
"LIVE" = "EN VIU";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "S'estan carregant els fluxos...";
|
"Loading streams…" = "S'estan carregant els fluxos…";
|
||||||
"Loading..." = "Carregant...";
|
"Loading..." = "Carregant...";
|
||||||
"Locations" = "Ubicacions";
|
"Locations" = "Ubicacions";
|
||||||
"Lock portrait mode" = "Bloqueja el mode vertical";
|
"Lock portrait mode" = "Bloqueja el mode vertical";
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
"Offtopic in Music Videos" = "Offtopic als vídeos musicals";
|
"Offtopic in Music Videos" = "Offtopic als vídeos musicals";
|
||||||
"Open \"Playlists\" tab to create new one" = "Obriu la pestanya \"Llistes de reproducció\" per crear-ne una de nova";
|
"Open \"Playlists\" tab to create new one" = "Obriu la pestanya \"Llistes de reproducció\" per crear-ne una de nova";
|
||||||
"Open Settings" = "Obriu Configuració";
|
"Open Settings" = "Obriu Configuració";
|
||||||
"Opening audio stream..." = "Obrint la reproducció d'àudio...";
|
"Opening audio stream…" = "Obrint la reproducció d'àudio…";
|
||||||
"Orientation" = "Orientació";
|
"Orientation" = "Orientació";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -454,7 +454,7 @@
|
|||||||
"Low" = "Baix";
|
"Low" = "Baix";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "S'està obrint %@...";
|
"Opening %@ stream…" = "S'està obrint %@…";
|
||||||
"Only when signed in" = "Només quan s'ha iniciat la sessió";
|
"Only when signed in" = "Només quan s'ha iniciat la sessió";
|
||||||
"Password" = "Contrasenya";
|
"Password" = "Contrasenya";
|
||||||
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "Part d'un vídeo que promociona un producte o servei no relacionat directament amb el creador. El creador rebrà un pagament o compensació en forma de diners o productes gratuïts.";
|
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "Part d'un vídeo que promociona un producte o servei no relacionat directament amb el creador. El creador rebrà un pagament o compensació en forma de diners o productes gratuïts.";
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
"Only when signed in" = "Pouze když přihlášený";
|
"Only when signed in" = "Pouze když přihlášený";
|
||||||
"Open \"Playlists\" tab to create new one" = "Otevřete kartu \"Playlisty\", aby jste vytvořili nový";
|
"Open \"Playlists\" tab to create new one" = "Otevřete kartu \"Playlisty\", aby jste vytvořili nový";
|
||||||
"Open Settings" = "Otevřete Nastavení";
|
"Open Settings" = "Otevřete Nastavení";
|
||||||
"Opening audio stream..." = "Otevírám audio stream...";
|
"Opening audio stream…" = "Otevírám audio stream…";
|
||||||
"Orientation" = "Orientace";
|
"Orientation" = "Orientace";
|
||||||
"Password" = "Heslo";
|
"Password" = "Heslo";
|
||||||
"Pause" = "Pauza";
|
"Pause" = "Pauza";
|
||||||
@@ -401,11 +401,11 @@
|
|||||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Propagace produktu nebo služby, která přímo souvisí s tvůrcem samotným. Obvykle se jedná o zboží nebo zpeněžené platformy.";
|
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Propagace produktu nebo služby, která přímo souvisí s tvůrcem samotným. Obvykle se jedná o zboží nebo zpeněžené platformy.";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Načítaní streamu...";
|
"Loading streams…" = "Načítaní streamu…";
|
||||||
"Matrix Chat" = "Matrix Chat";
|
"Matrix Chat" = "Matrix Chat";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Otevírám %@ stream...";
|
"Opening %@ stream…" = "Otevírám %@ stream…";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
"Outro" = "Zakončení";
|
"Outro" = "Zakončení";
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
"Available" = "Dostupné";
|
"Available" = "Dostupné";
|
||||||
"Are you sure you want to remove %@ from Favorites?" = "Opravdu chcete odstranit %@ z oblíbených položek?";
|
"Are you sure you want to remove %@ from Favorites?" = "Opravdu chcete odstranit %@ z oblíbených položek?";
|
||||||
"Use system controls with AVPlayer" = "Použití systémových ovládacích prvků s AVPlayerem";
|
"Use system controls with AVPlayer" = "Použití systémových ovládacích prvků s AVPlayerem";
|
||||||
"Opening file..." = "Otvírání souboru...";
|
"Opening file…" = "Otvírání souboru…";
|
||||||
"No videos to show" = "Žádná videa k zobrazení";
|
"No videos to show" = "Žádná videa k zobrazení";
|
||||||
"Autoplay next" = "Automaticky přehrát další";
|
"Autoplay next" = "Automaticky přehrát další";
|
||||||
"Inspector" = "Inspektor";
|
"Inspector" = "Inspektor";
|
||||||
|
|||||||
@@ -182,7 +182,7 @@
|
|||||||
"Large" = "Groß";
|
"Large" = "Groß";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Lädt Streams …";
|
"Loading streams…" = "Lädt Streams …";
|
||||||
|
|
||||||
/* Video duration filter in search */
|
/* Video duration filter in search */
|
||||||
"Long" = "Lang";
|
"Long" = "Lang";
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
"Only when signed in" = "Nur wenn Sie eingeloggt sind";
|
"Only when signed in" = "Nur wenn Sie eingeloggt sind";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Öffne %@-stream …";
|
"Opening %@ stream…" = "Öffne %@-Stream …";
|
||||||
"Connection failed" = "Verbindung fehlgeschlagen";
|
"Connection failed" = "Verbindung fehlgeschlagen";
|
||||||
"Continue from %@" = "Ab %@ fortsetzen";
|
"Continue from %@" = "Ab %@ fortsetzen";
|
||||||
"Contributing" = "Beitragen";
|
"Contributing" = "Beitragen";
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
"I want to ask a question" = "Ich möchte eine Frage stellen";
|
"I want to ask a question" = "Ich möchte eine Frage stellen";
|
||||||
"If you are interested what's coming in future updates, you can track project Milestones." = "Wenn Sie sich für künftige Updates interessieren, können Sie die Meilensteine des Projekts verfolgen.";
|
"If you are interested what's coming in future updates, you can track project Milestones." = "Wenn Sie sich für künftige Updates interessieren, können Sie die Meilensteine des Projekts verfolgen.";
|
||||||
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Das große Layout ist nicht für alle Geräte geeignet und kann dazu führen, dass die Bedienelemente nicht auf den Bildschirm passen.";
|
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Das große Layout ist nicht für alle Geräte geeignet und kann dazu führen, dass die Bedienelemente nicht auf den Bildschirm passen.";
|
||||||
"Opening audio stream..." = "Audiostream wird geöffnet …";
|
"Opening audio stream…" = "Audiostream wird geöffnet …";
|
||||||
"Orientation" = "Ausrichtung";
|
"Orientation" = "Ausrichtung";
|
||||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Die Wiedergabeliste \"%@\" wird gelöscht.\nDies kann nicht rückgängig gemacht werden.";
|
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Die Wiedergabeliste \"%@\" wird gelöscht.\nDies kann nicht rückgängig gemacht werden.";
|
||||||
"Preferred Formats" = "Bevorzugte Formate";
|
"Preferred Formats" = "Bevorzugte Formate";
|
||||||
@@ -569,7 +569,7 @@
|
|||||||
"Enter location address to connect..." = "Geben Sie die Internetadresse ein, um eine Verbindung herzustellen …";
|
"Enter location address to connect..." = "Geben Sie die Internetadresse ein, um eine Verbindung herzustellen …";
|
||||||
"Opened File" = "Geöffnete Datei";
|
"Opened File" = "Geöffnete Datei";
|
||||||
"File Extension" = "Dateierweiterung";
|
"File Extension" = "Dateierweiterung";
|
||||||
"Opening file..." = "Datei öffnen …";
|
"Opening file…" = "Datei öffnen …";
|
||||||
"Close video and player on end" = "Video und Player am Ende beenden";
|
"Close video and player on end" = "Video und Player am Ende beenden";
|
||||||
"Use system controls with AVPlayer" = "Systemsteuerung mit AVPlayer verwenden";
|
"Use system controls with AVPlayer" = "Systemsteuerung mit AVPlayer verwenden";
|
||||||
"Public account" = "Öffentliches Konto";
|
"Public account" = "Öffentliches Konto";
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
"LIVE" = "LIVE";
|
"LIVE" = "LIVE";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Loading streams...";
|
"Loading streams…" = "Loading streams…";
|
||||||
"Loading..." = "Loading...";
|
"Loading..." = "Loading...";
|
||||||
"Locations" = "Locations";
|
"Locations" = "Locations";
|
||||||
"Lock portrait mode" = "Lock portrait mode";
|
"Lock portrait mode" = "Lock portrait mode";
|
||||||
@@ -202,8 +202,8 @@
|
|||||||
"Open Settings" = "Open Settings";
|
"Open Settings" = "Open Settings";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Opening %@ stream...";
|
"Opening %@ stream…" = "Opening %@ stream…";
|
||||||
"Opening audio stream..." = "Opening audio stream...";
|
"Opening audio stream…" = "Opening audio stream…";
|
||||||
"Orientation" = "Orientation";
|
"Orientation" = "Orientation";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -567,7 +567,7 @@
|
|||||||
"Seek" = "Seek";
|
"Seek" = "Seek";
|
||||||
"Opened File" = "Opened File";
|
"Opened File" = "Opened File";
|
||||||
"File Extension" = "File Extension";
|
"File Extension" = "File Extension";
|
||||||
"Opening file..." = "Opening file...";
|
"Opening file…" = "Opening file…";
|
||||||
"Public account" = "Public account";
|
"Public account" = "Public account";
|
||||||
"Your Accounts" = "Your Accounts";
|
"Your Accounts" = "Your Accounts";
|
||||||
"Browse without account" = "Browse without account";
|
"Browse without account" = "Browse without account";
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
"Decreased opacity" = "Opacidad disminuida";
|
"Decreased opacity" = "Opacidad disminuida";
|
||||||
"High" = "Alto";
|
"High" = "Alto";
|
||||||
"%lld videos" = "%lld videos";
|
"%lld videos" = "%lld videos";
|
||||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Recordatorios explícitos para que indiquen les guste, se suscriban o interactúen con ellos en cualquier plataforma de pago o gratuita (por ejemplo, haciendo clic en un 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)." = "Solicitudes explícitas para dar me gusta, suscribirse o interactuar con ellos en una o más plataformas gratuitas o de pago (por ejemplo, hacer clic en un vídeo).";
|
||||||
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Los formatos se seleccionarán en orden como se indica.\nHLS es un formato adaptable (no aplica la configuración de resolución).";
|
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Los formatos se seleccionarán en orden como se indica.\nHLS es un formato adaptable (no aplica la configuración de resolución).";
|
||||||
"Fullscreen size" = "Tamaño de pantalla completa";
|
"Fullscreen size" = "Tamaño de pantalla completa";
|
||||||
"Badge & Decreased opacity" = "Insignia y opacidad dosminuída";
|
"Badge & Decreased opacity" = "Insignia y opacidad dosminuída";
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
|
|
||||||
/* Video date filter in search */
|
/* Video date filter in search */
|
||||||
"Today" = "Hoy";
|
"Today" = "Hoy";
|
||||||
"Opening audio stream..." = "Abriendo transmisión de audio...";
|
"Opening audio stream…" = "Abriendo transmisión de audio…";
|
||||||
"Open Video" = "Abrir Video";
|
"Open Video" = "Abrir Video";
|
||||||
"I want to ask a question" = "Quiero hacer una pregunta";
|
"I want to ask a question" = "Quiero hacer una pregunta";
|
||||||
"Save history of played videos" = "Guardar historial de videos reproducidos";
|
"Save history of played videos" = "Guardar historial de videos reproducidos";
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
"No documents" = "Sin documentos";
|
"No documents" = "Sin documentos";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Abriendo %@ emisión...";
|
"Opening %@ stream…" = "Abriendo %@ emisión…";
|
||||||
"Documents" = "Documentos";
|
"Documents" = "Documentos";
|
||||||
"Thumbnails" = "Miniaturas";
|
"Thumbnails" = "Miniaturas";
|
||||||
"Password" = "Contraseña";
|
"Password" = "Contraseña";
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
"Shuffle" = "Mezclar";
|
"Shuffle" = "Mezclar";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Cargando secuencias...";
|
"Loading streams…" = "Cargando secuencias…";
|
||||||
"Public Locations" = "Ubicaciones públicas";
|
"Public Locations" = "Ubicaciones públicas";
|
||||||
"Yattee" = "Yattee";
|
"Yattee" = "Yattee";
|
||||||
"No results" = "No hay resultados";
|
"No results" = "No hay resultados";
|
||||||
@@ -558,7 +558,7 @@
|
|||||||
"Available" = "Disponible";
|
"Available" = "Disponible";
|
||||||
"Loop one" = "Bucle uno";
|
"Loop one" = "Bucle uno";
|
||||||
"Use system controls with AVPlayer" = "Utilizar los controles del sistema con AVPlayer";
|
"Use system controls with AVPlayer" = "Utilizar los controles del sistema con AVPlayer";
|
||||||
"Opening file..." = "Abriendo el archivo...";
|
"Opening file…" = "Abriendo el archivo…";
|
||||||
"No videos to show" = "No hay vídeos que mostrar";
|
"No videos to show" = "No hay vídeos que mostrar";
|
||||||
"Autoplay next" = "Reproducir automáticamente la siguiente";
|
"Autoplay next" = "Reproducir automáticamente la siguiente";
|
||||||
"Home Settings" = "Ajustes iniciales";
|
"Home Settings" = "Ajustes iniciales";
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"Profiles" = "نمایهها";
|
"Profiles" = "نمایهها";
|
||||||
"New Playlist" = "فهرست پخش جدید";
|
"New Playlist" = "فهرست پخش جدید";
|
||||||
"Automatic" = "خودکار";
|
"Automatic" = "خودکار";
|
||||||
"Opening file..." = "در حال باز کردن فایل…";
|
"Opening file…" = "در حال باز کردن فایل…";
|
||||||
"Add Quality Profile" = "افزودن نمایهٔ کیفیت";
|
"Add Quality Profile" = "افزودن نمایهٔ کیفیت";
|
||||||
"Close video after playing last in the queue" = "ویدیو را پس از پخش آخرین مورد فهرست ببند";
|
"Close video after playing last in the queue" = "ویدیو را پس از پخش آخرین مورد فهرست ببند";
|
||||||
|
|
||||||
@@ -279,14 +279,14 @@
|
|||||||
"Controls" = "کنترلها";
|
"Controls" = "کنترلها";
|
||||||
"This URL could not be opened" = "این نشانی باز نمیشود";
|
"This URL could not be opened" = "این نشانی باز نمیشود";
|
||||||
"Trending" = "پرطرفدار";
|
"Trending" = "پرطرفدار";
|
||||||
"Opening audio stream..." = "باز کردن استریم صوتی…";
|
"Opening audio stream…" = "باز کردن استریم صوتی…";
|
||||||
"Statistics" = "آمار";
|
"Statistics" = "آمار";
|
||||||
"Pause when player is closed" = "پس از بسته شدن پخشکننده مکث کن";
|
"Pause when player is closed" = "پس از بسته شدن پخشکننده مکث کن";
|
||||||
"Play All" = "همه را پخش کن";
|
"Play All" = "همه را پخش کن";
|
||||||
"Sort: %@" = "ترتیب: %@";
|
"Sort: %@" = "ترتیب: %@";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "باز کردن استریم %@…";
|
"Opening %@ stream…" = "باز کردن استریم %@…";
|
||||||
"Next in Queue" = "مورد بعد در صف";
|
"Next in Queue" = "مورد بعد در صف";
|
||||||
"Honor orientation lock" = "قفل چرخش صفحه را در نظر بگیر";
|
"Honor orientation lock" = "قفل چرخش صفحه را در نظر بگیر";
|
||||||
"Rate" = "امتیاز";
|
"Rate" = "امتیاز";
|
||||||
@@ -405,7 +405,7 @@
|
|||||||
"Info" = "اطلاعات";
|
"Info" = "اطلاعات";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "درحال دریافت استریم…";
|
"Loading streams…" = "درحال دریافت استریم…";
|
||||||
"No rotation" = "بدون چرخش";
|
"No rotation" = "بدون چرخش";
|
||||||
"Codec" = "کدک (Codec)";
|
"Codec" = "کدک (Codec)";
|
||||||
"Startup section" = "بخش آغازین";
|
"Startup section" = "بخش آغازین";
|
||||||
|
|||||||
@@ -282,11 +282,11 @@
|
|||||||
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Le grand format n'est pas adapté à tous les appareils et son utilisation peut empêcher certains contrôles de s'afficher à l'écran.";
|
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Le grand format n'est pas adapté à tous les appareils et son utilisation peut empêcher certains contrôles de s'afficher à l'écran.";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Chargement des flux...";
|
"Loading streams…" = "Chargement des flux…";
|
||||||
"Lock portrait mode" = "Verrouille l'orientation en mode portrait";
|
"Lock portrait mode" = "Verrouille l'orientation en mode portrait";
|
||||||
"Matrix Channel" = "Salon Matrix";
|
"Matrix Channel" = "Salon Matrix";
|
||||||
"Only when signed in" = "Uniquement lorsque vous êtes connecté";
|
"Only when signed in" = "Uniquement lorsque vous êtes connecté";
|
||||||
"Opening audio stream..." = "Ouverture du flux audio…";
|
"Opening audio stream…" = "Ouverture du flux audio…";
|
||||||
"Orientation" = "Orientation";
|
"Orientation" = "Orientation";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
"Milestones" = "Étapes";
|
"Milestones" = "Étapes";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Ouverture du flux %@…";
|
"Opening %@ stream…" = "Ouverture du flux %@…";
|
||||||
"Regular size" = "Taille normale";
|
"Regular size" = "Taille normale";
|
||||||
"Regular Size" = "Taille normale";
|
"Regular Size" = "Taille normale";
|
||||||
"Related" = "En relation";
|
"Related" = "En relation";
|
||||||
@@ -567,7 +567,7 @@
|
|||||||
"Seek" = "Recherche";
|
"Seek" = "Recherche";
|
||||||
"Show scroll to top button in comments" = "Afficher le bouton de retour en haut de la page dans les commentaires";
|
"Show scroll to top button in comments" = "Afficher le bouton de retour en haut de la page dans les commentaires";
|
||||||
"Opened File" = "Fichier ouvert";
|
"Opened File" = "Fichier ouvert";
|
||||||
"Opening file..." = "Ouverture du fichier...";
|
"Opening file…" = "Ouverture du fichier…";
|
||||||
"Enter location address to connect..." = "Entrez l'adresse de l'instance pour se connecter...";
|
"Enter location address to connect..." = "Entrez l'adresse de l'instance pour se connecter...";
|
||||||
"File Extension" = "Extension de fichier";
|
"File Extension" = "Extension de fichier";
|
||||||
"Public account" = "Compte publique";
|
"Public account" = "Compte publique";
|
||||||
|
|||||||
@@ -84,8 +84,8 @@
|
|||||||
"Open Settings" = "सेटिंग खोलें";
|
"Open Settings" = "सेटिंग खोलें";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "%@ स्ट्रीम खुल रहा…";
|
"Opening %@ stream…" = "%@ स्ट्रीम खुल रहा…";
|
||||||
"Opening audio stream..." = "ऑडियो स्ट्रीम खुल रहा…";
|
"Opening audio stream…" = "ऑडियो स्ट्रीम खुल रहा…";
|
||||||
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "यदि आप किसी बग की रिपोर्ट कर रहे हैं, तो सभी प्रासंगिक विवरण शामिल करें (विशेषकर: ऐप संस्करण, प्रयुक्त डिवाइस और सिस्टम संस्करण, पुन: पेश करने के चरण)।";
|
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "यदि आप किसी बग की रिपोर्ट कर रहे हैं, तो सभी प्रासंगिक विवरण शामिल करें (विशेषकर: ऐप संस्करण, प्रयुक्त डिवाइस और सिस्टम संस्करण, पुन: पेश करने के चरण)।";
|
||||||
"Increase rate" = "दर बढ़ाएँ";
|
"Increase rate" = "दर बढ़ाएँ";
|
||||||
"Info" = "जानकारी";
|
"Info" = "जानकारी";
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"LIVE" = "लाइव";
|
"LIVE" = "लाइव";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "स्ट्रीम लोड हो रहें…";
|
"Loading streams…" = "स्ट्रीम लोड हो रहें…";
|
||||||
"Loading..." = "लोड हो रहा…";
|
"Loading..." = "लोड हो रहा…";
|
||||||
"Locations" = "स्थान";
|
"Locations" = "स्थान";
|
||||||
"Lock portrait mode" = "पोर्ट्रेट मोड लॉक करें";
|
"Lock portrait mode" = "पोर्ट्रेट मोड लॉक करें";
|
||||||
|
|||||||
@@ -158,8 +158,8 @@
|
|||||||
"Open Settings" = "Apri Impostazioni";
|
"Open Settings" = "Apri Impostazioni";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Apertura stream %@...";
|
"Opening %@ stream…" = "Apertura stream %@…";
|
||||||
"Opening audio stream..." = "Apertura stream audio...";
|
"Opening audio stream…" = "Apertura stream audio…";
|
||||||
"Orientation" = "Orientamento";
|
"Orientation" = "Orientamento";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
"LIVE" = "DIRETTA";
|
"LIVE" = "DIRETTA";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Caricamento stream...";
|
"Loading streams…" = "Caricamento stream…";
|
||||||
"Loading..." = "Caricamento...";
|
"Loading..." = "Caricamento...";
|
||||||
"Locations" = "Posizioni";
|
"Locations" = "Posizioni";
|
||||||
"Low quality" = "Qualità bassa";
|
"Low quality" = "Qualità bassa";
|
||||||
@@ -569,7 +569,7 @@
|
|||||||
"Enter location address to connect..." = "Inserisci posizione per connetterti...";
|
"Enter location address to connect..." = "Inserisci posizione per connetterti...";
|
||||||
"Opened File" = "File aperto";
|
"Opened File" = "File aperto";
|
||||||
"File Extension" = "Estensione file";
|
"File Extension" = "Estensione file";
|
||||||
"Opening file..." = "Apro file...";
|
"Opening file…" = "Apro file…";
|
||||||
"Close video and player on end" = "Chiudi video e riproduttore alla fine";
|
"Close video and player on end" = "Chiudi video e riproduttore alla fine";
|
||||||
"Use system controls with AVPlayer" = "Usa controlli di sistema con AVPlayer";
|
"Use system controls with AVPlayer" = "Usa controlli di sistema con AVPlayer";
|
||||||
"Public account" = "Account pubblico";
|
"Public account" = "Account pubblico";
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
"Large" = "大";
|
"Large" = "大";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "ストリーム読込中...";
|
"Loading streams…" = "ストリーム読込中…";
|
||||||
"Lock portrait mode" = "縦モードをロック";
|
"Lock portrait mode" = "縦モードをロック";
|
||||||
"LIVE" = "ライブ";
|
"LIVE" = "ライブ";
|
||||||
"Locations" = "場所";
|
"Locations" = "場所";
|
||||||
@@ -108,10 +108,10 @@
|
|||||||
"Offtopic in Music Videos" = "音楽動画の非音楽部分";
|
"Offtopic in Music Videos" = "音楽動画の非音楽部分";
|
||||||
"Only when signed in" = "ログイン時のみ";
|
"Only when signed in" = "ログイン時のみ";
|
||||||
"Orientation" = "向き";
|
"Orientation" = "向き";
|
||||||
"Opening audio stream..." = "音声ストリーム 開始中...";
|
"Opening audio stream…" = "音声ストリーム 開始中…";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "%@ ストリーム 開始中...";
|
"Opening %@ stream…" = "%@ ストリーム 開始中…";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
"Outro" = "終了シーン";
|
"Outro" = "終了シーン";
|
||||||
@@ -566,7 +566,7 @@
|
|||||||
"Show scroll to top button in comments" = "コメント欄に「上に戻る」表示";
|
"Show scroll to top button in comments" = "コメント欄に「上に戻る」表示";
|
||||||
"Opened File" = "開いたファイル";
|
"Opened File" = "開いたファイル";
|
||||||
"File Extension" = "ファイル拡張子";
|
"File Extension" = "ファイル拡張子";
|
||||||
"Opening file..." = "ファイルを読み込み中...";
|
"Opening file…" = "ファイルを読み込み中…";
|
||||||
"Your Accounts" = "アカウントを使用";
|
"Your Accounts" = "アカウントを使用";
|
||||||
"Public account" = "公開アカウント";
|
"Public account" = "公開アカウント";
|
||||||
"Browse without account" = "アカウントなしで閲覧";
|
"Browse without account" = "アカウントなしで閲覧";
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
"LIVE" = "Direkte";
|
"LIVE" = "Direkte";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Laster inn strømmer …";
|
"Loading streams…" = "Laster inn strømmer …";
|
||||||
|
|
||||||
/* Video duration filter in search */
|
/* Video duration filter in search */
|
||||||
"Long" = "Lang";
|
"Long" = "Lang";
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
"Rate" = "Takt";
|
"Rate" = "Takt";
|
||||||
"Not Playing" = "Spiller ikke";
|
"Not Playing" = "Spiller ikke";
|
||||||
"Open \"Playlists\" tab to create new one" = "Åpne «Spillelister»-fanen for å opprette ny";
|
"Open \"Playlists\" tab to create new one" = "Åpne «Spillelister»-fanen for å opprette ny";
|
||||||
"Opening audio stream..." = "Åpner lydstrøm …";
|
"Opening audio stream…" = "Åpner lydstrøm …";
|
||||||
"Password" = "Passord";
|
"Password" = "Passord";
|
||||||
"Nothing" = "Ingenting";
|
"Nothing" = "Ingenting";
|
||||||
"Picture in Picture" = "Bilde-i-bilde";
|
"Picture in Picture" = "Bilde-i-bilde";
|
||||||
@@ -303,7 +303,7 @@
|
|||||||
"Offtopic in Music Videos" = "Urelaterte ting i musikkvideoer";
|
"Offtopic in Music Videos" = "Urelaterte ting i musikkvideoer";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Åpner %@-strøm …";
|
"Opening %@ stream…" = "Åpner %@-strøm …";
|
||||||
"Play in PiP" = "Bilde-i-bilde";
|
"Play in PiP" = "Bilde-i-bilde";
|
||||||
"Pause when entering background" = "Pause ved forsendelse til bakgrunnen";
|
"Pause when entering background" = "Pause ved forsendelse til bakgrunnen";
|
||||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Promoterer noe som har å gjøre med skaperen direkte. Vanligvis effekter eller betalte plattformer.";
|
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Promoterer noe som har å gjøre med skaperen direkte. Vanligvis effekter eller betalte plattformer.";
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
"LIVE" = "LIVE";
|
"LIVE" = "LIVE";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Ładowanie strumieni...";
|
"Loading streams…" = "Ładowanie strumieni…";
|
||||||
"Loading..." = "Ładowanie…";
|
"Loading..." = "Ładowanie…";
|
||||||
"Locations" = "Lokalizacje";
|
"Locations" = "Lokalizacje";
|
||||||
"Lock portrait mode" = "Zablokuj tryb portretowy";
|
"Lock portrait mode" = "Zablokuj tryb portretowy";
|
||||||
@@ -202,8 +202,8 @@
|
|||||||
"Open Settings" = "Otwórz Ustawienia";
|
"Open Settings" = "Otwórz Ustawienia";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Otwieranie strumienia %@…";
|
"Opening %@ stream…" = "Otwieranie strumienia %@…";
|
||||||
"Opening audio stream..." = "Otwieranie strumienia audio...";
|
"Opening audio stream…" = "Otwieranie strumienia audio…";
|
||||||
"Orientation" = "Orientacja";
|
"Orientation" = "Orientacja";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
"Enter location address to connect..." = "Wprowadź adres lokalizacji, aby połączyć...";
|
"Enter location address to connect..." = "Wprowadź adres lokalizacji, aby połączyć...";
|
||||||
"Opened File" = "Otwarty plik";
|
"Opened File" = "Otwarty plik";
|
||||||
"File Extension" = "Rozszerzenie pliku";
|
"File Extension" = "Rozszerzenie pliku";
|
||||||
"Opening file..." = "Otwieranie pliku...";
|
"Opening file…" = "Otwieranie pliku…";
|
||||||
"Public account" = "Konto publiczne";
|
"Public account" = "Konto publiczne";
|
||||||
"Your Accounts" = "Twoje konta";
|
"Your Accounts" = "Twoje konta";
|
||||||
"Browse without account" = "Przeglądanie bez konta";
|
"Browse without account" = "Przeglądanie bez konta";
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
"Just watched" = "Acabou de assistir";
|
"Just watched" = "Acabou de assistir";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Carregando streams…";
|
"Loading streams…" = "Carregando streams…";
|
||||||
"Medium quality" = "Qualidade média";
|
"Medium quality" = "Qualidade média";
|
||||||
"No description" = "Sem descrição";
|
"No description" = "Sem descrição";
|
||||||
"No Playlists" = "Sem playlists";
|
"No Playlists" = "Sem playlists";
|
||||||
@@ -114,8 +114,8 @@
|
|||||||
"Open Settings" = "Abrir Ajustes";
|
"Open Settings" = "Abrir Ajustes";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Abrindo stream %@…";
|
"Opening %@ stream…" = "Abrindo stream %@…";
|
||||||
"Opening audio stream..." = "Abrindo stream de áudio…";
|
"Opening audio stream…" = "Abrindo stream de áudio…";
|
||||||
"Orientation" = "Orientação";
|
"Orientation" = "Orientação";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -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.";
|
||||||
@@ -569,7 +569,7 @@
|
|||||||
"Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…";
|
"Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…";
|
||||||
"Opened File" = "Arquivo Aberto";
|
"Opened File" = "Arquivo Aberto";
|
||||||
"File Extension" = "Extensão do Arquivo";
|
"File Extension" = "Extensão do Arquivo";
|
||||||
"Opening file..." = "Abrindo arquivo…";
|
"Opening file…" = "Abrindo arquivo…";
|
||||||
"Browse without account" = "Navegar sem uma conta";
|
"Browse without account" = "Navegar sem uma conta";
|
||||||
"Rotate when entering fullscreen on landscape video" = "Girar quando entrar no modo tela cheia em vídeo em paisagem";
|
"Rotate when entering fullscreen on landscape video" = "Girar quando entrar no modo tela cheia em vídeo em paisagem";
|
||||||
"Landscape left" = "Paisagem à esquerda";
|
"Landscape left" = "Paisagem à esquerda";
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
"LIVE" = "AO VIVO";
|
"LIVE" = "AO VIVO";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Carregando streams…";
|
"Loading streams…" = "Carregando streams…";
|
||||||
"Loading..." = "Carregando…";
|
"Loading..." = "Carregando…";
|
||||||
"Locations" = "Localizações";
|
"Locations" = "Localizações";
|
||||||
"Lock portrait mode" = "Travar modo retrato";
|
"Lock portrait mode" = "Travar modo retrato";
|
||||||
@@ -237,8 +237,8 @@
|
|||||||
"Open Settings" = "Abrir Ajustes";
|
"Open Settings" = "Abrir Ajustes";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Abrindo stream %@…";
|
"Opening %@ stream…" = "Abrindo stream %@…";
|
||||||
"Opening audio stream..." = "Abrindo stream de áudio…";
|
"Opening audio stream…" = "Abrindo stream de áudio…";
|
||||||
"Orientation" = "Orientação";
|
"Orientation" = "Orientação";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -557,7 +557,7 @@
|
|||||||
"Keep channels with unwatched videos on top of subscriptions list" = "Manter canais com vídeos não vistos no topo da lista de inscrições";
|
"Keep channels with unwatched videos on top of subscriptions list" = "Manter canais com vídeos não vistos no topo da lista de inscrições";
|
||||||
"Opened File" = "Ficheiro Aberto";
|
"Opened File" = "Ficheiro Aberto";
|
||||||
"File Extension" = "Extensão do Ficheiro";
|
"File Extension" = "Extensão do Ficheiro";
|
||||||
"Opening file..." = "A abrir ficheiro…";
|
"Opening file…" = "A abrir ficheiro…";
|
||||||
"Close video and player on end" = "Fechar vídeo e player ao final";
|
"Close video and player on end" = "Fechar vídeo e player ao final";
|
||||||
"Use system controls with AVPlayer" = "Usar controles do sistema com o AVPlayer";
|
"Use system controls with AVPlayer" = "Usar controles do sistema com o AVPlayer";
|
||||||
"Public account" = "Conta pública";
|
"Public account" = "Conta pública";
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"Edit" = "Editați";
|
"Edit" = "Editați";
|
||||||
"Edit Playlist" = "Editați Playlist";
|
"Edit Playlist" = "Editați Playlist";
|
||||||
"Enter fullscreen in landscape" = "Introduceți ecranul complet în peisaj";
|
"Enter fullscreen in landscape" = "Introduceți ecranul complet în peisaj";
|
||||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Mementouri explicite de a aprecia, de a vă abona sau de a interacționa cu ele pe orice platformă (platforme) plătite sau gratuite (de exemplu, faceți clic pe un videoclip).\n";
|
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Mementouri explicite de a aprecia, de a vă abona sau de a interacționa cu ele pe orice platformă (platforme) plătite sau gratuite (de exemplu, faceți clic pe un videoclip).";
|
||||||
"Find Other" = "Găsiți alte";
|
"Find Other" = "Găsiți alte";
|
||||||
"Finding something to play..." = "Să găsești ceva de jucat...";
|
"Finding something to play..." = "Să găsești ceva de jucat...";
|
||||||
"For videos which feature music as the primary content." = "Pentru videoclipurile care includ muzica ca conținut principal.";
|
"For videos which feature music as the primary content." = "Pentru videoclipurile care includ muzica ca conținut principal.";
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
"LIVE" = "LIVE";
|
"LIVE" = "LIVE";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Se încarcă fluxurile...";
|
"Loading streams…" = "Se încarcă fluxurile…";
|
||||||
"Locations" = "Locații";
|
"Locations" = "Locații";
|
||||||
"Mark watched videos with" = "Marcați videoclipurile vizionate cu";
|
"Mark watched videos with" = "Marcați videoclipurile vizionate cu";
|
||||||
"Matrix Channel" = "Canal Matrix";
|
"Matrix Channel" = "Canal Matrix";
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
"Nothing" = "Nimic";
|
"Nothing" = "Nimic";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Se deschide %@ flux...";
|
"Opening %@ stream…" = "Se deschide %@ flux…";
|
||||||
"Play Last" = "Reda ultimul";
|
"Play Last" = "Reda ultimul";
|
||||||
"Player" = "Player";
|
"Player" = "Player";
|
||||||
"Playlist" = "Playlist";
|
"Playlist" = "Playlist";
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
"Outro" = "Outro";
|
"Outro" = "Outro";
|
||||||
"Orientation" = "Orientare";
|
"Orientation" = "Orientare";
|
||||||
"Opening audio stream..." = "Se deschide fluxul audio...";
|
"Opening audio stream…" = "Se deschide fluxul audio…";
|
||||||
"Password" = "Parolă";
|
"Password" = "Parolă";
|
||||||
"Pause" = "Pauză";
|
"Pause" = "Pauză";
|
||||||
"Pause when entering background" = "Pauză când intrați în fundal";
|
"Pause when entering background" = "Pauză când intrați în fundal";
|
||||||
@@ -568,7 +568,7 @@
|
|||||||
"Enter account credentials to connect..." = "Introduceți acreditările contului pentru a vă conecta...";
|
"Enter account credentials to connect..." = "Introduceți acreditările contului pentru a vă conecta...";
|
||||||
"Enter location address to connect..." = "Introdu adresa locației pentru a te conecta...";
|
"Enter location address to connect..." = "Introdu adresa locației pentru a te conecta...";
|
||||||
"Opened File" = "Fișier deschis";
|
"Opened File" = "Fișier deschis";
|
||||||
"Opening file..." = "Deschiderea fișierului...";
|
"Opening file…" = "Deschiderea fișierului…";
|
||||||
"File Extension" = "Extensie fișier";
|
"File Extension" = "Extensie fișier";
|
||||||
"Use system controls with AVPlayer" = "Utilizați controalele de sistem cu AVPlayer";
|
"Use system controls with AVPlayer" = "Utilizați controalele de sistem cu AVPlayer";
|
||||||
"Rotate when entering fullscreen on landscape video" = "Rotiți când intrați pe ecran complet în videoclipul peisaj";
|
"Rotate when entering fullscreen on landscape video" = "Rotiți când intrați pe ecran complet în videoclipul peisaj";
|
||||||
|
|||||||
@@ -370,7 +370,7 @@
|
|||||||
"LIVE" = "ПРЯМАЯ ТРАНСЛЯЦИЯ";
|
"LIVE" = "ПРЯМАЯ ТРАНСЛЯЦИЯ";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Загрузка прямой трансляции...";
|
"Loading streams…" = "Загрузка прямой трансляции…";
|
||||||
"Loading..." = "Загрузка...";
|
"Loading..." = "Загрузка...";
|
||||||
"Lock portrait mode" = "Блокировка портретного режима";
|
"Lock portrait mode" = "Блокировка портретного режима";
|
||||||
"Low" = "Низкое";
|
"Low" = "Низкое";
|
||||||
@@ -405,8 +405,8 @@
|
|||||||
"Open Settings" = "Отрыть настройки";
|
"Open Settings" = "Отрыть настройки";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Открытие %@ прямой трансляции...";
|
"Opening %@ stream…" = "Открытие %@ прямой трансляции…";
|
||||||
"Opening audio stream..." = "Открытие прямой трансляции аудио...";
|
"Opening audio stream…" = "Открытие прямой трансляции аудио…";
|
||||||
"Orientation" = "Ориентация";
|
"Orientation" = "Ориентация";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -591,7 +591,7 @@
|
|||||||
"Actions buttons" = "Кнопки действия";
|
"Actions buttons" = "Кнопки действия";
|
||||||
"Show sidebar" = "Показать боковую панель";
|
"Show sidebar" = "Показать боковую панель";
|
||||||
"Browse without account" = "Искать без аккаунта";
|
"Browse without account" = "Искать без аккаунта";
|
||||||
"Opening file..." = "Отрытие файла...";
|
"Opening file…" = "Отрытие файла…";
|
||||||
"Public account" = "Публичный аккаунт";
|
"Public account" = "Публичный аккаунт";
|
||||||
"Your Accounts" = "Ваши аккаунты";
|
"Your Accounts" = "Ваши аккаунты";
|
||||||
"Close video and player on end" = "Закрыть видео и плеер в конце";
|
"Close video and player on end" = "Закрыть видео и плеер в конце";
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
"I want to ask a question" = "Bir soru sormak istiyorum";
|
"I want to ask a question" = "Bir soru sormak istiyorum";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Akışlar yükleniyor...";
|
"Loading streams…" = "Akışlar yükleniyor…";
|
||||||
"Edit Quality Profile" = "Kalite Profilini Düzenle";
|
"Edit Quality Profile" = "Kalite Profilini Düzenle";
|
||||||
"Frontend URL" = "Ön uç URL'si";
|
"Frontend URL" = "Ön uç URL'si";
|
||||||
"Close player when starting PiP" = "Resim içinde Resim modu başlatılırken oynatıcıyı kapat";
|
"Close player when starting PiP" = "Resim içinde Resim modu başlatılırken oynatıcıyı kapat";
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
"No description" = "Açıklama yok";
|
"No description" = "Açıklama yok";
|
||||||
"Normal" = "Normal";
|
"Normal" = "Normal";
|
||||||
"Open \"Playlists\" tab to create new one" = "Yeni bir tane oluşturmak için \"Oynatma Listeleri\" sekmesini açın";
|
"Open \"Playlists\" tab to create new one" = "Yeni bir tane oluşturmak için \"Oynatma Listeleri\" sekmesini açın";
|
||||||
"Opening audio stream..." = "Ses akışı açılıyor...";
|
"Opening audio stream…" = "Ses akışı açılıyor…";
|
||||||
"Rate" = "Derecelendir";
|
"Rate" = "Derecelendir";
|
||||||
"Orientation" = "Yönlendirme";
|
"Orientation" = "Yönlendirme";
|
||||||
"No results" = "Sonuç yok";
|
"No results" = "Sonuç yok";
|
||||||
@@ -396,7 +396,7 @@
|
|||||||
"Open" = "Aç";
|
"Open" = "Aç";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "%@ akışı açılıyor...";
|
"Opening %@ stream…" = "%@ akışı açılıyor…";
|
||||||
"Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle";
|
"Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle";
|
||||||
"Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala";
|
"Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala";
|
||||||
"Show Inspector" = "Denetleyiciyi Göster";
|
"Show Inspector" = "Denetleyiciyi Göster";
|
||||||
|
|||||||
@@ -259,7 +259,7 @@
|
|||||||
"LIVE" = "В ЕФІРІ";
|
"LIVE" = "В ЕФІРІ";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "Завантаження трансляції...";
|
"Loading streams…" = "Завантаження трансляції…";
|
||||||
"Loading..." = "Завантаження...";
|
"Loading..." = "Завантаження...";
|
||||||
"Locations" = "Локації";
|
"Locations" = "Локації";
|
||||||
"Lock portrait mode" = "Заблокувати портретний режим";
|
"Lock portrait mode" = "Заблокувати портретний режим";
|
||||||
@@ -298,8 +298,8 @@
|
|||||||
"Open Settings" = "Відрити налаштування";
|
"Open Settings" = "Відрити налаштування";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "Запуск трансляції %@...";
|
"Opening %@ stream…" = "Запуск трансляції %@…";
|
||||||
"Opening audio stream..." = "Запуск аудіо трансляції...";
|
"Opening audio stream…" = "Запуск аудіо трансляції…";
|
||||||
"Orientation" = "Орієнтація";
|
"Orientation" = "Орієнтація";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
"Loading..." = "加载中...";
|
"Loading..." = "加载中...";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "加载流中...";
|
"Loading streams…" = "加载流中…";
|
||||||
"Locations" = "地址";
|
"Locations" = "地址";
|
||||||
"Lock portrait mode" = "锁定竖屏模式";
|
"Lock portrait mode" = "锁定竖屏模式";
|
||||||
|
|
||||||
@@ -191,8 +191,8 @@
|
|||||||
"Open Settings" = "打开设置";
|
"Open Settings" = "打开设置";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "正在打开 %@ 的流...";
|
"Opening %@ stream…" = "正在打开 %@ 的流…";
|
||||||
"Opening audio stream..." = "正在打开音频流...";
|
"Opening audio stream…" = "正在打开音频流…";
|
||||||
"Orientation" = "方向";
|
"Orientation" = "方向";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
@@ -530,7 +530,7 @@
|
|||||||
"Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮";
|
"Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮";
|
||||||
"Opened File" = "打开的文件";
|
"Opened File" = "打开的文件";
|
||||||
"File Extension" = "文件扩展";
|
"File Extension" = "文件扩展";
|
||||||
"Opening file..." = "打开文件中...";
|
"Opening file…" = "打开文件中…";
|
||||||
"Single tap gesture" = "单击手势";
|
"Single tap gesture" = "单击手势";
|
||||||
"Right click channel thumbnail to open context menu with more actions" = "右键单击频道缩略图以打开具有更多操作的上下文菜单";
|
"Right click channel thumbnail to open context menu with more actions" = "右键单击频道缩略图以打开具有更多操作的上下文菜单";
|
||||||
"Show unwatched feed badges" = "显示未观看的 Feed 标志";
|
"Show unwatched feed badges" = "显示未观看的 Feed 标志";
|
||||||
|
|||||||
@@ -252,7 +252,7 @@
|
|||||||
"Interface" = "介面";
|
"Interface" = "介面";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams..." = "加載中...";
|
"Loading streams…" = "加載中…";
|
||||||
"Loading..." = "加載中...";
|
"Loading..." = "加載中...";
|
||||||
"Locations" = "地址";
|
"Locations" = "地址";
|
||||||
"Lock portrait mode" = "鎖定直屏";
|
"Lock portrait mode" = "鎖定直屏";
|
||||||
@@ -292,8 +292,8 @@
|
|||||||
"Open Settings" = "打開設置";
|
"Open Settings" = "打開設置";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream..." = "正在打開 %@ ...";
|
"Opening %@ stream…" = "正在打開 %@ …";
|
||||||
"Opening audio stream..." = "正在打開音訊...";
|
"Opening audio stream…" = "正在打開音訊…";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
"Outro" = "結尾";
|
"Outro" = "結尾";
|
||||||
@@ -554,7 +554,7 @@
|
|||||||
"Queue - shuffled" = "隊列 - 隨機";
|
"Queue - shuffled" = "隊列 - 隨機";
|
||||||
"Loop one" = "單個循環";
|
"Loop one" = "單個循環";
|
||||||
"File Extension" = "副檔名";
|
"File Extension" = "副檔名";
|
||||||
"Opening file..." = "正在打開文件...";
|
"Opening file…" = "正在打開文件…";
|
||||||
"Public account" = "公共帳戶";
|
"Public account" = "公共帳戶";
|
||||||
"Enter account credentials to connect..." = "輸入帳號密碼來連接...";
|
"Enter account credentials to connect..." = "輸入帳號密碼來連接...";
|
||||||
"Enter location address to connect..." = "輸入站台地址來連接...";
|
"Enter location address to connect..." = "輸入站台地址來連接...";
|
||||||
|
|||||||
@@ -1070,6 +1070,18 @@
|
|||||||
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
||||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
||||||
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
||||||
|
E24DC6582BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
|
||||||
|
E24DC6592BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
|
||||||
|
E24DC65A2BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
|
||||||
|
E25028B02BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
|
||||||
|
E25028B12BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
|
||||||
|
E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
|
||||||
|
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 */; };
|
FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -1539,6 +1551,10 @@
|
|||||||
3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = "<group>"; };
|
3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = "<group>"; };
|
||||||
3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = "<group>"; };
|
3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = "<group>"; };
|
||||||
3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
|
3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; 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>"; };
|
||||||
|
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 */
|
||||||
@@ -2286,6 +2302,8 @@
|
|||||||
372915E52687E3B900F5A35B /* Defaults.swift */,
|
372915E52687E3B900F5A35B /* Defaults.swift */,
|
||||||
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
|
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
|
||||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
|
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.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 */,
|
||||||
@@ -2293,6 +2311,8 @@
|
|||||||
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
|
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
|
||||||
37FFC43F272734C3009FFD26 /* Throttle.swift */,
|
37FFC43F272734C3009FFD26 /* Throttle.swift */,
|
||||||
378FFBC328660172009E3FBE /* URLParser.swift */,
|
378FFBC328660172009E3FBE /* URLParser.swift */,
|
||||||
|
E258F3892BF61BD2005B8C28 /* URLTester.swift */,
|
||||||
|
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */,
|
||||||
37D4B0C22671614700C925CA /* YatteeApp.swift */,
|
37D4B0C22671614700C925CA /* YatteeApp.swift */,
|
||||||
37D4B0C42671614800C925CA /* Assets.xcassets */,
|
37D4B0C42671614800C925CA /* Assets.xcassets */,
|
||||||
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
|
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
|
||||||
@@ -3115,6 +3135,7 @@
|
|||||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
|
37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
|
||||||
375EC95D289EEEE000751258 /* QualityProfile.swift in Sources */,
|
375EC95D289EEEE000751258 /* QualityProfile.swift in Sources */,
|
||||||
|
E25028B02BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
|
||||||
3773B8102ADC076800B5FEF3 /* ScrollViewMatcher.swift in Sources */,
|
3773B8102ADC076800B5FEF3 /* ScrollViewMatcher.swift in Sources */,
|
||||||
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */,
|
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */,
|
||||||
@@ -3138,6 +3159,8 @@
|
|||||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||||
3752069D285E910600CA655F /* ChapterView.swift in Sources */,
|
3752069D285E910600CA655F /* ChapterView.swift in Sources */,
|
||||||
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */,
|
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */,
|
||||||
|
E24DC6582BFA124100BF6187 /* UserAgentManager.swift in Sources */,
|
||||||
|
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */,
|
||||||
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
||||||
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
@@ -3213,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 */,
|
||||||
@@ -3393,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 */,
|
||||||
@@ -3401,6 +3426,7 @@
|
|||||||
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||||
|
E24DC6592BFA124100BF6187 /* UserAgentManager.swift in Sources */,
|
||||||
374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */,
|
374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */,
|
||||||
371AC0A0294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */,
|
371AC0A0294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */,
|
||||||
379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */,
|
379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */,
|
||||||
@@ -3587,6 +3613,7 @@
|
|||||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||||
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
|
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
|
||||||
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
|
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||||
|
E25028B12BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
|
||||||
37A7D71C2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */,
|
37A7D71C2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */,
|
||||||
37BC50A92778A84700510953 /* HistorySettings.swift in Sources */,
|
37BC50A92778A84700510953 /* HistorySettings.swift in Sources */,
|
||||||
374DE88128BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
|
374DE88128BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
|
||||||
@@ -3621,6 +3648,7 @@
|
|||||||
378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */,
|
378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */,
|
||||||
370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */,
|
370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */,
|
||||||
37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */,
|
37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */,
|
||||||
|
E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */,
|
||||||
370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */,
|
370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */,
|
||||||
3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
|
3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
|
||||||
37FB28422721B22200A57617 /* ContentItem.swift in Sources */,
|
37FB28422721B22200A57617 /* ContentItem.swift in Sources */,
|
||||||
@@ -3864,8 +3892,10 @@
|
|||||||
37BA221329526A19000DAD1F /* ControlsGradientView.swift in Sources */,
|
37BA221329526A19000DAD1F /* ControlsGradientView.swift in Sources */,
|
||||||
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
37130A61277657300033018A /* PersistenceController.swift in Sources */,
|
37130A61277657300033018A /* PersistenceController.swift in Sources */,
|
||||||
|
E24DC65A2BFA124100BF6187 /* UserAgentManager.swift in Sources */,
|
||||||
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
|
3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
|
||||||
|
E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
|
||||||
370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */,
|
370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */,
|
||||||
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
3763C98B290C7A50004D3B5F /* OpenVideosView.swift in Sources */,
|
3763C98B290C7A50004D3B5F /* OpenVideosView.swift in Sources */,
|
||||||
@@ -3908,6 +3938,7 @@
|
|||||||
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
||||||
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
||||||
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
|
E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */,
|
||||||
37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
|
37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
|
||||||
374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
||||||
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
||||||
@@ -3984,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 */,
|
||||||
@@ -4071,7 +4103,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||||
@@ -4102,7 +4134,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
@@ -4133,7 +4165,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
@@ -4153,7 +4185,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
@@ -4317,7 +4349,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"DEBUG=1",
|
"DEBUG=1",
|
||||||
@@ -4370,7 +4402,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||||
@@ -4422,7 +4454,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -4461,7 +4493,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
@@ -4496,7 +4528,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4520,7 +4552,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4546,7 +4578,7 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4571,7 +4603,7 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4597,7 +4629,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -4637,7 +4669,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -4678,7 +4710,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -4702,7 +4734,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 182;
|
CURRENT_PROJECT_VERSION = 185;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|||||||
Reference in New Issue
Block a user