Compare commits

...

30 Commits

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
34 changed files with 944 additions and 252 deletions

6
.gitignore vendored
View File

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

View File

@@ -1,9 +1,8 @@
import Foundation import Foundation
extension Video { extension Video {
static var fixtureID: Video.ID { static var fixtureID: Video.ID = "video-fixture"
"FIXTURE" static var fixtureChannelID: Channel.ID = "channel-fixture"
}
static var fixture: Video { static var fixture: Video {
let thumbnailURL = "https://yt3.ggpht.com/ytc/AKedOLR-pT_JEsz_hcaA4Gjx8DHcqJ8mS42aTRqcVy6P7w=s88-c-k-c0x00ffffff-no-rj-mo" 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", description: "Some relaxing live piano music",
genre: "Music", genre: "Music",
channel: Channel( channel: Channel(
id: "AbCdEFgHI", id: fixtureChannelID,
name: "The Channel", name: "The Channel",
thumbnailURL: URL(string: thumbnailURL)!, thumbnailURL: URL(string: thumbnailURL)!,
subscriptionsCount: 2300, subscriptionsCount: 2300,

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

@@ -239,6 +239,66 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
playlist(playlistID)?.child("videos").child(videoID) 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? { func channelPlaylist(_ id: String) -> Resource? {
resource(baseURL: account.url, path: basePathAppending("playlists/\(id)")) resource(baseURL: account.url, path: basePathAppending("playlists/\(id)"))
} }

View File

@@ -4,7 +4,7 @@ import Siesta
import SwiftyJSON import SwiftyJSON
final class PipedAPI: Service, ObservableObject, VideosAPI { final class PipedAPI: Service, ObservableObject, VideosAPI {
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"] static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"]
@Published var account: Account! @Published var account: Account!
@@ -43,6 +43,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
self.extractChannelPlaylist(from: content.json) 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 configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
self.extractVideo(from: content.json) self.extractVideo(from: content.json)
} }
@@ -81,6 +86,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled) 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 { if account.token.isNil {
updateToken() updateToken()
} }
@@ -166,7 +175,9 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
var home: Resource? { nil } var home: Resource? { nil }
var popular: 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 = {}) { func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resource(baseURL: account.instance.apiURL, path: "subscribe") resource(baseURL: account.instance.apiURL, path: "subscribe")
@@ -180,10 +191,79 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.onCompletion { _ in onCompletion() } .onCompletion { _ in onCompletion() }
} }
func playlist(_: String) -> Resource? { nil } func playlist(_ id: String) -> Resource? {
channelPlaylist(id)
}
func playlistVideo(_: String, _: String) -> Resource? { nil } func playlistVideo(_: String, _: String) -> Resource? { nil }
func playlistVideos(_: 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? { func comments(_ id: Video.ID, page: String?) -> Resource? {
let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)" let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)"
let resource = resource(baseURL: account.url, path: path) let resource = resource(baseURL: account.url, path: path)
@@ -280,7 +360,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
} }
return ChannelPlaylist( return ChannelPlaylist(
id: id, id: id,
title: details["name"]!.stringValue, title: details["name"]?.stringValue ?? "",
thumbnailURL: thumbnailURL, thumbnailURL: thumbnailURL,
channel: extractChannel(from: json)!, channel: extractChannel(from: json)!,
videos: videos, videos: videos,
@@ -310,9 +390,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url
let subscriptionsCount = details["uploaderSubscriberCount"]?.int
let uploaded = details["uploaded"]?.doubleValue 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 { if published.isNil {
published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ?? "" published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ?? ""
} }
@@ -327,7 +408,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
published: published!, published: published!,
views: details["views"]!.intValue, views: details["views"]!.intValue,
description: extractDescription(from: content), description: extractDescription(from: content),
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL), channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
thumbnails: thumbnails, thumbnails: thumbnails,
live: live, live: live,
likes: details["likes"]?.int, 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? { private func extractDescription(from content: JSON) -> String? {
guard var description = content.dictionaryValue["description"]?.string else { guard var description = content.dictionaryValue["description"]?.string else {
return nil return nil
@@ -399,8 +488,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let videoStreams = compatibleVideoStream(from: content) let videoStreams = compatibleVideoStream(from: content)
videoStreams.forEach { videoStream in videoStreams.forEach { videoStream in
let audioAsset = AVURLAsset(url: audioStream.dictionaryValue["url"]!.url!) guard let audioAssetUrl = audioStream.dictionaryValue["url"]?.url,
let videoAsset = AVURLAsset(url: videoStream.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 videoOnly = videoStream.dictionaryValue["videoOnly"]?.boolValue ?? true
let resolution = Stream.Resolution.from(resolution: videoStream.dictionaryValue["quality"]!.stringValue) 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 playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
func playlistVideos(_ id: 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 channelPlaylist(_ id: String) -> Resource?
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void) func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)

View File

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

View File

@@ -74,6 +74,10 @@ final class NavigationModel: ObservableObject {
navigationStyle: NavigationStyle, navigationStyle: NavigationStyle,
delay: Bool = true delay: Bool = true
) { ) {
guard channel.id != Video.fixtureChannelID else {
return
}
let recent = RecentItem(from: channel) let recent = RecentItem(from: channel)
#if os(macOS) #if os(macOS)
Windows.main.open() Windows.main.open()

View File

@@ -45,8 +45,6 @@ final class PlayerModel: ObservableObject {
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } } @Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
@Published var restoredSegments = [Segment]() @Published var restoredSegments = [Segment]()
@Published var channelWithDetails: Channel?
#if os(iOS) #if os(iOS)
@Published var motionManager: CMMotionManager! @Published var motionManager: CMMotionManager!
@Published var lockedOrientation: UIInterfaceOrientation? @Published var lockedOrientation: UIInterfaceOrientation?
@@ -210,11 +208,7 @@ final class PlayerModel: ObservableObject {
self?.sponsorBlock.loadSegments( self?.sponsorBlock.loadSegments(
videoID: video.videoID, videoID: video.videoID,
categories: Defaults[.sponsorBlockCategories] 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! } 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 { func rateLabel(_ rate: Float) -> String {
let formatter = NumberFormatter() let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0 formatter.minimumFractionDigits = 0

View File

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

View File

@@ -18,11 +18,11 @@ struct Playlist: Identifiable, Equatable, Hashable {
var title: String var title: String
var visibility: Visibility var visibility: Visibility
var updated: TimeInterval var updated: TimeInterval?
var videos = [Video]() 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.id = id
self.title = title self.title = title
self.visibility = visibility self.visibility = visibility

View File

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

View File

@@ -17,7 +17,7 @@ struct Video: Identifiable, Equatable, Hashable {
var genre: String? var genre: String?
// index used when in the Playlist // index used when in the Playlist
let indexID: String? var indexID: String?
var live: Bool var live: Bool
var upcoming: 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) [![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 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) [![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) ![Screenshot](https://r.yattee.stream/screenshots/all-platforms.png)
</div> </div>
@@ -19,31 +23,32 @@
* Fullscreen playback, Picture in Picture and AirPlay support * Fullscreen playback, Picture in Picture and AirPlay support
* Stream quality selection * Stream quality selection
### Features in alpha testing ### Features in development
* New player component with custom controls, gestures and support for 4K playback * 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/93) or join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat. Thanks! 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 ### Availability
|| Invidious | Piped | || Invidious | Piped |
| - | - | - | | - | - | - |
| User Accounts | ✅ | ✅ | | User Accounts | ✅ | ✅ |
| Subscriptions | ✅ | ✅ | | Subscriptions | ✅ | ✅ |
| Popular | ✅ | 🔴 | | User Playlists | ✅ | |
| User Playlists | ✅ | 🔴 |
| Trending | ✅ | ✅ | | Trending | ✅ | ✅ |
| Channels | ✅ | ✅ | | Channels | ✅ | ✅ |
| Channel Playlists | ✅ | ✅ | | Channel Playlists | ✅ | ✅ |
| Search | ✅ | ✅ | | Search | ✅ | ✅ |
| Search Suggestions | ✅ | ✅ | | Search Suggestions | ✅ | ✅ |
| Search Filters | ✅ | 🔴 | | Search Filters | ✅ | 🔴 |
| Popular | ✅ | 🔴 |
| Subtitles | 🔴 | ✅ | | Subtitles | 🔴 | ✅ |
| Comments | 🔴 | ✅ | | 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. 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 ## 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) * [FAQ](https://github.com/yattee/yattee/wiki)
* [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery) * [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery)
* [Tips](https://github.com/yattee/yattee/wiki/Tips) * [Tips](https://github.com/yattee/yattee/wiki/Tips)
@@ -53,11 +58,16 @@ You can browse and use accounts from one app and play videos with another (for e
## Contributing ## 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. 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.
Join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat if you need an advice or want to discuss the project. Use [building instructions](https://github.com/yattee/yattee/wiki/Building-instructions) or
## License and Liability 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. 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. This tool is an open source software built for learning and research purposes.

View File

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

View File

@@ -11,9 +11,7 @@ struct AppSidebarPlaylists: View {
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) { NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) {
LazyView(PlaylistVideosView(playlist)) LazyView(PlaylistVideosView(playlist))
} label: { } label: {
Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title)) playlistLabel(playlist)
.backport
.badge(Text("\(playlist.videos.count)"))
} }
.id(playlist.id) .id(playlist.id)
.contextMenu { .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 { var newPlaylistButton: some View {
Button(action: { navigation.presentNewPlaylistForm() }) { Button(action: { navigation.presentNewPlaylistForm() }) {
Label("New Playlist", systemImage: "plus.circle") Label("New Playlist", systemImage: "plus.circle")

View File

@@ -104,7 +104,8 @@ struct PlaybackBar: View {
} }
guard let video = player.currentVideo, guard let video = player.currentVideo,
let time = player.time else { let time = player.time
else {
return "" return ""
} }

View File

@@ -30,7 +30,6 @@ struct VideoDetails: View {
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions @EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.showChannelSubscribers) private var showChannelSubscribers
@Default(.showKeywords) private var showKeywords @Default(.showKeywords) private var showKeywords
init( init(
@@ -208,15 +207,13 @@ struct VideoDetails: View {
.font(.system(size: 14)) .font(.system(size: 14))
.bold() .bold()
if showChannelSubscribers { Group {
Group { if let subscribers = video!.channel.subscriptionsString {
if let subscribers = video!.channel.subscriptionsString { Text("\(subscribers) subscribers")
Text("\(subscribers) subscribers")
}
} }
.foregroundColor(.secondary)
.font(.caption2)
} }
.foregroundColor(.secondary)
.font(.caption2)
} }
} }
} }

View File

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

View File

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

View File

@@ -11,6 +11,8 @@ struct PlaylistsView: View {
@State private var showingEditPlaylist = false @State private var showingEditPlaylist = false
@State private var editedPlaylist: Playlist? @State private var editedPlaylist: Playlist?
@StateObject private var store = Store<ChannelPlaylist>()
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model @EnvironmentObject<PlaylistsModel> private var model
@@ -18,7 +20,36 @@ struct PlaylistsView: View {
@Namespace private var focusNamespace @Namespace private var focusNamespace
var items: [ContentItem] { 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 { var body: some View {
@@ -112,10 +143,17 @@ struct PlaylistsView: View {
#endif #endif
.onAppear { .onAppear {
model.load() model.load()
resource?.load()
} }
.onChange(of: accounts.current) { _ in .onChange(of: accounts.current) { _ in
model.load(force: true) model.load(force: true)
} }
.onChange(of: selectedPlaylistID) { _ in
resource?.load()
}
.onChange(of: model.reloadPlaylists) { _ in
resource?.load()
}
#if os(iOS) #if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode) .navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif #endif

View File

@@ -198,7 +198,7 @@ struct SearchView: View {
visibleSections.append(.subscriptions) visibleSections.append(.subscriptions)
} }
if accounts.app.supportsUserPlaylists && preferred.contains(.playlists) { if accounts.app.supportsUserPlaylists && accounts.signedIn && preferred.contains(.playlists) {
visibleSections.append(.playlists) visibleSections.append(.playlists)
} }
@@ -286,52 +286,7 @@ struct SearchView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
ForEach(recentItems) { item in ForEach(recentItems) { item in
Button { recentItemButton(item)
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
}
} }
} }
.redrawOn(change: recentsChanged) .redrawOn(change: recentsChanged)
@@ -342,6 +297,55 @@ struct SearchView: View {
#endif #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 { private func removeButton(_ item: RecentItem) -> some View {
Button { Button {
recents.close(item) recents.close(item)

View File

@@ -14,7 +14,6 @@ struct PlayerSettings: View {
@Default(.playerSidebar) private var playerSidebar @Default(.playerSidebar) private var playerSidebar
@Default(.showHistoryInPlayer) private var showHistory @Default(.showHistoryInPlayer) private var showHistory
@Default(.showKeywords) private var showKeywords @Default(.showKeywords) private var showKeywords
@Default(.showChannelSubscribers) private var channelSubscribers
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
#if os(iOS) #if os(iOS)
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock @Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
@@ -83,7 +82,6 @@ struct PlayerSettings: View {
keywordsToggle keywordsToggle
showHistoryToggle showHistoryToggle
channelSubscribersToggle
} }
Section(header: SettingsHeader(text: "Picture in Picture")) { Section(header: SettingsHeader(text: "Picture in Picture")) {
@@ -196,10 +194,6 @@ struct PlayerSettings: View {
Toggle("Show history", isOn: $showHistory) Toggle("Show history", isOn: $showHistory)
} }
private var channelSubscribersToggle: some View {
Toggle("Show subscribers count", isOn: $channelSubscribers)
}
private var pauseOnHidingPlayerToggle: some View { private var pauseOnHidingPlayerToggle: some View {
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer) 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 query: String = ""
@State private var selection: Country? @State private var selection: Country?
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
var body: some View { var body: some View {
@@ -30,8 +31,8 @@ struct TrendingCountry: View {
countriesList countriesList
} }
#if os(tvOS) #if os(tvOS)
.searchable(text: $query, placement: .automatic, prompt: Text(TrendingCountry.prompt)) .searchable(text: $query, placement: .automatic, prompt: Text(Self.prompt))
.background(Color.black) .background(Color.background(scheme: colorScheme))
#endif #endif
} }

View File

@@ -6,9 +6,35 @@ struct PlaylistVideosView: View {
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.inNavigationView) private var inNavigationView
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model
@StateObject private var store = Store<ChannelPlaylist>()
var contentItems: [ContentItem] { 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] { var videos: [Video] {
@@ -22,6 +48,14 @@ struct PlaylistVideosView: View {
var body: some View { var body: some View {
PlayerControlsView { PlayerControlsView {
VerticalCells(items: contentItems) VerticalCells(items: contentItems)
.onAppear {
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
resource?.load()
}
}
.onChange(of: model.reloadPlaylists) { _ in
resource?.load()
}
#if !os(tvOS) #if !os(tvOS)
.navigationTitle("\(playlist.title) Playlist") .navigationTitle("\(playlist.title) Playlist")
#endif #endif

View File

@@ -201,7 +201,7 @@ struct VideoContextMenuView: View {
func removeFromPlaylistButton(playlistID: String) -> some View { func removeFromPlaylistButton(playlistID: String) -> some View {
Button { Button {
playlists.removeVideo(videoIndexID: video.indexID!, playlistID: playlistID) playlists.removeVideo(index: video.indexID!, playlistID: playlistID)
} label: { } label: {
Label("Remove from playlist", systemImage: "text.badge.minus") 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

@@ -827,6 +827,9 @@
37FD43E22704847C0073EE42 /* View+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Fixtures.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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -1231,6 +1234,7 @@
37D4B0BC2671614700C925CA = { 37D4B0BC2671614700C925CA = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3DA101AC287C30F50027D920 /* Xcode-config */,
37992DC826CC50CD003D4C27 /* iOS */, 37992DC826CC50CD003D4C27 /* iOS */,
37BE0BD826A214500092E2DB /* macOS */, 37BE0BD826A214500092E2DB /* macOS */,
37D4B159267164AE00C925CA /* tvOS */, 37D4B159267164AE00C925CA /* tvOS */,
@@ -1249,6 +1253,7 @@
37D9169A27388A81002B1BAA /* README.md */, 37D9169A27388A81002B1BAA /* README.md */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
usesTabs = 0;
}; };
37D4B0C12671614700C925CA /* Shared */ = { 37D4B0C12671614700C925CA /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
@@ -1429,6 +1434,16 @@
path = Search; path = Search;
sourceTree = "<group>"; 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 */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -2474,8 +2489,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2487,7 +2501,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -2508,8 +2522,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2521,7 +2534,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -2540,8 +2553,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee"; INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -2552,7 +2564,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -2572,8 +2584,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee"; INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -2584,7 +2595,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -2604,7 +2615,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Debug; name = Debug;
@@ -2613,13 +2623,13 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Release; name = Release;
}; };
37D4B0EA2671614900C925CA /* Debug */ = { 37D4B0EA2671614900C925CA /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 3DA101AF287C30F50027D920 /* Shared.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@@ -2679,6 +2689,7 @@
}; };
37D4B0EB2671614900C925CA /* Release */ = { 37D4B0EB2671614900C925CA /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 3DA101AF287C30F50027D920 /* Shared.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@@ -2734,9 +2745,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iOS/Info.plist; INFOPLIST_FILE = iOS/Info.plist;
@@ -2751,9 +2762,10 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -2766,9 +2778,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iOS/Info.plist; INFOPLIST_FILE = iOS/Info.plist;
@@ -2783,9 +2795,10 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -2803,8 +2816,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2818,7 +2830,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = macosx; SDKROOT = macosx;
@@ -2836,8 +2848,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2851,7 +2862,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = macosx; SDKROOT = macosx;
@@ -2865,8 +2876,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -2890,8 +2900,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -2917,8 +2926,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -2942,8 +2950,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -2967,9 +2974,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = tvOS/Info.plist; INFOPLIST_FILE = tvOS/Info.plist;
@@ -2982,7 +2988,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = appletvos; SDKROOT = appletvos;
@@ -2999,9 +3005,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = tvOS/Info.plist; INFOPLIST_FILE = tvOS/Info.plist;
@@ -3014,7 +3019,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3.3; MARKETING_VERSION = 1.3.5;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = appletvos; SDKROOT = appletvos;
@@ -3031,8 +3036,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -3056,8 +3060,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -3081,7 +3084,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Debug; name = Debug;
@@ -3090,7 +3092,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Release; name = Release;
@@ -3099,7 +3100,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Debug; name = Debug;
@@ -3108,7 +3108,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Release; name = Release;

View File

@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git", "location" : "https://github.com/Alamofire/Alamofire.git",
"state" : { "state" : {
"revision" : "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864", "revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
"version" : "5.5.0" "version" : "5.6.2"
} }
}, },
{ {
@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/Defaults", "location" : "https://github.com/sindresorhus/Defaults",
"state" : { "state" : {
"revision" : "119f654d44f7b90f00dc11f7dd1c94a36f12576b", "revision" : "981ccb0a01c54abbe3c12ccb8226108527bbf115",
"version" : "6.2.1" "version" : "6.3.0"
} }
}, },
{ {
@@ -23,8 +23,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/libwebp-Xcode.git", "location" : "https://github.com/SDWebImage/libwebp-Xcode.git",
"state" : { "state" : {
"revision" : "2b3b43faaef54d1b897482428428357b7f7cd08b", "revision" : "0f3bdb28a1edc5e8e43876d3835d20c601ef331f",
"version" : "1.2.1" "version" : "1.2.3"
} }
}, },
{ {
@@ -50,8 +50,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage.git", "location" : "https://github.com/SDWebImage/SDWebImage.git",
"state" : { "state" : {
"revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", "revision" : "3e48cb68d8e668d146dc59c73fb98cb628616236",
"version" : "5.12.5" "version" : "5.13.2"
} }
}, },
{ {
@@ -77,8 +77,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git", "location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
"state" : { "state" : {
"revision" : "95a6838df13bc08d8064cf7e048b787b6e52348d", "revision" : "8a0c5e1ae08ed763739262b9dcef64cfb241c14b",
"version" : "0.8.4" "version" : "0.9.0"
} }
}, },
{ {
@@ -96,7 +96,7 @@
"location" : "https://github.com/sparkle-project/Sparkle", "location" : "https://github.com/sparkle-project/Sparkle",
"state" : { "state" : {
"branch" : "2.x", "branch" : "2.x",
"revision" : "f250bead4b943ef9711c61274a1f52e380afa0e8" "revision" : "2c81393ec25a463c393f4a04ae4405571df0291d"
} }
}, },
{ {

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).