Compare commits

..

54 Commits

Author SHA1 Message Date
Arkadiusz Fal
46725bf4d9 Bump build number to 186 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
8697ec8faf Update packages 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
8a015d29c3 Update CHANGELOG 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
4097d11b5e Fix build issues 2024-06-13 19:12:19 +02:00
Arkadiusz Fal
5323d53f9e Fix swiftformat offenses 2024-06-13 19:05:09 +02:00
Arkadiusz Fal
e3e0c4a92f Remove duplicated button 2024-06-13 19:04:46 +02:00
Arkadiusz Fal
9e5efc1aa6 Merge pull request #697 from patelhiren/main
tvOS: Refined Subscriptions View
2024-06-13 19:00:15 +02:00
Arkadiusz Fal
1ed4c20c3a Merge pull request #696 from stonerl/improved-conditional-proxying
improved conditional proxying
2024-06-13 18:59:42 +02:00
Arkadiusz Fal
ced9eb28d7 Merge pull request #695 from stonerl/more-async-work
more responsive UI when Favorites are used.
2024-06-13 18:59:30 +02:00
Arkadiusz Fal
ea49758ed2 Merge pull request #694 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-06-13 18:58:58 +02:00
Hiren Patel
65ec675859 - Improved Subscription View for tvOS
- Refined Subscriptions view on tvOS

- Refined Subscriptions view on tvOS

- Refined Subscriptions view on tvOS

- Refined Subscriptions view on tvOS
2024-05-31 19:06:15 -04:00
Hyunjae Chung
9a650799d3 Translated using Weblate (Korean)
Currently translated at 6.4% (36 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ko/
2024-05-25 08:09:13 +02:00
Ophiushi
ddd1f243f7 Translated using Weblate (French)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2024-05-25 08:09:12 +02:00
Toni Förster
94f19d55c8 more responsive UI when Favorites are used.
- The reloading of items in the favorite widgets is now done async, so it does not block the UI when opening Home.
- Tasks that check for changes of the widget and favorites are now terminated when another view e.g. Subscriptions is opened.
- We only update the Favourites when the player is not in the foreground, to avoid unresponsiveness.
2024-05-24 19:46:42 +02:00
Toni Förster
30cdaf88e1 improved conditional proxying
We don't test every single stream anymore. If we get an 200, 206 or 403 we immediately stop testing streams. Unknown streams are also skipped. This speeds up starting playback, since we don'T have to wait for the network anymore.
2024-05-24 18:44:41 +02:00
Hyunjae Chung
8139bba31e Added translation using Weblate (Korean) 2024-05-24 07:37:13 +02:00
Arkadiusz Fal
d6cfadab9a Bump build number to 185 2024-05-23 12:11:58 +02:00
Arkadiusz Fal
5b917ef91d Update CHANGELOG 2024-05-23 11:55:31 +02:00
Arkadiusz Fal
34cb7860b3 Fix build issues 2024-05-23 11:55:31 +02:00
Arkadiusz Fal
934bd65752 Fix swiftformat offenses 2024-05-23 11:44:58 +02:00
Arkadiusz Fal
e53985534e Update packages 2024-05-23 11:44:26 +02:00
Arkadiusz Fal
03e4c6d4e6 Merge pull request #689 from stonerl/more-mpv-settings
MPV: speed up playback start
2024-05-23 11:41:56 +02:00
Arkadiusz Fal
335e99cb7b Merge pull request #680 from stonerl/add-user-agent-to-header
Add User-Agent to request
2024-05-23 11:37:53 +02:00
Arkadiusz Fal
ae9aa6fac7 Merge branch 'main' into add-user-agent-to-header 2024-05-23 11:37:46 +02:00
Arkadiusz Fal
2f4fb9fc67 Merge pull request #684 from stonerl/better-caption-handling
Improved Captions handling
2024-05-23 11:37:03 +02:00
Arkadiusz Fal
f6bea6e045 Merge pull request #685 from stonerl/chapter-images-for-invidious
Invidious: add images to chapters
2024-05-23 11:35:51 +02:00
Arkadiusz Fal
fa712d8177 Merge pull request #688 from patelhiren/main
Fix thumbnails failing to load on tvOS
2024-05-23 11:34:19 +02:00
Arkadiusz Fal
03d24fbc42 Merge pull request #682 from stonerl/faster-chapter-extraction
faster chapter extraction
2024-05-23 11:34:07 +02:00
Arkadiusz Fal
4fd3a37705 Merge pull request #681 from stonerl/speed-up-sorting
speed up sorting for Stream
2024-05-23 11:33:51 +02:00
Arkadiusz Fal
a66857b1fb Merge pull request #683 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-05-23 11:33:14 +02:00
Toni Förster
e44c7f84c8 MPV: speed up playback start
We initialize MPV with more options so it does not have to guess. This dramatically speeds up playback start.
2024-05-23 10:53:45 +02:00
Hiren Patel
6b5ecbdd8b - Fix thumbnails failing to load on tvOS
Thumbnails fail to load on tvOS when using SDImageAWebPCoder. Use SDImageWebPCoder on tvOS.
2024-05-21 22:58:14 -04:00
Toni Förster
15ce82a686 added en-GB to LanguageCodes 2024-05-20 21:53:48 +02:00
Toni Förster
7e3e393c65 Invidious: add images to chapters
Invidious, by design, has no images attached to chapters, in contrast to Piped.

Since the majority of videos with chapters don't have chapter-specific images and only use the videos' thumbnail, there is no difference here when compared to Piped's native thumbnail support.
2024-05-20 20:11:41 +02:00
Toni Förster
108b4de483 allow user to choose captions color 2024-05-20 17:50:47 +02:00
Toni Förster
7c9810ddf0 tvOS does not support WebKit 2024-05-20 16:03:13 +02:00
Toni Förster
96df7fdec5 let the user select caption size 2024-05-20 15:47:35 +02:00
Toni Förster
4fa5a15ad4 fallback language for captions 2024-05-20 14:42:24 +02:00
Toni Förster
c9125644ed improvements to captions on tvOS 2024-05-20 14:20:08 +02:00
Toni Förster
4db02b2638 Improved Captions handling
New options for captions in `Settings-Player`:

- Always show captions
- Default language

User can now select whether they want to show captions automatically when the video starts, and select the language.

Captions selector now shows proper name -> `English (en)` instead of only `en`
2024-05-20 03:50:51 +02:00
Mohammed Al Otaibi
9c5f066e55 Translated using Weblate (Arabic)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-05-19 23:01:50 +02:00
joaooliva
c7908d08ae Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/
2024-05-19 23:01:49 +02:00
Toni Förster
c9fb41c8e8 faster chapter extraction
The extraction of chapters is now faster since it is run in parallel for each pattern. Also a new pattern hast been added: "(start) title"
2024-05-19 17:43:35 +02:00
Toni Förster
2e9cceafa5 Add User-Agent to request
We generate a User-Agent for the platform we are running on and add it to the URLTester requests and also to MPV and AVPlayer
2024-05-19 13:46:14 +02:00
Toni Förster
fa09b2021c speed up sorting for Stream
This should help to start playback a bit faster.
2024-05-19 12:39:47 +02:00
Arkadiusz Fal
90777d91f6 Bump build number to 184 2024-05-19 11:52:38 +02:00
Arkadiusz Fal
6959778775 Update CHANGELOG 2024-05-19 11:52:38 +02:00
Arkadiusz Fal
0f43efef6f Fix build issue 2024-05-19 11:52:37 +02:00
Arkadiusz Fal
959fb0d1fc Merge pull request #676 from stonerl/fix-pip-broken-with-mpv
fix PiP Mode Not Working Using MPV
2024-05-19 09:51:38 +02:00
Arkadiusz Fal
81be57904b Merge pull request #679 from stonerl/mpv-settings
Advanced Settings: cache-pause-initial
2024-05-19 09:51:26 +02:00
Arkadiusz Fal
a42345896d Merge pull request #677 from stonerl/quality-reorderdering-iOS16
changed description for Format reordering
2024-05-19 09:51:10 +02:00
Toni Förster
43fc9e20c0 Advanced Settings: cache-pause-initial
`cache-pause-initial` status can now be selected by the user.

Also, on macOS and iOS, a link next to the option leads the user to the info on the mpv website.
2024-05-19 03:50:33 +02:00
Toni Förster
1a1bd1ba5b fix PiP Mode Not Working Using MPV
fixes #674

I accidentally broke PiP when using MPV. While testing this, I noticed that PiP sometimes does not start, so I tried to make MPV to PiP a bit more robust.
2024-05-19 00:58:52 +02:00
Toni Förster
99aca8e23c changed description for Format reordering
reordering Formats only works on iOS 16 and newer
2024-05-19 00:46:01 +02:00
31 changed files with 1209 additions and 260 deletions

View File

@@ -1,8 +1,8 @@
## Build 183
* Conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/662
* 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
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/673
## Build 186
* tvOS: Refined Subscriptions View by @patelhiren in https://github.com/yattee/yattee/pull/697
* More responsive UI when Favorites are used. by @stonerl in https://github.com/yattee/yattee/pull/695
* Improved conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/696
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/694
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
@@ -36,6 +36,19 @@
* 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
* XCode enable IDEPreferLogStreaming by @stonerl in https://github.com/yattee/yattee/pull/638
* Conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/662
* 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
* Fix PiP Mode Not Working Using MPV by @stonerl in https://github.com/yattee/yattee/pull/676
* 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
* Advanced Settings: cache-pause-initial by @stonerl in https://github.com/yattee/yattee/pull/679
* Changed description for Format reordering by @stonerl in https://github.com/yattee/yattee/pull/677
* Add Chinese (Traditional) localization (by @rexcsk)
* Localization fixes
* Updated localizations

View File

@@ -57,17 +57,17 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.931.0)
aws-sdk-core (3.196.1)
aws-partitions (1.944.0)
aws-sdk-core (3.197.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.81.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sdk-kms (1.83.0)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.151.0)
aws-sdk-core (~> 3, >= 3.194.0)
aws-sdk-s3 (1.152.2)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
@@ -153,7 +153,7 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.6)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
@@ -177,8 +177,8 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.8)
strscan (>= 3.0.9)
rexml (3.2.9)
strscan
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)

View File

@@ -502,7 +502,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
keywords: json["keywords"].arrayValue.compactMap { $0.string },
streams: extractStreams(from: json),
related: extractRelated(from: json),
chapters: extractChapters(from: description),
chapters: createChapters(from: description, thumbnails: json),
captions: extractCaptions(from: json)
)
}
@@ -575,6 +575,22 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
}
private func createChapters(from description: String, thumbnails: JSON) -> [Chapter] {
var chapters = extractChapters(from: description)
if !chapters.isEmpty {
let thumbnailsData = extractThumbnails(from: thumbnails)
let thumbnailURL = thumbnailsData.first { $0.quality == .medium }?.url
for chapter in chapters.indices {
if let url = thumbnailURL {
chapters[chapter].image = url
}
}
}
return chapters
}
private static var contentItemsKeys = ["items", "videos", "latestVideos", "playlists", "relatedChannels"]
private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage {

View File

@@ -152,58 +152,94 @@ extension VideosAPI {
/*
The following chapter patterns are covered:
start - end - title / start - end: Title / start - end title
start - title / start: title / start title / [start] - title / [start]: title / [start] title
index. title - start / index. title start
title: (start)
1) "start - end - title" / "start - end: Title" / "start - end title"
2) "start - title" / "start: title" / "start title" / "[start] - title" / "[start]: title" / "[start] title"
3) "index. title - start" / "index. title start"
4) "title: (start)"
5) "(start) title"
The order is important!
These represent:
- "start" and "end" are timestamps, defining the start and end of the individual chapter
- "title" is the name of the chapter
- "index" is the chapter's position in a list
The order of these patterns is important as it determines the priority. The patterns listed first have a higher priority.
In the case of multiple matches, the pattern with the highest priority will be chosen - lower number means higher priority.
*/
let patterns = [
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<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|^)(?<index>[0-9]+\\.\\s)(?<title>.+?)(?:\\s*-\\s*)?(?<start>(?:[0-9]+:){1,2}[0-9]+)(?=\\n|$)",
"(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)"
"(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)",
"(?<=^|\\n)\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)\\s*(?<title>.+?)(?=\\n|$)"
]
for pattern in patterns {
guard let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { continue }
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
let extractChaptersGroup = DispatchGroup()
var capturedChapters: [Int: [Chapter]] = [:]
let lock = NSLock()
if !chapterLines.isEmpty {
return chapterLines.compactMap { line in
let titleRange = line.range(withName: "title")
let startRange = line.range(withName: "start")
guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description)
else {
return nil
}
let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces)
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil }
for (index, pattern) in patterns.enumerated() {
extractChaptersGroup.enter()
DispatchQueue.global().async {
if let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) {
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
let extractedChapters = chapterLines.compactMap { line -> Chapter? in
let titleRange = line.range(withName: "title")
let startRange = line.range(withName: "start")
var hours: Double?
var minutes: Double?
var seconds: Double?
guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description)
else {
return nil
}
if startComponents.count == 3 {
hours = Double(startComponents[0])
minutes = Double(startComponents[1])
seconds = Double(startComponents[2])
} else if startComponents.count == 2 {
minutes = Double(startComponents[0])
seconds = Double(startComponents[1])
let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces)
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil }
var hours: Double?
var minutes: Double?
var seconds: Double?
if startComponents.count == 3 {
hours = Double(startComponents[0])
minutes = Double(startComponents[1])
seconds = Double(startComponents[2])
} else if startComponents.count == 2 {
minutes = Double(startComponents[0])
seconds = Double(startComponents[1])
}
guard var startSeconds = seconds else { return nil }
startSeconds += (minutes ?? 0) * 60
startSeconds += (hours ?? 0) * 60 * 60
return Chapter(title: titleCapture, start: startSeconds)
}
guard var startSeconds = seconds else { return nil }
startSeconds += (minutes ?? 0) * 60
startSeconds += (hours ?? 0) * 60 * 60
return .init(title: titleCapture, start: startSeconds)
if !extractedChapters.isEmpty {
lock.lock()
capturedChapters[index] = extractedChapters
lock.unlock()
}
}
extractChaptersGroup.leave()
}
}
extractChaptersGroup.wait()
// Now we sort the keys of the capturedChapters dictionary.
// These keys correspond to the priority of each pattern.
let sortedKeys = Array(capturedChapters.keys).sorted(by: <)
// Return first non-empty result in the order of patterns
for key in sortedKeys {
if let chapters = capturedChapters[key], !chapters.isEmpty {
return chapters
}
}
return []

View File

@@ -232,7 +232,10 @@ final class AVPlayerBackend: PlayerBackend {
upgrading: Bool = false
) {
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
var error: NSError?
switch self?.asset?.statusOfValue(forKey: "duration", error: &error) {

View File

@@ -217,9 +217,22 @@ final class MPVBackend: PlayerBackend {
#endif
var captions: Captions?
if let captionsLanguageCode = Defaults[.captionsLanguageCode] {
captions = video.captions.first { $0.code == captionsLanguageCode } ??
video.captions.first { $0.code.contains(captionsLanguageCode) }
if Defaults[.captionsAutoShow] == true {
let 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 = {
@@ -254,9 +267,8 @@ final class MPVBackend: PlayerBackend {
self.startClientUpdates()
// Captions should only be displayed when selected by the user,
// not when the video starts. So, we remove them.
self.client?.removeSubs()
if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() }
PlayerModel.shared.captions = self.captions
if !preservingTime,
!upgrading,

View File

@@ -60,14 +60,43 @@ final class MPVClient: ObservableObject {
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
#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-pause-wait", Defaults[.mpvCachePauseWait]))
// PLAYBACK //
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, "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))
@@ -405,6 +434,22 @@ final class MPVClient: ObservableObject {
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 {
Int(getString("track-list/count") ?? "-1") ?? -1
}

View File

@@ -133,23 +133,22 @@ extension PlayerBackend {
}
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
// filter out non HLS streams
let nonHLSStreams = streams.filter { $0.kind != .hls }
// filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter {
$0.kind != .hls && $0.resolution <= maxResolution.value
}
// find max resolution from non HLS streams
let bestResolution = nonHLSStreams
.filter { $0.resolution <= maxResolution.value }
.max { $0.resolution < $1.resolution }
// find max resolution and bitrate from non-HLS streams
let bestResolutionStream = nonHLSStreams.max { $0.resolution < $1.resolution }
let bestBitrateStream = nonHLSStreams.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
// finde max bitrate from non HLS streams
let bestBitrate = nonHLSStreams
.filter { $0.resolution <= maxResolution.value }
.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value
let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate
return streams.map { stream in
if stream.kind == .hls {
stream.resolution = bestResolution?.resolution ?? maxResolution.value
stream.bitrate = bestBitrate?.bitrate ?? (bestResolution?.resolution.bitrate ?? maxResolution.value.bitrate)
stream.resolution = bestResolution
stream.bitrate = bestBitrate
stream.format = .hls
} else if stream.kind == .stream {
stream.format = .stream

View File

@@ -683,10 +683,11 @@ final class PlayerModel: ObservableObject {
return
}
guard let video = currentVideo else { return }
guard let stream = backend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: qualityProfile!.formats) else { return }
// First, we need to create an array with supported formats.
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 activeBackend != .appleAVPlayer {
@@ -698,7 +699,19 @@ final class PlayerModel: ObservableObject {
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 {
@@ -726,12 +739,19 @@ final class PlayerModel: ObservableObject {
show()
#endif
backend.closePiP()
if previousActiveBackend == .mpv {
saveTime {
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()
}
}

View File

@@ -56,82 +56,101 @@ extension PlayerModel {
}
}
func streamsWithInstance(instance _: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) {
func streamsWithInstance(instance: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) {
// Queue for stream processing
let streamProcessingQueue = DispatchQueue(label: "stream.yattee.streamProcessing.Queue", qos: .userInitiated)
let streamProcessingQueue = DispatchQueue(label: "stream.yattee.streamProcessing.Queue")
// Queue for accessing the processedStreams array
let processedStreamsQueue = DispatchQueue(label: "stream.yattee.processedStreams.Queue")
// DispatchGroup for managing multiple tasks
let streamProcessingGroup = DispatchGroup()
var processedStreams = [Stream]()
let instance = instance
var hasForbiddenAsset = false
var hasAllowedAsset = false
for stream in streams {
streamProcessingQueue.async(group: streamProcessingGroup) {
let forbiddenAssetTestGroup = DispatchGroup()
var hasForbiddenAsset = false
if !hasAllowedAsset, !hasForbiddenAsset, !instance.proxiesVideos, stream.format != Stream.Format.unknown {
let (nonHLSAssets, hlsURLs) = self.getAssets(from: [stream])
if let firstStream = nonHLSAssets.first {
let asset = firstStream.0
let url = firstStream.1
let requestRange = firstStream.2
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
self.testAsset(url: url, range: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
}
} else if instance.app == .piped {
self.testPipedAssets(asset: asset, requestRange: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in
hasForbiddenAsset = isForbidden
self.testPipedAssets(asset: asset!, requestRange: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
}
}
}
} 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
} else if let firstHLS = hlsURLs.first {
let asset = AVURLAsset(url: firstHLS)
if instance.app == .piped {
self.testPipedAssets(asset: asset, requestRange: nil, isHLS: true, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
}
}
}
}
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)
if instance.app == .invidious, 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 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
}
} 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
}
}
if let video = stream.videoAsset {
PipedAPI.nonProxiedAsset(asset: video) { nonProxiedVideoAsset in
stream.videoAsset = nonProxiedVideoAsset
}
}
}
@@ -152,21 +171,21 @@ extension PlayerModel {
}
}
private func getAssets(from streams: [Stream]) -> (nonHLSAssets: [(Instance?, AVURLAsset?, URL, String?)], hlsURLs: [(Instance?, URL)]) {
var nonHLSAssets = [(Instance?, AVURLAsset?, URL, String?)]()
var hlsURLs = [(Instance?, URL)]()
private func getAssets(from streams: [Stream]) -> (nonHLSAssets: [(AVURLAsset?, URL, String?)], hlsURLs: [URL]) {
var nonHLSAssets = [(AVURLAsset?, URL, String?)]()
var hlsURLs = [URL]()
for stream in streams {
if stream.isHLS {
if let url = stream.hlsURL?.url {
hlsURLs.append((stream.instance, url))
hlsURLs.append(url)
}
} else {
if let asset = stream.audioAsset {
nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange))
nonHLSAssets.append((asset, asset.url, stream.requestRange))
}
if let asset = stream.videoAsset {
nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange))
nonHLSAssets.append((asset, asset.url, stream.requestRange))
}
}
}
@@ -174,33 +193,35 @@ extension PlayerModel {
return (nonHLSAssets, hlsURLs)
}
private func testAsset(url: URL, range: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) {
private func testAsset(url: URL, range: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Int) -> 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)
completion(statusCode)
forbiddenAssetTestGroup.leave()
}
}
private func testPipedAssets(asset: AVURLAsset, requestRange: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) {
private func testPipedAssets(asset: AVURLAsset, requestRange: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Int) -> 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)
completion(0)
}
}
}
func streamsSorter(_ lhs: Stream, _ rhs: Stream) -> Bool {
if lhs.resolution.isNil || rhs.resolution.isNil {
func streamsSorter(lhs: Stream, rhs: Stream) -> Bool {
// Use optional chaining to simplify nil handling
guard let lhsRes = lhs.resolution?.height, let rhsRes = rhs.resolution?.height else {
return lhs.kind < rhs.kind
}
return lhs.kind == rhs.kind ? (lhs.resolution.height > rhs.resolution.height) : (lhs.kind < rhs.kind)
// Compare either kind or resolution based on conditions
return lhs.kind == rhs.kind ? (lhsRes > rhsRes) : (lhs.kind < rhs.kind)
}
}

View File

@@ -103,9 +103,8 @@ enum Constants {
#elseif os(iOS)
if isIPad {
return .sidebar
} else {
return .tab
}
return .tab
#else
return .tab
#endif

View File

@@ -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 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 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))
#if os(iOS)
@@ -266,7 +266,10 @@ extension Defaults.Keys {
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "120")
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 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 feedCacheSize = Key<String>("feedCacheSize", default: "50")
@@ -300,7 +303,12 @@ extension Defaults.Keys {
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
static let captionsAutoShow = Key<Bool>("captionsAutoShow", default: false)
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue)
static let 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 lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)

View File

@@ -88,26 +88,38 @@ struct FavoriteItemView: View {
reloadVisibleWatches()
} else {
resource?.addObserver(store)
loadCacheAndResource()
DispatchQueue.main.async {
self.loadCacheAndResource()
}
}
}
.onDisappear {
resource?.removeObservers(ownedBy: store)
}
.onChange(of: player.currentVideo) { _ in reloadVisibleWatches() }
.onChange(of: hideShorts) { _ in reloadVisibleWatches() }
.onChange(of: hideWatched) { _ in reloadVisibleWatches() }
.onChange(of: favoritesChanged) { _ in reloadVisibleWatches() }
.onChange(of: player.currentVideo) { _ in if !player.presentingPlayer { reloadVisibleWatches() } }
.onChange(of: hideShorts) { _ in if !player.presentingPlayer { reloadVisibleWatches() } }
.onChange(of: hideWatched) { _ in if !player.presentingPlayer { reloadVisibleWatches() } }
// Delay is necessary to update the list with the new items.
.onChange(of: favoritesChanged) { _ in if !player.presentingPlayer { Delay.by(1.0) { reloadVisibleWatches() } } }
.onChange(of: player.presentingPlayer) { _ in
if player.presentingPlayer {
resource?.removeObservers(ownedBy: store)
} else {
resource?.addObserver(store)
}
}
}
}
.id(watchModel.historyToken)
.onChange(of: accounts.current) { _ in
resource?.removeObservers(ownedBy: store)
resource?.addObserver(store)
loadCacheAndResource(force: true)
DispatchQueue.main.async {
loadCacheAndResource(force: true)
}
}
.onChange(of: watchModel.historyToken) { _ in
reloadVisibleWatches()
if !player.presentingPlayer {
reloadVisibleWatches()
}
}
}
@@ -159,24 +171,26 @@ struct FavoriteItemView: View {
}
func reloadVisibleWatches() {
guard item.section == .history else { return }
DispatchQueue.main.async {
guard item.section == .history else { return }
visibleWatches = []
visibleWatches = []
let watches = Array(
watches
.filter { $0.videoID != player.currentVideo?.videoID && itemVisible(.init(video: $0.video)) }
.prefix(favoritesModel.limit(item))
)
let last = watches.last
let watches = Array(
watches
.filter { $0.videoID != player.currentVideo?.videoID && itemVisible(.init(video: $0.video)) }
.prefix(favoritesModel.limit(item))
)
let last = watches.last
for watch in watches {
player.loadHistoryVideoDetails(watch) {
guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return }
visibleWatches.append(watch)
for watch in watches {
player.loadHistoryVideoDetails(watch) {
guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return }
visibleWatches.append(watch)
if watch == last {
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() }
if watch == last {
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() }
}
}
}
}
@@ -227,6 +241,9 @@ struct FavoriteItemView: View {
onSuccess = { response in
if let videos: [Video] = response.typedContent() {
FeedCacheModel.shared.storeFeed(account: accounts.current, videos: videos)
DispatchQueue.main.async {
store.contentItems = contentItems
}
}
}
case let .channel(_, id, name):
@@ -239,19 +256,21 @@ struct FavoriteItemView: View {
}
onSuccess = { response in
if let channel: Channel = response.typedContent() {
ChannelsCacheModel.shared.store(channel)
store.contentItems = ContentItem.array(of: channel.videos)
} else if let videos: [Video] = response.typedContent() {
channel.videos = videos
ChannelsCacheModel.shared.store(channel)
store.contentItems = ContentItem.array(of: videos)
} else if let channelPage: ChannelPage = response.typedContent() {
if let channel = channelPage.channel {
DispatchQueue.main.async {
if let channel: Channel = response.typedContent() {
ChannelsCacheModel.shared.store(channel)
}
store.contentItems = ContentItem.array(of: channel.videos)
} else if let videos: [Video] = response.typedContent() {
channel.videos = videos
ChannelsCacheModel.shared.store(channel)
store.contentItems = ContentItem.array(of: videos)
} else if let channelPage: ChannelPage = response.typedContent() {
if let channel = channelPage.channel {
ChannelsCacheModel.shared.store(channel)
}
store.contentItems = channelPage.results
store.contentItems = channelPage.results
}
}
}
case let .channelPlaylist(_, id, title):
@@ -264,6 +283,9 @@ struct FavoriteItemView: View {
onSuccess = { response in
if let playlist: ChannelPlaylist = response.typedContent() {
ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist)
DispatchQueue.main.async {
store.contentItems = contentItems
}
}
}
case let .playlist(_, id):
@@ -272,12 +294,16 @@ struct FavoriteItemView: View {
if let playlist = playlists.first(where: { $0.id == id }) {
contentItems = ContentItem.array(of: playlist.videos)
}
DispatchQueue.main.async {
store.contentItems = contentItems
}
default:
contentItems = []
}
if !contentItems.isEmpty {
store.contentItems = contentItems
DispatchQueue.main.async {
store.contentItems = contentItems
}
}
if force {

View File

@@ -5,9 +5,11 @@ import UniformTypeIdentifiers
struct HomeView: View {
@ObservedObject private var accounts = AccountsModel.shared
@ObservedObject private var player = PlayerModel.shared
@State private var presentingHomeSettings = false
@State private var favoritesChanged = false
@State private var updateTask: Task<Void, Never>?
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
var watches: FetchedResults<Watch>
@@ -16,8 +18,6 @@ struct HomeView: View {
@State private var recentDocumentsID = UUID()
#endif
var favoritesObserver: Any?
#if !os(tvOS)
@Default(.favorites) private var favorites
@Default(.widgetsSettings) private var widgetsSettings
@@ -124,6 +124,24 @@ struct HomeView: View {
}
}
}
.onDisappear {
updateTask?.cancel()
}
.onChange(of: player.presentingPlayer) { _ in
if player.presentingPlayer {
updateTask?.cancel()
} else {
Task {
for await _ in Defaults.updates(.favorites) {
favoritesChanged.toggle()
}
for await _ in Defaults.updates(.widgetsSettings) {
favoritesChanged.toggle()
}
}
}
}
.redrawOn(change: favoritesChanged)

109
Shared/LanguageCodes.swift Normal file
View 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"
}
}

View File

@@ -333,9 +333,19 @@ struct ControlsOverlay: View {
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue {
Text(captions.code)
.foregroundColor(.primary)
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
}
.frame(width: 240)
@@ -351,8 +361,18 @@ struct ControlsOverlay: View {
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
HStack(spacing: 8) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue {
Text(captions.code)
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
}
.frame(maxWidth: 320)

View File

@@ -1,3 +1,4 @@
import Combine
import Defaults
import SwiftUI
@@ -383,23 +384,35 @@ struct PlaybackSettings: View {
}
@ViewBuilder private var captionsButton: some View {
let videoCaptions = player.currentVideo?.captions
#if os(macOS)
captionsPicker
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
captionsPicker
if videoCaptions?.isEmpty == false {
captionsPicker
}
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = player.captions {
Text(captions.code)
if let captions = player.captions,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if videoCaptions?.isEmpty == true {
Text("Not available")
} else {
Text("Disabled")
}
}
}
.frame(alignment: .trailing)
.frame(height: 40)
.disabled(videoCaptions?.isEmpty == true)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)

View File

@@ -65,7 +65,7 @@ import SwiftUI
}
static var thumbnailHeight: Double {
thumbnailWidth / 1.7777
thumbnailWidth / (16 / 9)
}
}

View File

@@ -5,8 +5,11 @@ struct AdvancedSettings: View {
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
@Default(.mpvCacheSecs) private var mpvCacheSecs
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
@Default(.mpvCachePauseInital) private var mpvCachePauseInital
@Default(.mpvDeinterlace) private var mpvDeinterlace
@Default(.mpvEnableLogging) private var mpvEnableLogging
@Default(.mpvHWdec) private var mpvHWdec
@Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo
@Default(.showCacheStatus) private var showCacheStatus
@Default(.feedCacheSize) private var feedCacheSize
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
@@ -68,9 +71,39 @@ struct AdvancedSettings: View {
mpvEnableLoggingToggle
#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 {
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)
#if !os(macOS)
.keyboardType(.numberPad)
@@ -79,8 +112,22 @@ struct AdvancedSettings: View {
.multilineTextAlignment(.trailing)
HStack {
Text("cache-pause-wait")
.frame(minWidth: 140, alignment: .leading)
Group {
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)
#if !os(macOS)
.keyboardType(.numberPad)
@@ -88,7 +135,75 @@ struct AdvancedSettings: View {
}
.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
}
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 {
logButton
@@ -103,20 +218,19 @@ struct AdvancedSettings: View {
}
@ViewBuilder var mpvFooter: some View {
let url = "https://mpv.io/manual/master"
let url = "https://mpv.io/manual/stable/"
VStack(alignment: .leading) {
Text("Restart the app to apply the settings above.")
.padding(.bottom, 1)
VStack(alignment: .leading, spacing: 2) {
#if os(tvOS)
Text("More info can be found in MPV Documentation:")
Text("More info can be found in MPV reference manual:")
Text(url)
#else
Text("More info can be found in:")
Link("MPV Documentation", destination: URL(string: url)!)
#if os(macOS)
.onHover(perform: onHover(_:))
#endif
Text("Further information can be found in the ")
+ Text("MPV reference manual").underline().bold()
+ Text(" by clicking on the link icon next to the option.")
#endif
}
}

View File

@@ -30,12 +30,19 @@ struct PlayerSettings: View {
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
@Default(.showRelated) private var showRelated
@Default(.showInspector) private var showInspector
@Default(.showChapters) private var showChapters
@Default(.showChapterThumbnails) private var showThumbnails
@Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent
@Default(.expandChapters) private var expandChapters
@Default(.showRelated) private var showRelated
@Default(.captionsAutoShow) private var captionsAutoShow
@Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode
@Default(.captionsFallbackLanguageCode) private var captionsFallbackLanguageCode
@Default(.captionsFontScaleSize) private var captionsFontScaleSize
@Default(.captionsFontColor) private var captionsFontColor
@ObservedObject private var accounts = AccountsModel.shared
@@ -45,6 +52,11 @@ struct PlayerSettings: View {
}
#endif
#if os(tvOS)
@State private var isShowingDefaultLanguagePicker = false
@State private var isShowingFallbackLanguagePicker = false
#endif
var body: some View {
Group {
#if os(macOS)
@@ -93,7 +105,54 @@ struct PlayerSettings: View {
inspectorVisibilityPicker
#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())) {
showChaptersToggle
showThumbnailsToggle
@@ -279,6 +338,103 @@ struct PlayerSettings: View {
}
#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)
private var inspectorVisibilityPicker: some View {
Picker("Inspector", selection: $showInspector) {

View File

@@ -136,9 +136,20 @@ struct QualityProfileForm: View {
var formatsFooter: some View {
VStack(alignment: .leading) {
Text("Formats can be reordered and will be selected in this order.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
if #available(iOS 16.0, *) {
Text("Formats can be reordered and will be selected in this order.")
.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.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
@@ -304,7 +315,7 @@ struct QualityProfileForm: View {
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
guard backend == .appleAVPlayer else { return false }
return resolution.value > .hd1080p60
return resolution.value > .hd720p30
}
func initializeForm() {

View File

@@ -4,6 +4,8 @@ import SwiftUI
struct FeedView: View {
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var feedCount = UnwatchedFeedCountModel.shared
@Default(.showCacheStatus) private var showCacheStatus
@@ -12,10 +14,155 @@ struct FeedView: View {
#endif
var videos: [ContentItem] {
ContentItem.array(of: feed.videos)
guard let selectedChannel = selectedChannel else {
return ContentItem.array(of: feed.videos)
}
return ContentItem.array(of: feed.videos.filter {
$0.channel.id == selectedChannel.id
})
}
var channels: [Channel] {
feed.videos.map {
$0.channel
}.unique()
}
@State private var selectedChannel: Channel?
#if os(tvOS)
@FocusState private var focusedChannel: String?
#endif
@State private var feedChannelsViewVisible = false
private var navigation = NavigationModel.shared
private let dismiss_channel_list_id = "dismiss_channel_list_id"
var body: some View {
#if os(tvOS)
GeometryReader { geometry in
ZStack {
// selected channel feed view
HStack(spacing: 0) {
// sidebar - show channels
if feedChannelsViewVisible {
Spacer()
.frame(width: geometry.size.width * 0.3)
}
selectedFeedView
}
.disabled(feedChannelsViewVisible)
.frame(width: geometry.size.width, height: geometry.size.height)
if feedChannelsViewVisible {
HStack(spacing: 0) {
// sidebar - show channels
feedChannelsView
.padding(.all)
.frame(width: geometry.size.width * 0.3)
.background()
.clipShape(RoundedRectangle(cornerRadius: 16))
.contentShape(RoundedRectangle(cornerRadius: 16))
Rectangle()
.fill(.clear)
.id(dismiss_channel_list_id)
.focusable()
.focused(self.$focusedChannel, equals: dismiss_channel_list_id)
}
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
}
}
#else
selectedFeedView
#endif
}
#if os(tvOS)
var feedChannelsView: some View {
ScrollViewReader { proxy in
VStack {
Text("Channels")
.font(.subheadline)
if #available(tvOS 17.0, *) {
List(selection: $selectedChannel) {
Button(action: {
self.selectedChannel = nil
self.feedChannelsViewVisible = false
}) {
HStack(spacing: 16) {
Image(systemName: RecentsModel.symbolSystemImage("A"))
.imageScale(.large)
.foregroundColor(.accentColor)
.frame(width: 35, height: 35)
Text("All")
Spacer()
feedCount.unwatchedText
}
}
.padding(.all)
.background(RoundedRectangle(cornerRadius: 8.0)
.fill(self.selectedChannel == nil ? Color.secondary : Color.clear))
.font(.caption)
.buttonStyle(PlainButtonStyle())
.focused(self.$focusedChannel, equals: "all")
ForEach(channels, id: \.self) { channel in
Button(action: {
self.selectedChannel = channel
self.feedChannelsViewVisible = false
}) {
HStack(spacing: 16) {
ChannelAvatarView(channel: channel, subscribedBadge: false)
.frame(width: 50, height: 50)
Text(channel.name)
.lineLimit(1)
Spacer()
if let unwatchedCount = feedCount.unwatchedByChannelText(channel) {
unwatchedCount
}
}
}
.padding(.all)
.background(RoundedRectangle(cornerRadius: 8.0)
.fill(self.selectedChannel == channel ? Color.secondary : Color.clear))
.font(.caption)
.buttonStyle(PlainButtonStyle())
.focused(self.$focusedChannel, equals: channel.id)
}
}
.onChange(of: self.focusedChannel) {
if self.focusedChannel == "all" {
withAnimation {
self.selectedChannel = nil
}
} else if self.focusedChannel == dismiss_channel_list_id {
self.feedChannelsViewVisible = false
} else {
withAnimation {
self.selectedChannel = channels.first {
$0.id == self.focusedChannel
}
}
}
}
.onAppear {
guard let selectedChannel = self.selectedChannel else {
return
}
proxy.scrollTo(selectedChannel, anchor: .top)
}
.onExitCommand {
withAnimation {
self.feedChannelsViewVisible = false
}
}
}
}
}
}
#endif
var selectedFeedView: some View {
VerticalCells(items: videos) { if shouldDisplayHeader { header } }
.environment(\.loadMoreContentHandler) { feed.loadNextPage() }
.onAppear {
@@ -49,33 +196,51 @@ struct FeedView: View {
}
var header: some View {
HStack {
HStack(spacing: 16) {
#if os(tvOS)
SubscriptionsPageButton()
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
HideWatchedButtons()
HideShortsButtons()
#endif
if showCacheStatus {
Spacer()
CacheStatusHeader(
refreshTime: feed.formattedFeedTime,
isLoading: feed.isLoading
)
}
#if os(tvOS)
Button {
feed.loadResources(force: true)
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
Button(action: {
withAnimation {
self.feedChannelsViewVisible = true
self.focusedChannel = selectedChannel?.id ?? "all"
}
}) {
Label("Channels", systemImage: "filemenu.and.selection")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
}
.opacity(feedChannelsViewVisible ? 0 : 1)
.frame(minWidth: feedChannelsViewVisible ? 0 : nil, maxWidth: feedChannelsViewVisible ? 0 : nil)
channelHeaderView
if selectedChannel == nil {
Spacer()
}
if feedChannelsViewVisible == false {
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
HideWatchedButtons()
HideShortsButtons()
}
#endif
if feedChannelsViewVisible == false {
if showCacheStatus {
CacheStatusHeader(
refreshTime: feed.formattedFeedTime,
isLoading: feed.isLoading
)
}
#if os(tvOS)
Button {
feed.loadResources(force: true)
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
}
#endif
}
}
.padding(.leading, 30)
#if os(tvOS)
@@ -84,6 +249,46 @@ struct FeedView: View {
#endif
}
var channelHeaderView: some View {
guard let selectedChannel = selectedChannel else {
return AnyView(
Text("All Channels")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.padding(0)
.padding(.leading, 16)
)
}
return AnyView(
HStack(spacing: 16) {
ChannelAvatarView(channel: selectedChannel, subscribedBadge: false)
.id("channel-avatar-\(selectedChannel.id)")
.frame(width: 80, height: 80)
Text("\(selectedChannel.name)")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
Spacer()
if feedChannelsViewVisible == false {
Button(action: {
navigation.openChannel(selectedChannel, navigationStyle: .tab)
}) {
Text("Visit Channel")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
}
}
.padding(0)
.padding(.leading, 16)
)
}
var shouldDisplayHeader: Bool {
#if os(tvOS)
true

View File

@@ -20,6 +20,7 @@ enum URLTester {
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

View 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
}
}

View File

@@ -154,7 +154,12 @@ struct YatteeApp: App {
#if DEBUG
SiestaLog.Category.enabled = .common
#endif
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
#if os(tvOS)
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
#else
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
#endif
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
if !Defaults[.lastAccountIsPublic] {
@@ -206,6 +211,9 @@ struct YatteeApp: App {
}
#endif
// Initialize UserAgentManager
_ = UserAgentManager.shared
DispatchQueue.global(qos: .userInitiated).async {
URLBookmarkModel.shared.refreshAll()
}

View File

@@ -387,7 +387,7 @@
"Backend" = "الواجهة الخلفية";
"Badge" = "الشارة";
"Close PiP when starting playing other video" = "غلق الفيديو المصغر عند بدء تشغيل فيديو آخر";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).";
"Filter" = " عامل التصفية";
"Frontend URL" = "عنوان URL للواجهة الأمامية";
"Fullscreen size" = "حجم ملء الشاشة";
@@ -628,4 +628,4 @@
"Password required to import" = "كلمة المرور مطلوبة للإستيراد";
"Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد";
"Export in progress..." = "جارِ التصدير...";
"In progress..." = "في تَقَدم…";
"In progress..." = "في طور الأجراء…";

View File

@@ -264,7 +264,7 @@
"Don't use public locations" = "Ne pas utiliser d'instances publiques";
"Enable Return YouTube Dislike" = "Activer Return YouTube Dislike";
"Enter fullscreen in landscape" = "Entrer en plein écran en mode paysage";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappels d'aimer la vidéo, de s'abonner ou d'interagir avec le créateur sur une plateforme gratuite ou payante.\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Rappels d'aimer la vidéo, de s'abonner ou d'interagir avec le créateur sur une plateforme gratuite ou payante.";
"Frontend URL" = "URL frontale";
"Public Locations" = "Instances publiques";
"Public Manifest" = "Manifeste publique";

View File

@@ -0,0 +1,43 @@
"%@ Playlist" = "%@ 플레이리스트";
"%@ subscribers" = "%@ 구독자";
"10 seconds forwards/backwards" = "10초 건너뛰기/뒤로 가기";
"Accounts" = "계정";
"Accounts are not supported for the application of this instance" = "해당 애플리케이션 인스턴스는 계정을 지원하지 않습니다";
"Add Location" = "주소 추가";
"Add Location..." = "주소 추가...";
"Add profile..." = "프로필 추가...";
"Add Quality Profile" = "품질 프로필 추가";
"Add to %@" = "%@에 추가";
"Add to Favorites" = "즐겨찾기에 추가";
"Add to Playlist" = "플레이리스트에 추가";
"Add to Playlist..." = "플레이리스트에 추가...";
"Advanced" = "고급";
/* Trending category, section containing all kinds of videos */
"All" = "전체";
"Always use AVPlayer for live videos" = "라이브 동영상에 항상 AVPlayer 사용";
"Anonymous" = "익명";
/* Video date filter in search
Video duration filter in search */
"Any" = "모두";
"Apply to all" = "모두 적용";
"Are you sure you want to clear search history?" = "검색 기록을 삭제하시겠습니까?";
"Are you sure you want to delete playlist?" = "플레이리스트를 삭제하시겠습니까?";
"Are you sure you want to restore default quality profiles?" = "기본 품질 프로필로 복구하시겠습니까?";
"Are you sure you want to unsubscribe from %@?" = "%@을/를 구독 해제하시겠습니까?";
"Autoplaying Next" = "다음으로 자동재생";
"Backend" = "백엔드";
"Badge" = "배지";
"Battery" = "배터리";
"Cellular" = "셀룰러";
" subscribers" = " 구독자";
"%@ Channel" = "%@ 채널";
"%lld videos" = "%lld 동영상";
"Are you sure you want to clear history of watched videos?" = "동영상 시청 기록을 삭제하시겠습니까?";
"Add Account" = "계정 추가";
"Add Account..." = "계정 추가...";
"Automatic" = "자동";
"Close" = "닫기";

View File

@@ -406,7 +406,7 @@
"Country" = "País";
"Clear All" = "Limpar Tudo";
"Clear All Recents" = "Limpar Todos os Recentes";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).";
"Duration" = "Duração";
"Edit Quality Profile" = "Editar Perfil de Qualidade";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussões acontecem no Discord e no Matrix. É um bom lugar para perguntas gerais.";

View File

@@ -1070,12 +1070,18 @@
37FFC440272734C3009FFD26 /* 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 */; };
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 */; };
/* End PBXBuildFile section */
@@ -1545,8 +1551,10 @@
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -2295,6 +2303,7 @@
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */,
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */,
375B537828DF6CBB004C1D19 /* Localizable.strings */,
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
@@ -2303,6 +2312,7 @@
37FFC43F272734C3009FFD26 /* Throttle.swift */,
378FFBC328660172009E3FBE /* URLParser.swift */,
E258F3892BF61BD2005B8C28 /* URLTester.swift */,
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */,
37D4B0C22671614700C925CA /* YatteeApp.swift */,
37D4B0C42671614800C925CA /* Assets.xcassets */,
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
@@ -3149,6 +3159,7 @@
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
3752069D285E910600CA655F /* ChapterView.swift in Sources */,
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */,
E24DC6582BFA124100BF6187 /* UserAgentManager.swift in Sources */,
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */,
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
@@ -3225,6 +3236,7 @@
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
@@ -3405,6 +3417,7 @@
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */,
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
@@ -3413,6 +3426,7 @@
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
E24DC6592BFA124100BF6187 /* UserAgentManager.swift in Sources */,
374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */,
371AC0A0294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */,
379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */,
@@ -3878,6 +3892,7 @@
37BA221329526A19000DAD1F /* ControlsGradientView.swift in Sources */,
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
37130A61277657300033018A /* PersistenceController.swift in Sources */,
E24DC65A2BFA124100BF6187 /* UserAgentManager.swift in Sources */,
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
@@ -4000,6 +4015,7 @@
37FB28432721B22200A57617 /* ContentItem.swift in Sources */,
37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */,
37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */,
37AAF2A226741C97007FC770 /* FeedView.swift in Sources */,
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,
@@ -4087,7 +4103,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -4118,7 +4134,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -4149,7 +4165,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4169,7 +4185,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4333,7 +4349,7 @@
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@@ -4386,7 +4402,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -4438,7 +4454,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -4477,7 +4493,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
DEAD_CODE_STRIPPING = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
ENABLE_APP_SANDBOX = YES;
@@ -4512,7 +4528,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4536,7 +4552,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4562,7 +4578,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4587,7 +4603,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4613,7 +4629,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -4653,7 +4669,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_ASSET_PATHS = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
@@ -4694,7 +4710,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4718,7 +4734,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 186;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -25,7 +25,7 @@
"location" : "https://github.com/hyperoslo/Cache.git",
"state" : {
"branch" : "master",
"revision" : "f44a8f6b5ec27730198725ccc542fef0d1cc6b3d"
"revision" : "fd97438a85d44aa4f7186145cdba90e3c36f1c94"
}
},
{
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"revision" : "57da4b1270cab7c2228919eabc0e4e1bf93e48ea",
"version" : "5.2.2"
"revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a",
"version" : "5.2.3"
}
},
{
@@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "5642d1ffe3dbe628592443bd14154e31929727b4",
"version" : "5.19.2"
"revision" : "b8523c1642f3c142b06dd98443ea7c48343a4dfd",
"version" : "5.19.3"
}
},
{