Compare commits

..

2 Commits
v1.3 ... v1.3.2

Author SHA1 Message Date
Arkadiusz Fal
088d5a69dd Bump version number 2022-01-30 23:05:26 +01:00
Arkadiusz Fal
1dfdb5ece3 Fix opening playlists when recents is not saved (fix #57) 2022-01-30 22:52:20 +01:00
50 changed files with 414 additions and 1326 deletions

6
.gitignore vendored
View File

@@ -81,9 +81,6 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
fastlane/builds
fastlane/.env
fastlane/*.p8
# Code Injection
#
@@ -94,6 +91,3 @@ 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 {}
set {} // swiftlint:disable:this unused_setter_value
}
}

View File

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

View File

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

View File

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

View File

@@ -1,218 +0,0 @@
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,18 +92,15 @@ 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 nil
return ContentItem(video: self.extractVideo(from: json))
}
return SearchPage(results: results, last: results.isEmpty)
@@ -239,66 +236,6 @@ 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", "user/playlists"]
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"]
@Published var account: Account!
@@ -43,11 +43,6 @@ 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)
}
@@ -86,10 +81,6 @@ 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()
}
@@ -175,9 +166,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
var home: Resource? { nil }
var popular: Resource? { nil }
var playlists: Resource? {
resource(baseURL: account.instance.apiURL, path: "user/playlists")
}
var playlists: Resource? { nil }
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resource(baseURL: account.instance.apiURL, path: "subscribe")
@@ -191,79 +180,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.onCompletion { _ in onCompletion() }
}
func playlist(_ id: String) -> Resource? {
channelPlaylist(id)
}
func playlist(_: String) -> Resource? { nil }
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)
@@ -312,8 +232,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
if let channel = extractChannel(from: content) {
return ContentItem(channel: channel)
}
default:
return nil
}
return nil
@@ -360,7 +278,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,
@@ -390,14 +308,9 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url
let subscriptionsCount = details["uploaderSubscriberCount"]?.int
let uploaded = details["uploaded"]?.doubleValue
var published = (uploaded.isNil || uploaded == -1) ? nil : (uploaded! / 1000).formattedAsRelativeTime()
if published.isNil {
published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ?? ""
}
let published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ??
(details["uploaded"]!.double! / 1000).formattedAsRelativeTime()!
let live = details["livestream"]?.boolValue ?? (details["duration"]?.intValue == -1)
return Video(
@@ -405,10 +318,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
title: details["title"]!.stringValue,
author: author,
length: details["duration"]!.doubleValue,
published: published!,
published: published,
views: details["views"]!.intValue,
description: extractDescription(from: content),
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL),
thumbnails: thumbnails,
live: live,
likes: details["likes"]?.int,
@@ -440,14 +353,6 @@ 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
@@ -488,14 +393,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let videoStreams = compatibleVideoStream(from: content)
videoStreams.forEach { videoStream in
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 audioAsset = AVURLAsset(url: audioStream.dictionaryValue["url"]!.url!)
let videoAsset = AVURLAsset(url: videoStream.dictionaryValue["url"]!.url!)
let videoOnly = videoStream.dictionaryValue["videoOnly"]?.boolValue ?? true
let resolution = Stream.Resolution.from(resolution: videoStream.dictionaryValue["quality"]!.stringValue)

View File

@@ -27,34 +27,6 @@ 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)
@@ -102,8 +74,6 @@ extension VideosAPI {
case .playlist:
urlComponents.path = "/playlist"
queryItems.append(.init(name: "list", value: item.playlist.id))
default:
return nil
}
if !time.isNil, time!.seconds.isFinite {

View File

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

View File

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

View File

@@ -14,31 +14,6 @@ final class NavigationModel: ObservableObject {
case nowPlaying
case search
var stringValue: String {
switch self {
case .favorites:
return "favorites"
case .subscriptions:
return "subscriptions"
case .popular:
return "popular"
case .trending:
return "trending"
case .playlists:
return "playlists"
case let .channel(string):
return "channel\(string)"
case let .playlist(string):
return "playlist\(string)"
case .recentlyOpened:
return "recentlyOpened"
case .search:
return "search"
default:
return ""
}
}
var playlistID: Playlist.ID? {
if case let .playlist(id) = self {
return id
@@ -74,10 +49,6 @@ 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()
@@ -170,12 +141,6 @@ final class NavigationModel: ObservableObject {
channelToUnsubscribe = channel
presentingUnsubscribeAlert = channelToUnsubscribe != nil
}
func hideKeyboard() {
#if os(iOS)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
#endif
}
}
typealias TabSelection = NavigationModel.TabSelection

View File

@@ -45,6 +45,8 @@ 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?
@@ -208,7 +210,11 @@ final class PlayerModel: ObservableObject {
self?.sponsorBlock.loadSegments(
videoID: video.videoID,
categories: Defaults[.sponsorBlockCategories]
)
) { [weak self] in
if Defaults[.showChannelSubscribers] {
self?.loadCurrentItemChannelDetails()
}
}
}
}
@@ -550,6 +556,8 @@ final class PlayerModel: ObservableObject {
controller?.playerView.dismiss(animated: false) { [weak self] in
self?.controller?.dismiss(animated: true)
}
#else
hide()
#endif
} else {
advanceToNextItem()
@@ -700,6 +708,36 @@ 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,6 +74,7 @@ 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? = nil, videos: [Video] = []) {
init(id: String, title: String, visibility: Visibility, updated: TimeInterval, videos: [Video] = []) {
self.id = id
self.title = title
self.visibility = visibility

View File

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

View File

@@ -115,7 +115,7 @@ final class SearchModel: ObservableObject {
resource?.removeObservers(ownedBy: store)
resource = accounts.api.search(query, page: pageToLoad.nextPage)
resource = accounts.api.search(query, page: page?.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
var indexID: String?
let indexID: String?
var live: Bool
var upcoming: Bool

View File

@@ -6,11 +6,7 @@
[![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)
Now on **Discord**:
[![Discord](https://img.shields.io/discord/992481375817052251?style=for-the-badge)](https://yattee.stream/discord)
[![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#yattee:matrix.org)
![Screenshot](https://r.yattee.stream/screenshots/all-platforms.png)
</div>
@@ -23,32 +19,26 @@ Now on **Discord**:
* Fullscreen playback, Picture in Picture and AirPlay support
* Stream quality selection
### Features in development
* New player component with custom controls, gestures and support for 4K playback
You can leave your feedback in [discussions](https://github.com/yattee/yattee/discussions) or join [Discord](https://yattee.stream/discord) or [Matrix Channel](https://matrix.to/#/#Yattee:matrix.org) for a chat. Thanks!
### Availability
|| Invidious | Piped |
| Feature | Invidious | Piped |
| - | - | - |
| User Accounts | ✅ | ✅ |
| Subscriptions | ✅ | ✅ |
| User Playlists | ✅ | |
| Popular | ✅ | 🔴 |
| 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](https://github.com/yattee/yattee/wiki/Installation-Instructions)
* [Building](https://github.com/yattee/yattee/wiki/Building-instructions)
* [Installation Instructions](https://github.com/yattee/yattee/wiki/Installation-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)
@@ -58,16 +48,10 @@ 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.
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 and Liability
## License
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
## 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.
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.
This tool is an open source software built for learning and research purposes.

View File

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

View File

@@ -48,6 +48,7 @@ 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,18 +11,10 @@ struct DropFavorite: DropDelegate {
return
}
guard let current = current else {
return
}
let from = favorites.firstIndex(of: current!)!
let to = favorites.firstIndex(of: item)!
let from = favorites.firstIndex(of: current)
let to = favorites.firstIndex(of: item)
guard let from = from, let to = to else {
return
}
guard favorites[to].id != current.id else {
guard favorites[to].id != current!.id else {
return
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ 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
@@ -19,15 +18,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)
}
@@ -123,7 +122,7 @@ struct AddToPlaylistView: View {
HStack {
Spacer()
Button("Add to Playlist", action: addToPlaylist)
.disabled(submitButtonDisabled || selectedPlaylist.isNil)
.disabled(selectedPlaylist.isNil)
.padding(.top, 30)
.alert(isPresented: $presentingErrorAlert) {
Alert(
@@ -166,8 +165,6 @@ struct AddToPlaylistView: View {
Defaults[.lastUsedPlaylistID] = id
submitButtonDisabled = true
model.addVideo(
playlistID: id,
videoID: video.videoID,
@@ -177,7 +174,6 @@ struct AddToPlaylistView: View {
onFailure: { requestError in
error = "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)"
presentingErrorAlert = true
submitButtonDisabled = false
}
)
}

View File

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

View File

@@ -11,8 +11,6 @@ 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
@@ -20,36 +18,7 @@ struct PlaylistsView: View {
@Namespace private var focusNamespace
var items: [ContentItem] {
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
ContentItem.array(of: currentPlaylist?.videos ?? [])
}
var body: some View {
@@ -143,17 +112,10 @@ struct PlaylistsView: View {
#endif
.onAppear {
model.load()
resource?.load()
}
.onChange(of: accounts.current) { _ in
model.load(force: true)
}
.onChange(of: selectedPlaylistID) { _ in
resource?.load()
}
.onChange(of: model.reloadPlaylists) { _ in
resource?.load()
}
#if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif

View File

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

View File

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

View File

@@ -198,7 +198,7 @@ struct SearchView: View {
visibleSections.append(.subscriptions)
}
if accounts.app.supportsUserPlaylists && accounts.signedIn && preferred.contains(.playlists) {
if accounts.app.supportsUserPlaylists && preferred.contains(.playlists) {
visibleSections.append(.playlists)
}
@@ -234,7 +234,7 @@ struct SearchView: View {
}
.edgesIgnoringSafeArea(.horizontal)
#else
VerticalCells(items: items, allowEmpty: state.query.isEmpty)
VerticalCells(items: items)
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
#endif
@@ -286,7 +286,52 @@ struct SearchView: View {
.foregroundColor(.secondary)
}
ForEach(recentItems) { item in
recentItemButton(item)
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
}
}
}
.redrawOn(change: recentsChanged)
@@ -297,55 +342,6 @@ 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)
@@ -357,7 +353,7 @@ struct SearchView: View {
private var removeAllButton: some View {
Button {
recents.clear()
recents.clearQueries()
recentsChanged.toggle()
} label: {
Label("Remove All", systemImage: "trash.fill")

View File

@@ -14,6 +14,7 @@ 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
@@ -82,6 +83,7 @@ struct PlayerSettings: View {
keywordsToggle
showHistoryToggle
channelSubscribersToggle
}
Section(header: SettingsHeader(text: "Picture in Picture")) {
@@ -194,6 +196,10 @@ 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,7 +9,6 @@ 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 {
@@ -31,8 +30,8 @@ struct TrendingCountry: View {
countriesList
}
#if os(tvOS)
.searchable(text: $query, placement: .automatic, prompt: Text(Self.prompt))
.background(Color.background(scheme: colorScheme))
.searchable(text: $query, placement: .automatic, prompt: Text(TrendingCountry.prompt))
.background(Color.black)
#endif
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
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

View File

@@ -1,108 +0,0 @@
# 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

View File

@@ -1,58 +0,0 @@
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).