mirror of
https://github.com/yattee/yattee.git
synced 2025-12-12 19:18:16 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd32bae24b | ||
|
|
17e56dc69a | ||
|
|
ad715fa367 | ||
|
|
560d7c4b9a | ||
|
|
7a88f28170 | ||
|
|
419e8991c9 | ||
|
|
720bdde728 | ||
|
|
965a757031 | ||
|
|
811196ae55 | ||
|
|
5b0f6397d6 | ||
|
|
d5362519d6 | ||
|
|
e9ee0f5ee9 | ||
|
|
0a9fb75c0c | ||
|
|
eb33b65f3d | ||
|
|
1c71520d6f | ||
|
|
7a7e265ba1 | ||
|
|
c086112a49 | ||
|
|
0802fe0029 | ||
|
|
40813c2859 | ||
|
|
b1238869a6 | ||
|
|
453a5fa71b | ||
|
|
86be252bd5 | ||
|
|
20f96fb9d6 | ||
|
|
e539fb0067 | ||
|
|
03d5eefab0 | ||
|
|
0bc4a677d4 | ||
|
|
b374f82da4 | ||
|
|
b70697e1be | ||
|
|
db5765a84b | ||
|
|
8d36f57271 | ||
|
|
836057578f | ||
|
|
e39f4373bb | ||
|
|
1490437537 | ||
|
|
4f1b52826d | ||
|
|
15e62468bb |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -3,6 +3,6 @@ import AppKit
|
||||
extension NSTextField {
|
||||
override open var focusRingType: NSFocusRingType {
|
||||
get { .none }
|
||||
set {} // swiftlint:disable:this unused_setter_value
|
||||
set {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension Video {
|
||||
static var fixtureID: Video.ID {
|
||||
"FIXTURE"
|
||||
}
|
||||
static var fixtureID: Video.ID = "video-fixture"
|
||||
static var fixtureChannelID: Channel.ID = "channel-fixture"
|
||||
|
||||
static var fixture: Video {
|
||||
let thumbnailURL = "https://yt3.ggpht.com/ytc/AKedOLR-pT_JEsz_hcaA4Gjx8DHcqJ8mS42aTRqcVy6P7w=s88-c-k-c0x00ffffff-no-rj-mo"
|
||||
@@ -18,7 +17,7 @@ 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,
|
||||
|
||||
218
Gemfile.lock
Normal file
218
Gemfile.lock
Normal 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
|
||||
@@ -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)"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -280,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,
|
||||
@@ -310,9 +390,10 @@ 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 ? nil : (uploaded! / 1000).formattedAsRelativeTime()
|
||||
var published = (uploaded.isNil || uploaded == -1) ? nil : (uploaded! / 1000).formattedAsRelativeTime()
|
||||
if published.isNil {
|
||||
published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ?? ""
|
||||
}
|
||||
@@ -327,7 +408,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
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,
|
||||
@@ -359,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
|
||||
@@ -399,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -74,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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,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
|
||||
|
||||
@@ -74,7 +74,6 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
preservedTime = currentItem.playbackTime
|
||||
restoreLoadedChannel()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let video = self?.currentVideo else {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
27
README.md
27
README.md
@@ -6,7 +6,11 @@
|
||||
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
[](https://github.com/yattee/yattee/issues)
|
||||
[](https://github.com/yattee/yattee/pulls)
|
||||
[](https://matrix.to/#/#yattee:matrix.org)
|
||||
[](https://matrix.to/#/#Yattee:matrix.org)
|
||||
|
||||
Now on **Discord**:
|
||||
|
||||
[](https://yattee.stream/discord)
|
||||
|
||||

|
||||
</div>
|
||||
@@ -19,31 +23,32 @@
|
||||
* Fullscreen playback, Picture in Picture and AirPlay support
|
||||
* Stream quality selection
|
||||
|
||||
### Features in alpha testing for iOS and macOS
|
||||
### Features in development
|
||||
* New player component with custom controls, gestures and support for 4K playback
|
||||
|
||||
You can leave your feedback in [discussion on v1.4 release](https://github.com/yattee/yattee/discussions/75).
|
||||
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
|
||||
|| 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)
|
||||
@@ -53,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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -30,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(
|
||||
@@ -208,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -201,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")
|
||||
}
|
||||
|
||||
1
Xcode-config/DEVELOPMENT_TEAM.template.xcconfig
Normal file
1
Xcode-config/DEVELOPMENT_TEAM.template.xcconfig
Normal file
@@ -0,0 +1 @@
|
||||
DEVELOPMENT_TEAM = AB1234C5DE
|
||||
49
Xcode-config/Shared.xcconfig
Normal file
49
Xcode-config/Shared.xcconfig
Normal 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 doesn’t 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.
|
||||
|
||||
// Don’t 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 don’t 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.
|
||||
@@ -827,6 +827,9 @@
|
||||
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 */
|
||||
@@ -1231,6 +1234,7 @@
|
||||
37D4B0BC2671614700C925CA = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3DA101AC287C30F50027D920 /* Xcode-config */,
|
||||
37992DC826CC50CD003D4C27 /* iOS */,
|
||||
37BE0BD826A214500092E2DB /* macOS */,
|
||||
37D4B159267164AE00C925CA /* tvOS */,
|
||||
@@ -1249,6 +1253,7 @@
|
||||
37D9169A27388A81002B1BAA /* README.md */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
usesTabs = 0;
|
||||
};
|
||||
37D4B0C12671614700C925CA /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
@@ -1429,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 */
|
||||
@@ -2474,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 = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -2487,7 +2501,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -2508,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 = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -2521,7 +2534,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -2540,8 +2553,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@@ -2552,7 +2564,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -2572,8 +2584,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@@ -2584,7 +2595,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -2604,7 +2615,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -2613,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;
|
||||
@@ -2679,6 +2689,7 @@
|
||||
};
|
||||
37D4B0EB2671614900C925CA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3DA101AF287C30F50027D920 /* Shared.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
@@ -2734,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 = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
@@ -2751,9 +2762,10 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
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;
|
||||
@@ -2766,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 = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
@@ -2783,9 +2795,10 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
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;
|
||||
@@ -2803,8 +2816,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2818,7 +2830,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = macosx;
|
||||
@@ -2836,8 +2848,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2851,7 +2862,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = macosx;
|
||||
@@ -2865,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 = (
|
||||
@@ -2890,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 = (
|
||||
@@ -2917,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)",
|
||||
@@ -2942,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)",
|
||||
@@ -2967,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 = 20;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = tvOS/Info.plist;
|
||||
@@ -2982,7 +2988,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = appletvos;
|
||||
@@ -2999,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 = 20;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = tvOS/Info.plist;
|
||||
@@ -3014,7 +3019,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = appletvos;
|
||||
@@ -3031,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)",
|
||||
@@ -3056,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)",
|
||||
@@ -3081,7 +3084,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -3090,7 +3092,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
@@ -3099,7 +3100,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -3108,7 +3108,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864",
|
||||
"version" : "5.5.0"
|
||||
"revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
|
||||
"version" : "5.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14,8 +14,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/Defaults",
|
||||
"state" : {
|
||||
"revision" : "119f654d44f7b90f00dc11f7dd1c94a36f12576b",
|
||||
"version" : "6.2.1"
|
||||
"revision" : "981ccb0a01c54abbe3c12ccb8226108527bbf115",
|
||||
"version" : "6.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23,8 +23,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/libwebp-Xcode.git",
|
||||
"state" : {
|
||||
"revision" : "2b3b43faaef54d1b897482428428357b7f7cd08b",
|
||||
"version" : "1.2.1"
|
||||
"revision" : "0f3bdb28a1edc5e8e43876d3835d20c601ef331f",
|
||||
"version" : "1.2.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -50,8 +50,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state" : {
|
||||
"revision" : "2e63d0061da449ad0ed130768d05dceb1496de44",
|
||||
"version" : "5.12.5"
|
||||
"revision" : "3e48cb68d8e668d146dc59c73fb98cb628616236",
|
||||
"version" : "5.13.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -77,8 +77,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
|
||||
"state" : {
|
||||
"revision" : "95a6838df13bc08d8064cf7e048b787b6e52348d",
|
||||
"version" : "0.8.4"
|
||||
"revision" : "8a0c5e1ae08ed763739262b9dcef64cfb241c14b",
|
||||
"version" : "0.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -96,7 +96,7 @@
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"branch" : "2.x",
|
||||
"revision" : "f250bead4b943ef9711c61274a1f52e380afa0e8"
|
||||
"revision" : "2c81393ec25a463c393f4a04ae4405571df0291d"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
8
fastlane/Appfile
Normal file
8
fastlane/Appfile
Normal 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
108
fastlane/Fastfile
Normal 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
58
fastlane/README.md
Normal 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).
|
||||
Reference in New Issue
Block a user