Compare commits

...

54 Commits
v1.3.1 ... v1.3

Author SHA1 Message Date
Arkadiusz Fal
cd32bae24b Add Fastlane config 2022-08-15 16:27:38 +02:00
Arkadiusz Fal
17e56dc69a Bump build number 2022-08-15 16:27:03 +02:00
Arkadiusz Fal
ad715fa367 Fix #246 2022-08-15 14:51:19 +02:00
Arkadiusz Fal
560d7c4b9a Update packages 2022-08-05 09:23:58 +02:00
Arkadiusz Fal
7a88f28170 Update packages 2022-07-11 15:31:46 +02:00
Arkadiusz Fal
419e8991c9 Fix parsing Piped streams 2022-07-11 15:30:32 +02:00
Jan Weiß
720bdde728 Set project indentation default to spaces. 2022-07-11 14:39:47 +02:00
Jan Weiß
965a757031 Added unversioned TeamID infrastructure.
This is described in detail in the file "Shared.xcconfig".
2022-07-11 14:39:47 +02:00
Arkadiusz Fal
811196ae55 Update README 2022-07-04 19:57:12 +02:00
Arkadiusz Fal
5b0f6397d6 Minor style change 2022-06-15 01:09:16 +02:00
Arkadiusz Fal
d5362519d6 Update build number 2022-06-15 01:04:50 +02:00
Arkadiusz Fal
e9ee0f5ee9 Update packages 2022-06-15 01:04:32 +02:00
Arkadiusz Fal
0a9fb75c0c Update README 2022-06-15 00:42:47 +02:00
Arkadiusz Fal
eb33b65f3d Fix #161 2022-06-14 23:24:17 +02:00
Arkadiusz Fal
1c71520d6f Fix #166 2022-06-14 18:12:42 +02:00
Arkadiusz Fal
7a7e265ba1 Update build and version numbers 2022-05-27 22:59:10 +02:00
Arkadiusz Fal
c086112a49 Update README 2022-05-25 23:11:38 +02:00
Arkadiusz Fal
0802fe0029 Fix #133 2022-05-25 09:23:34 +02:00
Arkadiusz Fal
40813c2859 Update build number for Testflight 2022-05-25 09:14:27 +02:00
Arkadiusz Fal
b1238869a6 Update README 2022-05-22 23:27:59 +02:00
Arkadiusz Fal
453a5fa71b Update README 2022-05-22 23:23:05 +02:00
Arkadiusz Fal
86be252bd5 Bump build and version number 2022-05-22 18:45:19 +02:00
Bharathi
20f96fb9d6 Update README.md
Change in Piped "User Playlists" availability
2022-05-22 18:16:01 +02:00
Arkadiusz Fal
e539fb0067 Remaining playlists fixes 2022-05-22 18:08:14 +02:00
Arkadiusz Fal
03d5eefab0 Reload playlist on adding video
In case video was added to the saame playlist
2022-05-22 00:36:25 +02:00
Arkadiusz Fal
0bc4a677d4 Create/delete Piped playlists and add/remove videos to Piped playlists 2022-05-22 00:30:10 +02:00
Arkadiusz Fal
b374f82da4 Update package 2022-05-21 23:01:04 +02:00
Arkadiusz Fal
b70697e1be Improve subscriptions count
Piped API now includes it in the streams response, no need for separate
query
2022-04-16 20:05:20 +02:00
Arkadiusz Fal
db5765a84b Disable placeholder channel link 2022-04-16 20:03:25 +02:00
Arkadiusz Fal
8d36f57271 Preliminary support for Piped playlist (listing playlists and videos) 2022-04-10 17:07:10 +02:00
Arkadiusz Fal
836057578f Minor changes 2022-04-02 14:34:06 +02:00
Arkadiusz Fal
e39f4373bb Fix crashes (#69, #71) 2022-03-31 20:39:02 +02:00
Arkadiusz Fal
1490437537 Update README 2022-03-28 21:30:31 +02:00
Arkadiusz Fal
4f1b52826d Fix #109 2022-03-28 21:26:52 +02:00
Arkadiusz Fal
15e62468bb Update README 2022-03-27 23:13:53 +02:00
Arkadiusz Fal
1380036c44 Bump build and version number 2022-03-27 22:02:07 +02:00
Arkadiusz Fal
c893e5dc38 Fix menu commands 2022-03-27 22:02:07 +02:00
Arkadiusz Fal
8b4838dca5 Fix placeholders on tvOS 2022-03-27 20:31:56 +02:00
Arkadiusz Fal
1c520831d1 Improve placeholders 2022-03-27 20:27:59 +02:00
Arkadiusz Fal
8770bfb56d Fix #87 2022-03-27 13:26:38 +02:00
Arkadiusz Fal
ae4796a4c5 Add placeholders 2022-03-27 13:26:38 +02:00
Arkadiusz Fal
70b55ec2b2 Further subscribe buttons improvements 2022-03-26 19:01:38 +01:00
Arkadiusz Fal
c14a4a153d Fix #72 2022-03-26 15:22:29 +01:00
Arkadiusz Fal
c8fa972a61 Hide player on video end only on tvOS 2022-03-26 15:12:06 +01:00
Arkadiusz Fal
cc7bb83e74 Fix #84 2022-03-26 14:37:55 +01:00
Arkadiusz Fal
6a65123876 Hide subscribe button when not logged in 2022-03-26 14:07:00 +01:00
Arkadiusz Fal
aa42551c7c Fix #81 2022-03-26 13:50:01 +01:00
Arkadiusz Fal
9d8a2607ab Fix parsing subscriptions published date 2022-03-24 14:13:51 +01:00
Arkadiusz Fal
b4a0835a43 Fix #80 2022-03-24 14:04:31 +01:00
Arkadiusz Fal
066e048022 Add Defaults workaround 2022-03-24 14:03:38 +01:00
Arkadiusz Fal
d825cd8b20 Update README 2022-03-20 23:29:26 +01:00
Arkadiusz Fal
bb988764b4 Bump version number 2022-02-26 11:10:32 +01:00
Arkadiusz Fal
f7789c73d5 Fix opening playlists when recents is not saved (fix #57) 2022-02-26 11:10:29 +01:00
Ryan Stentz
1085bf0e9a fix issue #57 2022-02-26 11:07:50 +01:00
51 changed files with 1327 additions and 415 deletions

6
.gitignore vendored
View File

@@ -81,6 +81,9 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
fastlane/builds
fastlane/.env
fastlane/*.p8
# Code Injection
#
@@ -91,3 +94,6 @@ iOSInjectionProject/
# SwiftLint Remote Config Cache
.swiftlint/RemoteConfigCache
# User-specific xcconfig files
Xcode-config/DEVELOPMENT_TEAM.xcconfig

View File

@@ -3,6 +3,6 @@ import AppKit
extension NSTextField {
override open var focusRingType: NSFocusRingType {
get { .none }
set {} // swiftlint:disable:this unused_setter_value
set {}
}
}

View File

@@ -10,7 +10,7 @@ extension Thumbnail {
}
private static var fixturesHost: String {
"https://invidious.home.arekf.net"
"https://invidious.snopyta.org"
}
private static func fixtureUrl(videoId: String, quality: Thumbnail.Quality) -> URL {

View File

@@ -1,13 +1,15 @@
import Foundation
extension Video {
static var fixtureID: Video.ID = "video-fixture"
static var fixtureChannelID: Channel.ID = "channel-fixture"
static var fixture: Video {
let id = "D2sxamzaHkM"
let thumbnailURL = "https://yt3.ggpht.com/ytc/AKedOLR-pT_JEsz_hcaA4Gjx8DHcqJ8mS42aTRqcVy6P7w=s88-c-k-c0x00ffffff-no-rj-mo"
return Video(
videoID: UUID().uuidString,
title: "Relaxing Piano Music that will make you feel amazingly good",
videoID: fixtureID,
title: "Relaxing Piano Music to feel good",
author: "Fancy Videotuber",
length: 582,
published: "7 years ago",
@@ -15,13 +17,13 @@ extension Video {
description: "Some relaxing live piano music",
genre: "Music",
channel: Channel(
id: "AbCdEFgHI",
id: fixtureChannelID,
name: "The Channel",
thumbnailURL: URL(string: thumbnailURL)!,
subscriptionsCount: 2300,
videos: []
),
thumbnails: Thumbnail.fixturesForAllQualities(videoId: id),
thumbnails: [],
live: false,
upcoming: false,
publishedAt: Date(),

3
Gemfile Normal file
View File

@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

218
Gemfile.lock Normal file
View File

@@ -0,0 +1,218 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.618.0)
aws-sdk-core (3.132.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.92.4)
faraday (1.10.1)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.209.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.25.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-core (0.7.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.13.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.17.0)
google-apis-core (>= 0.7, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.38.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.17.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
jwt (2.4.1)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-21
DEPENDENCIES
fastlane
BUNDLED WITH
2.3.6

View File

@@ -92,15 +92,18 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> SearchPage in
let results = content.json.arrayValue.compactMap { json -> ContentItem in
let results = content.json.arrayValue.compactMap { json -> ContentItem? in
let type = json.dictionaryValue["type"]?.stringValue
if type == "channel" {
return ContentItem(channel: self.extractChannel(from: json))
} else if type == "playlist" {
return ContentItem(playlist: self.extractChannelPlaylist(from: json))
} else if type == "video" {
return ContentItem(video: self.extractVideo(from: json))
}
return ContentItem(video: self.extractVideo(from: json))
return nil
}
return SearchPage(results: results, last: results.isEmpty)
@@ -236,6 +239,66 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
playlist(playlistID)?.child("videos").child(videoID)
}
func addVideoToPlaylist(
_ videoID: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void = { _ in },
onSuccess: @escaping () -> Void = {}
) {
let resource = playlistVideos(playlistID)
let body = ["videoId": videoID]
resource?
.request(.post, json: body)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func removeVideoFromPlaylist(
_ index: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
) {
let resource = playlistVideo(playlistID, index)
resource?
.request(.delete)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func playlistForm(
_ name: String,
_ visibility: String,
playlist: Playlist?,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping (Playlist?) -> Void
) {
let body = ["title": name, "privacy": visibility]
let resource = !playlist.isNil ? self.playlist(playlist!.id) : playlists
resource?
.request(!playlist.isNil ? .patch : .post, json: body)
.onSuccess { response in
if let modifiedPlaylist: Playlist = response.typedContent() {
onSuccess(modifiedPlaylist)
}
}
.onFailure(onFailure)
}
func deletePlaylist(
_ playlist: Playlist,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
) {
self.playlist(playlist.id)?
.request(.delete)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func channelPlaylist(_ id: String) -> Resource? {
resource(baseURL: account.url, path: basePathAppending("playlists/\(id)"))
}

View File

@@ -4,7 +4,7 @@ import Siesta
import SwiftyJSON
final class PipedAPI: Service, ObservableObject, VideosAPI {
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"]
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"]
@Published var account: Account!
@@ -43,6 +43,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
self.extractChannelPlaylist(from: content.json)
}
configureTransformer(pathPattern("user/playlists/create")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("user/playlists/delete")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("user/playlists/add")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("user/playlists/remove")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
self.extractVideo(from: content.json)
}
@@ -81,6 +86,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
}
configureTransformer(pathPattern("user/playlists")) { (content: Entity<JSON>) -> [Playlist] in
content.json.arrayValue.map { self.extractUserPlaylist(from: $0)! }
}
if account.token.isNil {
updateToken()
}
@@ -166,7 +175,9 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
var home: Resource? { nil }
var popular: Resource? { nil }
var playlists: Resource? { nil }
var playlists: Resource? {
resource(baseURL: account.instance.apiURL, path: "user/playlists")
}
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resource(baseURL: account.instance.apiURL, path: "subscribe")
@@ -180,10 +191,79 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.onCompletion { _ in onCompletion() }
}
func playlist(_: String) -> Resource? { nil }
func playlist(_ id: String) -> Resource? {
channelPlaylist(id)
}
func playlistVideo(_: String, _: String) -> Resource? { nil }
func playlistVideos(_: String) -> Resource? { nil }
func addVideoToPlaylist(
_ videoID: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void = { _ in },
onSuccess: @escaping () -> Void = {}
) {
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/add")
let body = ["videoId": videoID, "playlistId": playlistID]
resource
.request(.post, json: body)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func removeVideoFromPlaylist(
_ index: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
) {
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/remove")
let body: [String: Any] = ["index": Int(index)!, "playlistId": playlistID]
resource
.request(.post, json: body)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func playlistForm(
_ name: String,
_: String,
playlist: Playlist?,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping (Playlist?) -> Void
) {
let body = ["name": name]
let resource = playlist.isNil ? resource(baseURL: account.instance.apiURL, path: "user/playlists/create") : nil
resource?
.request(.post, json: body)
.onSuccess { response in
if let modifiedPlaylist: Playlist = response.typedContent() {
onSuccess(modifiedPlaylist)
} else {
onSuccess(nil)
}
}
.onFailure(onFailure)
}
func deletePlaylist(
_ playlist: Playlist,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
) {
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/delete")
let body = ["playlistId": playlist.id]
resource
.request(.post, json: body)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func comments(_ id: Video.ID, page: String?) -> Resource? {
let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)"
let resource = resource(baseURL: account.url, path: path)
@@ -232,6 +312,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
if let channel = extractChannel(from: content) {
return ContentItem(channel: channel)
}
default:
return nil
}
return nil
@@ -278,7 +360,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
}
return ChannelPlaylist(
id: id,
title: details["name"]!.stringValue,
title: details["name"]?.stringValue ?? "",
thumbnailURL: thumbnailURL,
channel: extractChannel(from: json)!,
videos: videos,
@@ -308,9 +390,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url
let subscriptionsCount = details["uploaderSubscriberCount"]?.int
let uploaded = details["uploaded"]?.doubleValue
var published = (uploaded.isNil || uploaded == -1) ? nil : (uploaded! / 1000).formattedAsRelativeTime()
if published.isNil {
published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ?? ""
}
let published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ??
(details["uploaded"]!.double! / 1000).formattedAsRelativeTime()!
let live = details["livestream"]?.boolValue ?? (details["duration"]?.intValue == -1)
return Video(
@@ -318,10 +405,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
title: details["title"]!.stringValue,
author: author,
length: details["duration"]!.doubleValue,
published: published,
published: published!,
views: details["views"]!.intValue,
description: extractDescription(from: content),
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL),
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
thumbnails: thumbnails,
live: live,
likes: details["likes"]?.int,
@@ -353,6 +440,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
)!
}
private func extractUserPlaylist(from json: JSON) -> Playlist? {
let id = json["id"].stringValue
let title = json["name"].stringValue
let visibility = Playlist.Visibility.private
return Playlist(id: id, title: title, visibility: visibility)
}
private func extractDescription(from content: JSON) -> String? {
guard var description = content.dictionaryValue["description"]?.string else {
return nil
@@ -393,8 +488,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let videoStreams = compatibleVideoStream(from: content)
videoStreams.forEach { videoStream in
let audioAsset = AVURLAsset(url: audioStream.dictionaryValue["url"]!.url!)
let videoAsset = AVURLAsset(url: videoStream.dictionaryValue["url"]!.url!)
guard let audioAssetUrl = audioStream.dictionaryValue["url"]?.url,
let videoAssetUrl = videoStream.dictionaryValue["url"]?.url
else {
return
}
let audioAsset = AVURLAsset(url: audioAssetUrl)
let videoAsset = AVURLAsset(url: videoAssetUrl)
let videoOnly = videoStream.dictionaryValue["videoOnly"]?.boolValue ?? true
let resolution = Stream.Resolution.from(resolution: videoStream.dictionaryValue["quality"]!.stringValue)

View File

@@ -27,6 +27,34 @@ protocol VideosAPI {
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
func playlistVideos(_ id: String) -> Resource?
func addVideoToPlaylist(
_ videoID: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
)
func removeVideoFromPlaylist(
_ index: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
)
func playlistForm(
_ name: String,
_ visibility: String,
playlist: Playlist?,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping (Playlist?) -> Void
)
func deletePlaylist(
_ playlist: Playlist,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
)
func channelPlaylist(_ id: String) -> Resource?
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)
@@ -74,6 +102,8 @@ extension VideosAPI {
case .playlist:
urlComponents.path = "/playlist"
queryItems.append(.init(name: "list", value: item.playlist.id))
default:
return nil
}
if !time.isNil, time!.seconds.isFinite {

View File

@@ -32,6 +32,18 @@ enum VideosApp: String, CaseIterable {
}
var supportsUserPlaylists: Bool {
true
}
var userPlaylistsEndpointIncludesVideos: Bool {
self == .invidious
}
var userPlaylistsHaveVisibility: Bool {
self == .invidious
}
var userPlaylistsAreEditable: Bool {
self == .invidious
}

View File

@@ -2,7 +2,7 @@ import Foundation
struct ContentItem: Identifiable {
enum ContentType: String {
case video, playlist, channel
case video, playlist, channel, placeholder
private var sortOrder: Int {
switch self {
@@ -35,6 +35,6 @@ struct ContentItem: Identifiable {
}
var contentType: ContentType {
video.isNil ? (channel.isNil ? .playlist : .channel) : .video
video.isNil ? (channel.isNil ? (playlist.isNil ? .placeholder : .playlist) : .channel) : .video
}
}

View File

@@ -14,6 +14,31 @@ final class NavigationModel: ObservableObject {
case nowPlaying
case search
var stringValue: String {
switch self {
case .favorites:
return "favorites"
case .subscriptions:
return "subscriptions"
case .popular:
return "popular"
case .trending:
return "trending"
case .playlists:
return "playlists"
case let .channel(string):
return "channel\(string)"
case let .playlist(string):
return "playlist\(string)"
case .recentlyOpened:
return "recentlyOpened"
case .search:
return "search"
default:
return ""
}
}
var playlistID: Playlist.ID? {
if case let .playlist(id) = self {
return id
@@ -49,6 +74,10 @@ final class NavigationModel: ObservableObject {
navigationStyle: NavigationStyle,
delay: Bool = true
) {
guard channel.id != Video.fixtureChannelID else {
return
}
let recent = RecentItem(from: channel)
#if os(macOS)
Windows.main.open()
@@ -141,6 +170,12 @@ final class NavigationModel: ObservableObject {
channelToUnsubscribe = channel
presentingUnsubscribeAlert = channelToUnsubscribe != nil
}
func hideKeyboard() {
#if os(iOS)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
#endif
}
}
typealias TabSelection = NavigationModel.TabSelection

View File

@@ -45,8 +45,6 @@ final class PlayerModel: ObservableObject {
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
@Published var restoredSegments = [Segment]()
@Published var channelWithDetails: Channel?
#if os(iOS)
@Published var motionManager: CMMotionManager!
@Published var lockedOrientation: UIInterfaceOrientation?
@@ -210,11 +208,7 @@ final class PlayerModel: ObservableObject {
self?.sponsorBlock.loadSegments(
videoID: video.videoID,
categories: Defaults[.sponsorBlockCategories]
) { [weak self] in
if Defaults[.showChannelSubscribers] {
self?.loadCurrentItemChannelDetails()
}
}
)
}
}
@@ -556,8 +550,6 @@ final class PlayerModel: ObservableObject {
controller?.playerView.dismiss(animated: false) { [weak self] in
self?.controller?.dismiss(animated: true)
}
#else
hide()
#endif
} else {
advanceToNextItem()
@@ -708,36 +700,6 @@ final class PlayerModel: ObservableObject {
currentArtwork = MPMediaItemArtwork(boundsSize: image!.size) { _ in image! }
}
func loadCurrentItemChannelDetails() {
guard let video = currentVideo,
!video.channel.detailsLoaded
else {
return
}
if restoreLoadedChannel() {
return
}
accounts.api.channel(video.channel.id).load().onSuccess { [weak self] response in
if let channel: Channel = response.typedContent() {
self?.channelWithDetails = channel
withAnimation {
self?.currentItem?.video.channel = channel
}
}
}
}
@discardableResult func restoreLoadedChannel() -> Bool {
if !currentVideo.isNil, channelWithDetails?.id == currentVideo!.channel.id {
currentItem.video.channel = channelWithDetails!
return true
}
return false
}
func rateLabel(_ rate: Float) -> String {
let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0

View File

@@ -74,7 +74,6 @@ extension PlayerModel {
}
preservedTime = currentItem.playbackTime
restoreLoadedChannel()
DispatchQueue.main.async { [weak self] in
guard let video = self?.currentVideo else {

View File

@@ -18,11 +18,11 @@ struct Playlist: Identifiable, Equatable, Hashable {
var title: String
var visibility: Visibility
var updated: TimeInterval
var updated: TimeInterval?
var videos = [Video]()
init(id: String, title: String, visibility: Visibility, updated: TimeInterval, videos: [Video] = []) {
init(id: String, title: String, visibility: Visibility, updated: TimeInterval? = nil, videos: [Video] = []) {
self.id = id
self.title = title
self.visibility = visibility

View File

@@ -4,6 +4,7 @@ import SwiftUI
final class PlaylistsModel: ObservableObject {
@Published var playlists = [Playlist]()
@Published var reloadPlaylists = false
var accounts = AccountsModel()
@@ -58,24 +59,20 @@ final class PlaylistsModel: ObservableObject {
onSuccess: @escaping () -> Void = {},
onFailure: @escaping (RequestError) -> Void = { _ in }
) {
let resource = accounts.api.playlistVideos(playlistID)
let body = ["videoId": videoID]
resource?
.request(.post, json: body)
.onSuccess { _ in
self.load(force: true)
accounts.api.addVideoToPlaylist(videoID, playlistID, onFailure: onFailure) {
self.load(force: true) {
self.reloadPlaylists.toggle()
onSuccess()
}
.onFailure(onFailure)
}
}
func removeVideo(videoIndexID: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) {
let resource = accounts.api.playlistVideo(playlistID, videoIndexID)
resource?.request(.delete).onSuccess { _ in
self.load(force: true)
onSuccess()
func removeVideo(index: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) {
accounts.api.removeVideoFromPlaylist(index, playlistID, onFailure: { _ in }) {
self.load(force: true) {
self.reloadPlaylists.toggle()
onSuccess()
}
}
}

View File

@@ -16,7 +16,7 @@ final class RecentsModel: ObservableObject {
if !saveRecents {
clear()
if item.type != .channel {
if item.type == .query {
return
}
}

View File

@@ -115,7 +115,7 @@ final class SearchModel: ObservableObject {
resource?.removeObservers(ownedBy: store)
resource = accounts.api.search(query, page: page?.nextPage)
resource = accounts.api.search(query, page: pageToLoad.nextPage)
resource.addObserver(store)
resource

View File

@@ -17,7 +17,7 @@ struct Video: Identifiable, Equatable, Hashable {
var genre: String?
// index used when in the Playlist
let indexID: String?
var indexID: String?
var live: Bool
var upcoming: Bool

View File

@@ -6,7 +6,11 @@
[![AGPL v3](https://shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
[![GitHub issues](https://img.shields.io/github/issues/yattee/yattee)](https://github.com/yattee/yattee/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/yattee/yattee)](https://github.com/yattee/yattee/pulls)
[![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#yattee:matrix.org)
[![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#Yattee:matrix.org)
Now on **Discord**:
[![Discord](https://img.shields.io/discord/992481375817052251?style=for-the-badge)](https://yattee.stream/discord)
![Screenshot](https://r.yattee.stream/screenshots/all-platforms.png)
</div>
@@ -19,26 +23,32 @@
* Fullscreen playback, Picture in Picture and AirPlay support
* Stream quality selection
### Features in development
* New player component with custom controls, gestures and support for 4K playback
You can leave your feedback in [discussions](https://github.com/yattee/yattee/discussions) or join [Discord](https://yattee.stream/discord) or [Matrix Channel](https://matrix.to/#/#Yattee:matrix.org) for a chat. Thanks!
### Availability
| Feature | Invidious | Piped |
|| Invidious | Piped |
| - | - | - |
| User Accounts | ✅ | ✅ |
| Subscriptions | ✅ | ✅ |
| Popular | ✅ | 🔴 |
| User Playlists | ✅ | 🔴 |
| User Playlists | ✅ | |
| Trending | ✅ | ✅ |
| Channels | ✅ | ✅ |
| Channel Playlists | ✅ | ✅ |
| Search | ✅ | ✅ |
| Search Suggestions | ✅ | ✅ |
| Search Filters | ✅ | 🔴 |
| Popular | ✅ | 🔴 |
| Subtitles | 🔴 | ✅ |
| Comments | 🔴 | ✅ |
You can browse and use accounts from one app and play videos with another (for example: use Invidious account for subscriptions and use Piped as playback source). Comments can be displayed from Piped even when Invidious is used for browsing/playing.
## Documentation
* [Installation Instructions](https://github.com/yattee/yattee/wiki/Installation-Instructions)
* [Installation](https://github.com/yattee/yattee/wiki/Installation-Instructions)
* [Building](https://github.com/yattee/yattee/wiki/Building-instructions)
* [FAQ](https://github.com/yattee/yattee/wiki)
* [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery)
* [Tips](https://github.com/yattee/yattee/wiki/Tips)
@@ -48,10 +58,16 @@ You can browse and use accounts from one app and play videos with another (for e
## Contributing
If you're interestred in contributing, you can browse the [issues](https://github.com/yattee/yattee/issues) list or create a new one to discuss your feature idea. Every contribution is very welcome.
## License and Liability
Use [building instructions](https://github.com/yattee/yattee/wiki/Building-instructions) or
join [Discord](https://yattee.stream/discord) or [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat if you need an advice or want to discuss the project.
## License
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
Contributors take no responsibility for the use of the tool (Point 16. of the license). We strongly recommend you abide by the valid official regulations in your country. Furthermore, we refuse liability for any inappropriate use of the tool, such as downloading materials without proper consent.
## Disclaimer
The Yattee project and its contents are not affiliated with, funded, authorized, endorsed by, or in any way accociated with YouTube, Google LLC or any of its affiliates and subsidaries. The official YouTube website can be found at www.youtube.com.
Any trademark, service mark, trade name, or other intellectual property rights used in the Yattee project are owned by the respective owners.
This tool is an open source software built for learning and research purposes.

View File

@@ -0,0 +1,38 @@
import Defaults
import Foundation
extension Defaults.Serializable where Self: Codable {
static var bridge: Defaults.TopLevelCodableBridge<Self> { Defaults.TopLevelCodableBridge() }
}
extension Defaults.Serializable where Self: Codable & NSSecureCoding {
static var bridge: Defaults.CodableNSSecureCodingBridge<Self> { Defaults.CodableNSSecureCodingBridge() }
}
extension Defaults.Serializable where Self: Codable & NSSecureCoding & Defaults.PreferNSSecureCoding {
static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}
extension Defaults.Serializable where Self: Codable & RawRepresentable {
static var bridge: Defaults.RawRepresentableCodableBridge<Self> { Defaults.RawRepresentableCodableBridge() }
}
extension Defaults.Serializable where Self: Codable & RawRepresentable & Defaults.PreferRawRepresentable {
static var bridge: Defaults.RawRepresentableBridge<Self> { Defaults.RawRepresentableBridge() }
}
extension Defaults.Serializable where Self: RawRepresentable {
static var bridge: Defaults.RawRepresentableBridge<Self> { Defaults.RawRepresentableBridge() }
}
extension Defaults.Serializable where Self: NSSecureCoding {
static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}
extension Defaults.CollectionSerializable where Element: Defaults.Serializable {
static var bridge: Defaults.CollectionBridge<Self> { Defaults.CollectionBridge() }
}
extension Defaults.SetAlgebraSerializable where Element: Defaults.Serializable & Hashable {
static var bridge: Defaults.SetAlgebraBridge<Self> { Defaults.SetAlgebraBridge() }
}

View File

@@ -48,7 +48,6 @@ extension Defaults.Keys {
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
static let showKeywords = Key<Bool>("showKeywords", default: false)
static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", default: false)
static let showChannelSubscribers = Key<Bool>("showChannelSubscribers", default: true)
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
#if !os(tvOS)
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)

View File

@@ -11,10 +11,18 @@ struct DropFavorite: DropDelegate {
return
}
let from = favorites.firstIndex(of: current!)!
let to = favorites.firstIndex(of: item)!
guard let current = current else {
return
}
guard favorites[to].id != current!.id else {
let from = favorites.firstIndex(of: current)
let to = favorites.firstIndex(of: item)
guard let from = from, let to = to else {
return
}
guard favorites[to].id != current.id else {
return
}

View File

@@ -12,29 +12,29 @@ struct MenuCommands: Commands {
private var navigationMenu: some Commands {
CommandGroup(before: .windowSize) {
Button("Favorites") {
model.navigation?.tabSelection = .favorites
setTabSelection(.favorites)
}
.keyboardShortcut("1")
Button("Subscriptions") {
model.navigation?.tabSelection = .subscriptions
setTabSelection(.subscriptions)
}
.disabled(subscriptionsDisabled)
.keyboardShortcut("2")
Button("Popular") {
model.navigation?.tabSelection = .popular
setTabSelection(.popular)
}
.disabled(!(model.accounts?.app.supportsPopular ?? false))
.keyboardShortcut("3")
Button("Trending") {
model.navigation?.tabSelection = .trending
setTabSelection(.trending)
}
.keyboardShortcut("4")
Button("Search") {
model.navigation?.tabSelection = .search
setTabSelection(.search)
}
.keyboardShortcut("f")
@@ -42,6 +42,15 @@ struct MenuCommands: Commands {
}
}
private func setTabSelection(_ tabSelection: NavigationModel.TabSelection) {
guard let navigation = model.navigation else {
return
}
navigation.sidebarSectionChanged.toggle()
navigation.tabSelection = tabSelection
}
private var subscriptionsDisabled: Bool {
!(
(model.accounts?.app.supportsSubscriptions ?? false) && model.accounts?.signedIn ?? false

View File

@@ -11,9 +11,7 @@ struct AppSidebarPlaylists: View {
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) {
LazyView(PlaylistVideosView(playlist))
} label: {
Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title))
.backport
.badge(Text("\(playlist.videos.count)"))
playlistLabel(playlist)
}
.id(playlist.id)
.contextMenu {
@@ -34,6 +32,18 @@ struct AppSidebarPlaylists: View {
}
}
@ViewBuilder func playlistLabel(_ playlist: Playlist) -> some View {
let label = Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title))
if player.accounts.app.userPlaylistsEndpointIncludesVideos {
label
.backport
.badge(Text("\(playlist.videos.count)"))
} else {
label
}
}
var newPlaylistButton: some View {
Button(action: { navigation.presentNewPlaylistForm() }) {
Label("New Playlist", systemImage: "plus.circle")

View File

@@ -45,6 +45,7 @@ struct Sidebar: View {
Label("Favorites", systemImage: "heart")
.accessibility(label: Text("Favorites"))
}
.id("favorites")
}
if visibleSections.contains(.subscriptions),
accounts.app.supportsSubscriptions && accounts.signedIn
@@ -53,6 +54,7 @@ struct Sidebar: View {
Label("Subscriptions", systemImage: "star.circle")
.accessibility(label: Text("Subscriptions"))
}
.id("subscriptions")
}
if visibleSections.contains(.popular), accounts.app.supportsPopular {
@@ -60,6 +62,7 @@ struct Sidebar: View {
Label("Popular", systemImage: "arrow.up.right.circle")
.accessibility(label: Text("Popular"))
}
.id("popular")
}
if visibleSections.contains(.trending) {
@@ -67,12 +70,14 @@ struct Sidebar: View {
Label("Trending", systemImage: "chart.bar")
.accessibility(label: Text("Trending"))
}
.id("trending")
}
NavigationLink(destination: LazyView(SearchView()), tag: TabSelection.search, selection: $navigation.tabSelection) {
Label("Search", systemImage: "magnifyingglass")
.accessibility(label: Text("Search"))
}
.id("search")
.keyboardShortcut("f")
}
}
@@ -80,8 +85,12 @@ struct Sidebar: View {
private func scrollScrollViewToItem(scrollView: ScrollViewProxy, for selection: TabSelection) {
if case .recentlyOpened = selection {
scrollView.scrollTo("recentlyOpened")
return
} else if case let .playlist(id) = selection {
scrollView.scrollTo(id)
return
}
scrollView.scrollTo(selection.stringValue)
}
}

View File

@@ -103,8 +103,14 @@ struct PlaybackBar: View {
return "loading..."
}
let videoLengthAtRate = player.currentVideo!.length / Double(player.currentRate)
let remainingSeconds = videoLengthAtRate - player.time!.seconds
guard let video = player.currentVideo,
let time = player.time
else {
return ""
}
let videoLengthAtRate = video.length / Double(player.currentRate)
let remainingSeconds = videoLengthAtRate - time.seconds
if remainingSeconds < 60 {
return "less than a minute"

View File

@@ -12,6 +12,7 @@ struct VideoDetails: View {
@Binding var fullScreen: Bool
@State private var subscribed = false
@State private var subscriptionToggleButtonDisabled = false
@State private var presentingUnsubscribeAlert = false
@State private var presentingAddToPlaylist = false
@State private var presentingShareSheet = false
@@ -29,7 +30,6 @@ struct VideoDetails: View {
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.showChannelSubscribers) private var showChannelSubscribers
@Default(.showKeywords) private var showKeywords
init(
@@ -207,15 +207,13 @@ struct VideoDetails: View {
.font(.system(size: 14))
.bold()
if showChannelSubscribers {
Group {
if let subscribers = video!.channel.subscriptionsString {
Text("\(subscribers) subscribers")
}
Group {
if let subscribers = video!.channel.subscriptionsString {
Text("\(subscribers) subscribers")
}
.foregroundColor(.secondary)
.font(.caption2)
}
.foregroundColor(.secondary)
.font(.caption2)
}
}
}
@@ -236,7 +234,7 @@ struct VideoDetails: View {
}
}
if accounts.app.supportsSubscriptions {
if accounts.app.supportsSubscriptions, accounts.signedIn {
Spacer()
Section {
@@ -254,10 +252,13 @@ struct VideoDetails: View {
"Are you sure you want to unsubscribe from \(video!.channel.name)?"
),
primaryButton: .destructive(Text("Unsubscribe")) {
subscriptions.unsubscribe(video!.channel.id)
subscriptionToggleButtonDisabled = true
withAnimation {
subscribed.toggle()
subscriptions.unsubscribe(video!.channel.id) {
withAnimation {
subscriptionToggleButtonDisabled = false
subscribed.toggle()
}
}
},
secondaryButton: .cancel()
@@ -265,16 +266,20 @@ struct VideoDetails: View {
}
} else {
Button("Subscribe") {
subscriptions.subscribe(video!.channel.id)
subscriptionToggleButtonDisabled = true
withAnimation {
subscribed.toggle()
subscriptions.subscribe(video!.channel.id) {
withAnimation {
subscriptionToggleButtonDisabled = false
subscribed.toggle()
}
}
}
.backport
.tint(.blue)
.tint(subscriptionToggleButtonDisabled ? .gray : .blue)
}
}
.disabled(subscriptionToggleButtonDisabled)
.font(.system(size: 13))
.buttonStyle(.borderless)
}

View File

@@ -9,6 +9,7 @@ struct AddToPlaylistView: View {
@State private var error = ""
@State private var presentingErrorAlert = false
@State private var submitButtonDisabled = false
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode
@@ -18,15 +19,15 @@ struct AddToPlaylistView: View {
var body: some View {
Group {
VStack {
header
Spacer()
if model.isEmpty {
emptyPlaylistsMessage
} else {
header
Spacer()
form
Spacer()
footer
}
Spacer()
footer
}
.frame(maxWidth: 1000, maxHeight: height)
}
@@ -122,7 +123,7 @@ struct AddToPlaylistView: View {
HStack {
Spacer()
Button("Add to Playlist", action: addToPlaylist)
.disabled(selectedPlaylist.isNil)
.disabled(submitButtonDisabled || selectedPlaylist.isNil)
.padding(.top, 30)
.alert(isPresented: $presentingErrorAlert) {
Alert(
@@ -165,6 +166,8 @@ struct AddToPlaylistView: View {
Defaults[.lastUsedPlaylistID] = id
submitButtonDisabled = true
model.addVideo(
playlistID: id,
videoID: video.videoID,
@@ -174,6 +177,7 @@ struct AddToPlaylistView: View {
onFailure: { requestError in
error = "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)"
presentingErrorAlert = true
submitButtonDisabled = false
}
)
}

View File

@@ -43,9 +43,12 @@ struct PlaylistFormView: View {
TextField("Name", text: $name, onCommit: validate)
.frame(maxWidth: 450)
.padding(.leading, 10)
.disabled(editing && !accounts.app.userPlaylistsAreEditable)
visibilityFormItem
.pickerStyle(.segmented)
if accounts.app.userPlaylistsHaveVisibility {
visibilityFormItem
.pickerStyle(.segmented)
}
}
#if os(macOS)
.padding(.horizontal)
@@ -59,7 +62,7 @@ struct PlaylistFormView: View {
Spacer()
Button("Save", action: submitForm)
.disabled(!valid)
.disabled(!valid || (editing && !accounts.app.userPlaylistsAreEditable))
.alert(isPresented: $presentingErrorAlert) {
Alert(
title: Text("Error when accessing playlist"),
@@ -75,7 +78,7 @@ struct PlaylistFormView: View {
#if os(iOS)
.padding(.vertical)
#else
.frame(width: 400, height: 150)
.frame(width: 400, height: accounts.app.userPlaylistsHaveVisibility ? 150 : 120)
#endif
#else
@@ -119,20 +122,24 @@ struct PlaylistFormView: View {
.frame(maxWidth: .infinity, alignment: .leading)
TextField("Playlist Name", text: $name, onCommit: validate)
.disabled(editing && !accounts.app.userPlaylistsAreEditable)
}
HStack {
Text("Visibility")
.frame(maxWidth: .infinity, alignment: .leading)
if accounts.app.userPlaylistsHaveVisibility {
HStack {
Text("Visibility")
.frame(maxWidth: .infinity, alignment: .leading)
visibilityFormItem
visibilityFormItem
}
.padding(.top, 10)
}
.padding(.top, 10)
HStack {
Spacer()
Button("Save", action: submitForm).disabled(!valid)
Button("Save", action: submitForm)
.disabled(!valid || (editing && !accounts.app.userPlaylistsAreEditable))
}
.padding(.top, 40)
@@ -172,27 +179,15 @@ struct PlaylistFormView: View {
return
}
let body = ["title": name, "privacy": visibility.rawValue]
accounts.api.playlistForm(name, visibility.rawValue, playlist: playlist, onFailure: { error in
formError = "(\(error.httpStatusCode ?? -1)) \(error.userMessage)"
presentingErrorAlert = true
}) { modifiedPlaylist in
self.playlist = modifiedPlaylist
playlists.load(force: true)
resource?
.request(editing ? .patch : .post, json: body)
.onSuccess { response in
if let modifiedPlaylist: Playlist = response.typedContent() {
playlist = modifiedPlaylist
}
playlists.load(force: true)
presentationMode.wrappedValue.dismiss()
}
.onFailure { error in
formError = "(\(error.httpStatusCode ?? -1)) \(error.userMessage)"
presentingErrorAlert = true
}
}
var resource: Resource? {
editing ? accounts.api.playlist(playlist.id) : accounts.api.playlists
presentationMode.wrappedValue.dismiss()
}
}
var visibilityFormItem: some View {
@@ -236,17 +231,14 @@ struct PlaylistFormView: View {
}
func deletePlaylistAndDismiss() {
accounts.api.playlist(playlist.id)?
.request(.delete)
.onSuccess { _ in
playlist = nil
playlists.load(force: true)
presentationMode.wrappedValue.dismiss()
}
.onFailure { error in
formError = "(\(error.httpStatusCode ?? -1)) \(error.localizedDescription)"
presentingErrorAlert = true
}
accounts.api.deletePlaylist(playlist, onFailure: { error in
formError = "(\(error.httpStatusCode ?? -1)) \(error.localizedDescription)"
presentingErrorAlert = true
}) {
playlist = nil
playlists.load(force: true)
presentationMode.wrappedValue.dismiss()
}
}
}

View File

@@ -11,6 +11,8 @@ struct PlaylistsView: View {
@State private var showingEditPlaylist = false
@State private var editedPlaylist: Playlist?
@StateObject private var store = Store<ChannelPlaylist>()
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model
@@ -18,7 +20,36 @@ struct PlaylistsView: View {
@Namespace private var focusNamespace
var items: [ContentItem] {
ContentItem.array(of: currentPlaylist?.videos ?? [])
var videos = currentPlaylist?.videos ?? []
if videos.isEmpty {
videos = store.item?.videos ?? []
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
var i = 0
for index in videos.indices {
var video = videos[index]
video.indexID = "\(i)"
i += 1
videos[index] = video
}
}
}
return ContentItem.array(of: videos)
}
private var resource: Resource? {
guard !player.accounts.app.userPlaylistsEndpointIncludesVideos,
let playlist = currentPlaylist
else {
return nil
}
let resource = player.accounts.api.playlist(playlist.id)
resource?.addObserver(store)
return resource
}
var body: some View {
@@ -112,10 +143,17 @@ struct PlaylistsView: View {
#endif
.onAppear {
model.load()
resource?.load()
}
.onChange(of: accounts.current) { _ in
model.load(force: true)
}
.onChange(of: selectedPlaylistID) { _ in
resource?.load()
}
.onChange(of: model.reloadPlaylists) { _ in
resource?.load()
}
#if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif

View File

@@ -29,7 +29,10 @@ struct SearchTextField: View {
.opacity(0.8)
#endif
TextField("Search...", text: $state.queryText) {
state.changeQuery { query in query.query = state.queryText }
state.changeQuery { query in
query.query = state.queryText
navigation.hideKeyboard()
}
recents.addQuery(state.queryText, navigation: navigation)
}
.onChange(of: state.queryText) { _ in

View File

@@ -75,6 +75,7 @@ struct SearchSuggestions: View {
state.changeQuery { query in
query.query = state.queryText
state.fieldIsFocused = false
navigation.hideKeyboard()
}
recents.addQuery(state.queryText, navigation: navigation)

View File

@@ -198,7 +198,7 @@ struct SearchView: View {
visibleSections.append(.subscriptions)
}
if accounts.app.supportsUserPlaylists && preferred.contains(.playlists) {
if accounts.app.supportsUserPlaylists && accounts.signedIn && preferred.contains(.playlists) {
visibleSections.append(.playlists)
}
@@ -234,7 +234,7 @@ struct SearchView: View {
}
.edgesIgnoringSafeArea(.horizontal)
#else
VerticalCells(items: items)
VerticalCells(items: items, allowEmpty: state.query.isEmpty)
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
#endif
@@ -286,52 +286,7 @@ struct SearchView: View {
.foregroundColor(.secondary)
}
ForEach(recentItems) { item in
Button {
switch item.type {
case .query:
state.queryText = item.title
state.changeQuery { query in query.query = item.title }
updateFavoriteItem()
recents.add(item)
case .channel:
guard let channel = item.channel else {
return
}
NavigationModel.openChannel(
channel,
player: player,
recents: recents,
navigation: navigation,
navigationStyle: navigationStyle,
delay: false
)
case .playlist:
guard let playlist = item.playlist else {
return
}
NavigationModel.openChannelPlaylist(
playlist,
player: player,
recents: recents,
navigation: navigation,
navigationStyle: navigationStyle,
delay: false
)
}
} label: {
let systemImage = item.type == .query ? "magnifyingglass" :
item.type == .channel ? RecentsModel.symbolSystemImage(item.title) :
"list.and.film"
Label(item.title, systemImage: systemImage)
.lineLimit(1)
}
.contextMenu {
removeButton(item)
removeAllButton
}
recentItemButton(item)
}
}
.redrawOn(change: recentsChanged)
@@ -342,6 +297,55 @@ struct SearchView: View {
#endif
}
private func recentItemButton(_ item: RecentItem) -> some View {
Button {
switch item.type {
case .query:
state.queryText = item.title
state.changeQuery { query in query.query = item.title }
updateFavoriteItem()
recents.add(item)
case .channel:
guard let channel = item.channel else {
return
}
NavigationModel.openChannel(
channel,
player: player,
recents: recents,
navigation: navigation,
navigationStyle: navigationStyle,
delay: false
)
case .playlist:
guard let playlist = item.playlist else {
return
}
NavigationModel.openChannelPlaylist(
playlist,
player: player,
recents: recents,
navigation: navigation,
navigationStyle: navigationStyle,
delay: false
)
}
} label: {
let systemImage = item.type == .query ? "magnifyingglass" :
item.type == .channel ? RecentsModel.symbolSystemImage(item.title) :
"list.and.film"
Label(item.title, systemImage: systemImage)
.lineLimit(1)
}
.contextMenu {
removeButton(item)
removeAllButton
}
}
private func removeButton(_ item: RecentItem) -> some View {
Button {
recents.close(item)
@@ -353,7 +357,7 @@ struct SearchView: View {
private var removeAllButton: some View {
Button {
recents.clearQueries()
recents.clear()
recentsChanged.toggle()
} label: {
Label("Remove All", systemImage: "trash.fill")

View File

@@ -14,7 +14,6 @@ struct PlayerSettings: View {
@Default(.playerSidebar) private var playerSidebar
@Default(.showHistoryInPlayer) private var showHistory
@Default(.showKeywords) private var showKeywords
@Default(.showChannelSubscribers) private var channelSubscribers
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
#if os(iOS)
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
@@ -83,7 +82,6 @@ struct PlayerSettings: View {
keywordsToggle
showHistoryToggle
channelSubscribersToggle
}
Section(header: SettingsHeader(text: "Picture in Picture")) {
@@ -196,10 +194,6 @@ struct PlayerSettings: View {
Toggle("Show history", isOn: $showHistory)
}
private var channelSubscribersToggle: some View {
Toggle("Show subscribers count", isOn: $channelSubscribers)
}
private var pauseOnHidingPlayerToggle: some View {
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer)
}

View File

@@ -9,6 +9,7 @@ struct TrendingCountry: View {
@State private var query: String = ""
@State private var selection: Country?
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode
var body: some View {
@@ -30,8 +31,8 @@ struct TrendingCountry: View {
countriesList
}
#if os(tvOS)
.searchable(text: $query, placement: .automatic, prompt: Text(TrendingCountry.prompt))
.background(Color.black)
.searchable(text: $query, placement: .automatic, prompt: Text(Self.prompt))
.background(Color.background(scheme: colorScheme))
#endif
}

View File

@@ -11,7 +11,7 @@ struct HorizontalCells: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 20) {
ForEach(items) { item in
ForEach(contentItems) { item in
ContentItemView(item: item)
.environment(\.horizontalCells, true)
.onAppear { loadMoreContentItemsIfNeeded(current: item) }
@@ -36,6 +36,14 @@ struct HorizontalCells: View {
.edgesIgnoringSafeArea(.horizontal)
}
var contentItems: [ContentItem] {
items.isEmpty ? placeholders : items
}
var placeholders: [ContentItem] {
(0 ..< 9).map { _ in .init() }
}
func loadMoreContentItemsIfNeeded(current item: ContentItem) {
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {

View File

@@ -9,11 +9,12 @@ struct VerticalCells: View {
@Environment(\.loadMoreContentHandler) private var loadMoreContentHandler
var items = [ContentItem]()
var allowEmpty = false
var body: some View {
ScrollView(.vertical, showsIndicators: scrollViewShowsIndicators) {
LazyVGrid(columns: columns, alignment: .center) {
ForEach(items.sorted { $0 < $1 }) { item in
ForEach(contentItems) { item in
ContentItemView(item: item)
.onAppear { loadMoreContentItemsIfNeeded(current: item) }
}
@@ -27,6 +28,14 @@ struct VerticalCells: View {
#endif
}
var contentItems: [ContentItem] {
items.isEmpty ? (allowEmpty ? items : placeholders) : items.sorted { $0 < $1 }
}
var placeholders: [ContentItem] {
(0 ..< 9).map { _ in .init() }
}
func loadMoreContentItemsIfNeeded(current item: ContentItem) {
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
@@ -36,7 +45,7 @@ struct VerticalCells: View {
var columns: [GridItem] {
#if os(tvOS)
items.count < 3 ? Array(repeating: GridItem(.fixed(500)), count: [items.count, 1].max()!) : adaptiveItem
contentItems.count < 3 ? Array(repeating: GridItem(.fixed(500)), count: [contentItems.count, 1].max()!) : adaptiveItem
#else
adaptiveItem
#endif

View File

@@ -6,6 +6,7 @@ struct VideoCell: View {
private var video: Video
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.navigationStyle) private var navigationStyle
#if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass
@@ -13,7 +14,9 @@ struct VideoCell: View {
#endif
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<ThumbnailsModel> private var thumbnails
@Default(.channelOnThumbnail) private var channelOnThumbnail
@@ -59,6 +62,10 @@ struct VideoCell: View {
}
private func playAction() {
guard video.videoID != Video.fixtureID else {
return
}
if watchingNow {
if !player.playingInPictureInPicture {
player.show()
@@ -147,9 +154,7 @@ struct VideoCell: View {
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if !channelOnThumbnail {
Text(video.channel.name)
.fontWeight(.semibold)
.foregroundColor(.secondary)
channelButton(badge: false)
}
if additionalDetailsAvailable {
@@ -231,9 +236,7 @@ struct VideoCell: View {
.frame(minHeight: 40, alignment: .top)
#endif
if !channelOnThumbnail {
Text(video.channel.name)
.fontWeight(.semibold)
.foregroundColor(.secondary)
channelButton(badge: false)
.padding(.top, 4)
.padding(.bottom, 6)
}
@@ -289,6 +292,29 @@ struct VideoCell: View {
}
}
private func channelButton(badge: Bool = true) -> some View {
Button {
NavigationModel.openChannel(
video.channel,
player: player,
recents: recents,
navigation: navigation,
navigationStyle: navigationStyle
)
} label: {
if badge {
DetailBadge(text: video.author, style: .prominent)
.foregroundColor(.primary)
} else {
Text(video.channel.name)
.fontWeight(.semibold)
.foregroundColor(.secondary)
}
}
.buttonStyle(.plain)
.help("\(video.channel.name) Channel")
}
private var additionalDetailsAvailable: Bool {
video.publishedDate != nil || video.views != 0 ||
(!timeOnThumbnail && !video.length.formattedAsPlaybackTime().isNil)
@@ -325,7 +351,7 @@ struct VideoCell: View {
Spacer()
if channelOnThumbnail {
DetailBadge(text: video.author, style: .prominent)
channelButton()
}
}
#if os(tvOS)
@@ -415,7 +441,7 @@ struct VideoCell: View {
stoppedAt.isFinite,
let stoppedAtFormatted = stoppedAt.formattedAsPlaybackTime()
{
if watch?.videoDuration ?? 0 > 0 {
if (watch?.videoDuration ?? 0) > 0 {
videoTime = watch!.videoDuration.formattedAsPlaybackTime() ?? "?"
}
return "\(stoppedAtFormatted) / \(videoTime)"

View File

@@ -6,6 +6,7 @@ struct ChannelVideosView: View {
@State private var presentingShareSheet = false
@State private var shareURL: URL?
@State private var subscriptionToggleButtonDisabled = false
@StateObject private var store = Store<Channel>()
@@ -146,17 +147,25 @@ struct ChannelVideosView: View {
if accounts.app.supportsSubscriptions && accounts.signedIn {
if subscriptions.isSubscribing(channel.id) {
Button("Unsubscribe") {
navigation.presentUnsubscribeAlert(channel)
subscriptionToggleButtonDisabled = true
subscriptions.unsubscribe(channel.id) {
subscriptionToggleButtonDisabled = false
}
}
} else {
Button("Subscribe") {
subscriptionToggleButtonDisabled = true
subscriptions.subscribe(channel.id) {
subscriptionToggleButtonDisabled = false
navigation.sidebarSectionChanged.toggle()
}
}
}
}
}
.disabled(subscriptionToggleButtonDisabled)
}
private var contentItem: ContentItem {

View File

@@ -11,8 +11,10 @@ struct ContentItemView: View {
ChannelPlaylistCell(playlist: item.playlist)
case .channel:
ChannelCell(channel: item.channel)
default:
case .video:
VideoCell(video: item.video)
default:
PlaceholderCell()
}
}
}

View File

@@ -0,0 +1,16 @@
import Defaults
import SwiftUI
struct PlaceholderCell: View {
var body: some View {
VideoCell(video: .fixture)
.redacted(reason: .placeholder)
}
}
struct PlaceholderCell_Previews: PreviewProvider {
static var previews: some View {
PlaceholderCell()
.injectFixtureEnvironmentObjects()
}
}

View File

@@ -6,9 +6,35 @@ struct PlaylistVideosView: View {
@Environment(\.inNavigationView) private var inNavigationView
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model
@StateObject private var store = Store<ChannelPlaylist>()
var contentItems: [ContentItem] {
ContentItem.array(of: playlist.videos)
var videos = playlist.videos
if videos.isEmpty {
videos = store.item?.videos ?? []
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
var i = 0
for index in videos.indices {
var video = videos[index]
video.indexID = "\(i)"
i += 1
videos[index] = video
}
}
}
return ContentItem.array(of: videos)
}
private var resource: Resource? {
let resource = player.accounts.api.playlist(playlist.id)
resource?.addObserver(store)
return resource
}
var videos: [Video] {
@@ -22,6 +48,14 @@ struct PlaylistVideosView: View {
var body: some View {
PlayerControlsView {
VerticalCells(items: contentItems)
.onAppear {
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
resource?.load()
}
}
.onChange(of: model.reloadPlaylists) { _ in
resource?.load()
}
#if !os(tvOS)
.navigationTitle("\(playlist.title) Playlist")
#endif

View File

@@ -33,6 +33,12 @@ struct VideoContextMenuView: View {
}
var body: some View {
if video.videoID != Video.fixtureID {
contextMenu
}
}
@ViewBuilder var contextMenu: some View {
if saveHistory {
Section {
if let watchedAtString = watchedAtString {
@@ -195,7 +201,7 @@ struct VideoContextMenuView: View {
func removeFromPlaylistButton(playlistID: String) -> some View {
Button {
playlists.removeVideo(videoIndexID: video.indexID!, playlistID: playlistID)
playlists.removeVideo(index: video.indexID!, playlistID: playlistID)
} label: {
Label("Remove from playlist", systemImage: "text.badge.minus")
}

View File

@@ -0,0 +1 @@
DEVELOPMENT_TEAM = AB1234C5DE

View File

@@ -0,0 +1,49 @@
#include? "DEVELOPMENT_TEAM.xcconfig"
// Create the file DEVELOPMENT_TEAM.xcconfig
// in the "Xcode-config" directory within the project directory
// with the following build setting:
// DEVELOPMENT_TEAM = [Your TeamID]
// One way to find your Team ID is to set the “Development Team”
// build setting (Xcode key: "DEVELOPMENT_TEAM") and have a look
// at the “Source Control” changes (menu item “Commit…”/⌥⌘C).
// Just make sure that this change doesnt actually get commited.
// You can also find your Team ID by logging into your Apple Developer account
// and going to
// https://developer.apple.com/account/#/membership
// It should be listed under “Team ID”.
// To set this system up for your own project,
// copy the "Xcode-config" directory there,
// add it to your Xcode project,
// navigate to your project settings
// (root icon in the Xcode Project Navigator)
// click on the project icon there,
// click on the “Info” tab
// under “Configurations”
// open the “Debug”, “Release”,
// and any other build configurations you might have.
// There you can set the pull-down menus in the
// “Based on Configuration File” column to “Shared”.
// Your work in Xcode is done.
// Dont forget to add the DEVELOPMENT_TEAM.xcconfig file to your .gitignore:
// # User-specific xcconfig files
// Xcode-config/DEVELOPMENT_TEAM.xcconfig
// The two lines above are an example.
// Please dont copy the comment slashes over though.
// You can and should now replace the “DevelopmentTeam = AB1234C5DE;” entries in
// .xcodeproj/project.pbxproj
// with “DevelopmentTeam = "";”
// They would otherwise override the Team ID set via this config file and its include.
// Please note that .pbxproj files use straight quotes.
// When you commit changes to the Xcode project file
// after changing settings in “Signing & Capabilities”
// Entries like these may appear in your file changes/commit diff preview:
// CODE_SIGN_IDENTITY = …;
// CODE_SIGN_STYLE = Manual/Automatic;
// Please make sure not to commit them to source control.

View File

@@ -514,6 +514,9 @@
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D526DD2720AC4400ED2F5E /* VideosAPI.swift */; };
37D526E32720B4BE00ED2F5E /* View+SwipeGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */; };
37D526E42720B4BE00ED2F5E /* View+SwipeGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */; };
37D6F3A127ECA1FF006FE38B /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6F3A027ECA1FF006FE38B /* Defaults+Workaround.swift */; };
37D6F3A227ECA1FF006FE38B /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6F3A027ECA1FF006FE38B /* Defaults+Workaround.swift */; };
37D6F3A327ECA1FF006FE38B /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6F3A027ECA1FF006FE38B /* Defaults+Workaround.swift */; };
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
@@ -587,6 +590,9 @@
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; };
37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; };
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; };
37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; };
37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; };
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
@@ -791,6 +797,7 @@
37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
37D526DD2720AC4400ED2F5E /* VideosAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosAPI.swift; sourceTree = "<group>"; };
37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+SwipeGesture.swift"; sourceTree = "<group>"; };
37D6F3A027ECA1FF006FE38B /* Defaults+Workaround.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Defaults+Workaround.swift"; sourceTree = "<group>"; };
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
@@ -818,7 +825,11 @@
37FB285D272225E800A57617 /* ContentItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItemView.swift; sourceTree = "<group>"; };
37FD43DB270470B70073EE42 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = "<group>"; };
37FD43E22704847C0073EE42 /* View+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Fixtures.swift"; sourceTree = "<group>"; };
37FEF11227EFD8580033912F /* PlaceholderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderCell.swift; sourceTree = "<group>"; };
37FFC43F272734C3009FFD26 /* Throttle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Throttle.swift; sourceTree = "<group>"; };
3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = "<group>"; };
3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = "<group>"; };
3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -1001,6 +1012,7 @@
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */,
37FEF11227EFD8580033912F /* PlaceholderCell.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -1222,6 +1234,7 @@
37D4B0BC2671614700C925CA = {
isa = PBXGroup;
children = (
3DA101AC287C30F50027D920 /* Xcode-config */,
37992DC826CC50CD003D4C27 /* iOS */,
37BE0BD826A214500092E2DB /* macOS */,
37D4B159267164AE00C925CA /* tvOS */,
@@ -1240,6 +1253,7 @@
37D9169A27388A81002B1BAA /* README.md */,
);
sourceTree = "<group>";
usesTabs = 0;
};
37D4B0C12671614700C925CA /* Shared */ = {
isa = PBXGroup;
@@ -1257,6 +1271,7 @@
371AAE2826CEC7D900901972 /* Views */,
375168D52700FAFF008F96A6 /* Debounce.swift */,
372915E52687E3B900F5A35B /* Defaults.swift */,
37D6F3A027ECA1FF006FE38B /* Defaults+Workaround.swift */,
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
@@ -1419,6 +1434,16 @@
path = Search;
sourceTree = "<group>";
};
3DA101AC287C30F50027D920 /* Xcode-config */ = {
isa = PBXGroup;
children = (
3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */,
3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */,
3DA101AF287C30F50027D920 /* Shared.xcconfig */,
);
path = "Xcode-config";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -2009,12 +2034,14 @@
37484C2526FC83E000287258 /* InstanceForm.swift in Sources */,
37DD9DBD2785D60300539416 /* ScrollViewMatcher.swift in Sources */,
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
37D6F3A127ECA1FF006FE38B /* Defaults+Workaround.swift in Sources */,
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
37141673267A8E10006CA35D /* Country.swift in Sources */,
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */,
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */,
376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */,
@@ -2099,6 +2126,7 @@
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */,
378AE944274EF00A006A4EE1 /* Color+Background.swift in Sources */,
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
37D6F3A227ECA1FF006FE38B /* Defaults+Workaround.swift in Sources */,
37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */,
@@ -2160,6 +2188,7 @@
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
3784B23E2728B85300B09468 /* ShareButton.swift in Sources */,
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */,
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */,
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
@@ -2379,11 +2408,13 @@
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
37D6F3A327ECA1FF006FE38B /* Defaults+Workaround.swift in Sources */,
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */,
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */,
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
@@ -2458,8 +2489,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2471,7 +2501,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -2492,8 +2522,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2505,7 +2534,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -2524,8 +2553,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -2536,7 +2564,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -2556,8 +2584,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -2568,7 +2595,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -2588,7 +2615,6 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
@@ -2597,13 +2623,13 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
37D4B0EA2671614900C925CA /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3DA101AF287C30F50027D920 /* Shared.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -2663,6 +2689,7 @@
};
37D4B0EB2671614900C925CA /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3DA101AF287C30F50027D920 /* Shared.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -2718,9 +2745,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iOS/Info.plist;
@@ -2735,9 +2762,10 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@@ -2750,9 +2778,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iOS/Info.plist;
@@ -2767,9 +2795,10 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@@ -2787,8 +2816,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -2802,7 +2830,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
SDKROOT = macosx;
@@ -2820,8 +2848,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -2835,7 +2862,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
SDKROOT = macosx;
@@ -2849,8 +2876,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -2874,8 +2900,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -2901,8 +2926,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -2926,8 +2950,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -2951,9 +2974,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = tvOS/Info.plist;
@@ -2966,7 +2988,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
SDKROOT = appletvos;
@@ -2983,9 +3005,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = tvOS/Info.plist;
@@ -2998,7 +3019,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.1;
MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee;
SDKROOT = appletvos;
@@ -3015,8 +3036,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -3040,8 +3060,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 25;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -3065,7 +3084,6 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
@@ -3074,7 +3092,6 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
@@ -3083,7 +3100,6 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
@@ -3092,7 +3108,6 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;

View File

@@ -1,133 +1,131 @@
{
"object": {
"pins": [
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"state": {
"branch": null,
"revision": "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864",
"version": "5.5.0"
}
},
{
"package": "Defaults",
"repositoryURL": "https://github.com/sindresorhus/Defaults",
"state": {
"branch": null,
"revision": "55f3302c3ab30a8760f10042d0ebc0a6907f865a",
"version": "6.1.0"
}
},
{
"package": "libwebp",
"repositoryURL": "https://github.com/SDWebImage/libwebp-Xcode.git",
"state": {
"branch": null,
"revision": "2b3b43faaef54d1b897482428428357b7f7cd08b",
"version": "1.2.1"
}
},
{
"package": "PINCache",
"repositoryURL": "https://github.com/pinterest/PINCache",
"state": {
"branch": "master",
"revision": "9ca06045b5aff12ee8c0ef5880aa8469c4896144",
"version": null
}
},
{
"package": "PINOperation",
"repositoryURL": "https://github.com/pinterest/PINOperation.git",
"state": {
"branch": null,
"revision": "44d8ca154a4e75a028a5548c31ff3a53b90cef15",
"version": "1.2.1"
}
},
{
"package": "SDWebImage",
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
"state": {
"branch": null,
"revision": "0fff0d7505b5306348263ea64fcc561253bbeb21",
"version": "5.12.2"
}
},
{
"package": "SDWebImagePINPlugin",
"repositoryURL": "https://github.com/SDWebImage/SDWebImagePINPlugin.git",
"state": {
"branch": null,
"revision": "bd73a4fb30352ec311303d811559c9c46df4caa4",
"version": "0.3.0"
}
},
{
"package": "SDWebImageSwiftUI",
"repositoryURL": "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
"state": {
"branch": null,
"revision": "cd8625b7cf11a97698e180d28bb7d5d357196678",
"version": "2.0.2"
}
},
{
"package": "SDWebImageWebPCoder",
"repositoryURL": "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
"state": {
"branch": null,
"revision": "95a6838df13bc08d8064cf7e048b787b6e52348d",
"version": "0.8.4"
}
},
{
"package": "Siesta",
"repositoryURL": "https://github.com/bustoutsolutions/siesta",
"state": {
"branch": null,
"revision": "43f34046ebb5beb6802200353c473af303bbc31e",
"version": "1.5.2"
}
},
{
"package": "Sparkle",
"repositoryURL": "https://github.com/sparkle-project/Sparkle",
"state": {
"branch": "2.x",
"revision": "71fc8d7b1182d24879edacefccb06151e99c34fe",
"version": null
}
},
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
"revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
"version": "1.4.2"
}
},
{
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
"state": {
"branch": null,
"revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2",
"version": "0.1.3"
}
},
{
"package": "SwiftyJSON",
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state": {
"branch": null,
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version": "5.0.1"
}
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
"version" : "5.6.2"
}
]
},
"version": 1
},
{
"identity" : "defaults",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/Defaults",
"state" : {
"revision" : "981ccb0a01c54abbe3c12ccb8226108527bbf115",
"version" : "6.3.0"
}
},
{
"identity" : "libwebp-xcode",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/libwebp-Xcode.git",
"state" : {
"revision" : "0f3bdb28a1edc5e8e43876d3835d20c601ef331f",
"version" : "1.2.3"
}
},
{
"identity" : "pincache",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pinterest/PINCache",
"state" : {
"branch" : "master",
"revision" : "9ca06045b5aff12ee8c0ef5880aa8469c4896144"
}
},
{
"identity" : "pinoperation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pinterest/PINOperation.git",
"state" : {
"revision" : "44d8ca154a4e75a028a5548c31ff3a53b90cef15",
"version" : "1.2.1"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage.git",
"state" : {
"revision" : "3e48cb68d8e668d146dc59c73fb98cb628616236",
"version" : "5.13.2"
}
},
{
"identity" : "sdwebimagepinplugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImagePINPlugin.git",
"state" : {
"revision" : "bd73a4fb30352ec311303d811559c9c46df4caa4",
"version" : "0.3.0"
}
},
{
"identity" : "sdwebimageswiftui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
"state" : {
"revision" : "cd8625b7cf11a97698e180d28bb7d5d357196678",
"version" : "2.0.2"
}
},
{
"identity" : "sdwebimagewebpcoder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
"state" : {
"revision" : "8a0c5e1ae08ed763739262b9dcef64cfb241c14b",
"version" : "0.9.0"
}
},
{
"identity" : "siesta",
"kind" : "remoteSourceControl",
"location" : "https://github.com/bustoutsolutions/siesta",
"state" : {
"revision" : "43f34046ebb5beb6802200353c473af303bbc31e",
"version" : "1.5.2"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"branch" : "2.x",
"revision" : "2c81393ec25a463c393f4a04ae4405571df0291d"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
"version" : "1.4.2"
}
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
"state" : {
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
"version" : "0.1.4"
}
},
{
"identity" : "swiftyjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
"revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version" : "5.0.1"
}
}
],
"version" : 2
}

8
fastlane/Appfile Normal file
View File

@@ -0,0 +1,8 @@
app_identifier("stream.yattee.app") # The bundle identifier of your app
apple_id(ENV['APPLE_ID']) # Your Apple email address
itc_team_id(ENV['ITC_TEAM_ID']) # App Store Connect Team ID
team_id(ENV['TEAM_ID']) # Developer Portal Team ID
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile

108
fastlane/Fastfile Normal file
View File

@@ -0,0 +1,108 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
DEVELOPER_KEY_ID = ENV['DEVELOPER_KEY_ID']
DEVELOPER_KEY_ISSUER_ID = ENV['DEVELOPER_KEY_ISSUER_ID']
DEVELOPER_KEY_FILEPATH = ENV['DEVELOPER_KEY_FILEPATH']
add_extra_platforms(platforms: [:tvos])
before_all do
update_fastlane
end
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
xcode_select("/Applications/Xcode-13.4.1.app")
app_store_connect_api_key(
key_id: DEVELOPER_KEY_ID,
issuer_id: DEVELOPER_KEY_ISSUER_ID,
key_filepath: DEVELOPER_KEY_FILEPATH
)
increment_build_number(xcodeproj: "Yattee.xcodeproj")
version = get_version_number(
xcodeproj: "Yattee.xcodeproj",
target: "Yattee (iOS)"
)
build_app(
scheme: "Yattee (iOS)",
output_directory: "fastlane/builds/#{version}-#{lane_context[SharedValues::BUILD_NUMBER]}/iOS",
output_name: "Yattee-#{version}-iOS.ipa",
)
upload_to_testflight
end
end
platform :tvos do
desc "Push a new beta build to TestFlight"
lane :beta do
xcode_select("/Applications/Xcode-13.4.1.app")
app_store_connect_api_key(
key_id: DEVELOPER_KEY_ID,
issuer_id: DEVELOPER_KEY_ISSUER_ID,
key_filepath: DEVELOPER_KEY_FILEPATH
)
increment_build_number(xcodeproj: "Yattee.xcodeproj")
version = get_version_number(
xcodeproj: "Yattee.xcodeproj",
target: "Yattee (tvOS)"
)
build_app(
scheme: "Yattee (tvOS)",
output_directory: "fastlane/builds/#{version}-#{lane_context[SharedValues::BUILD_NUMBER]}/tvOS",
output_name: "Yattee-#{version}-tvOS.ipa",
)
upload_to_testflight
end
end
platform :mac do
desc "Push a new beta build to TestFlight"
lane :beta do
xcode_select("/Applications/Xcode-13.4.1.app")
app_store_connect_api_key(
key_id: DEVELOPER_KEY_ID,
issuer_id: DEVELOPER_KEY_ISSUER_ID,
key_filepath: DEVELOPER_KEY_FILEPATH
)
increment_build_number(xcodeproj: "Yattee.xcodeproj")
version = get_version_number(
xcodeproj: "Yattee.xcodeproj",
target: "Yattee (macOS)"
)
build_app(
scheme: "Yattee (macOS)",
output_directory: "fastlane/builds/#{version}-#{lane_context[SharedValues::BUILD_NUMBER]}/macOS",
output_name: "Yattee-#{version}-macOS.app",
)
upload_to_testflight
end
end

58
fastlane/README.md Normal file
View File

@@ -0,0 +1,58 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## iOS
### ios beta
```sh
[bundle exec] fastlane ios beta
```
Push a new beta build to TestFlight
----
## tvos
### tvos beta
```sh
[bundle exec] fastlane tvos beta
```
Push a new beta build to TestFlight
----
## Mac
### mac beta
```sh
[bundle exec] fastlane mac beta
```
Push a new beta build to TestFlight
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).