mirror of
https://github.com/yattee/yattee.git
synced 2025-12-16 13:08:14 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e88219ce88 | ||
|
|
adf4157ff3 | ||
|
|
c78dd4a35e | ||
|
|
8d2694df33 | ||
|
|
0e3effd512 | ||
|
|
a70d4f3b38 | ||
|
|
6328bfbfab | ||
|
|
184992ea32 | ||
|
|
dd8d6b6c4a |
@@ -14,4 +14,13 @@ extension Double {
|
|||||||
|
|
||||||
return formatter.string(from: self)
|
return formatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formattedAsRelativeTime() -> String? {
|
||||||
|
let date = Date(timeIntervalSince1970: self)
|
||||||
|
|
||||||
|
let formatter = RelativeDateTimeFormatter()
|
||||||
|
formatter.unitsStyle = .full
|
||||||
|
|
||||||
|
return formatter.localizedString(for: date, relativeTo: Date())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
let instanceID: String
|
let instanceID: String
|
||||||
var name: String?
|
var name: String?
|
||||||
let url: String
|
let url: String
|
||||||
let sid: String
|
let username: String
|
||||||
|
let password: String?
|
||||||
|
var token: String?
|
||||||
let anonymous: Bool
|
let anonymous: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@@ -16,7 +18,9 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
instanceID: String? = nil,
|
instanceID: String? = nil,
|
||||||
name: String? = nil,
|
name: String? = nil,
|
||||||
url: String? = nil,
|
url: String? = nil,
|
||||||
sid: String? = nil,
|
username: String? = nil,
|
||||||
|
password: String? = nil,
|
||||||
|
token: String? = nil,
|
||||||
anonymous: Bool = false
|
anonymous: Bool = false
|
||||||
) {
|
) {
|
||||||
self.anonymous = anonymous
|
self.anonymous = anonymous
|
||||||
@@ -25,27 +29,29 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
self.instanceID = instanceID ?? UUID().uuidString
|
self.instanceID = instanceID ?? UUID().uuidString
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = url ?? ""
|
self.url = url ?? ""
|
||||||
self.sid = sid ?? ""
|
self.username = username ?? ""
|
||||||
|
self.token = token
|
||||||
|
self.password = password ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance: Instance {
|
var instance: Instance! {
|
||||||
Defaults[.instances].first { $0.id == instanceID }!
|
Defaults[.instances].first { $0.id == instanceID }
|
||||||
}
|
}
|
||||||
|
|
||||||
var anonymizedSID: String {
|
var shortUsername: String {
|
||||||
guard sid.count > 3 else {
|
guard username.count > 10 else {
|
||||||
return ""
|
return username
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = sid.index(sid.startIndex, offsetBy: 4)
|
let index = username.index(username.startIndex, offsetBy: 11)
|
||||||
return String(sid[..<index])
|
return String(username[..<index])
|
||||||
}
|
}
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
(name != nil && name!.isEmpty) ? "Unnamed (\(anonymizedSID))" : name!
|
(name != nil && name!.isEmpty) ? shortUsername : name!
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(sid)
|
hasher.combine(username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import SwiftUI
|
|||||||
final class AccountValidator: Service {
|
final class AccountValidator: Service {
|
||||||
let app: Binding<VideosApp>
|
let app: Binding<VideosApp>
|
||||||
let url: String
|
let url: String
|
||||||
let account: Account?
|
let account: Account!
|
||||||
|
|
||||||
var formObjectID: Binding<String>
|
var formObjectID: Binding<String>
|
||||||
var isValid: Binding<Bool>
|
var isValid: Binding<Bool>
|
||||||
@@ -46,7 +46,11 @@ final class AccountValidator: Service {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.headers["Cookie"] = self.cookieHeader
|
$0.headers["Cookie"] = self.invidiousCookieHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
configure("/login", requestMethods: [.post]) {
|
||||||
|
$0.headers["Content-Type"] = "application/json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,20 +88,27 @@ final class AccountValidator: Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateInvidiousAccount() {
|
func validateAccount() {
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
feed
|
accountRequest
|
||||||
.load()
|
.onSuccess { response in
|
||||||
.onSuccess { _ in
|
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||||
guard self.account!.sid == self.formObjectID.wrappedValue else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isValid.wrappedValue = true
|
switch self.app.wrappedValue {
|
||||||
|
case .invidious:
|
||||||
|
self.isValid.wrappedValue = true
|
||||||
|
case .piped:
|
||||||
|
let error = response.json.dictionaryValue["error"]?.string
|
||||||
|
let token = response.json.dictionaryValue["token"]?.string
|
||||||
|
self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true)
|
||||||
|
self.error!.wrappedValue = error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onFailure { _ in
|
.onFailure { _ in
|
||||||
guard self.account!.sid == self.formObjectID.wrappedValue else {
|
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +120,15 @@ final class AccountValidator: Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var accountRequest: Request {
|
||||||
|
switch app.wrappedValue {
|
||||||
|
case .invidious:
|
||||||
|
return feed.load()
|
||||||
|
case .piped:
|
||||||
|
return login.request(.post, json: ["username": account.username, "password": account.password])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
isValid.wrappedValue = false
|
isValid.wrappedValue = false
|
||||||
isValidated.wrappedValue = false
|
isValidated.wrappedValue = false
|
||||||
@@ -116,8 +136,12 @@ final class AccountValidator: Service {
|
|||||||
error?.wrappedValue = nil
|
error?.wrappedValue = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var cookieHeader: String {
|
var invidiousCookieHeader: String {
|
||||||
"SID=\(account!.sid)"
|
"SID=\(account.username)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var login: Resource {
|
||||||
|
resource("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
var feed: Resource {
|
var feed: Resource {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ struct AccountsBridge: Defaults.Bridge {
|
|||||||
"instanceID": value.instanceID,
|
"instanceID": value.instanceID,
|
||||||
"name": value.name ?? "",
|
"name": value.name ?? "",
|
||||||
"apiURL": value.url,
|
"apiURL": value.url,
|
||||||
"sid": value.sid
|
"username": value.username,
|
||||||
|
"password": value.password ?? ""
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +26,14 @@ struct AccountsBridge: Defaults.Bridge {
|
|||||||
let id = object["id"],
|
let id = object["id"],
|
||||||
let instanceID = object["instanceID"],
|
let instanceID = object["instanceID"],
|
||||||
let url = object["apiURL"],
|
let url = object["apiURL"],
|
||||||
let sid = object["sid"]
|
let username = object["username"]
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = object["name"] ?? ""
|
let name = object["name"] ?? ""
|
||||||
|
let password = object["password"]
|
||||||
|
|
||||||
return Account(id: id, instanceID: instanceID, name: name, url: url, sid: sid)
|
return Account(id: id, instanceID: instanceID, name: name, url: url, username: username, password: password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ final class AccountsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var app: VideosApp {
|
var app: VideosApp {
|
||||||
current?.instance.app ?? .invidious
|
current?.instance?.app ?? .invidious
|
||||||
}
|
}
|
||||||
|
|
||||||
var api: VideosAPI {
|
var api: VideosAPI {
|
||||||
@@ -35,7 +35,7 @@ final class AccountsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var signedIn: Bool {
|
var signedIn: Bool {
|
||||||
!isEmpty && !current.anonymous
|
!isEmpty && !current.anonymous && api.signedIn
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -74,8 +74,14 @@ final class AccountsModel: ObservableObject {
|
|||||||
Defaults[.accounts].first { $0.id == id }
|
Defaults[.accounts].first { $0.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func add(instance: Instance, name: String, sid: String) -> Account {
|
static func add(instance: Instance, name: String, username: String, password: String? = nil) -> Account {
|
||||||
let account = Account(instanceID: instance.id, name: name, url: instance.apiURL, sid: sid)
|
let account = Account(
|
||||||
|
instanceID: instance.id,
|
||||||
|
name: name,
|
||||||
|
url: instance.apiURL,
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
)
|
||||||
Defaults[.accounts].append(account)
|
Defaults[.accounts].append(account)
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
func configure() {
|
func configure() {
|
||||||
configure {
|
configure {
|
||||||
if !self.account.sid.isEmpty {
|
if !self.account.username.isEmpty {
|
||||||
$0.headers["Cookie"] = self.cookieHeader
|
$0.headers["Cookie"] = self.cookieHeader
|
||||||
}
|
}
|
||||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||||
@@ -160,7 +160,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var cookieHeader: String {
|
private var cookieHeader: String {
|
||||||
"SID=\(account.sid)"
|
"SID=\(account.username)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var popular: Resource? {
|
var popular: Resource? {
|
||||||
@@ -185,8 +185,18 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelSubscription(_ id: String) -> Resource? {
|
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
|
||||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")).child(id)
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||||
|
.child(channelID)
|
||||||
|
.request(.post)
|
||||||
|
.onCompletion { _ in onCompletion() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) {
|
||||||
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||||
|
.child(channelID)
|
||||||
|
.request(.delete)
|
||||||
|
.onCompletion { _ in onCompletion() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func channel(_ id: String) -> Resource {
|
func channel(_ id: String) -> Resource {
|
||||||
@@ -202,7 +212,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playlists: Resource? {
|
var playlists: Resource? {
|
||||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
if account.isNil || account.anonymous {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func playlist(_ id: String) -> Resource? {
|
func playlist(_ id: String) -> Resource? {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import Siesta
|
|||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class PipedAPI: Service, ObservableObject, VideosAPI {
|
final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||||
|
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"]
|
||||||
|
|
||||||
@Published var account: Account!
|
@Published var account: Account!
|
||||||
|
|
||||||
var anonymousAccount: Account {
|
var anonymousAccount: Account {
|
||||||
@@ -27,10 +29,16 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func configure() {
|
func configure() {
|
||||||
|
invalidateConfiguration()
|
||||||
|
|
||||||
configure {
|
configure {
|
||||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure(whenURLMatches: { url in self.needsAuthorization(url) }) {
|
||||||
|
$0.headers["Authorization"] = self.account.token
|
||||||
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> Channel? in
|
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> Channel? in
|
||||||
PipedAPI.extractChannel(from: content.json)
|
PipedAPI.extractChannel(from: content.json)
|
||||||
}
|
}
|
||||||
@@ -54,6 +62,38 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
configureTransformer(pathPattern("suggestions")) { (content: Entity<JSON>) -> [String] in
|
configureTransformer(pathPattern("suggestions")) { (content: Entity<JSON>) -> [String] in
|
||||||
content.json.arrayValue.map(String.init)
|
content.json.arrayValue.map(String.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("subscriptions")) { (content: Entity<JSON>) -> [Channel] in
|
||||||
|
content.json.arrayValue.map { PipedAPI.extractChannel(from: $0)! }
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("feed")) { (content: Entity<JSON>) -> [Video] in
|
||||||
|
content.json.arrayValue.map { PipedAPI.extractVideo(from: $0)! }
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.token.isNil {
|
||||||
|
updateToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func needsAuthorization(_ url: URL) -> Bool {
|
||||||
|
PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult func updateToken() -> Request {
|
||||||
|
account.token = nil
|
||||||
|
return login.request(
|
||||||
|
.post,
|
||||||
|
json: ["username": account.username, "password": account.password]
|
||||||
|
)
|
||||||
|
.onSuccess { response in
|
||||||
|
self.account.token = response.json.dictionaryValue["token"]?.string ?? ""
|
||||||
|
self.configure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var login: Resource {
|
||||||
|
resource(baseURL: account.url, path: "login")
|
||||||
}
|
}
|
||||||
|
|
||||||
func channel(_ id: String) -> Resource {
|
func channel(_ id: String) -> Resource {
|
||||||
@@ -88,15 +128,34 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
resource(baseURL: account.instance.apiURL, path: "streams/\(id)")
|
resource(baseURL: account.instance.apiURL, path: "streams/\(id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
var signedIn: Bool { false }
|
var signedIn: Bool {
|
||||||
|
!account.anonymous && !(account.token?.isEmpty ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptions: Resource? {
|
||||||
|
resource(baseURL: account.instance.apiURL, path: "subscriptions")
|
||||||
|
}
|
||||||
|
|
||||||
|
var feed: Resource? {
|
||||||
|
resource(baseURL: account.instance.apiURL, path: "feed")
|
||||||
|
.withParam("authToken", account.token)
|
||||||
|
}
|
||||||
|
|
||||||
var subscriptions: Resource? { nil }
|
|
||||||
var feed: Resource? { nil }
|
|
||||||
var home: Resource? { nil }
|
var home: Resource? { nil }
|
||||||
var popular: Resource? { nil }
|
var popular: Resource? { nil }
|
||||||
var playlists: Resource? { nil }
|
var playlists: Resource? { nil }
|
||||||
|
|
||||||
func channelSubscription(_: String) -> Resource? { nil }
|
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
|
||||||
|
resource(baseURL: account.instance.apiURL, path: "subscribe")
|
||||||
|
.request(.post, json: ["channelId": channelID])
|
||||||
|
.onCompletion { _ in onCompletion() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
|
||||||
|
resource(baseURL: account.instance.apiURL, path: "unsubscribe")
|
||||||
|
.request(.post, json: ["channelId": channelID])
|
||||||
|
.onCompletion { _ in onCompletion() }
|
||||||
|
}
|
||||||
|
|
||||||
func playlist(_: String) -> Resource? { nil }
|
func playlist(_: String) -> Resource? { nil }
|
||||||
func playlistVideo(_: String, _: String) -> Resource? { nil }
|
func playlistVideo(_: String, _: String) -> Resource? { nil }
|
||||||
@@ -211,13 +270,15 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
|
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
|
||||||
|
let published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ??
|
||||||
|
(details["uploaded"]!.double! / 1000).formattedAsRelativeTime()!
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
videoID: PipedAPI.extractID(from: content),
|
videoID: PipedAPI.extractID(from: content),
|
||||||
title: details["title"]!.stringValue,
|
title: details["title"]!.stringValue,
|
||||||
author: author,
|
author: author,
|
||||||
length: details["duration"]!.doubleValue,
|
length: details["duration"]!.doubleValue,
|
||||||
published: details["uploadedDate"]?.stringValue ?? details["uploadDate"]!.stringValue,
|
published: published,
|
||||||
views: details["views"]!.intValue,
|
views: details["views"]!.intValue,
|
||||||
description: PipedAPI.extractDescription(from: content),
|
description: PipedAPI.extractDescription(from: content),
|
||||||
channel: Channel(id: channelId, name: author),
|
channel: Channel(id: channelId, name: author),
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ protocol VideosAPI {
|
|||||||
var popular: Resource? { get }
|
var popular: Resource? { get }
|
||||||
var playlists: Resource? { get }
|
var playlists: Resource? { get }
|
||||||
|
|
||||||
func channelSubscription(_ id: String) -> Resource?
|
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void)
|
||||||
|
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void)
|
||||||
|
|
||||||
func playlist(_ id: String) -> Resource?
|
func playlist(_ id: String) -> Resource?
|
||||||
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
|
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
|
||||||
@@ -80,6 +81,6 @@ extension VideosAPI {
|
|||||||
urlComponents.queryItems = queryItems
|
urlComponents.queryItems = queryItems
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlComponents.url!
|
return urlComponents.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ enum VideosApp: String, CaseIterable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var supportsAccounts: Bool {
|
var supportsAccounts: Bool {
|
||||||
self == .invidious
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountsUsePassword: Bool {
|
||||||
|
self == .piped
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportsPopular: Bool {
|
var supportsPopular: Bool {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ final class PlaylistsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) {
|
func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) {
|
||||||
|
guard !resource.isNil else {
|
||||||
|
playlists = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let request = force ? resource?.load() : resource?.loadIfNeeded()
|
let request = force ? resource?.load() : resource?.loadIfNeeded()
|
||||||
|
|
||||||
guard !request.isNil else {
|
guard !request.isNil else {
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ final class SubscriptionsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func subscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
func subscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
||||||
performRequest(channelID, method: .post, onSuccess: onSuccess)
|
accounts.api.subscribe(channelID) {
|
||||||
|
self.scheduleLoad(onSuccess: onSuccess)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
func unsubscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
||||||
performRequest(channelID, method: .delete, onSuccess: onSuccess)
|
accounts.api.unsubscribe(channelID) {
|
||||||
|
self.scheduleLoad(onSuccess: onSuccess)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSubscribing(_ channelID: String) -> Bool {
|
func isSubscribing(_ channelID: String) -> Bool {
|
||||||
@@ -31,6 +35,9 @@ final class SubscriptionsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) {
|
func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) {
|
||||||
|
guard accounts.app.supportsSubscriptions else {
|
||||||
|
return
|
||||||
|
}
|
||||||
let request = force ? resource?.load() : resource?.loadIfNeeded()
|
let request = force ? resource?.load() : resource?.loadIfNeeded()
|
||||||
|
|
||||||
request?
|
request?
|
||||||
@@ -45,8 +52,8 @@ final class SubscriptionsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func performRequest(_ channelID: String, method: RequestMethod, onSuccess: @escaping () -> Void = {}) {
|
private func scheduleLoad(onSuccess: @escaping () -> Void) {
|
||||||
accounts.api.channelSubscription(channelID)?.request(method).onCompletion { _ in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
self.load(force: true, onSuccess: onSuccess)
|
self.load(force: true, onSuccess: onSuccess)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -3,25 +3,29 @@
|
|||||||
Video player with support for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) instances built for iOS 15, tvOS 15 and macOS Monterey.
|
Video player with support for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) instances built for iOS 15, tvOS 15 and macOS Monterey.
|
||||||
|
|
||||||
|
|
||||||
|
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||||
|

|
||||||
|

|
||||||
|
[](https://matrix.to/#/#yattee:matrix.org)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Native user interface built with [SwiftUI](https://developer.apple.com/xcode/swiftui/)
|
* Native user interface built with [SwiftUI](https://developer.apple.com/xcode/swiftui/)
|
||||||
* Multiple instances and accounts, fast switching
|
* Multiple instances and accounts, fast switching
|
||||||
* [SponsorBlock](https://sponsor.ajay.app/) with selection of categories to skip
|
* [SponsorBlock](https://sponsor.ajay.app/), configurable categories to skip
|
||||||
* Player queue and history
|
* Player queue and history
|
||||||
* Fullscreen playback and Picture in Picture
|
* Fullscreen playback, Picture in Picture and AirPlay support
|
||||||
* Stream quality selection
|
* Stream quality selection
|
||||||
* Favorites: customizable section of channels, playlists, trending, searches and other views
|
* Favorites: customizable section of channels, playlists, trending, searches and other views
|
||||||
* AirPlay support
|
* URL Scheme for integrations
|
||||||
* Safari Extension for macOS and iOS for redirecting to the app
|
|
||||||
* URL Scheme for easy integrations
|
|
||||||
|
|
||||||
### Availability
|
### Availability
|
||||||
| Feature | Invidious | Piped |
|
| Feature | Invidious | Piped |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| User Accounts | ✅ | 🔴 |
|
| User Accounts | ✅ | ✅ |
|
||||||
| Subscriptions | ✅ | 🔴 |
|
| Subscriptions | ✅ | ✅ |
|
||||||
| Popular | ✅ | 🔴 |
|
| Popular | ✅ | 🔴 |
|
||||||
| User Playlists | ✅ | 🔴 |
|
| User Playlists | ✅ | 🔴 |
|
||||||
| Trending | ✅ | ✅ |
|
| Trending | ✅ | ✅ |
|
||||||
@@ -34,24 +38,21 @@ Video player with support for [Invidious](https://github.com/iv-org/invidious) a
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
### Requirements
|
### Requirements
|
||||||
Application is built using latest APIs, that's why for now **only recent** software versions: iOS/tvOS 15 or macOS Monterey are supported.
|
Only iOS/tvOS 15 and macOS Monterey are supported.
|
||||||
|
|
||||||
### How to install?
|
### How to install?
|
||||||
#### [AltStore](https://altstore.io/)
|
#### [AltStore](https://altstore.io/)
|
||||||
You can sideload IPA files that you can download from Releases page.
|
You can sideload IPA files that you can download from Releases page.
|
||||||
Alternatively, if you have to access to the beta AltStore version (v1.5), you can add the following repository in `Browse > Sources` screen: `https://alt.yattee.stream`
|
Alternatively, if you have to access to the beta AltStore version (v1.5), you can add the following repository in `Browse > Sources` screen:
|
||||||
|
|
||||||
|
`https://alt.yattee.stream`
|
||||||
|
|
||||||
#### Manual installation
|
#### Manual installation
|
||||||
Download sources and compile them on a Mac using Xcode, install to your devices. Please note that if you are not registered in Apple Developer Program then the applications will require reinstalling every 7 days.
|
Download sources and compile them on a Mac using Xcode, install to your devices. Please note that if you are not registered in Apple Developer Program then the applications will require reinstalling every 7 days.
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
### Safari
|
|
||||||
macOS and iOS apps include Safari extension which will redirect opened YouTube tabs to the app.
|
|
||||||
### Firefox
|
|
||||||
You can use [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) extension to make the videos open in the app. In extension settings put the following URL as Invidious instance: `https://r.yatte.stream`
|
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
With [Finicky](https://github.com/johnste/finicky) you can configure your systems so the video links across the entire system will get opened in the app. Example configuration:
|
With [Finicky](https://github.com/johnste/finicky) you can configure your system to open all the video links in the app. Example configuration:
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
match: [
|
match: [
|
||||||
@@ -62,6 +63,14 @@ With [Finicky](https://github.com/johnste/finicky) you can configure your system
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Experimental: Safari
|
||||||
|
macOS and iOS apps include Safari extension which will redirect opened YouTube tabs to the app.
|
||||||
|
|
||||||
|
### Expermiental: Firefox
|
||||||
|
You can use [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) extension to make the videos open in the app. In extension settings put the following URL as Invidious instance:
|
||||||
|
|
||||||
|
`https://r.yatte.stream`
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
### iOS
|
### iOS
|
||||||
| Player | Search | Playlists |
|
| Player | Search | Playlists |
|
||||||
@@ -82,7 +91,7 @@ With [Finicky](https://github.com/johnste/finicky) you can configure your system
|
|||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
### Settings
|
### Settings
|
||||||
* [tvOS] To open settings press Play/Pause button while hovering over navigation menu or video
|
* [tvOS] To open settings, press Play/Pause button while hovering over navigation menu or video
|
||||||
### Navigation
|
### Navigation
|
||||||
* Use videos context menus to add to queue, open or subscribe channel and add to playlist
|
* Use videos context menus to add to queue, open or subscribe channel and add to playlist
|
||||||
* [tvOS] Pressing buttons in the app trigger switch to next available option (for example: next account in Settings). If you want to access list of all options, press and hold to open the context menu.
|
* [tvOS] Pressing buttons in the app trigger switch to next available option (for example: next account in Settings). If you want to access list of all options, press and hold to open the context menu.
|
||||||
@@ -102,11 +111,6 @@ With [Finicky](https://github.com/johnste/finicky) you can configure your system
|
|||||||
* `Command+S` - Play Next
|
* `Command+S` - Play Next
|
||||||
* `Command+O` - Toggle Player
|
* `Command+O` - Toggle Player
|
||||||
|
|
||||||
## Contributing
|
|
||||||
Every contribution to make this tool better is very welcome. Start with [creating issue](https://github.com/yattee/app/issues/new) to have discussion which can be later transformed into a Pull Request.
|
|
||||||
|
|
||||||
Review existing Issues and Pull Requests before creating new ones.
|
|
||||||
|
|
||||||
## License and Liability
|
## License and Liability
|
||||||
|
|
||||||
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
|
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ struct FavoriteItemView: View {
|
|||||||
.opacity(dragging?.id == item.id ? 0.5 : 1)
|
.opacity(dragging?.id == item.id ? 0.5 : 1)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
resource?.addObserver(store)
|
resource?.addObserver(store)
|
||||||
resource?.loadIfNeeded()
|
resource?.load()
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.onDrag {
|
.onDrag {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ struct AccountsMenuView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(model.current?.name ?? "Select Account", systemImage: "person.crop.circle")
|
Label(model.current?.description ?? "Select Account", systemImage: "person.crop.circle")
|
||||||
.labelStyle(.titleAndIcon)
|
.labelStyle(.titleAndIcon)
|
||||||
}
|
}
|
||||||
.disabled(instances.isEmpty)
|
.disabled(instances.isEmpty)
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ struct AppSidebarPlaylists: View {
|
|||||||
newPlaylistButton
|
newPlaylistButton
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
playlists.load()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPlaylistButton: some View {
|
var newPlaylistButton: some View {
|
||||||
|
|||||||
@@ -22,8 +22,5 @@ struct AppSidebarSubscriptions: View {
|
|||||||
.id("channel\(channel.id)")
|
.id("channel\(channel.id)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
subscriptions.load()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import SwiftUI
|
|||||||
struct Sidebar: View {
|
struct Sidebar: View {
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollViewReader { scrollView in
|
ScrollViewReader { scrollView in
|
||||||
@@ -13,12 +15,25 @@ struct Sidebar: View {
|
|||||||
AppSidebarRecents()
|
AppSidebarRecents()
|
||||||
.id("recentlyOpened")
|
.id("recentlyOpened")
|
||||||
|
|
||||||
if accounts.signedIn {
|
if accounts.api.signedIn {
|
||||||
AppSidebarSubscriptions()
|
if accounts.app.supportsSubscriptions {
|
||||||
AppSidebarPlaylists()
|
AppSidebarSubscriptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
if accounts.app.supportsUserPlaylists {
|
||||||
|
AppSidebarPlaylists()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
subscriptions.load()
|
||||||
|
playlists.load()
|
||||||
|
}
|
||||||
|
.onChange(of: accounts.signedIn) { _ in
|
||||||
|
subscriptions.load(force: true)
|
||||||
|
playlists.load(force: true)
|
||||||
|
}
|
||||||
.onChange(of: navigation.sidebarSectionChanged) { _ in
|
.onChange(of: navigation.sidebarSectionChanged) { _ in
|
||||||
scrollScrollViewToItem(scrollView: scrollView, for: navigation.tabSelection)
|
scrollScrollViewToItem(scrollView: scrollView, for: navigation.tabSelection)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct VideoDetails: View {
|
|||||||
@State private var confirmationShown = false
|
@State private var confirmationShown = false
|
||||||
@State private var presentingAddToPlaylist = false
|
@State private var presentingAddToPlaylist = false
|
||||||
@State private var presentingShareSheet = false
|
@State private var presentingShareSheet = false
|
||||||
@State private var shareURL = ""
|
@State private var shareURL: URL?
|
||||||
|
|
||||||
@State private var currentPage = Page.details
|
@State private var currentPage = Page.details
|
||||||
|
|
||||||
@@ -309,7 +309,9 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.sheet(isPresented: $presentingShareSheet) {
|
.sheet(isPresented: $presentingShareSheet) {
|
||||||
ShareSheet(activityItems: [shareURL])
|
if let shareURL = shareURL {
|
||||||
|
ShareSheet(activityItems: [shareURL])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -337,6 +339,7 @@ struct VideoDetails: View {
|
|||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
if let description = video.description {
|
if let description = video.description {
|
||||||
Text(description)
|
Text(description)
|
||||||
|
.textSelection(.enabled)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ struct AccountForm: View {
|
|||||||
var selectedAccount: Binding<Account?>?
|
var selectedAccount: Binding<Account?>?
|
||||||
|
|
||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var sid = ""
|
@State private var username = ""
|
||||||
|
@State private var password = ""
|
||||||
|
|
||||||
@State private var isValid = false
|
@State private var isValid = false
|
||||||
@State private var isValidated = false
|
@State private var isValidated = false
|
||||||
@State private var isValidating = false
|
@State private var isValidating = false
|
||||||
|
@State private var validationError: String?
|
||||||
@State private var validationDebounce = Debounce()
|
@State private var validationDebounce = Debounce()
|
||||||
|
|
||||||
@FocusState private var focused: Bool
|
@FocusState private var focused: Bool
|
||||||
@@ -67,21 +69,42 @@ struct AccountForm: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onAppear(perform: initializeForm)
|
.onAppear(perform: initializeForm)
|
||||||
.onChange(of: sid) { _ in validate() }
|
.onChange(of: username) { _ in validate() }
|
||||||
|
.onChange(of: password) { _ in validate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
var formFields: some View {
|
var formFields: some View {
|
||||||
Group {
|
Group {
|
||||||
TextField("Name", text: $name, prompt: Text("Account Name (optional)"))
|
if !instance.app.accountsUsePassword {
|
||||||
.focused($focused)
|
TextField("Name", text: $name, prompt: Text("Account Name (optional)"))
|
||||||
|
.focused($focused)
|
||||||
|
}
|
||||||
|
|
||||||
TextField("SID", text: $sid, prompt: Text("Invidious SID Cookie"))
|
TextField("Username", text: $username, prompt: usernamePrompt)
|
||||||
|
|
||||||
|
if instance.app.accountsUsePassword {
|
||||||
|
SecureField("Password", text: $password, prompt: Text("Password"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var usernamePrompt: Text {
|
||||||
|
switch instance.app {
|
||||||
|
case .invidious:
|
||||||
|
return Text("SID Cookie")
|
||||||
|
default:
|
||||||
|
return Text("Username")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var footer: some View {
|
var footer: some View {
|
||||||
HStack {
|
HStack {
|
||||||
AccountValidationStatus(isValid: $isValid, isValidated: $isValidated, isValidating: $isValidating, error: .constant(nil))
|
AccountValidationStatus(
|
||||||
|
isValid: $isValid,
|
||||||
|
isValidated: $isValidated,
|
||||||
|
isValidating: $isValidating,
|
||||||
|
error: $validationError
|
||||||
|
)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -106,7 +129,9 @@ struct AccountForm: View {
|
|||||||
isValid = false
|
isValid = false
|
||||||
validationDebounce.invalidate()
|
validationDebounce.invalidate()
|
||||||
|
|
||||||
guard !sid.isEmpty else {
|
let passwordIsValid = instance.app.accountsUsePassword ? !password.isEmpty : true
|
||||||
|
|
||||||
|
guard !username.isEmpty, passwordIsValid else {
|
||||||
validator.reset()
|
validator.reset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -114,7 +139,7 @@ struct AccountForm: View {
|
|||||||
isValidating = true
|
isValidating = true
|
||||||
|
|
||||||
validationDebounce.debouncing(1) {
|
validationDebounce.debouncing(1) {
|
||||||
validator.validateInvidiousAccount()
|
validator.validateAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +148,7 @@ struct AccountForm: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let account = AccountsModel.add(instance: instance, name: name, sid: sid)
|
let account = AccountsModel.add(instance: instance, name: name, username: username, password: password)
|
||||||
selectedAccount?.wrappedValue = account
|
selectedAccount?.wrappedValue = account
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -133,11 +158,12 @@ struct AccountForm: View {
|
|||||||
AccountValidator(
|
AccountValidator(
|
||||||
app: .constant(instance.app),
|
app: .constant(instance.app),
|
||||||
url: instance.apiURL,
|
url: instance.apiURL,
|
||||||
account: Account(instanceID: instance.id, url: instance.apiURL, sid: sid),
|
account: Account(instanceID: instance.id, url: instance.apiURL, username: username, password: password),
|
||||||
id: $sid,
|
id: $username,
|
||||||
isValid: $isValid,
|
isValid: $isValid,
|
||||||
isValidated: $isValidated,
|
isValidated: $isValidated,
|
||||||
isValidating: $isValidating
|
isValidating: $isValidating,
|
||||||
|
error: $validationError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ struct AccountValidationStatus: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(isValid ? "Connected successfully" : "Connection failed")
|
Text(isValid ? "Connected successfully" : "Connection failed")
|
||||||
if !isValid && !error.isNil {
|
if let error = error, !isValid {
|
||||||
Text(error!)
|
Text(error)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ struct SettingsView: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
TabView {
|
TabView {
|
||||||
Form {
|
Form {
|
||||||
InstancesSettings()
|
InstancesSettings()
|
||||||
|
.environmentObject(accounts)
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Instances", systemImage: "server.rack")
|
Label("Instances", systemImage: "server.rack")
|
||||||
@@ -49,7 +52,7 @@ struct SettingsView: View {
|
|||||||
.tag(Tabs.services)
|
.tag(Tabs.services)
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(width: 400, height: 310)
|
.frame(width: 400, height: 380)
|
||||||
#else
|
#else
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List {
|
List {
|
||||||
@@ -63,6 +66,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
InstancesSettings()
|
InstancesSettings()
|
||||||
|
.environmentObject(accounts)
|
||||||
BrowsingSettings()
|
BrowsingSettings()
|
||||||
PlaybackSettings()
|
PlaybackSettings()
|
||||||
ServicesSettings()
|
ServicesSettings()
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ struct VerticalCells: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var scrollViewShowsIndicators: Bool {
|
var scrollViewShowsIndicators: Bool {
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ struct ChannelPlaylistView: View {
|
|||||||
var playlist: ChannelPlaylist
|
var playlist: ChannelPlaylist
|
||||||
|
|
||||||
@State private var presentingShareSheet = false
|
@State private var presentingShareSheet = false
|
||||||
|
@State private var shareURL: URL?
|
||||||
|
|
||||||
@StateObject private var store = Store<ChannelPlaylist>()
|
@StateObject private var store = Store<ChannelPlaylist>()
|
||||||
|
|
||||||
@@ -56,8 +57,8 @@ struct ChannelPlaylistView: View {
|
|||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.sheet(isPresented: $presentingShareSheet) {
|
.sheet(isPresented: $presentingShareSheet) {
|
||||||
if let url = accounts.api.shareURL(contentItem) {
|
if let shareURL = shareURL {
|
||||||
ShareSheet(activityItems: [url])
|
ShareSheet(activityItems: [shareURL])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -70,7 +71,8 @@ struct ChannelPlaylistView: View {
|
|||||||
ToolbarItem(placement: .navigation) {
|
ToolbarItem(placement: .navigation) {
|
||||||
ShareButton(
|
ShareButton(
|
||||||
contentItem: contentItem,
|
contentItem: contentItem,
|
||||||
presentingShareSheet: $presentingShareSheet
|
presentingShareSheet: $presentingShareSheet,
|
||||||
|
shareURL: $shareURL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ struct ChannelVideosView: View {
|
|||||||
let channel: Channel
|
let channel: Channel
|
||||||
|
|
||||||
@State private var presentingShareSheet = false
|
@State private var presentingShareSheet = false
|
||||||
|
@State private var shareURL: URL?
|
||||||
|
|
||||||
@StateObject private var store = Store<Channel>()
|
@StateObject private var store = Store<Channel>()
|
||||||
|
|
||||||
@@ -79,7 +80,8 @@ struct ChannelVideosView: View {
|
|||||||
ToolbarItem(placement: .navigation) {
|
ToolbarItem(placement: .navigation) {
|
||||||
ShareButton(
|
ShareButton(
|
||||||
contentItem: contentItem,
|
contentItem: contentItem,
|
||||||
presentingShareSheet: $presentingShareSheet
|
presentingShareSheet: $presentingShareSheet,
|
||||||
|
shareURL: $shareURL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +102,8 @@ struct ChannelVideosView: View {
|
|||||||
#endif
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.sheet(isPresented: $presentingShareSheet) {
|
.sheet(isPresented: $presentingShareSheet) {
|
||||||
if let url = accounts.api.shareURL(contentItem) {
|
if let shareURL = shareURL {
|
||||||
ShareSheet(activityItems: [url])
|
ShareSheet(activityItems: [shareURL])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import SwiftUI
|
|||||||
struct ShareButton: View {
|
struct ShareButton: View {
|
||||||
let contentItem: ContentItem
|
let contentItem: ContentItem
|
||||||
@Binding var presentingShareSheet: Bool
|
@Binding var presentingShareSheet: Bool
|
||||||
@Binding var shareURL: String
|
@Binding var shareURL: URL?
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@@ -11,11 +11,11 @@ struct ShareButton: View {
|
|||||||
init(
|
init(
|
||||||
contentItem: ContentItem,
|
contentItem: ContentItem,
|
||||||
presentingShareSheet: Binding<Bool>,
|
presentingShareSheet: Binding<Bool>,
|
||||||
shareURL: Binding<String>? = nil
|
shareURL: Binding<URL?>? = nil
|
||||||
) {
|
) {
|
||||||
self.contentItem = contentItem
|
self.contentItem = contentItem
|
||||||
_presentingShareSheet = presentingShareSheet
|
_presentingShareSheet = presentingShareSheet
|
||||||
_shareURL = shareURL ?? .constant("")
|
_shareURL = shareURL ?? .constant(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -81,7 +81,7 @@ struct ShareButton: View {
|
|||||||
NSPasteboard.general.clearContents()
|
NSPasteboard.general.clearContents()
|
||||||
NSPasteboard.general.setString(url.absoluteString, forType: .string)
|
NSPasteboard.general.setString(url.absoluteString, forType: .string)
|
||||||
#else
|
#else
|
||||||
shareURL = url.absoluteString
|
shareURL = url
|
||||||
presentingShareSheet = true
|
presentingShareSheet = true
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,13 @@ struct SubscriptionsView: View {
|
|||||||
fileprivate func loadResources(force: Bool = false) {
|
fileprivate func loadResources(force: Bool = false) {
|
||||||
feed?.addObserver(store)
|
feed?.addObserver(store)
|
||||||
|
|
||||||
if let request = force ? accounts.api.home?.load() : accounts.api.home?.loadIfNeeded() {
|
if accounts.app == .invidious {
|
||||||
request.onSuccess { _ in
|
// Invidious for some reason won't refresh feed until homepage is loaded
|
||||||
|
if let request = force ? accounts.api.home?.load() : accounts.api.home?.loadIfNeeded() {
|
||||||
|
request.onSuccess { _ in
|
||||||
|
loadFeed(force: force)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
loadFeed(force: force)
|
loadFeed(force: force)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ struct YatteeApp: App {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Settings {
|
Settings {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
|
.environmentObject(AccountsModel())
|
||||||
.environmentObject(InstancesModel())
|
.environmentObject(InstancesModel())
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -2216,8 +2216,8 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
@@ -2250,8 +2250,8 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
@@ -2282,8 +2282,8 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||||
@@ -2314,8 +2314,8 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||||
@@ -2346,7 +2346,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -2355,7 +2355,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -2478,8 +2478,8 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
@@ -2493,7 +2493,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||||
PRODUCT_NAME = Yattee;
|
PRODUCT_NAME = Yattee;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -2510,8 +2510,8 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
@@ -2525,7 +2525,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||||
PRODUCT_NAME = Yattee;
|
PRODUCT_NAME = Yattee;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -2546,14 +2546,15 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = macOS/Info.plist;
|
INFOPLIST_FILE = macOS/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -2561,7 +2562,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||||
PRODUCT_NAME = Yattee;
|
PRODUCT_NAME = Yattee;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -2580,14 +2581,15 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = macOS/Info.plist;
|
INFOPLIST_FILE = macOS/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -2595,7 +2597,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||||
PRODUCT_NAME = Yattee;
|
PRODUCT_NAME = Yattee;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -2610,7 +2612,7 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -2635,7 +2637,7 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -2662,7 +2664,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -2687,7 +2689,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -2711,9 +2713,9 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = tvOS/Info.plist;
|
INFOPLIST_FILE = tvOS/Info.plist;
|
||||||
@@ -2727,7 +2729,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = appletvos;
|
SDKROOT = appletvos;
|
||||||
@@ -2744,9 +2746,9 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = tvOS/Info.plist;
|
INFOPLIST_FILE = tvOS/Info.plist;
|
||||||
@@ -2760,7 +2762,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = appletvos;
|
SDKROOT = appletvos;
|
||||||
@@ -2778,7 +2780,7 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -2803,7 +2805,7 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -2827,7 +2829,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -2836,7 +2838,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -2845,7 +2847,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -2854,7 +2856,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
|
"revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc",
|
||||||
"version": "5.4.3"
|
"version": "5.4.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ struct InstancesSettings: View {
|
|||||||
Text("If provided, you can copy links from videos, channels and playlist")
|
Text("If provided, you can copy links from videos, channels and playlist")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if selectedInstance != nil, !selectedInstance.app.supportsAccounts {
|
if selectedInstance != nil, !selectedInstance.app.supportsAccounts {
|
||||||
|
|||||||
@@ -44,12 +44,7 @@ struct EditFavorites: View {
|
|||||||
|
|
||||||
ForEach(model.addableItems()) { item in
|
ForEach(model.addableItems()) { item in
|
||||||
HStack {
|
HStack {
|
||||||
HStack {
|
Text(label(item))
|
||||||
Text(label(item))
|
|
||||||
Spacer()
|
|
||||||
Text("only with Invidious")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user