Compare commits

...

29 Commits

Author SHA1 Message Date
Arkadiusz Fal
f7b35925b4 Change syntax to overcome compiler issue 2023-02-28 22:13:52 +01:00
Arkadiusz Fal
5bac92fdbf Bump build number to 138 2023-02-28 21:31:58 +01:00
Arkadiusz Fal
bf2c9a9e43 Update changelog 2023-02-28 21:31:26 +01:00
Arkadiusz Fal
f8d79bb08c Add browsing setting for unwatched feed
Fix #383
2023-02-28 21:27:47 +01:00
Arkadiusz Fal
b9ad5bc633 Hide share button when it should not be available 2023-02-28 21:04:42 +01:00
Arkadiusz Fal
f1e132a909 Add channel tabs and pagination
Fix #135
2023-02-28 21:04:42 +01:00
Arkadiusz Fal
d58026bcef Fix favorite channel button on tvOS 2023-02-26 19:14:06 +01:00
Anonymous
4697aa9696 Translated using Weblate (Swedish)
Currently translated at 24.1% (105 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/sv/
2023-02-26 18:55:38 +01:00
Anonymous
dbf3537f22 Translated using Weblate (Arabic)
Currently translated at 97.4% (424 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2023-02-26 18:55:38 +01:00
Anonymous
474e280faa Translated using Weblate (Czech)
Currently translated at 98.8% (430 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/cs/
2023-02-26 18:55:38 +01:00
Anonymous
04fe18ac4c Translated using Weblate (Spanish)
Currently translated at 26.2% (114 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/es/
2023-02-26 18:55:38 +01:00
Anonymous
66665db344 Translated using Weblate (Turkish)
Currently translated at 52.8% (230 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/tr/
2023-02-26 18:55:38 +01:00
Arkadiusz Fal
38612eae52 Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.1% (301 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/nb_NO/
2023-02-26 18:55:38 +01:00
Anonymous
80c1f63e35 Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.1% (301 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/nb_NO/
2023-02-26 18:55:38 +01:00
Arkadiusz Fal
f4fb31e9e2 Translated using Weblate (Hindi)
Currently translated at 78.1% (340 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hi/
2023-02-26 18:55:38 +01:00
Anonymous
44ff68b3c1 Translated using Weblate (Hindi)
Currently translated at 78.1% (340 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/hi/
2023-02-26 18:55:38 +01:00
Anonymous
737f762bfb Translated using Weblate (French)
Currently translated at 90.8% (395 of 435 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/fr/
2023-02-26 18:55:38 +01:00
Arkadiusz Fal
2844838485 Update workflows 2023-02-26 18:54:12 +01:00
Arkadiusz Fal
f39accdf7e Update workflows 2023-02-26 18:42:43 +01:00
github-actions[bot]
75ac11c60d Bump build number to 137 2023-02-25 21:29:30 +00:00
Arkadiusz Fal
0d3138b36e Update release workflow 2023-02-25 22:18:24 +01:00
Arkadiusz Fal
582f07388e Update changelog 2023-02-25 17:21:23 +01:00
Arkadiusz Fal
2b18f0cffa Add hiding short videos 2023-02-25 17:18:35 +01:00
Arkadiusz Fal
ef401168ec Use only tab navigation on iPhone 2023-02-25 16:46:09 +01:00
Arkadiusz Fal
0c7af0351b Add bump build action 2023-02-25 14:51:50 +01:00
Arkadiusz Fal
0995e3ee2f Add Arabic, Portugese and Portugese (Brazil) localizations 2023-02-25 14:51:50 +01:00
Arkadiusz Fal
cbd95ebc58 Update xcodeproj 2023-02-25 14:51:50 +01:00
Arkadiusz Fal
528863bf1b Update release workflow 2023-02-25 14:51:50 +01:00
Arkadiusz Fal
c931aa09a2 Update packages 2023-02-25 14:51:50 +01:00
50 changed files with 1820 additions and 180 deletions

34
.github/workflows/bump-build.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Bump build number
on:
workflow_dispatch:
env:
APP_NAME: Yattee
jobs:
bump_build:
name: Bump build number
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Configure git
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- uses: maierj/fastlane-action@v3.0.0
with:
lane: bump_build
- run: echo "BUILD_NUMBER=$(cat Yattee.xcodeproj/project.pbxproj | grep -m 1 CURRENT_PROJECT_VERSION | cut -d' ' -f3 | sed 's/;//g')" >> $GITHUB_ENV
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GIT_AUTHORIZATION }}
branch: actions/bump-build-to-${{ env.BUILD_NUMBER }}
base: main
title: Bump build number to ${{ env.BUILD_NUMBER }}

View File

@@ -1,10 +1,9 @@
name: Build and release to TestFlight and GitHub
on:
push:
branches: [ main ]
workflow_dispatch:
env:
APP_NAME: Yattee
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
@@ -85,9 +84,6 @@ jobs:
- uses: actions/download-artifact@v3
with:
path: artifacts
- name: Display structure of downloaded files
run: ls -R
working-directory: artifacts
- uses: ncipollo/release-action@v1
with:
artifacts: artifacts/**/*.ipa,artifacts/**/*.zip

4
.gitignore vendored
View File

@@ -97,3 +97,7 @@ iOSInjectionProject/
# User-specific xcconfig files
Xcode-config/DEVELOPMENT_TEAM.xcconfig
# Bundler
.bundle/
Vendor/bundle/

View File

@@ -11,10 +11,10 @@ extension Backport where Content: View {
}
@ViewBuilder func scrollDismissesKeyboardInteractively() -> some View {
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) {
content.scrollDismissesKeyboard(.interactively)
} else {
content
}
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) {
content.scrollDismissesKeyboard(.interactively)
} else {
content
}
}
}

View File

@@ -1,4 +1,11 @@
## Build 136
* Fixed issue with loading channels in Favorites with Invidious
## Build 138
* Added pagination/infinite scroll for channel contents (Invidious and Piped)
* Added support for channel tabs for Invidious (previously available only for Piped)
* New browsing setting: "Show unwatched feed badges"
* Other minor changes and improvements
### Previous Builds
* Added filter to hide Short videos, available via view menu/toolbar button
* Added localizations: Arabic, Portugese, Portuguese (Brazil)
* Fixed reported crashes
* Other minor changes and improvements

View File

@@ -211,6 +211,7 @@ GEM
PLATFORMS
arm64-darwin-21
x86_64-darwin-19
x86_64-linux
DEPENDENCIES
fastlane

View File

@@ -109,17 +109,22 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
content.json.arrayValue.map(self.extractChannel)
}
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
self.extractChannel(from: content.json)
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPage in
self.extractChannelPage(from: content.json, forceNotLast: true)
}
configureTransformer(pathPattern("channels/*/videos"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPage in
self.extractChannelPage(from: content.json)
}
configureTransformer(pathPattern("channels/*/latest"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
content.json.dictionaryValue["videos"]?.arrayValue.map(self.extractVideo) ?? []
}
configureTransformer(pathPattern("channels/*/playlists"), requestMethods: [.get]) { (content: Entity<JSON>) -> [ContentItem] in
let playlists = (content.json.dictionaryValue["playlists"]?.arrayValue ?? []).compactMap { self.extractChannelPlaylist(from: $0) }
return ContentItem.array(of: playlists)
["latest", "playlists", "streams", "shorts", "channels", "videos"].forEach { type in
configureTransformer(pathPattern("channels/*/\(type)"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPage in
self.extractChannelPage(from: content.json)
}
}
configureTransformer(pathPattern("playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPlaylist in
@@ -266,11 +271,18 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
.onCompletion { _ in onCompletion() }
}
func channel(_ id: String, contentType: Channel.ContentType, data _: String?) -> Resource {
if contentType == .playlists {
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)/playlists"))
func channel(_ id: String, contentType: Channel.ContentType, data _: String?, page: String?) -> Resource {
if page.isNil, contentType == .videos {
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)"))
}
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)"))
var resource = resource(baseURL: account.url, path: basePathAppending("channels/\(id)/\(contentType.invidiousID)"))
if let page, !page.isEmpty {
resource = resource.withParam("continuation", page)
}
return resource
}
func channelByName(_: String) -> Resource? {
@@ -461,6 +473,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
let description = json["description"].stringValue
let length = json["lengthSeconds"].doubleValue
return Video(
instanceID: account.instanceID,
@@ -470,7 +483,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
videoID: videoID,
title: json["title"].stringValue,
author: json["author"].stringValue,
length: json["lengthSeconds"].doubleValue,
length: length,
published: published,
views: json["viewCount"].intValue,
description: description,
@@ -480,6 +493,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
indexID: indexID,
live: json["liveNow"].boolValue,
upcoming: json["isUpcoming"].boolValue,
short: length <= Video.shortLength,
publishedAt: publishedAt,
likes: json["likeCount"].int,
dislikes: json["dislikeCount"].int,
@@ -502,6 +516,14 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
thumbnailURL = "\(accountUrlComponents.scheme ?? "https"):\(thumbnailURL)"
}
let tabs = json["tabs"].arrayValue.compactMap { name in
if let name = name.string, let type = Channel.ContentType.from(name) {
return Channel.Tab(contentType: type, data: "")
}
return nil
}
return Channel(
app: .invidious,
id: json["authorId"].stringValue,
@@ -512,7 +534,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
subscriptionsCount: json["subCount"].int,
subscriptionsText: json["subCountText"].string,
totalViews: json["totalViews"].int,
videos: json.dictionaryValue["latestVideos"]?.arrayValue.map(extractVideo) ?? []
videos: json.dictionaryValue["latestVideos"]?.arrayValue.map(extractVideo) ?? [],
tabs: tabs
)
}
@@ -550,6 +573,33 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
}
private static var contentItemsKeys = ["items", "videos", "latestVideos", "playlists", "relatedChannels"]
private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage {
let nextPage = json.dictionaryValue["continuation"]?.string
var contentItems = [ContentItem]()
var items = [ContentItem]()
if let key = Self.contentItemsKeys.first(where: { json.dictionaryValue.keys.contains($0) }),
let items = json.dictionaryValue[key]
{
contentItems = extractContentItems(from: items)
}
var last = false
if !forceNotLast {
last = nextPage?.isEmpty ?? true
}
return ChannelPage(
results: contentItems,
channel: extractChannel(from: json),
nextPage: nextPage,
last: last
)
}
private func extractStreams(from json: JSON) -> [Stream] {
let hls = extractHLSStreams(from: json)
if json["liveNow"].boolValue {
@@ -666,4 +716,33 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
)
}
}
private func extractContentItems(from json: JSON) -> [ContentItem] {
json.arrayValue.compactMap { extractContentItem(from: $0) }
}
private func extractContentItem(from json: JSON) -> ContentItem? {
let type = json.dictionaryValue["type"]?.string
if type == "channel" {
return ContentItem(channel: extractChannel(from: json))
} else if type == "playlist" {
return ContentItem(playlist: extractChannelPlaylist(from: json))
} else if type == "video" {
return ContentItem(video: extractVideo(from: json))
}
return nil
}
}
extension Channel.ContentType {
var invidiousID: String {
switch self {
case .livestreams:
return "streams"
default:
return rawValue
}
}
}

View File

@@ -284,7 +284,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
.onCompletion { _ in onCompletion() }
}
func channel(_ id: String, contentType: Channel.ContentType, data _: String?) -> Resource {
func channel(_ id: String, contentType: Channel.ContentType, data _: String?, page _: String?) -> Resource {
if contentType == .playlists {
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)/playlists"))
}

View File

@@ -6,6 +6,7 @@ import SwiftyJSON
final class PipedAPI: Service, ObservableObject, VideosAPI {
static var disallowedVideoCodecs = ["av01"]
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"]
static var contentItemsKeys = ["items", "content", "relatedStreams"]
@Published var account: Account!
@@ -40,8 +41,25 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
$0.headers["Authorization"] = self.account.token
}
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> Channel? in
self.extractChannel(from: content.json)
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> ChannelPage in
let nextPage = content.json.dictionaryValue["nextpage"]?.string
let channel = self.extractChannel(from: content.json)
return ChannelPage(
results: self.extractContentItems(from: self.contentItemsDictionary(from: content.json)),
channel: channel,
nextPage: nextPage,
last: nextPage.isNil
)
}
configureTransformer(pathPattern("/nextpage/channel/*")) { (content: Entity<JSON>) -> ChannelPage in
let nextPage = content.json.dictionaryValue["nextpage"]?.string
return ChannelPage(
results: self.extractContentItems(from: self.contentItemsDictionary(from: content.json)),
channel: self.extractChannel(from: content.json),
nextPage: nextPage,
last: nextPage.isNil
)
}
configureTransformer(pathPattern("channels/tabs*")) { (content: Entity<JSON>) -> [ContentItem] in
@@ -159,13 +177,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.url, path: "login")
}
func channel(_ id: String, contentType: Channel.ContentType, data: String?) -> Resource {
if contentType == .videos {
return resource(baseURL: account.url, path: "channel/\(id)")
func channel(_ id: String, contentType: Channel.ContentType, data: String?, page: String?) -> Resource {
let path = page.isNil ? "channel" : "nextpage/channel"
var channel: Siesta.Resource
if contentType == .videos || data.isNil {
channel = resource(baseURL: account.url, path: "\(path)/\(id)")
} else {
channel = resource(baseURL: account.url, path: "channels/tabs")
.withParam("data", data)
}
return resource(baseURL: account.url, path: "channels/tabs")
.withParam("data", data)
if let page, !page.isEmpty {
channel = channel.withParam("nextpage", page)
}
return channel
}
func channelByName(_ name: String) -> Resource? {
@@ -481,6 +509,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
chapters = extractChapters(from: description)
}
let length = details["duration"]?.double ?? 0
return Video(
instanceID: account.instanceID,
app: .piped,
@@ -488,13 +518,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
videoID: extractID(from: content),
title: details["title"]?.string ?? "",
author: author,
length: details["duration"]?.double ?? 0,
length: length,
published: published ?? "",
views: details["views"]?.int ?? 0,
description: description,
channel: Channel(app: .piped, id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
thumbnails: thumbnails,
live: live,
short: details["isShort"]?.bool ?? (length <= Video.shortLength),
likes: details["likes"]?.int,
dislikes: details["dislikes"]?.int,
streams: extractStreams(from: content),
@@ -697,4 +728,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return Chapter(title: title, image: image, start: start)
}
}
private func contentItemsDictionary(from content: JSON) -> JSON {
if let key = Self.contentItemsKeys.first(where: { content.dictionaryValue.keys.contains($0) }),
let items = content.dictionaryValue[key]
{
return items
}
return .null
}
}

View File

@@ -8,7 +8,7 @@ protocol VideosAPI {
static func withAnonymousAccountForInstanceURL(_ url: URL) -> Self
func channel(_ id: String, contentType: Channel.ContentType, data: String?) -> Resource
func channel(_ id: String, contentType: Channel.ContentType, data: String?, page: String?) -> Resource
func channelByName(_ name: String) -> Resource?
func channelByUsername(_ username: String) -> Resource?
func channelVideos(_ id: String) -> Resource
@@ -72,8 +72,8 @@ protocol VideosAPI {
}
extension VideosAPI {
func channel(_ id: String, contentType: Channel.ContentType, data: String? = nil) -> Resource {
channel(id, contentType: contentType, data: data)
func channel(_ id: String, contentType: Channel.ContentType, data: String? = nil, page: String? = nil) -> Resource {
channel(id, contentType: contentType, data: data, page: page)
}
func loadDetails(

View File

@@ -34,11 +34,11 @@ struct ChannelsCacheModel: CacheModel {
store(channel)
}
func retrieve(_ cacheKey: String) -> Channel? {
func retrieve(_ cacheKey: String) -> ChannelPage? {
logger.debug("retrieving cache for \(cacheKey)")
if let json = try? storage?.object(forKey: cacheKey) {
return Channel.from(json)
return ChannelPage(channel: Channel.from(json))
}
return nil

View File

@@ -110,12 +110,12 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
if let json = try? storage?.object(forKey: channelsCacheKey(account)),
let channels = json.dictionaryValue["channels"]
{
return channels.arrayValue.map { json in
return channels.arrayValue.compactMap { json in
let channel = Channel.from(json)
if !channel.hasExtendedDetails,
let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey)
{
return cache
return cache.channel
}
return channel

View File

@@ -11,6 +11,15 @@ struct Channel: Identifiable, Hashable {
case shorts
case channels
static func from(_ name: String) -> Self? {
let rawValueMatch = allCases.first { $0.rawValue == name }
guard rawValueMatch.isNil else { return rawValueMatch! }
if name == "streams" { return .livestreams }
return nil
}
var id: String {
rawValue
}
@@ -53,6 +62,10 @@ struct Channel: Identifiable, Hashable {
return "person.3"
}
}
var alwaysAvailable: Bool {
self == .videos || self == .playlists
}
}
struct Tab: Identifiable, Hashable {
@@ -110,7 +123,6 @@ struct Channel: Identifiable, Hashable {
}
func hasData(for contentType: ContentType) -> Bool {
guard contentType != .videos, contentType != .playlists else { return true }
return tabs.contains { $0.contentType == contentType }
}
@@ -132,7 +144,7 @@ struct Channel: Identifiable, Hashable {
}
var thumbnailURLOrCached: URL? {
thumbnailURL ?? ChannelsCacheModel.shared.retrieve(cacheKey)?.thumbnailURL
thumbnailURL ?? ChannelsCacheModel.shared.retrieve(cacheKey)?.channel?.thumbnailURL
}
var json: JSON {

8
Model/ChannelPage.swift Normal file
View File

@@ -0,0 +1,8 @@
import Foundation
struct ChannelPage {
var results = [ContentItem]()
var channel: Channel?
var nextPage: String?
var last = false
}

View File

@@ -1,5 +1,6 @@
import Cache
import CoreData
import Defaults
import Foundation
import Siesta
import SwiftyJSON
@@ -114,7 +115,7 @@ final class FeedModel: ObservableObject, CacheModel {
}
func calculateUnwatchedFeed() {
guard let account = accounts.current, accounts.signedIn else { return }
guard let account = accounts.current, accounts.signedIn, Defaults[.showUnwatchedFeedBadges] else { return }
let feed = cacheModel.retrieveFeed(account: account)
backgroundContext.perform { [weak self] in
guard let self else { return }
@@ -237,6 +238,10 @@ final class FeedModel: ObservableObject, CacheModel {
let watches = watchFetchRequestResult(videos, context: backgroundContext)
let watchesIDs = watches.map(\.videoID)
let unwatched = videos.filter { video in
if Defaults[.hideShorts], video.short {
return false
}
if !watchesIDs.contains(video.videoID) {
return true
}

View File

@@ -5,6 +5,8 @@ import SwiftUI
import SwiftyJSON
struct Video: Identifiable, Equatable, Hashable {
static let shortLength = 61.0
enum VideoID {
static func isValid(_ id: Video.ID) -> Bool {
isYouTube(id) || isPeerTube(id)
@@ -40,6 +42,7 @@ struct Video: Identifiable, Equatable, Hashable {
var live: Bool
var upcoming: Bool
var short: Bool
var streams = [Stream]()
@@ -74,6 +77,7 @@ struct Video: Identifiable, Equatable, Hashable {
indexID: String? = nil,
live: Bool = false,
upcoming: Bool = false,
short: Bool = false,
publishedAt: Date? = nil,
likes: Int? = nil,
dislikes: Int? = nil,
@@ -101,6 +105,7 @@ struct Video: Identifiable, Equatable, Hashable {
self.indexID = indexID
self.live = live
self.upcoming = upcoming
self.short = short
self.publishedAt = publishedAt
self.likes = likes
self.dislikes = dislikes
@@ -154,6 +159,7 @@ struct Video: Identifiable, Equatable, Hashable {
"indexID": indexID ?? "",
"live": live,
"upcoming": upcoming,
"short": short,
"publishedAt": publishedAt
]
}
@@ -180,6 +186,7 @@ struct Video: Identifiable, Equatable, Hashable {
indexID: json["indexID"].stringValue,
live: json["live"].boolValue,
upcoming: json["upcoming"].boolValue,
short: json["short"].boolValue,
publishedAt: dateFormatter.date(from: json["publishedAt"].stringValue)
)
}

View File

@@ -14,6 +14,7 @@ struct ChannelPlaylistView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.navigationStyle) private var navigationStyle
@Default(.channelPlaylistListingStyle) private var channelPlaylistListingStyle
@Default(.hideShorts) private var hideShorts
@ObservedObject private var accounts = AccountsModel.shared
var player = PlayerModel.shared
@@ -104,6 +105,7 @@ struct ChannelPlaylistView: View {
ToolbarItem(placement: playlistButtonsPlacement) {
HStack {
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
HideShortsButtons(hide: $hideShorts)
ShareButton(contentItem: contentItem)
favoriteButton
@@ -131,6 +133,10 @@ struct ChannelPlaylistView: View {
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
Section {
HideShortsButtons(hide: $hideShorts)
}
Section {
SettingsButtons()
}

View File

@@ -11,11 +11,12 @@ struct ChannelVideosView: View {
@State private var shareURL: URL?
@State private var subscriptionToggleButtonDisabled = false
@State private var page: ChannelPage?
@State private var contentType = Channel.ContentType.videos
@StateObject private var contentTypeItems = Store<[ContentItem]>()
@State private var descriptionExpanded = false
@StateObject private var store = Store<Channel>()
@StateObject private var store = Store<ChannelPage>()
@Environment(\.colorScheme) private var colorScheme
@@ -32,16 +33,13 @@ struct ChannelVideosView: View {
@Default(.channelPlaylistListingStyle) private var channelPlaylistListingStyle
@Default(.expandChannelDescription) private var expandChannelDescription
@Default(.hideShorts) private var hideShorts
var presentedChannel: Channel? {
store.item ?? channel ?? recents.presentedChannel
store.item?.channel ?? channel ?? recents.presentedChannel
}
var contentItems: [ContentItem] {
guard contentType != .videos else {
return ContentItem.array(of: presentedChannel?.videos ?? [])
}
return contentTypeItems.collection
}
@@ -62,6 +60,8 @@ struct ChannelVideosView: View {
viewsLabel
subscriptionToggleButton
favoriteButton
.labelStyle(.iconOnly)
}
contentTypePicker
.pickerStyle(.automatic)
@@ -98,8 +98,10 @@ struct ChannelVideosView: View {
banner
}
}
.environment(\.loadMoreContentHandler) { loadNextPage() }
.environment(\.inChannelView, true)
.environment(\.listingStyle, channelPlaylistListingStyle)
.environment(\.hideShorts, hideShorts)
#if os(tvOS)
.prefersDefaultFocus(in: focusNamespace)
#endif
@@ -131,6 +133,9 @@ struct ChannelVideosView: View {
ToolbarItem {
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
}
ToolbarItem {
HideShortsButtons(hide: $hideShorts)
}
ToolbarItem {
contentTypePicker
}
@@ -154,9 +159,7 @@ struct ChannelVideosView: View {
}
ToolbarItem {
if let presentedChannel {
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, presentedChannel.id, presentedChannel.name)))
}
favoriteButton
}
ToolbarItem {
@@ -175,14 +178,10 @@ struct ChannelVideosView: View {
store.replace(cache)
}
resource?.loadIfNeeded()?.onSuccess { response in
if let channel: Channel = response.typedContent() {
ChannelsCacheModel.shared.store(channel)
}
}
load()
}
.onChange(of: contentType) { _ in
resource?.load()
load()
}
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
@@ -206,8 +205,14 @@ struct ChannelVideosView: View {
}
}
@ViewBuilder var favoriteButton: some View {
if let presentedChannel {
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, presentedChannel.id, presentedChannel.name)))
}
}
var thumbnail: some View {
ChannelAvatarView(channel: store.item)
ChannelAvatarView(channel: store.item?.channel)
#if os(tvOS)
.frame(width: 80, height: 80, alignment: .trailing)
#else
@@ -227,7 +232,7 @@ struct ChannelVideosView: View {
var subscriptionsLabel: some View {
Group {
if let subscribers = store.item?.subscriptionsString {
if let subscribers = store.item?.channel?.subscriptionsString {
HStack(spacing: 0) {
Text(subscribers)
Image(systemName: "person.2.fill")
@@ -246,7 +251,7 @@ struct ChannelVideosView: View {
var viewsLabel: some View {
HStack(spacing: 0) {
if let views = store.item?.totalViewsString {
if let views = store.item?.channel?.totalViewsString {
Text(views)
Image(systemName: "eye.fill")
@@ -271,6 +276,10 @@ struct ChannelVideosView: View {
}
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
Section {
HideShortsButtons(hide: $hideShorts)
}
}
} label: {
HStack(spacing: 12) {
@@ -313,7 +322,7 @@ struct ChannelVideosView: View {
Picker("Content type", selection: $contentType) {
if let channel = presentedChannel {
ForEach(Channel.ContentType.allCases, id: \.self) { type in
if channel.hasData(for: type) {
if typeAvailable(type) {
Label(type.description, systemImage: type.systemImage).tag(type)
}
}
@@ -321,16 +330,20 @@ struct ChannelVideosView: View {
}
}
private func typeAvailable(_ type: Channel.ContentType) -> Bool {
type.alwaysAvailable || (presentedChannel?.hasData(for: type) ?? false)
}
private var resource: Resource? {
guard let channel = presentedChannel else { return nil }
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
let resource = accounts.api.channel(channel.id, contentType: contentType, data: data)
if contentType == .videos {
resource.addObserver(store)
} else {
resource.addObserver(contentTypeItems)
}
resource.addObserver(contentTypeItems)
return resource
}
@@ -409,6 +422,42 @@ struct ChannelVideosView: View {
Label("Mark channel feed as unwatched", systemImage: "checkmark.circle")
}
}
func load() {
resource?.load().onSuccess { response in
if let page: ChannelPage = response.typedContent() {
if let channel = page.channel {
ChannelsCacheModel.shared.store(channel)
}
self.page = page
self.contentTypeItems.replace(page.results)
}
}
.onFailure { error in
navigation.presentAlert(title: "Could not load channel data", message: error.userMessage)
}
}
func loadNextPage() {
guard let channel = presentedChannel, let pageToLoad = page, !pageToLoad.last else {
return
}
var next = pageToLoad.nextPage
if contentType == .videos, !pageToLoad.last {
next = next ?? ""
}
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
accounts.api.channel(channel.id, contentType: contentType, data: data, page: next).load().onSuccess { response in
if let page: ChannelPage = response.typedContent() {
self.page = page
let keys = self.contentTypeItems.collection.map(\.cacheKey)
let items = self.contentTypeItems.collection + page.results.filter { !keys.contains($0.cacheKey) }
self.contentTypeItems.replace(items)
}
}
}
}
struct ChannelVideosView_Previews: PreviewProvider {

View File

@@ -5,6 +5,14 @@ import SwiftUI
struct Constants {
static let yatteeProtocol = "yattee://"
static let overlayAnimation = Animation.linear(duration: 0.2)
static var isIPhone: Bool {
#if os(iOS)
UIDevice.current.userInterfaceIdiom == .phone
#else
false
#endif
}
static var progressViewScale: Double {
#if os(macOS)
0.4

View File

@@ -51,6 +51,7 @@ extension Defaults.Keys {
#if os(iOS)
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
#endif
static let showUnwatchedFeedBadges = Key<Bool>("showUnwatchedFeedBadges", default: false)
static let expandChannelDescription = Key<Bool>("expandChannelDescription", default: false)
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
@@ -238,6 +239,8 @@ extension Defaults.Keys {
static let openWatchNextOnFinishedWatching = Key<Bool>("openWatchNextOnFinishedWatching", default: true)
static let openWatchNextOnClose = Key<Bool>("openWatchNextOnClose", default: false)
static let openWatchNextOnFinishedWatchingDelay = Key<String>("openWatchNextOnFinishedWatchingDelay", default: "5")
static let hideShorts = Key<Bool>("hideShorts", default: false)
}
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {

View File

@@ -66,6 +66,10 @@ private struct ScrollViewBottomPaddingKey: EnvironmentKey {
static let defaultValue: Double = 30
}
private struct HideShortsKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
var inChannelView: Bool {
get { self[InChannelViewKey.self] }
@@ -121,4 +125,9 @@ extension EnvironmentValues {
get { self[NoListingDividersKey.self] }
set { self[NoListingDividersKey.self] = newValue }
}
var hideShorts: Bool {
get { self[HideShortsKey.self] }
set { self[HideShortsKey.self] = newValue }
}
}

View File

@@ -74,9 +74,10 @@ struct FavoriteItemView: View {
case let .channel(_, id, name):
var channel = Channel(app: .invidious, id: id, name: name)
if let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey),
!cache.videos.isEmpty
let cacheChannel = cache.channel,
!cacheChannel.videos.isEmpty
{
contentItems = ContentItem.array(of: cache.videos)
contentItems = ContentItem.array(of: cacheChannel.videos)
}
onSuccess = { response in

View File

@@ -8,6 +8,8 @@ struct AppSidebarSubscriptions: View {
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var accounts = AccountsModel.shared
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
var body: some View {
Section(header: Text("Subscriptions")) {
ForEach(subscriptions.all) { channel in
@@ -26,7 +28,7 @@ struct AppSidebarSubscriptions: View {
Spacer()
}
.backport
.badge(feedCount.unwatchedByChannelText(channel))
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedByChannelText(channel) : nil)
}
.contextMenu {
if subscriptions.isSubscribing(channel.id) {

View File

@@ -13,6 +13,7 @@ struct AppTabNavigation: View {
@Default(.showDocuments) private var showDocuments
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
@Default(.visibleSections) private var visibleSections
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
let persistenceController = PersistenceController.shared
@@ -95,7 +96,7 @@ struct AppTabNavigation: View {
}
.tag(TabSelection.subscriptions)
.backport
.badge(feedCount.unwatchedText)
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedText : nil)
}
private var subscriptionsVisible: Bool {

View File

@@ -25,10 +25,14 @@ struct ContentView: View {
var body: some View {
Group {
#if os(iOS)
if horizontalSizeClass == .compact {
if Constants.isIPhone {
AppTabNavigation()
} else {
AppSidebarNavigation()
if horizontalSizeClass == .compact {
AppTabNavigation()
} else {
AppSidebarNavigation()
}
}
#elseif os(macOS)
AppSidebarNavigation()

View File

@@ -12,6 +12,7 @@ struct Sidebar: View {
#if os(iOS)
@Default(.showDocuments) private var showDocuments
#endif
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
var body: some View {
ScrollViewReader { scrollView in
@@ -79,7 +80,7 @@ struct Sidebar: View {
.accessibility(label: Text("Subscriptions"))
}
.backport
.badge(feedCount.unwatchedText)
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedText : nil)
.contextMenu {
playUnwatchedButton
toggleWatchedButton

View File

@@ -24,6 +24,7 @@ struct PlaylistsView: View {
@Default(.playlistListingStyle) private var playlistListingStyle
@Default(.showCacheStatus) private var showCacheStatus
@Default(.hideShorts) private var hideShorts
var items: [ContentItem] {
var videos = currentPlaylist?.videos ?? []
@@ -95,6 +96,7 @@ struct PlaylistsView: View {
}
.environment(\.currentPlaylistID, currentPlaylist?.id)
.environment(\.listingStyle, playlistListingStyle)
.environment(\.hideShorts, hideShorts)
}
}
}
@@ -167,6 +169,9 @@ struct PlaylistsView: View {
ToolbarItem {
ListingStyleButtons(listingStyle: $playlistListingStyle)
}
ToolbarItem {
HideShortsButtons(hide: $hideShorts)
}
}
#else
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
@@ -234,6 +239,10 @@ struct PlaylistsView: View {
ListingStyleButtons(listingStyle: $playlistListingStyle)
Section {
HideShortsButtons(hide: $hideShorts)
}
Section {
SettingsButtons()
}

View File

@@ -30,6 +30,7 @@ struct SearchView: View {
@Default(.saveRecents) private var saveRecents
@Default(.showHome) private var showHome
@Default(.searchListingStyle) private var searchListingStyle
@Default(.hideShorts) private var hideShorts
private var videos = [Video]()
@@ -70,10 +71,12 @@ struct SearchView: View {
#endif
}
.environment(\.listingStyle, searchListingStyle)
.environment(\.hideShorts, hideShorts)
.toolbar {
#if os(macOS)
ToolbarItemGroup(placement: toolbarPlacement) {
ListingStyleButtons(listingStyle: $searchListingStyle)
HideShortsButtons(hide: $hideShorts)
FavoriteButton(item: favoriteItem)
.id(favoriteItem?.id)
@@ -210,6 +213,10 @@ struct SearchView: View {
ListingStyleButtons(listingStyle: $searchListingStyle)
Section {
HideShortsButtons(hide: $hideShorts)
}
Section {
SettingsButtons()
}

View File

@@ -7,6 +7,7 @@ struct BrowsingSettings: View {
@Default(.roundedThumbnails) private var roundedThumbnails
#endif
@Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
#if os(iOS)
@Default(.homeRecentDocumentsItems) private var homeRecentDocumentsItems
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
@@ -223,6 +224,12 @@ struct BrowsingSettings: View {
#endif
Toggle("Show anonymous accounts", isOn: $accountPickerDisplaysAnonymousAccounts)
Toggle("Show unwatched feed badges", isOn: $showUnwatchedFeedBadges)
.onChange(of: showUnwatchedFeedBadges) { newValue in
if newValue {
FeedModel.shared.calculateUnwatchedFeed()
}
}
}
Toggle("Open channels with description expanded", isOn: $expandChannelDescription)

View File

@@ -243,7 +243,7 @@ struct SettingsView: View {
private var windowHeight: Double {
switch selection {
case .browsing:
return 820
return 840
case .player:
return 450
case .controls:

View File

@@ -9,6 +9,7 @@ struct ChannelsView: View {
@ObservedObject private var feedCount = UnwatchedFeedCountModel.shared
@Default(.showCacheStatus) private var showCacheStatus
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
var body: some View {
List {
@@ -26,7 +27,7 @@ struct ChannelsView: View {
}
}
.backport
.badge(feedCount.unwatchedByChannelText(channel))
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedByChannelText(channel) : nil)
}
.contextMenu {
if subscriptions.isSubscribing(channel.id) {

View File

@@ -10,6 +10,7 @@ struct FeedView: View {
#if os(tvOS)
@Default(.subscriptionsListingStyle) private var subscriptionsListingStyle
@Default(.hideShorts) private var hideShorts
#endif
var videos: [ContentItem] {
@@ -54,6 +55,7 @@ struct FeedView: View {
#if os(tvOS)
SubscriptionsPageButton()
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
HideShortsButtons(hide: $hideShorts)
#endif
if showCacheStatus {
@@ -82,6 +84,7 @@ struct FeedView: View {
.padding(.leading, 30)
#if os(tvOS)
.padding(.bottom, 15)
.padding(.trailing, 30)
#endif
}
@@ -94,7 +97,7 @@ struct FeedView: View {
}
}
struct SubscriptonsView_Previews: PreviewProvider {
struct FeedView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
FeedView()

View File

@@ -10,6 +10,7 @@ struct SubscriptionsView: View {
@Default(.subscriptionsViewPage) private var subscriptionsViewPage
@Default(.subscriptionsListingStyle) private var subscriptionsListingStyle
@Default(.hideShorts) private var hideShorts
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@@ -27,6 +28,7 @@ struct SubscriptionsView: View {
}
}
.environment(\.listingStyle, subscriptionsListingStyle)
.environment(\.hideShorts, hideShorts)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
@@ -46,6 +48,10 @@ struct SubscriptionsView: View {
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
}
ToolbarItem {
HideShortsButtons(hide: $hideShorts)
}
ToolbarItem {
toggleWatchedButton
}
@@ -73,6 +79,10 @@ struct SubscriptionsView: View {
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
}
Section {
HideShortsButtons(hide: $hideShorts)
}
playUnwatchedButton
toggleWatchedButton

View File

@@ -10,6 +10,7 @@ struct TrendingView: View {
@Default(.trendingCountry) private var country
@Default(.trendingListingStyle) private var trendingListingStyle
@Default(.hideShorts) private var hideShorts
@State private var presentingCountrySelection = false
@@ -51,6 +52,7 @@ struct TrendingView: View {
#endif
}
.environment(\.listingStyle, trendingListingStyle)
.environment(\.hideShorts, hideShorts)
}
.toolbar {
@@ -133,6 +135,10 @@ struct TrendingView: View {
ToolbarItem {
ListingStyleButtons(listingStyle: $trendingListingStyle)
}
ToolbarItem {
HideShortsButtons(hide: $hideShorts)
}
}
#else
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
@@ -182,6 +188,10 @@ struct TrendingView: View {
ListingStyleButtons(listingStyle: $trendingListingStyle)
Section {
HideShortsButtons(hide: $hideShorts)
}
Section {
SettingsButtons()
}

View File

@@ -5,21 +5,32 @@ struct ContentItemView: View {
let item: ContentItem
@Environment(\.listingStyle) private var listingStyle
@Environment(\.noListingDividers) private var noListingDividers
@Environment(\.hideShorts) private var hideShorts
var body: some View {
Group {
switch item.contentType {
case .video:
videoItem(item.video)
case .channel:
channelItem(item.channel)
case .playlist:
playlistItem(item.playlist)
default:
placeholderItem()
@ViewBuilder var body: some View {
if itemVisible {
Group {
switch item.contentType {
case .video:
videoItem(item.video)
case .channel:
channelItem(item.channel)
case .playlist:
playlistItem(item.playlist)
default:
placeholderItem()
}
}
.id(item.cacheKey)
}
.id(item.cacheKey)
}
var itemVisible: Bool {
guard hideShorts, item.contentType == .video, let video = item.video else {
return true
}
return !video.short
}
@ViewBuilder func videoItem(_ video: Video) -> some View {

View File

@@ -0,0 +1,33 @@
import SwiftUI
struct HideShortsButtons: View {
@Binding var hide: Bool
var body: some View {
Button {
hide.toggle()
} label: {
Group {
if hide {
Label("Short videos: hidden", systemImage: "bolt.slash.fill")
.help("Short videos: hidden")
} else {
Label("Short videos: visible", systemImage: "bolt.fill")
.help("Short videos: visible")
}
}
#if os(tvOS)
.font(.caption2)
.imageScale(.small)
#endif
}
}
}
struct HideShortsButtons_Previews: PreviewProvider {
static var previews: some View {
VStack {
HideShortsButtons(hide: .constant(true))
}
}
}

View File

@@ -10,6 +10,7 @@ struct PopularView: View {
@State private var error: RequestError?
@Default(.popularListingStyle) private var popularListingStyle
@Default(.hideShorts) private var hideShorts
var resource: Resource? {
accounts.api.popular
@@ -69,6 +70,10 @@ struct PopularView: View {
ToolbarItem {
ListingStyleButtons(listingStyle: $popularListingStyle)
}
ToolbarItem {
HideShortsButtons(hide: $hideShorts)
}
}
#else
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
@@ -84,6 +89,10 @@ struct PopularView: View {
Menu {
ListingStyleButtons(listingStyle: $popularListingStyle)
Section {
HideShortsButtons(hide: $hideShorts)
}
Section {
SettingsButtons()
}

View File

@@ -21,27 +21,27 @@ struct ShareButton<LabelView: View>: View {
@ViewBuilder var body: some View {
// TODO: this should work with other content item types
Menu {
if let video = contentItem.video,
!video.localStreamIsFile
{
if video.localStreamIsRemoteURL {
remoteURLAction
} else {
instanceActions
Divider()
if !accounts.isEmpty {
youtubeActions
if let video = contentItem.video {
Menu {
if !video.localStreamIsFile {
if video.localStreamIsRemoteURL {
remoteURLAction
} else {
instanceActions
Divider()
if !accounts.isEmpty {
youtubeActions
}
}
}
} label: {
label
}
} label: {
label
.menuStyle(.borderlessButton)
#if os(macOS)
.frame(maxWidth: 60)
#endif
}
.menuStyle(.borderlessButton)
#if os(macOS)
.frame(maxWidth: 60)
#endif
}
private var instanceActions: some View {

View File

@@ -493,3 +493,12 @@
"Share%@link" = "مشاركة الرابط %@";
"Instance of current account" = "مثيل الحساب الحالي";
"Seek gesture sensitivity" = "إطار حساسية الإيماءات";
"Video" = "";
"Audio" = "";
"Honor orientation lock" = "";
"Proxy videos" = "";
"Seek gesture speed" = "";
"Seek with horizontal swipe on video" = "";
"System controls show buttons for %@" = "";
"Wiki" = "";
"Sample Rate" = "";

View File

@@ -498,3 +498,7 @@
"Default Profile" = "Výchozí profil";
"Copy%@link" = "Zkopírovat%@odkaz";
"Share%@link" = "Sdílet%@odkaz";
"Verified" = "";
"Shorts" = "";
"Channel" = "";
"Live Streams" = "";

View File

@@ -124,3 +124,381 @@
"Browsing" = "Navegando por";
"Buffering stream..." = "Cargando flujo de datos...";
"Cellular" = "Celular";
"Save" = "";
"Search history is empty" = "";
"Address" = "";
"Could not refresh Subscriptions" = "";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "";
"URL to Open" = "";
"Could not delete document" = "";
"Show Favorites" = "";
"Current Location" = "";
"Show Home" = "";
"Are you sure you want to remove this document?" = "";
/* Video date filter in search */
"Today" = "";
"Opening audio stream..." = "";
"Open Video" = "";
"I want to ask a question" = "";
"Save history of played videos" = "";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "";
"Show only icons" = "";
"FPS" = "";
"Enter links to open, one per line" = "";
"Sample Rate" = "";
"Playback Mode" = "";
"Channels" = "";
"For custom locations you can configure Frontend URL in Locations settings" = "";
"Cached time" = "";
"Could not find any links to open in your clipboard" = "";
"Orientation" = "";
/* SponsorBlock category name */
"Offtopic in Music Videos" = "";
"Public Manifest" = "";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "";
"Left" = "";
"Video Details" = "";
"Only for local files and URLs" = "";
"\"%@\" will be irreversibly removed from this device." = "";
/* Player controls layout size */
"Small" = "";
"Now Playing" = "";
"Stream & Player" = "";
"Picture in Picture" = "";
"Visibility" = "";
"Edit Favorites…" = "";
"Center" = "";
"Wiki" = "";
"Live Streams" = "";
"Make default" = "";
"Dropped frames" = "";
"Show icons and text when space permits" = "";
"Hardware decoder" = "";
"Show Open Videos quick actions" = "";
"Upload date" = "";
"Home" = "";
"Switch to public locations" = "";
"Verified" = "";
"History" = "";
"Locations Manifest" = "";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "";
"Play Now" = "";
"Enter link to open" = "";
"Share..." = "";
"Restore default profiles..." = "";
"Round corners" = "";
"SponsorBlock" = "";
"Statistics" = "";
"Matrix Channel" = "";
"More info can be found in:" = "";
"Resolution" = "";
"Open Videos" = "";
"Show progress of watching on thumbnails" = "";
"Medium quality" = "";
"Welcome" = "";
"Could not open Files" = "";
"Add Channels, Playlists and Searches to Favorites using" = "";
"Restart" = "";
"Shorts" = "";
"Not available" = "";
"Pages buttons" = "";
"No documents" = "";
/* Loading stream OSD */
"Opening %@ stream..." = "";
"Documents" = "";
"Thumbnails" = "";
"Password" = "";
"Show playback statistics" = "";
"Remove Location" = "";
"Inspector visibility" = "";
"Open \"Playlists\" tab to create new one" = "";
"Show Inspector" = "";
/* Video date filter in search */
"Week" = "";
"Recent Documents" = "";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "";
"Restart the app to apply the settings above." = "";
"Show sidebar when space permits" = "";
"Share %@ link with time" = "";
"Rate & Captions" = "";
/* SponsorBlock category name */
"Outro" = "";
"Always" = "";
"Show anonymous accounts" = "";
"Play in PiP" = "";
/* SponsorBlock category name */
"Sponsor" = "";
"Rate" = "";
"Reload manifest" = "";
"Playback queue is empty" = "";
"Videos" = "";
/* Selected video was played on given date */
"Watched %@" = "";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "";
"I like this app!" = "";
/* Video sort order in search */
"Rating" = "";
"Actions buttons" = "";
"Files" = "";
"Sign In Required" = "";
"Buttons labels" = "";
"Next" = "";
"Default Profile" = "";
"Paste" = "";
"Regular Size" = "";
"File" = "";
"Right" = "";
"Reset search filters" = "";
"Channel" = "";
"Open Files" = "";
"You can find information about using Yattee in the Wiki pages." = "";
"Lock portrait mode" = "";
"Shuffle" = "";
/* Loading stream OSD */
"Loading streams..." = "";
"Public Locations" = "";
"Yattee" = "";
"No results" = "";
"Driver" = "";
"Remove from history" = "";
"Red" = "";
"Are you sure you want to remove %@ location?" = "";
"Share" = "";
"Share %@ link" = "";
"Reset watched status when playing again" = "";
"Show sidebar" = "";
"You need to create an instance and accounts\nto access %@ section" = "";
"Refresh" = "";
"I am lost" = "";
/* Player controls layout size */
"Medium" = "";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "";
"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." = "";
"You have no Playlists" = "";
"System controls show buttons for %@" = "";
"No chapters information available" = "";
/* Video date filter in search */
"Month" = "";
"Shuffle All" = "";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "";
"Music" = "";
"Name" = "";
"New Playlist" = "";
"Quality Profile" = "";
"Queue" = "";
"You can switch between profiles in playback settings controls." = "";
"Could not load streams" = "";
"Settings" = "";
"Show history" = "";
"unknown" = "";
"%@ formats" = "";
"Quality" = "";
"Any format" = "";
"Honor orientation lock" = "";
"I found a bug /" = "";
"I have a feature request" = "";
"LIVE" = "";
/* Video duration filter in search */
"Long" = "";
"Low quality" = "";
"Lowest" = "";
"Movies" = "";
"MPV Documentation" = "";
"Pause when entering background" = "";
"Pause when player is closed" = "";
"Play" = "";
"Play All" = "";
"Preferred Formats" = "";
"Profiles" = "";
"Queue is empty" = "";
"Recents" = "";
"Remove from Playlist" = "";
"Remove from the queue" = "";
"Replies" = "";
"Reset" = "";
"Save history of searches, channels and playlists" = "";
"Search" = "";
"Seek gesture sensitivity" = "";
"Select location closest to you:" = "";
/* SponsorBlock category name */
"Self-promotion" = "";
"Show keywords" = "";
"Show channel name" = "";
"Show video length" = "";
"Sort" = "";
"Sort: %@" = "";
"Trending" = "";
/* Player controls layout size for TV */
"TV" = "";
"You need to select an account\nto access %@ section" = "";
"Unlisted" = "";
"Could not open playlist" = "";
"Could not extract video ID" = "";
"Highest" = "";
"Highest quality" = "";
/* Video date filter in search */
"Hour" = "";
"If you are interested what's coming in future updates, you can track project Milestones." = "";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "";
"Info" = "";
"Instance of current account" = "";
/* SponsorBlock category name */
"Interaction" = "";
"Interface" = "";
/* SponsorBlock category name */
"Intro" = "";
"Issues Tracker" = "";
/* Selected video has just finished playing */
"Just watched" = "";
/* Player controls layout size */
"Large" = "";
"Loading..." = "";
"Locations" = "";
"Low" = "";
"Mark as watched" = "";
"Mark video as watched after playing" = "";
"Mark watched videos with" = "";
"Matrix Chat" = "";
"Milestones" = "";
"No description" = "";
"No Playlists" = "";
"Not Playing" = "";
"Normal" = "";
"Nothing" = "";
"Only when signed in" = "";
"Open Settings" = "";
"Player" = "";
"Pause" = "";
"Play Last" = "";
"Play Music" = "";
"Play Next" = "";
"Playlist" = "";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "";
"Playlists" = "";
"Popular" = "";
"Proxy videos" = "";
"Regular size" = "";
"Related" = "";
/* Video sort order in search */
"Relevance" = "";
"Remove" = "";
"Remove from Favorites" = "";
"Rotate to portrait when exiting fullscreen" = "";
"Search..." = "";
"Sections" = "";
"Seek gesture speed" = "";
"Seek with horizontal swipe on video" = "";
/* Video duration filter in search */
"Short" = "";
"Show account username" = "";
"Sidebar" = "";
/* Player controls layout size */
"Smaller" = "";
"Source" = "";
"SponsorBlock API Instance" = "";
"Subscribe" = "";
/* Subscriptions title */
"Subscriptions" = "";
"Switch to other public location" = "";
"System controls buttons" = "";
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "";
"Unsubscribe" = "";
"URL" = "";
"Used to create links from videos, channels and playlists" = "";
"Username" = "";
/* Player controls layout size */
"Very Large" = "";
/* Video sort order in search */
"Views" = "";
"Watched" = "";
"When partially watched video is played" = "";
"Wi-Fi" = "";
"Yattee %@ (build %@)" = "";
/* Video date filter in search */
"Year" = "";
"Public" = "";
"Private" = "";
"Playing Next" = "";
"Current Playlist" = "";
"Stream FPS" = "";
"Keep last played video in the queue after restart" = "";
"It can be changed later in settings. You can use your own locations too." = "";
"Press and hold remote button to open captions and quality menus" = "";
"Comments are disabled" = "";
"No comments" = "";
"Share Logs..." = "";
"Open logs in Finder" = "";
"Could not open video" = "";
"Channel could not be found" = "";
"Could not extract channel information" = "";
"Could not extract SID from received cookies: %@" = "";
"Could not update your token." = "";
"Could not refresh Trending" = "";
"This URL could not be opened" = "";
"Could not refresh Popular" = "";
"Could not create share link" = "";
"This video could not be opened" = "";
"No locations available at the moment" = "";
"Could not refresh Playlists" = "";
/* Selected video is being played */
"Watching now" = "";
"If you want this app to be available in your language, join translation project." = "";
"Translations" = "";
"Increase rate" = "";
"Could not extract playlist ID" = "";
"Could not load video" = "";
"Playback" = "";
"Restart/Play next" = "";
"This cannot be reverted" = "";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "";
"Could not open channel" = "";
"Recent History" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Pages toolbar position" = "";
"Video actions buttons" = "";
"Video" = "";
"Show Open Videos toolbar button" = "";
"Show Documents" = "";
"Clear Queue before opening" = "";
"Open" = "";
"Add" = "";
"Hide" = "";
"Format" = "";
"Audio" = "";
"Codec" = "";
"Size" = "";
"Remove…" = "";
"Playback history is empty" = "";
"Copy%@link" = "";
"Share%@link" = "";

View File

@@ -462,3 +462,43 @@
"Playback Mode" = "Mode Playback";
"Add" = "Ajouter";
"Hide" = "Cacher";
"Size" = "";
"Address" = "";
"Could not delete document" = "";
"Could not find any links to open in your clipboard" = "";
"Center" = "";
"Locations Manifest" = "";
"Actions buttons" = "";
"Sample Rate" = "";
"FPS" = "";
"Remove…" = "";
"Live Streams" = "";
"\"%@\" will be irreversibly removed from this device." = "";
"Remove Location" = "";
"Open Video" = "";
"Video actions buttons" = "";
"Documents" = "";
"Show Open Videos toolbar button" = "";
"File" = "";
"Are you sure you want to remove this document?" = "";
"Left" = "";
"Shorts" = "";
"Copy%@link" = "";
"Verified" = "";
"Show sidebar" = "";
"Driver" = "";
"Show Open Videos quick actions" = "";
"Format" = "";
"Are you sure you want to remove %@ location?" = "";
"Show only icons" = "";
"Audio" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Channel" = "";
"Inspector visibility" = "";
"Video" = "";
"Right" = "";
"Show icons and text when space permits" = "";
"Codec" = "";
"Default Profile" = "";
"Playback history is empty" = "";
"Share%@link" = "";

View File

@@ -407,3 +407,98 @@
"No comments" = "कोई टिप्पणी नहीं";
"No chapters information available" = "कोई अध्याय जानकारी उपलब्ध नहीं";
"Comments are disabled" = "टिप्पणियाँ अक्षम हैं";
"Open Video" = "";
"Reload manifest" = "";
"Pages buttons" = "";
"Open Files" = "";
"Channels" = "";
"Locations Manifest" = "";
"Could not delete document" = "";
"Show Inspector" = "";
"Could not load streams" = "";
"Address" = "";
"Playback Mode" = "";
"Left" = "";
"Show icons and text when space permits" = "";
"FPS" = "";
"Buttons labels" = "";
"Remove…" = "";
"Driver" = "";
"Show only icons" = "";
"Could not load video" = "";
"Are you sure you want to remove this document?" = "";
"Hide" = "";
"\"%@\" will be irreversibly removed from this device." = "";
"Could not open Files" = "";
"Video Details" = "";
"Open" = "";
"Open Videos" = "";
"Show Documents" = "";
"Inspector visibility" = "";
"Add" = "";
"Show Favorites" = "";
"Clear Queue before opening" = "";
"If you want this app to be available in your language, join translation project." = "";
"Documents" = "";
"Playback history is empty" = "";
"Show sidebar" = "";
"Live Streams" = "";
"Enter links to open, one per line" = "";
"Verified" = "";
"No documents" = "";
"Show Open Videos toolbar button" = "";
"Could not refresh Subscriptions" = "";
"Remove Location" = "";
"Only for local files and URLs" = "";
"Codec" = "";
"File" = "";
"Could not extract playlist ID" = "";
"Paste" = "";
"Could not extract SID from received cookies: %@" = "";
"Files" = "";
"Show Home" = "";
"Could not extract channel information" = "";
"Enter link to open" = "";
"Home" = "";
"No locations available at the moment" = "";
"Could not find any links to open in your clipboard" = "";
"Audio" = "";
"Actions buttons" = "";
"Share" = "";
"Always" = "";
"Format" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Share%@link" = "";
"Could not open video" = "";
"Center" = "";
"Size" = "";
"Recent Documents" = "";
"This video could not be opened" = "";
"Video" = "";
"URL to Open" = "";
"Edit Favorites…" = "";
"Video actions buttons" = "";
"Right" = "";
"Shorts" = "";
"For custom locations you can configure Frontend URL in Locations settings" = "";
"Could not create share link" = "";
"Sample Rate" = "";
"Translations" = "";
"Open logs in Finder" = "";
"Could not refresh Playlists" = "";
"Could not update your token." = "";
"Could not refresh Trending" = "";
"Could not open playlist" = "";
"This URL could not be opened" = "";
"Could not open channel" = "";
"Channel could not be found" = "";
"Could not refresh Popular" = "";
"Could not extract video ID" = "";
"Channel" = "";
"Are you sure you want to remove %@ location?" = "";
"Recent History" = "";
"Show Open Videos quick actions" = "";
"Pages toolbar position" = "";
"Default Profile" = "";
"Copy%@link" = "";
"Share Logs..." = "";

View File

@@ -405,3 +405,98 @@
"No chapters information available" = "Ingen tilgjengelig kapittelinfo";
"Comments are disabled" = "Kommentarer er avskrudd";
"Press and hold remote button to open captions and quality menus" = "Trykk og hold fjernknappen for å åpne meny for undertekster og kvalitet";
"Paste" = "";
"Codec" = "";
"Open Videos" = "";
"Files" = "";
"Open Video" = "";
"Show only icons" = "";
"Show Open Videos toolbar button" = "";
"Channels" = "";
"Buttons labels" = "";
"Could not open Files" = "";
"Reload manifest" = "";
"Right" = "";
"Show Favorites" = "";
"Only for local files and URLs" = "";
"Enter link to open" = "";
"Left" = "";
"Are you sure you want to remove this document?" = "";
"Recent Documents" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Address" = "";
"File" = "";
"Share" = "";
"Could not delete document" = "";
"Are you sure you want to remove %@ location?" = "";
"Size" = "";
"Always" = "";
"Video actions buttons" = "";
"Edit Favorites…" = "";
"Sample Rate" = "";
"Show Inspector" = "";
"Remove Location" = "";
"Format" = "";
"Verified" = "";
"Show icons and text when space permits" = "";
"Could not extract video ID" = "";
"Open Files" = "";
"Driver" = "";
"Show Open Videos quick actions" = "";
"Enter links to open, one per line" = "";
"No locations available at the moment" = "";
"Video Details" = "";
"Add" = "";
"Show Home" = "";
"Pages buttons" = "";
"Center" = "";
"Shorts" = "";
"Open" = "";
"Locations Manifest" = "";
"FPS" = "";
"Inspector visibility" = "";
"Show Documents" = "";
"Open logs in Finder" = "";
"Documents" = "";
"Could not update your token." = "";
"Remove…" = "";
"Hide" = "";
"Actions buttons" = "";
"Audio" = "";
"Could not extract SID from received cookies: %@" = "";
"Playback Mode" = "";
"Clear Queue before opening" = "";
"Could not create share link" = "";
"Could not refresh Playlists" = "";
"Could not refresh Subscriptions" = "";
"Translations" = "";
"This URL could not be opened" = "";
"For custom locations you can configure Frontend URL in Locations settings" = "";
"Could not refresh Trending" = "";
"If you want this app to be available in your language, join translation project." = "";
"This video could not be opened" = "";
"Could not open channel" = "";
"Could not open playlist" = "";
"Could not load streams" = "";
"Could not open video" = "";
"Could not extract channel information" = "";
"Could not load video" = "";
"Could not extract playlist ID" = "";
"Could not refresh Popular" = "";
"Channel could not be found" = "";
"Live Streams" = "";
"Channel" = "";
"No documents" = "";
"\"%@\" will be irreversibly removed from this device." = "";
"Recent History" = "";
"Home" = "";
"Pages toolbar position" = "";
"URL to Open" = "";
"Video" = "";
"Could not find any links to open in your clipboard" = "";
"Show sidebar" = "";
"Default Profile" = "";
"Playback history is empty" = "";
"Copy%@link" = "";
"Share%@link" = "";
"Share Logs..." = "";

View File

@@ -111,3 +111,394 @@
"Find Other" = "Letar annat";
"Finding something to play..." = "Letar något att spela...";
"10 seconds forwards/backwards" = "10 sekunder framåt/bakåt";
"Low" = "";
/* Player controls layout size */
"Large" = "";
"Interface" = "";
"Increase rate" = "";
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "";
"You need to select an account\nto access %@ section" = "";
"Playlists" = "";
/* Video date filter in search
Video duration filter in search */
"Any" = "";
"Password" = "";
"Play All" = "";
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "";
"Quality Profile" = "";
"Popular" = "";
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "";
"Now Playing" = "";
/* Loading stream OSD */
"Loading streams..." = "";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "";
"Picture in Picture" = "";
"Playlist" = "";
"Queue is empty" = "";
"Search history is empty" = "";
"Pause" = "";
"LIVE" = "";
"Play Next" = "";
"Play Music" = "";
"Channels" = "";
"Show icons and text when space permits" = "";
"Instance of current account" = "";
"You can find information about using Yattee in the Wiki pages." = "";
"Driver" = "";
/* Selected video has just finished playing */
"Just watched" = "";
"Preferred Formats" = "";
"Play in PiP" = "";
"Pause when entering background" = "";
"You need to create an instance and accounts\nto access %@ section" = "";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "";
"Are you sure you want to remove %@ location?" = "";
"Trending" = "";
"Regular Size" = "";
"Public Manifest" = "";
"Player" = "";
"Info" = "";
"Sort" = "";
"Unlisted" = "";
"System controls buttons" = "";
"Show Open Videos quick actions" = "";
"Orientation" = "";
"Not Playing" = "";
"Restore default profiles..." = "";
"More info can be found in:" = "";
"Rate" = "";
"Restart the app to apply the settings above." = "";
/* Player controls layout size for TV */
"TV" = "";
"Recent History" = "";
"Remove from Favorites" = "";
"No documents" = "";
"Play Last" = "";
"Documents" = "";
"Could not load video" = "";
"Could not open playlist" = "";
"System controls show buttons for %@" = "";
/* SponsorBlock category name */
"Intro" = "";
"Next" = "";
"Sort: %@" = "";
"Locations" = "";
"Could not extract video ID" = "";
"Unsubscribe" = "";
"Nothing" = "";
"File" = "";
"Restart" = "";
"You have no Playlists" = "";
/* Video date filter in search */
"Hour" = "";
"Playback history is empty" = "";
"Refresh" = "";
"For custom locations you can configure Frontend URL in Locations settings" = "";
"Remove" = "";
"Default Profile" = "";
"Buttons labels" = "";
"Public Locations" = "";
"I like this app!" = "";
"Current Location" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"Could not delete document" = "";
"Video actions buttons" = "";
"Show anonymous accounts" = "";
"Show Documents" = "";
"Center" = "";
"Not available" = "";
/* SponsorBlock category name */
"Offtopic in Music Videos" = "";
"SponsorBlock API Instance" = "";
"Remove Location" = "";
"Resolution" = "";
"Show Inspector" = "";
"Low quality" = "";
/* SponsorBlock category name */
"Outro" = "";
"Lock portrait mode" = "";
"Issues Tracker" = "";
"Copy%@link" = "";
"Switch to public locations" = "";
"Private" = "";
"Backend" = "";
"For videos which feature music as the primary content." = "";
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "";
"Frontend URL" = "";
"Fullscreen size" = "";
"Gaming" = "";
"Help" = "";
"Hide sidebar" = "";
"High" = "";
"Highest" = "";
"Highest quality" = "";
"History" = "";
"Honor orientation lock" = "";
"I am lost" = "";
"I found a bug /" = "";
"I have a feature request" = "";
"I want to ask a question" = "";
"If you are interested what's coming in future updates, you can track project Milestones." = "";
/* SponsorBlock category name */
"Interaction" = "";
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "";
"Loading..." = "";
/* Video duration filter in search */
"Long" = "";
"Lowest" = "";
"Mark as watched" = "";
"Mark video as watched after playing" = "";
"Mark watched videos with" = "";
"Matrix Channel" = "";
"Matrix Chat" = "";
/* Player controls layout size */
"Medium" = "";
"Medium quality" = "";
"Milestones" = "";
/* Video date filter in search */
"Month" = "";
"Movies" = "";
"MPV Documentation" = "";
"Music" = "";
"Name" = "";
"New Playlist" = "";
"No description" = "";
"No Playlists" = "";
"No results" = "";
"Normal" = "";
"Only when signed in" = "";
"Open \"Playlists\" tab to create new one" = "";
"Open Settings" = "";
/* Loading stream OSD */
"Opening %@ stream..." = "";
"Opening audio stream..." = "";
"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." = "";
"Pause when player is closed" = "";
"Play" = "";
"Play Now" = "";
"Playback" = "";
"Profiles" = "";
"Proxy videos" = "";
"Quality" = "";
"Queue" = "";
/* Video sort order in search */
"Rating" = "";
"Recents" = "";
"Red" = "";
"Regular size" = "";
"Related" = "";
/* Video sort order in search */
"Relevance" = "";
"Remove from history" = "";
"Remove from Playlist" = "";
"Remove from the queue" = "";
"Replies" = "";
"Reset" = "";
"Reset search filters" = "";
"Reset watched status when playing again" = "";
"Restart/Play next" = "";
"Rotate to portrait when exiting fullscreen" = "";
"Round corners" = "";
"Save history of played videos" = "";
"Save history of searches, channels and playlists" = "";
"Search" = "";
"Search..." = "";
"Sections" = "";
"Save" = "";
"Seek gesture speed" = "";
"Seek gesture sensitivity" = "";
"Seek with horizontal swipe on video" = "";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "";
"Select location closest to you:" = "";
/* SponsorBlock category name */
"Self-promotion" = "";
"Settings" = "";
"Share %@ link" = "";
"Share %@ link with time" = "";
"Share..." = "";
/* Video duration filter in search */
"Short" = "";
"Show account username" = "";
"Show channel name" = "";
"Show history" = "";
"Show keywords" = "";
"Show playback statistics" = "";
"Show progress of watching on thumbnails" = "";
"Show sidebar when space permits" = "";
"Show video length" = "";
"Shuffle" = "";
"Shuffle All" = "";
"Sidebar" = "";
"Sign In Required" = "";
/* Player controls layout size */
"Small" = "";
/* Player controls layout size */
"Smaller" = "";
"Source" = "";
"SponsorBlock" = "";
"Subscribe" = "";
/* Subscriptions title */
"Subscriptions" = "";
"Switch to other public location" = "";
"This cannot be reverted" = "";
/* SponsorBlock category name */
"Sponsor" = "";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "";
/* Video date filter in search */
"Today" = "";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "";
"Thumbnails" = "";
"unknown" = "";
"Upload date" = "";
"URL" = "";
"Used to create links from videos, channels and playlists" = "";
"Username" = "";
/* Player controls layout size */
"Very Large" = "";
"Videos" = "";
/* Video sort order in search */
"Views" = "";
"Watched" = "";
/* Selected video was played on given date */
"Watched %@" = "";
/* Selected video is being played */
"Watching now" = "";
/* Video date filter in search */
"Week" = "";
"Welcome" = "";
"When partially watched video is played" = "";
"Wi-Fi" = "";
"Wiki" = "";
"Yattee" = "";
"Yattee %@ (build %@)" = "";
/* Video date filter in search */
"Year" = "";
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "";
"Public" = "";
"Playback queue is empty" = "";
"Playing Next" = "";
"You can switch between profiles in playback settings controls." = "";
"Add Channels, Playlists and Searches to Favorites using" = "";
"Make default" = "";
"Visibility" = "";
"Current Playlist" = "";
"Stream & Player" = "";
"Statistics" = "";
"Hardware decoder" = "";
"Stream FPS" = "";
"Cached time" = "";
"Rate & Captions" = "";
"Dropped frames" = "";
"Any format" = "";
"%@ formats" = "";
"Keep last played video in the queue after restart" = "";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "";
"It can be changed later in settings. You can use your own locations too." = "";
"Press and hold remote button to open captions and quality menus" = "";
"Comments are disabled" = "";
"No comments" = "";
"No chapters information available" = "";
"Share Logs..." = "";
"Open logs in Finder" = "";
"Could not refresh Subscriptions" = "";
"Could not load streams" = "";
"Could not open video" = "";
"Channel could not be found" = "";
"Could not extract channel information" = "";
"Could not extract SID from received cookies: %@" = "";
"Could not update your token." = "";
"Could not refresh Trending" = "";
"This URL could not be opened" = "";
"Could not open channel" = "";
"Could not refresh Popular" = "";
"Could not create share link" = "";
"This video could not be opened" = "";
"Could not extract playlist ID" = "";
"No locations available at the moment" = "";
"Could not refresh Playlists" = "";
"If you want this app to be available in your language, join translation project." = "";
"Translations" = "";
"Recent Documents" = "";
"Home" = "";
"Show Home" = "";
"Show Favorites" = "";
"Inspector visibility" = "";
"Edit Favorites…" = "";
"Show Open Videos toolbar button" = "";
"Files" = "";
"Pages toolbar position" = "";
"Video Details" = "";
"Reload manifest" = "";
"Clear Queue before opening" = "";
"Open" = "";
"Pages buttons" = "";
"URL to Open" = "";
"Enter link to open" = "";
"Could not open Files" = "";
"Paste" = "";
"Open Videos" = "";
"Enter links to open, one per line" = "";
"Playback Mode" = "";
"Add" = "";
"Hide" = "";
"Always" = "";
"Only for local files and URLs" = "";
"Right" = "";
"Open Files" = "";
"Share" = "";
"Left" = "";
"Format" = "";
"Show only icons" = "";
"Audio" = "";
"Video" = "";
"Codec" = "";
"Size" = "";
"FPS" = "";
"Sample Rate" = "";
"Could not find any links to open in your clipboard" = "";
"Address" = "";
"Remove…" = "";
"Actions buttons" = "";
"Show sidebar" = "";
"Locations Manifest" = "";
"Open Video" = "";
"Share%@link" = "";
"Are you sure you want to remove this document?" = "";
"\"%@\" will be irreversibly removed from this device." = "";
"Live Streams" = "";
"Shorts" = "";
"Verified" = "";
"Channel" = "";

View File

@@ -266,3 +266,239 @@
"Sections" = "Bölümler";
"Save history of searches, channels and playlists" = "Arama, kanal ve çalma listelerinin geçmişini kaydet";
"Restore default profiles..." = "Varsayılan profilleri geri yükle...";
"Visibility" = "";
"Translations" = "";
"Enter links to open, one per line" = "";
"Open Videos" = "";
"Playback Mode" = "";
/* Selected video was played on given date */
"Watched %@" = "";
"Yattee %@ (build %@)" = "";
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "";
"Thumbnails" = "";
"Dropped frames" = "";
"SponsorBlock API Instance" = "";
/* Selected video is being played */
"Watching now" = "";
"Video Details" = "";
"Live Streams" = "";
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "";
"Default Profile" = "";
/* Player controls layout size for TV */
"TV" = "";
"Add" = "";
"This URL could not be opened" = "";
"Hide" = "";
"Playing Next" = "";
"Are you sure you want to remove this document?" = "";
"Show channel name" = "";
"Unlisted" = "";
"Paste" = "";
"Rate & Captions" = "";
"Format" = "";
"Right" = "";
"Stream FPS" = "";
"Cached time" = "";
"Sign In Required" = "";
"Could not create share link" = "";
"Locations Manifest" = "";
"When partially watched video is played" = "";
"Open Video" = "";
"Add Channels, Playlists and Searches to Favorites using" = "";
"Always" = "";
/* Video date filter in search */
"Year" = "";
"Playback queue is empty" = "";
"Show Favorites" = "";
"Driver" = "";
"Show progress of watching on thumbnails" = "";
"Left" = "";
"URL to Open" = "";
"Subscribe" = "";
"Yattee" = "";
"Show Documents" = "";
"Press and hold remote button to open captions and quality menus" = "";
"No locations available at the moment" = "";
"Show account username" = "";
"Used to create links from videos, channels and playlists" = "";
"Size" = "";
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "";
"Sort: %@" = "";
"Select location closest to you:" = "";
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "";
/* Video duration filter in search */
"Short" = "";
"Home" = "";
"Remove Location" = "";
"Edit Favorites…" = "";
"Show Open Videos toolbar button" = "";
"Sample Rate" = "";
"Private" = "";
"Browsing" = "";
"Documents" = "";
"FPS" = "";
"Only for local files and URLs" = "";
"Show sidebar" = "";
"Current Playlist" = "";
"Center" = "";
"Address" = "";
"Video actions buttons" = "";
"Keep last played video in the queue after restart" = "";
"Remove…" = "";
"Trending" = "";
"Statistics" = "";
"Copy%@link" = "";
"Now Playing" = "";
"Could not delete document" = "";
"No comments" = "";
"Could not open Files" = "";
"You need to select an account\nto access %@ section" = "";
"Reload manifest" = "";
"Could not refresh Subscriptions" = "";
/* Subscriptions title */
"Subscriptions" = "";
"Upload date" = "";
"This information will be processed only on your device and used to connect you to the server in the specified country." = "";
"Shuffle" = "";
"Buttons labels" = "";
"Share %@ link" = "";
"Could not load streams" = "";
"Playback history is empty" = "";
"Show icons and text when space permits" = "";
"unknown" = "";
"Share..." = "";
/* Video sort order in search */
"Views" = "";
"You need to create an instance and accounts\nto access %@ section" = "";
"Verified" = "";
"Open Files" = "";
"Could not refresh Playlists" = "";
"Actions buttons" = "";
"Any format" = "";
"Show playback statistics" = "";
"Pages buttons" = "";
"Videos" = "";
"Codec" = "";
"Comments are disabled" = "";
"Audio" = "";
"Public" = "";
"Files" = "";
"Show Home" = "";
"Open" = "";
/* Loading stream OSD */
"Opening %@ stream..." = "";
"Clear Queue before opening" = "";
"Copy %@ link with time" = "";
"Show Inspector" = "";
"Make default" = "";
"Are you sure you want to remove %@ location?" = "";
"No chapters information available" = "";
"Share Logs..." = "";
"Enter link to open" = "";
"No documents" = "";
"Inspector visibility" = "";
"Could not update your token." = "";
"Could not find any links to open in your clipboard" = "";
/* Video date filter in search */
"Week" = "";
"Sidebar" = "";
"Show only icons" = "";
"Current: %@\n%@" = "";
"Show anonymous accounts" = "";
"Could not open playlist" = "";
"Round corners" = "";
"URL" = "";
"Recents" = "";
"Show sidebar when space permits" = "";
"System controls buttons" = "";
"Could not extract channel information" = "";
"Public Locations" = "";
"You can find information about using Yattee in the Wiki pages." = "";
/* Player controls layout size */
"Very Large" = "";
"Continue from %@" = "";
"Copy %@ link" = "";
"Seek gesture sensitivity" = "";
"Show keywords" = "";
"Wiki" = "";
"Username" = "";
"Could not extract video ID" = "";
/* Player controls layout size */
"Smaller" = "";
"Sort" = "";
"This cannot be reverted" = "";
"Public Manifest" = "";
"You have no Playlists" = "";
"Watched" = "";
"Could not open video" = "";
"Channel could not be found" = "";
"Show video length" = "";
"Source" = "";
"Welcome" = "";
"Wi-Fi" = "";
"Could not open channel" = "";
"This video could not be opened" = "";
"Could not extract playlist ID" = "";
"Could not load video" = "";
"Decreased opacity" = "";
"Shuffle All" = "";
"Share %@ link with time" = "";
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "";
"Unsubscribe" = "";
"Current Location" = "";
"Stream & Player" = "";
"Hardware decoder" = "";
"Honor orientation lock" = "";
"Seek with horizontal swipe on video" = "";
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "";
"Switch to public locations" = "";
"%@ formats" = "";
"Open logs in Finder" = "";
"Could not refresh Popular" = "";
/* SponsorBlock category name */
"Self-promotion" = "";
/* Player controls layout size */
"Small" = "";
/* SponsorBlock category name */
"Sponsor" = "";
"System controls show buttons for %@" = "";
"Show history" = "";
"Could not extract SID from received cookies: %@" = "";
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "";
"Switch to other public location" = "";
"SponsorBlock" = "";
"Seek gesture speed" = "";
"If you want this app to be available in your language, join translation project." = "";
"Could not refresh Trending" = "";
/* Video date filter in search */
"Today" = "";
"Shorts" = "";
"Channel" = "";
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
"\"%@\" will be irreversibly removed from this device." = "";
"Recent Documents" = "";
"Recent History" = "";
"Show Open Videos quick actions" = "";
"Pages toolbar position" = "";
"Video" = "";
"Channels" = "";
"Share" = "";
"File" = "";
"Share%@link" = "";

View File

@@ -679,6 +679,9 @@
379DC3D128BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; };
379DC3D228BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; };
379DC3D328BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; };
379EF9E029AA585F009FE6C6 /* HideShortsButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */; };
379EF9E129AA585F009FE6C6 /* HideShortsButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */; };
379EF9E229AA585F009FE6C6 /* HideShortsButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */; };
379F141F289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
379F1420289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
379F1421289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
@@ -827,6 +830,9 @@
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
37C7B21429ABD9F20013C196 /* ChannelPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7B21329ABD9F20013C196 /* ChannelPage.swift */; };
37C7B21529ABD9F20013C196 /* ChannelPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7B21329ABD9F20013C196 /* ChannelPage.swift */; };
37C7B21629ABD9F20013C196 /* ChannelPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7B21329ABD9F20013C196 /* ChannelPage.swift */; };
37C89322294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
37C89323294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
37C89324294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
@@ -1364,6 +1370,7 @@
37992DC726CC50BC003D4C27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = "<group>"; };
379DC3D028BA4EB400B09677 /* Seek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Seek.swift; sourceTree = "<group>"; };
379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */ = {isa = PBXFileReference; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = HideShortsButtons.swift; sourceTree = "<group>"; };
379F141E289ECE7F00DE48B5 /* QualitySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettings.swift; sourceTree = "<group>"; };
37A2B345294723850050933E /* CacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheModel.swift; sourceTree = "<group>"; };
37A362B92953707F00BDF328 /* ClearQueueButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearQueueButton.swift; sourceTree = "<group>"; };
@@ -1428,6 +1435,7 @@
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = "<group>"; };
37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = "<group>"; };
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
37C7B21329ABD9F20013C196 /* ChannelPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPage.swift; sourceTree = "<group>"; };
37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerOverlayModifier.swift; sourceTree = "<group>"; };
37C8E700294FC97D00EEAB14 /* QueueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueView.swift; sourceTree = "<group>"; };
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = "<group>"; };
@@ -1477,6 +1485,9 @@
37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettingsButton.swift; sourceTree = "<group>"; };
37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsOverlay.swift; sourceTree = "<group>"; };
37E80F3F287B472300561799 /* ScrollContentBackground+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollContentBackground+Backport.swift"; sourceTree = "<group>"; };
37E868FD29AA400B003128D0 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
37E868FE29AA402D003128D0 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = "<group>"; };
37E868FF29AA407B003128D0 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
37E8B0EB27B326C00024006F /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
37E8B0EF27B326F30024006F /* Comparable+Clamped.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comparable+Clamped.swift"; sourceTree = "<group>"; };
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
@@ -1869,6 +1880,7 @@
372CFD14285F2E2A00B0B54B /* ControlsBar.swift */,
3748186D26A769D60084E870 /* DetailBadge.swift */,
37599F37272B4D740087F250 /* FavoriteButton.swift */,
379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */,
37152EE926EFEB95004FB96D /* LazyView.swift */,
371CC7732946963000979C1A /* ListingStyleButtons.swift */,
37030FF627B0347C00ECDDAA /* MPVPlayerView.swift */,
@@ -2372,6 +2384,7 @@
377F9F79294403DC0043F856 /* Cache */,
3776ADD5287381240078EBC4 /* Captions.swift */,
37AAF28F26740715007FC770 /* Channel.swift */,
37C7B21329ABD9F20013C196 /* ChannelPage.swift */,
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
37520698285E8DD300CA655F /* Chapter.swift */,
371B7E5B27596B8400D21217 /* Comment.swift */,
@@ -2810,6 +2823,9 @@
cs,
"zh-Hans",
ca,
ar,
pt,
"pt-BR",
);
mainGroup = 37D4B0BC2671614700C925CA;
packageReferences = (
@@ -3226,6 +3242,7 @@
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
37520699285E8DD300CA655F /* Chapter.swift in Sources */,
37C7B21429ABD9F20013C196 /* ChannelPage.swift in Sources */,
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
@@ -3300,6 +3317,7 @@
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
377692562946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */,
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
379EF9E029AA585F009FE6C6 /* HideShortsButtons.swift in Sources */,
37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
37030FFB27B0398000ECDDAA /* MPVClient.swift in Sources */,
@@ -3428,6 +3446,7 @@
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
379EF9E129AA585F009FE6C6 /* HideShortsButtons.swift in Sources */,
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
378E510026FE8EEE00F49626 /* AccountViewButton.swift in Sources */,
370F4FA927CC163A001B35DC /* PlayerBackend.swift in Sources */,
@@ -3449,6 +3468,7 @@
37A362BB2953707F00BDF328 /* ClearQueueButton.swift in Sources */,
3752069E285E910600CA655F /* ChapterView.swift in Sources */,
37030FF827B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
37C7B21529ABD9F20013C196 /* ChannelPage.swift in Sources */,
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
@@ -3842,6 +3862,7 @@
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */,
379EF9E229AA585F009FE6C6 /* HideShortsButtons.swift in Sources */,
37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */,
37FAE000272ED58000330459 /* EditFavorites.swift in Sources */,
37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */,
@@ -3868,6 +3889,7 @@
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
37C8E703294FC97D00EEAB14 /* QueueView.swift in Sources */,
37C7B21629ABD9F20013C196 /* ChannelPage.swift in Sources */,
3754B01728B7F84D009717C8 /* Constants.swift in Sources */,
37BC50AE2778BCBA00510953 /* HistoryModel.swift in Sources */,
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */,
@@ -3946,6 +3968,9 @@
370D5E4F292423F400D053A6 /* cs */,
3744F85C293CC9B800B09AB9 /* zh-Hans */,
37FFCA1029523283005EC13C /* ca */,
37E868FD29AA400B003128D0 /* ar */,
37E868FE29AA402D003128D0 /* pt */,
37E868FF29AA407B003128D0 /* pt-BR */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -3959,11 +3984,8 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 138;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -3978,7 +4000,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.app.Open-in-Yattee";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development stream.yattee.app.Open-in-Yattee";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -3995,8 +4016,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 138;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -4027,7 +4047,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4047,7 +4067,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4206,11 +4226,8 @@
CLANG_CXX_LANGUAGE_STANDARD = "c++14";
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 138;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@@ -4242,7 +4259,6 @@
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development stream.yattee.app";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -4264,8 +4280,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 138;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -4315,13 +4330,10 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -4344,7 +4356,6 @@
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match AppStore stream.yattee.app macos";
SDKROOT = macosx;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = macOS/BridgingHeader.h;
@@ -4363,9 +4374,8 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -4402,7 +4412,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4426,7 +4436,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4452,7 +4462,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4477,7 +4487,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4501,12 +4511,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 136;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = tvOS/Info.plist;
@@ -4529,7 +4537,6 @@
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development stream.yattee.app tvos";
SDKROOT = appletvos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = tvOS/BridgingHeader.h;
@@ -4546,9 +4553,8 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -4588,7 +4594,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4612,7 +4618,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 138;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -96,7 +96,7 @@
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"branch" : "master",
"revision" : "554e05a4415fe20add53531c21e593c403685c3d"
"revision" : "a812079ae91a7f2e627364c42ea7b98abfa366ed"
}
},
{

View File

@@ -1,17 +1,19 @@
CERTIFICATES_GIT_URL="git@github.com:developer/yattee-certificates.git"
APP_NAME = "Yattee"
CERTIFICATES_GIT_URL = "git@github.com:developer/yattee-certificates.git"
GIT_AUTHORIZATION = "" # For certificates repo, https://github.com/settings/tokens/new (repo scope)
FASTLANE_USER="developer@mail.com" # Apple ID
FASTLANE_PASSWORD="" # Apple ID Password
ITC_TEAM_ID="" # https://sarunw.com/posts/fastlane-find-team-id/
TEAM_ID="" # Developer ID
DEVELOPER_NAME="" # Developer Name (Developer ID)
FASTLANE_USER = "developer@mail.com" # Apple ID
FASTLANE_PASSWORD = "" # Apple ID Password
ITC_TEAM_ID = "" # https://sarunw.com/posts/fastlane-find-team-id/
TEAM_ID = "" # Developer ID
DEVELOPER_NAME = "" # Developer Name (Developer ID)
# Developer Key
DEVELOPER_KEY_ID=""
DEVELOPER_KEY_ISSUER_ID=""
DEVELOPER_KEY_CONTENT=""
DEVELOPER_KEY_ID = ""
DEVELOPER_KEY_ISSUER_ID = ""
DEVELOPER_KEY_CONTENT = ""
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="" # Not needed for most users
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD = "" # Not needed for most users
TEMP_KEYCHAIN_USER = "keychain-user"
TEMP_KEYCHAIN_PASSWORD = "keychain-password"
DEVELOPER_APP_IDENTIFIER = "stream.yattee.app"

View File

@@ -13,6 +13,7 @@
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
APP_NAME = ENV['APP_NAME']
DEVELOPER_KEY_ID = ENV['DEVELOPER_KEY_ID']
DEVELOPER_KEY_ISSUER_ID = ENV['DEVELOPER_KEY_ISSUER_ID']
DEVELOPER_KEY_CONTENT = ENV['DEVELOPER_KEY_CONTENT']
@@ -23,6 +24,8 @@ DEVELOPER_APP_IDENTIFIER = ENV['DEVELOPER_APP_IDENTIFIER']
GIT_AUTHORIZATION = ENV['GIT_AUTHORIZATION']
TESTFLIGHT_EXTERNAL_GROUPS = ENV['TESTFLIGHT_EXTERNAL_GROUPS']
XCODEPROJ = "#{APP_NAME}.xcodeproj"
def delete_temp_keychain(name)
delete_keychain(
name: name
@@ -51,9 +54,11 @@ end
desc "Bump build number and commit"
lane :bump_build do
increment_build_number
increment_build_number(xcodeproj: XCODEPROJ)
commit_version_bump(
message: "Bump build number to #{get_build_number}"
message: "Bump build number to #{get_build_number(xcodeproj: XCODEPROJ)}",
xcodeproj: XCODEPROJ
)
end
@@ -61,7 +66,8 @@ desc "Bump version number and commit"
lane :bump_version do
increment_version_number
commit_version_bump(
message: "Bump version number to #{get_version_number}"
message: "Bump version number to #{get_version_number}",
xcodeproj: XCODEPROJ
)
end
@@ -76,16 +82,16 @@ platform :ios do
key_content: DEVELOPER_KEY_CONTENT
)
build = get_build_number(xcodeproj: "Yattee.xcodeproj")
build = get_build_number(xcodeproj: XCODEPROJ)
version = get_version_number(
xcodeproj: "Yattee.xcodeproj",
target: "Yattee (iOS)"
xcodeproj: XCODEPROJ,
target: "#{APP_NAME} (iOS)"
)
match(
type: 'appstore',
platform: 'ios',
app_identifier: ["#{DEVELOPER_APP_IDENTIFIER}", "#{DEVELOPER_APP_IDENTIFIER}.Open-in-Yattee"],
app_identifier: ["#{DEVELOPER_APP_IDENTIFIER}", "#{DEVELOPER_APP_IDENTIFIER}.Open-in-#{APP_NAME}"],
git_basic_authorization: Base64.strict_encode64(GIT_AUTHORIZATION),
readonly: true,
keychain_name: TEMP_KEYCHAIN_USER,
@@ -94,13 +100,13 @@ platform :ios do
)
build_app(
scheme: "Yattee (iOS)",
scheme: "#{APP_NAME} (iOS)",
output_directory: "fastlane/builds/#{version}-#{build}/iOS",
output_name: "Yattee-#{version}-iOS.ipa",
output_name: "#{APP_NAME}-#{version}-iOS.ipa",
export_options: {
provisioningProfiles: {
"#{DEVELOPER_APP_IDENTIFIER}" => "match AppStore #{DEVELOPER_APP_IDENTIFIER}",
"#{DEVELOPER_APP_IDENTIFIER}.Open-in-Yattee" => "match AppStore #{DEVELOPER_APP_IDENTIFIER}.Open-in-Yattee"
"#{DEVELOPER_APP_IDENTIFIER}.Open-in-#{APP_NAME}" => "match AppStore #{DEVELOPER_APP_IDENTIFIER}.Open-in-#{APP_NAME}"
}
}
)
@@ -128,10 +134,10 @@ platform :tvos do
key_content: DEVELOPER_KEY_CONTENT
)
build = get_build_number(xcodeproj: "Yattee.xcodeproj")
build = get_build_number(xcodeproj: XCODEPROJ)
version = get_version_number(
xcodeproj: "Yattee.xcodeproj",
target: "Yattee (tvOS)"
xcodeproj: XCODEPROJ,
target: "#{APP_NAME} (tvOS)"
)
match(
@@ -146,9 +152,9 @@ platform :tvos do
)
build_app(
scheme: "Yattee (tvOS)",
scheme: "#{APP_NAME} (tvOS)",
output_directory: "fastlane/builds/#{version}-#{build}/tvOS",
output_name: "Yattee-#{version}-tvOS.ipa",
output_name: "#{APP_NAME}-#{version}-tvOS.ipa",
export_method: "app-store",
export_options: {
provisioningProfiles: {
@@ -180,10 +186,10 @@ platform :mac do
key_content: DEVELOPER_KEY_CONTENT
)
build = get_build_number(xcodeproj: "Yattee.xcodeproj")
build = get_build_number(xcodeproj: XCODEPROJ)
version = get_version_number(
xcodeproj: "Yattee.xcodeproj",
target: "Yattee (macOS)"
xcodeproj: XCODEPROJ,
target: "#{APP_NAME} (macOS)"
)
match(
@@ -199,9 +205,9 @@ platform :mac do
)
build_app(
scheme: "Yattee (macOS)",
scheme: "#{APP_NAME} (macOS)",
output_directory: "fastlane/builds/#{version}-#{build}/macOS",
output_name: "Yattee-#{version}-macOS.app",
output_name: "#{APP_NAME}-#{version}-macOS.app",
export_method: "app-store",
export_options: {
provisioningProfiles: {
@@ -231,10 +237,10 @@ platform :mac do
key_content: DEVELOPER_KEY_CONTENT
)
build = get_build_number(xcodeproj: "Yattee.xcodeproj")
build = get_build_number(xcodeproj: XCODEPROJ)
version = get_version_number(
xcodeproj: "Yattee.xcodeproj",
target: "Yattee (macOS)"
xcodeproj: XCODEPROJ,
target: "#{APP_NAME} (macOS)"
)
match(
@@ -249,9 +255,9 @@ platform :mac do
)
build_mac_app(
scheme: "Yattee (macOS)",
scheme: "#{APP_NAME} (macOS)",
output_directory: "fastlane/builds/#{version}-#{build}/macOS",
output_name: "Yattee",
output_name: "#{APP_NAME}",
export_method: "developer-id",
export_options: {
provisioningProfiles: {
@@ -261,7 +267,7 @@ platform :mac do
)
notarize(
package: "fastlane/builds/#{version}-#{build}/macOS/Yattee.app",
package: "fastlane/builds/#{version}-#{build}/macOS/#{APP_NAME}.app",
bundle_id: "#{DEVELOPER_APP_IDENTIFIER}",
api_key: api_key,
)