Compare commits

..

35 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
Arkadiusz Fal
836057578f Minor changes 2022-04-02 14:34:06 +02:00
Arkadiusz Fal
e39f4373bb Fix crashes (#69, #71) 2022-03-31 20:39:02 +02:00
Arkadiusz Fal
1490437537 Update README 2022-03-28 21:30:31 +02:00
Arkadiusz Fal
4f1b52826d Fix #109 2022-03-28 21:26:52 +02:00
Arkadiusz Fal
15e62468bb Update README 2022-03-27 23:13:53 +02:00
36 changed files with 957 additions and 256 deletions

6
.gitignore vendored
View File

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

View File

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

View File

@@ -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,

3
Gemfile Normal file
View File

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

218
Gemfile.lock Normal file
View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import Siesta
import SwiftyJSON
final class PipedAPI: Service, ObservableObject, VideosAPI {
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"]
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"]
@Published var account: Account!
@@ -43,6 +43,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
self.extractChannelPlaylist(from: content.json)
}
configureTransformer(pathPattern("user/playlists/create")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("user/playlists/delete")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("user/playlists/add")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("user/playlists/remove")) { (_: Entity<JSON>) in }
configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
self.extractVideo(from: content.json)
}
@@ -81,6 +86,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
}
configureTransformer(pathPattern("user/playlists")) { (content: Entity<JSON>) -> [Playlist] in
content.json.arrayValue.map { self.extractUserPlaylist(from: $0)! }
}
if account.token.isNil {
updateToken()
}
@@ -166,7 +175,9 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
var home: Resource? { nil }
var popular: Resource? { nil }
var playlists: Resource? { nil }
var playlists: Resource? {
resource(baseURL: account.instance.apiURL, path: "user/playlists")
}
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resource(baseURL: account.instance.apiURL, path: "subscribe")
@@ -180,10 +191,79 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.onCompletion { _ in onCompletion() }
}
func playlist(_: String) -> Resource? { nil }
func playlist(_ id: String) -> Resource? {
channelPlaylist(id)
}
func playlistVideo(_: String, _: String) -> Resource? { nil }
func playlistVideos(_: String) -> Resource? { nil }
func addVideoToPlaylist(
_ videoID: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void = { _ in },
onSuccess: @escaping () -> Void = {}
) {
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/add")
let body = ["videoId": videoID, "playlistId": playlistID]
resource
.request(.post, json: body)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func removeVideoFromPlaylist(
_ index: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
) {
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/remove")
let body: [String: Any] = ["index": Int(index)!, "playlistId": playlistID]
resource
.request(.post, json: body)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func playlistForm(
_ name: String,
_: String,
playlist: Playlist?,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping (Playlist?) -> Void
) {
let body = ["name": name]
let resource = playlist.isNil ? resource(baseURL: account.instance.apiURL, path: "user/playlists/create") : nil
resource?
.request(.post, json: body)
.onSuccess { response in
if let modifiedPlaylist: Playlist = response.typedContent() {
onSuccess(modifiedPlaylist)
} else {
onSuccess(nil)
}
}
.onFailure(onFailure)
}
func deletePlaylist(
_ playlist: Playlist,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
) {
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/delete")
let body = ["playlistId": playlist.id]
resource
.request(.post, json: body)
.onSuccess { _ in onSuccess() }
.onFailure(onFailure)
}
func comments(_ id: Video.ID, page: String?) -> Resource? {
let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)"
let resource = resource(baseURL: account.url, path: path)
@@ -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)

View File

@@ -27,6 +27,34 @@ protocol VideosAPI {
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
func playlistVideos(_ id: String) -> Resource?
func addVideoToPlaylist(
_ videoID: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
)
func removeVideoFromPlaylist(
_ index: String,
_ playlistID: String,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
)
func playlistForm(
_ name: String,
_ visibility: String,
playlist: Playlist?,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping (Playlist?) -> Void
)
func deletePlaylist(
_ playlist: Playlist,
onFailure: @escaping (RequestError) -> Void,
onSuccess: @escaping () -> Void
)
func channelPlaylist(_ id: String) -> Resource?
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)

View File

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

View File

@@ -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()

View File

@@ -45,8 +45,6 @@ final class PlayerModel: ObservableObject {
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
@Published var restoredSegments = [Segment]()
@Published var channelWithDetails: Channel?
#if os(iOS)
@Published var motionManager: CMMotionManager!
@Published var lockedOrientation: UIInterfaceOrientation?
@@ -210,11 +208,7 @@ final class PlayerModel: ObservableObject {
self?.sponsorBlock.loadSegments(
videoID: video.videoID,
categories: Defaults[.sponsorBlockCategories]
) { [weak self] in
if Defaults[.showChannelSubscribers] {
self?.loadCurrentItemChannelDetails()
}
}
)
}
}
@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,11 @@
[![AGPL v3](https://shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
[![GitHub issues](https://img.shields.io/github/issues/yattee/yattee)](https://github.com/yattee/yattee/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/yattee/yattee)](https://github.com/yattee/yattee/pulls)
[![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#yattee:matrix.org)
[![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#Yattee:matrix.org)
Now on **Discord**:
[![Discord](https://img.shields.io/discord/992481375817052251?style=for-the-badge)](https://yattee.stream/discord)
![Screenshot](https://r.yattee.stream/screenshots/all-platforms.png)
</div>
@@ -19,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.

View File

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

View File

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

View File

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

View File

@@ -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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")
}

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>"; };
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;

View File

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