mirror of
https://github.com/yattee/yattee.git
synced 2024-12-13 22:00:31 +05:30
Extended Piped support
This commit is contained in:
parent
2d075e7b3a
commit
c3326a56af
@ -14,23 +14,6 @@ extension Thumbnail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func fixtureUrl(videoId: String, quality: Thumbnail.Quality) -> URL {
|
private static func fixtureUrl(videoId: String, quality: Thumbnail.Quality) -> URL {
|
||||||
URL(string: "\(fixturesHost)/vi/\(videoId)/\(filenameForQuality(quality)).jpg")!
|
URL(string: "\(fixturesHost)/vi/\(videoId)/\(quality.filename).jpg")!
|
||||||
}
|
|
||||||
|
|
||||||
private static func filenameForQuality(_ quality: Thumbnail.Quality) -> String {
|
|
||||||
switch quality {
|
|
||||||
case .high:
|
|
||||||
return "hqdefault"
|
|
||||||
case .medium:
|
|
||||||
return "mqdefault"
|
|
||||||
case .start:
|
|
||||||
return "1"
|
|
||||||
case .middle:
|
|
||||||
return "2"
|
|
||||||
case .end:
|
|
||||||
return "3"
|
|
||||||
default:
|
|
||||||
return quality.rawValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
Model/Account.swift
Normal file
79
Model/Account.swift
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||||
|
struct AccountsBridge: Defaults.Bridge {
|
||||||
|
typealias Value = Account
|
||||||
|
typealias Serializable = [String: String]
|
||||||
|
|
||||||
|
func serialize(_ value: Value?) -> Serializable? {
|
||||||
|
guard let value = value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"id": value.id,
|
||||||
|
"instanceID": value.instanceID,
|
||||||
|
"name": value.name ?? "",
|
||||||
|
"url": value.url,
|
||||||
|
"sid": value.sid
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
|
guard
|
||||||
|
let object = object,
|
||||||
|
let id = object["id"],
|
||||||
|
let instanceID = object["instanceID"],
|
||||||
|
let url = object["url"],
|
||||||
|
let sid = object["sid"]
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = object["name"] ?? ""
|
||||||
|
|
||||||
|
return Account(id: id, instanceID: instanceID, name: name, url: url, sid: sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var bridge = AccountsBridge()
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
let instanceID: String
|
||||||
|
var name: String?
|
||||||
|
let url: String
|
||||||
|
let sid: String
|
||||||
|
let anonymous: Bool
|
||||||
|
|
||||||
|
init(id: String? = nil, instanceID: String? = nil, name: String? = nil, url: String? = nil, sid: String? = nil, anonymous: Bool = false) {
|
||||||
|
self.anonymous = anonymous
|
||||||
|
|
||||||
|
self.id = id ?? (anonymous ? "anonymous-\(instanceID!)" : UUID().uuidString)
|
||||||
|
self.instanceID = instanceID ?? UUID().uuidString
|
||||||
|
self.name = name
|
||||||
|
self.url = url ?? ""
|
||||||
|
self.sid = sid ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance: Instance {
|
||||||
|
Defaults[.instances].first { $0.id == instanceID }!
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymizedSID: String {
|
||||||
|
guard sid.count > 3 else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = sid.index(sid.startIndex, offsetBy: 4)
|
||||||
|
return String(sid[..<index])
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
(name != nil && name!.isEmpty) ? "Unnamed (\(anonymizedSID))" : name!
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(sid)
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,9 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class AccountValidator: Service {
|
final class AccountValidator: Service {
|
||||||
let app: Binding<Instance.App>
|
let app: Binding<VideosApp>
|
||||||
let url: String
|
let url: String
|
||||||
let account: Instance.Account?
|
let account: Account?
|
||||||
|
|
||||||
var formObjectID: Binding<String>
|
var formObjectID: Binding<String>
|
||||||
var isValid: Binding<Bool>
|
var isValid: Binding<Bool>
|
||||||
@ -14,9 +14,9 @@ final class AccountValidator: Service {
|
|||||||
var error: Binding<String?>?
|
var error: Binding<String?>?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
app: Binding<Instance.App>,
|
app: Binding<VideosApp>,
|
||||||
url: String,
|
url: String,
|
||||||
account: Instance.Account? = nil,
|
account: Account? = nil,
|
||||||
id: Binding<String>,
|
id: Binding<String>,
|
||||||
isValid: Binding<Bool>,
|
isValid: Binding<Bool>,
|
||||||
isValidated: Binding<Bool>,
|
isValidated: Binding<Bool>,
|
||||||
|
@ -3,18 +3,18 @@ import Defaults
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class AccountsModel: ObservableObject {
|
final class AccountsModel: ObservableObject {
|
||||||
@Published private(set) var current: Instance.Account!
|
@Published private(set) var current: Account!
|
||||||
|
|
||||||
@Published private(set) var invidious = InvidiousAPI()
|
@Published private var invidious = InvidiousAPI()
|
||||||
@Published private(set) var piped = PipedAPI()
|
@Published private var piped = PipedAPI()
|
||||||
|
|
||||||
private var cancellables = [AnyCancellable]()
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
var all: [Instance.Account] {
|
var all: [Account] {
|
||||||
Defaults[.accounts]
|
Defaults[.accounts]
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastUsed: Instance.Account? {
|
var lastUsed: Account? {
|
||||||
guard let id = Defaults[.lastAccountID] else {
|
guard let id = Defaults[.lastAccountID] else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -22,6 +22,14 @@ final class AccountsModel: ObservableObject {
|
|||||||
return AccountsModel.find(id)
|
return AccountsModel.find(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var app: VideosApp {
|
||||||
|
current?.instance.app ?? .invidious
|
||||||
|
}
|
||||||
|
|
||||||
|
var api: VideosAPI {
|
||||||
|
app == .piped ? piped : invidious
|
||||||
|
}
|
||||||
|
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
current.isNil
|
current.isNil
|
||||||
}
|
}
|
||||||
@ -40,7 +48,7 @@ final class AccountsModel: ObservableObject {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCurrent(_ account: Instance.Account! = nil) {
|
func setCurrent(_ account: Account! = nil) {
|
||||||
guard account != current else {
|
guard account != current else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -62,18 +70,18 @@ final class AccountsModel: ObservableObject {
|
|||||||
Defaults[.lastInstanceID] = account.instanceID
|
Defaults[.lastInstanceID] = account.instanceID
|
||||||
}
|
}
|
||||||
|
|
||||||
static func find(_ id: Instance.Account.ID) -> Instance.Account? {
|
static func find(_ id: Account.ID) -> Account? {
|
||||||
Defaults[.accounts].first { $0.id == id }
|
Defaults[.accounts].first { $0.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func add(instance: Instance, name: String, sid: String) -> Instance.Account {
|
static func add(instance: Instance, name: String, sid: String) -> Account {
|
||||||
let account = Instance.Account(instanceID: instance.id, name: name, url: instance.url, sid: sid)
|
let account = Account(instanceID: instance.id, name: name, url: instance.url, sid: sid)
|
||||||
Defaults[.accounts].append(account)
|
Defaults[.accounts].append(account)
|
||||||
|
|
||||||
return account
|
return account
|
||||||
}
|
}
|
||||||
|
|
||||||
static func remove(_ account: Instance.Account) {
|
static func remove(_ account: Account) {
|
||||||
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) {
|
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) {
|
||||||
Defaults[.accounts].remove(at: accountIndex)
|
Defaults[.accounts].remove(at: accountIndex)
|
||||||
}
|
}
|
||||||
|
@ -2,125 +2,6 @@ import Defaults
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
||||||
enum App: String, CaseIterable {
|
|
||||||
case invidious, piped
|
|
||||||
|
|
||||||
var name: String {
|
|
||||||
rawValue.capitalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Account: Defaults.Serializable, Hashable, Identifiable {
|
|
||||||
static var bridge = AccountsBridge()
|
|
||||||
|
|
||||||
let id: String
|
|
||||||
let instanceID: String
|
|
||||||
var name: String?
|
|
||||||
let url: String
|
|
||||||
let sid: String
|
|
||||||
let anonymous: Bool
|
|
||||||
|
|
||||||
init(id: String? = nil, instanceID: String? = nil, name: String? = nil, url: String? = nil, sid: String? = nil, anonymous: Bool = false) {
|
|
||||||
self.anonymous = anonymous
|
|
||||||
|
|
||||||
self.id = id ?? (anonymous ? "anonymous-\(instanceID!)" : UUID().uuidString)
|
|
||||||
self.instanceID = instanceID ?? UUID().uuidString
|
|
||||||
self.name = name
|
|
||||||
self.url = url ?? ""
|
|
||||||
self.sid = sid ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var instance: Instance {
|
|
||||||
Defaults[.instances].first { $0.id == instanceID }!
|
|
||||||
}
|
|
||||||
|
|
||||||
var anonymizedSID: String {
|
|
||||||
guard sid.count > 3 else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = sid.index(sid.startIndex, offsetBy: 4)
|
|
||||||
return String(sid[..<index])
|
|
||||||
}
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
(name != nil && name!.isEmpty) ? "Unnamed (\(anonymizedSID))" : name!
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(sid)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AccountsBridge: Defaults.Bridge {
|
|
||||||
typealias Value = Account
|
|
||||||
typealias Serializable = [String: String]
|
|
||||||
|
|
||||||
func serialize(_ value: Value?) -> Serializable? {
|
|
||||||
guard let value = value else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
"id": value.id,
|
|
||||||
"instanceID": value.instanceID,
|
|
||||||
"name": value.name ?? "",
|
|
||||||
"url": value.url,
|
|
||||||
"sid": value.sid
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
func deserialize(_ object: Serializable?) -> Value? {
|
|
||||||
guard
|
|
||||||
let object = object,
|
|
||||||
let id = object["id"],
|
|
||||||
let instanceID = object["instanceID"],
|
|
||||||
let url = object["url"],
|
|
||||||
let sid = object["sid"]
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = object["name"] ?? ""
|
|
||||||
|
|
||||||
return Account(id: id, instanceID: instanceID, name: name, url: url, sid: sid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var bridge = InstancesBridge()
|
|
||||||
|
|
||||||
let app: App
|
|
||||||
let id: String
|
|
||||||
let name: String
|
|
||||||
let url: String
|
|
||||||
|
|
||||||
init(app: App, id: String? = nil, name: String, url: String) {
|
|
||||||
self.app = app
|
|
||||||
self.id = id ?? UUID().uuidString
|
|
||||||
self.name = name
|
|
||||||
self.url = url
|
|
||||||
}
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
"\(app.name) - \(shortDescription)"
|
|
||||||
}
|
|
||||||
|
|
||||||
var longDescription: String {
|
|
||||||
name.isEmpty ? "\(app.name) - \(url)" : "\(app.name) - \(name) (\(url))"
|
|
||||||
}
|
|
||||||
|
|
||||||
var shortDescription: String {
|
|
||||||
name.isEmpty ? url : name
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportsAccounts: Bool {
|
|
||||||
app == .invidious
|
|
||||||
}
|
|
||||||
|
|
||||||
var anonymousAccount: Account {
|
|
||||||
Account(instanceID: id, name: "Anonymous", url: url, sid: "", anonymous: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InstancesBridge: Defaults.Bridge {
|
struct InstancesBridge: Defaults.Bridge {
|
||||||
typealias Value = Instance
|
typealias Value = Instance
|
||||||
typealias Serializable = [String: String]
|
typealias Serializable = [String: String]
|
||||||
@ -141,7 +22,7 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
func deserialize(_ object: Serializable?) -> Value? {
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
guard
|
guard
|
||||||
let object = object,
|
let object = object,
|
||||||
let app = App(rawValue: object["app"] ?? ""),
|
let app = VideosApp(rawValue: object["app"] ?? ""),
|
||||||
let id = object["id"],
|
let id = object["id"],
|
||||||
let url = object["url"]
|
let url = object["url"]
|
||||||
else {
|
else {
|
||||||
@ -154,6 +35,45 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var bridge = InstancesBridge()
|
||||||
|
|
||||||
|
let app: VideosApp
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let url: String
|
||||||
|
|
||||||
|
init(app: VideosApp, id: String? = nil, name: String, url: String) {
|
||||||
|
self.app = app
|
||||||
|
self.id = id ?? UUID().uuidString
|
||||||
|
self.name = name
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymous: VideosAPI {
|
||||||
|
switch app {
|
||||||
|
case .invidious:
|
||||||
|
return InvidiousAPI(account: anonymousAccount)
|
||||||
|
case .piped:
|
||||||
|
return PipedAPI(account: anonymousAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
"\(app.name) - \(shortDescription)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var longDescription: String {
|
||||||
|
name.isEmpty ? "\(app.name) - \(url)" : "\(app.name) - \(name) (\(url))"
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortDescription: String {
|
||||||
|
name.isEmpty ? url : name
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymousAccount: Account {
|
||||||
|
Account(instanceID: id, name: "Anonymous", url: url, anonymous: true)
|
||||||
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(url)
|
hasher.combine(url)
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,11 @@ final class InstancesModel: ObservableObject {
|
|||||||
return Defaults[.instances].first { $0.id == id }
|
return Defaults[.instances].first { $0.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func accounts(_ id: Instance.ID?) -> [Instance.Account] {
|
static func accounts(_ id: Instance.ID?) -> [Account] {
|
||||||
Defaults[.accounts].filter { $0.instanceID == id }
|
Defaults[.accounts].filter { $0.instanceID == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func add(app: Instance.App, name: String, url: String) -> Instance {
|
static func add(app: VideosApp, name: String, url: String) -> Instance {
|
||||||
let instance = Instance(app: app, id: UUID().uuidString, name: name, url: url)
|
let instance = Instance(app: app, id: UUID().uuidString, name: name, url: url)
|
||||||
Defaults[.instances].append(instance)
|
Defaults[.instances].append(instance)
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ final class InstancesModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func setLastAccount(_ account: Instance.Account?) {
|
static func setLastAccount(_ account: Account?) {
|
||||||
Defaults[.lastAccountID] = account?.id
|
Defaults[.lastAccountID] = account?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,15 @@ import Foundation
|
|||||||
import Siesta
|
import Siesta
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class InvidiousAPI: Service, ObservableObject {
|
final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||||
static let basePath = "/api/v1"
|
static let basePath = "/api/v1"
|
||||||
|
|
||||||
@Published var account: Instance.Account!
|
@Published var account: Account!
|
||||||
|
|
||||||
@Published var validInstance = true
|
@Published var validInstance = true
|
||||||
@Published var signedIn = false
|
@Published var signedIn = false
|
||||||
|
|
||||||
init(account: Instance.Account? = nil) {
|
init(account: Account? = nil) {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
guard !account.isNil else {
|
guard !account.isNil else {
|
||||||
@ -22,7 +22,7 @@ final class InvidiousAPI: Service, ObservableObject {
|
|||||||
setAccount(account!)
|
setAccount(account!)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setAccount(_ account: Instance.Account) {
|
func setAccount(_ account: Account) {
|
||||||
self.account = account
|
self.account = account
|
||||||
|
|
||||||
validInstance = false
|
validInstance = false
|
||||||
@ -42,7 +42,7 @@ final class InvidiousAPI: Service, ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
home
|
home?
|
||||||
.load()
|
.load()
|
||||||
.onSuccess { _ in
|
.onSuccess { _ in
|
||||||
self.validInstance = true
|
self.validInstance = true
|
||||||
@ -57,7 +57,7 @@ final class InvidiousAPI: Service, ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
feed
|
feed?
|
||||||
.load()
|
.load()
|
||||||
.onSuccess { _ in
|
.onSuccess { _ in
|
||||||
self.signedIn = true
|
self.signedIn = true
|
||||||
@ -149,29 +149,29 @@ final class InvidiousAPI: Service, ObservableObject {
|
|||||||
"SID=\(account.sid)"
|
"SID=\(account.sid)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var popular: Resource {
|
var popular: Resource? {
|
||||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/popular")
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/popular")
|
||||||
}
|
}
|
||||||
|
|
||||||
func trending(category: TrendingCategory, country: Country) -> Resource {
|
func trending(country: Country, category: TrendingCategory?) -> Resource {
|
||||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/trending")
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/trending")
|
||||||
.withParam("type", category.name)
|
.withParam("type", category!.name)
|
||||||
.withParam("region", country.rawValue)
|
.withParam("region", country.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
var home: Resource {
|
var home: Resource? {
|
||||||
resource(baseURL: account.url, path: "/feed/subscriptions")
|
resource(baseURL: account.url, path: "/feed/subscriptions")
|
||||||
}
|
}
|
||||||
|
|
||||||
var feed: Resource {
|
var feed: Resource? {
|
||||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/auth/feed")
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/auth/feed")
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptions: Resource {
|
var subscriptions: Resource? {
|
||||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelSubscription(_ id: String) -> Resource {
|
func channelSubscription(_ id: String) -> Resource? {
|
||||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")).child(id)
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")).child(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,20 +187,20 @@ final class InvidiousAPI: Service, ObservableObject {
|
|||||||
resource(baseURL: account.url, path: basePathAppending("videos/\(id)"))
|
resource(baseURL: account.url, path: basePathAppending("videos/\(id)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var playlists: Resource {
|
var playlists: Resource? {
|
||||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func playlist(_ id: String) -> Resource {
|
func playlist(_ id: String) -> Resource? {
|
||||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func playlistVideos(_ id: String) -> Resource {
|
func playlistVideos(_ id: String) -> Resource? {
|
||||||
playlist(id).child("videos")
|
playlist(id)?.child("videos")
|
||||||
}
|
}
|
||||||
|
|
||||||
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource {
|
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource? {
|
||||||
playlist(playlistID).child("videos").child(videoID)
|
playlist(playlistID)?.child("videos").child(videoID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func search(_ query: SearchQuery) -> Resource {
|
func search(_ query: SearchQuery) -> Resource {
|
||||||
|
@ -3,14 +3,14 @@ import Foundation
|
|||||||
import Siesta
|
import Siesta
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class PipedAPI: Service, ObservableObject {
|
final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||||
@Published var account: Instance.Account!
|
@Published var account: Account!
|
||||||
|
|
||||||
var anonymousAccount: Instance.Account {
|
var anonymousAccount: Account {
|
||||||
.init(instanceID: account.instance.id, name: "Anonymous", url: account.instance.url)
|
.init(instanceID: account.instance.id, name: "Anonymous", url: account.instance.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(account: Instance.Account? = nil) {
|
init(account: Account? = nil) {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
guard account != nil else {
|
guard account != nil else {
|
||||||
@ -20,7 +20,7 @@ final class PipedAPI: Service, ObservableObject {
|
|||||||
setAccount(account!)
|
setAccount(account!)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setAccount(_ account: Instance.Account) {
|
func setAccount(_ account: Account) {
|
||||||
self.account = account
|
self.account = account
|
||||||
|
|
||||||
configure()
|
configure()
|
||||||
@ -31,15 +31,128 @@ final class PipedAPI: Service, ObservableObject {
|
|||||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("streams/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Stream] in
|
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> Channel? in
|
||||||
self.extractStreams(content)
|
self.extractChannel(content.json)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
|
||||||
|
self.extractVideo(content.json)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("trending")) { (content: Entity<JSON>) -> [Video] in
|
||||||
|
self.extractVideos(content.json)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("search")) { (content: Entity<JSON>) -> [Video] in
|
||||||
|
self.extractVideos(content.json.dictionaryValue["items"]!)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("suggestions")) { (content: Entity<JSON>) -> [String] in
|
||||||
|
content.json.arrayValue.map(String.init)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func extractStreams(_ content: Entity<JSON>) -> [Stream] {
|
private func extractChannel(_ content: JSON) -> Channel? {
|
||||||
|
Channel(
|
||||||
|
id: content.dictionaryValue["id"]!.stringValue,
|
||||||
|
name: content.dictionaryValue["name"]!.stringValue,
|
||||||
|
subscriptionsCount: content.dictionaryValue["subscriberCount"]!.intValue,
|
||||||
|
videos: extractVideos(content.dictionaryValue["relatedStreams"]!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractVideo(_ content: JSON) -> Video? {
|
||||||
|
let details = content.dictionaryValue
|
||||||
|
let url = details["url"]?.string
|
||||||
|
|
||||||
|
if !url.isNil {
|
||||||
|
guard url!.contains("/watch") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let channelId = details["uploaderUrl"]!.stringValue.components(separatedBy: "/").last!
|
||||||
|
|
||||||
|
let thumbnails: [Thumbnail] = Thumbnail.Quality.allCases.compactMap {
|
||||||
|
if let url = buildThumbnailURL(content, quality: $0) {
|
||||||
|
return Thumbnail(url: url, quality: $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
|
||||||
|
|
||||||
|
return Video(
|
||||||
|
videoID: extractID(content),
|
||||||
|
title: details["title"]!.stringValue,
|
||||||
|
author: author,
|
||||||
|
length: details["duration"]!.doubleValue,
|
||||||
|
published: details["uploadedDate"]?.stringValue ?? details["uploadDate"]!.stringValue,
|
||||||
|
views: details["views"]!.intValue,
|
||||||
|
description: extractDescription(content),
|
||||||
|
channel: Channel(id: channelId, name: author),
|
||||||
|
thumbnails: thumbnails,
|
||||||
|
likes: details["likes"]?.int,
|
||||||
|
dislikes: details["dislikes"]?.int,
|
||||||
|
streams: extractStreams(content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractID(_ content: JSON) -> Video.ID {
|
||||||
|
content.dictionaryValue["url"]?.stringValue.components(separatedBy: "?v=").last ??
|
||||||
|
extractThumbnailURL(content)!.relativeString.components(separatedBy: "/")[4]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractThumbnailURL(_ content: JSON) -> URL? {
|
||||||
|
content.dictionaryValue["thumbnail"]?.url! ?? content.dictionaryValue["thumbnailUrl"]!.url!
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildThumbnailURL(_ content: JSON, quality: Thumbnail.Quality) -> URL? {
|
||||||
|
let thumbnailURL = extractThumbnailURL(content)
|
||||||
|
guard !thumbnailURL.isNil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return URL(string: thumbnailURL!
|
||||||
|
.absoluteString
|
||||||
|
.replacingOccurrences(of: "_webp", with: "")
|
||||||
|
.replacingOccurrences(of: ".webp", with: ".jpg")
|
||||||
|
.replacingOccurrences(of: "hqdefault", with: quality.filename)
|
||||||
|
.replacingOccurrences(of: "maxresdefault", with: quality.filename)
|
||||||
|
)!
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractDescription(_ content: JSON) -> String? {
|
||||||
|
guard var description = content.dictionaryValue["description"]?.string else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
description = description.replacingOccurrences(
|
||||||
|
of: "<br/>|<br />|<br>",
|
||||||
|
with: "\n",
|
||||||
|
options: .regularExpression,
|
||||||
|
range: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
description = description.replacingOccurrences(
|
||||||
|
of: "<[^>]+>",
|
||||||
|
with: "",
|
||||||
|
options: .regularExpression,
|
||||||
|
range: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractVideos(_ content: JSON) -> [Video] {
|
||||||
|
content.arrayValue.compactMap(extractVideo(_:))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractStreams(_ content: JSON) -> [Stream] {
|
||||||
var streams = [Stream]()
|
var streams = [Stream]()
|
||||||
|
|
||||||
if let hlsURL = content.json.dictionaryValue["hls"]?.url {
|
if let hlsURL = content.dictionaryValue["hls"]?.url {
|
||||||
streams.append(Stream(hlsURL: hlsURL))
|
streams.append(Stream(hlsURL: hlsURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,9 +183,8 @@ final class PipedAPI: Service, ObservableObject {
|
|||||||
return streams
|
return streams
|
||||||
}
|
}
|
||||||
|
|
||||||
private func compatibleAudioStreams(_ content: Entity<JSON>) -> [JSON] {
|
private func compatibleAudioStreams(_ content: JSON) -> [JSON] {
|
||||||
content
|
content
|
||||||
.json
|
|
||||||
.dictionaryValue["audioStreams"]?
|
.dictionaryValue["audioStreams"]?
|
||||||
.arrayValue
|
.arrayValue
|
||||||
.filter { $0.dictionaryValue["format"]?.stringValue == "M4A" }
|
.filter { $0.dictionaryValue["format"]?.stringValue == "M4A" }
|
||||||
@ -81,19 +193,51 @@ final class PipedAPI: Service, ObservableObject {
|
|||||||
} ?? []
|
} ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func compatibleVideoStream(_ content: Entity<JSON>) -> [JSON] {
|
private func compatibleVideoStream(_ content: JSON) -> [JSON] {
|
||||||
content
|
content
|
||||||
.json
|
|
||||||
.dictionaryValue["videoStreams"]?
|
.dictionaryValue["videoStreams"]?
|
||||||
.arrayValue
|
.arrayValue
|
||||||
.filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? []
|
.filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func channel(_ id: String) -> Resource {
|
||||||
|
resource(baseURL: account.url, path: "channel/\(id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func trending(country: Country, category _: TrendingCategory? = nil) -> Resource {
|
||||||
|
resource(baseURL: account.instance.url, path: "trending")
|
||||||
|
.withParam("region", country.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func search(_ query: SearchQuery) -> Resource {
|
||||||
|
resource(baseURL: account.instance.url, path: "search")
|
||||||
|
.withParam("q", query.query)
|
||||||
|
.withParam("filter", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchSuggestions(query: String) -> Resource {
|
||||||
|
resource(baseURL: account.instance.url, path: "suggestions")
|
||||||
|
.withParam("query", query.lowercased())
|
||||||
|
}
|
||||||
|
|
||||||
|
func video(_ id: Video.ID) -> Resource {
|
||||||
|
resource(baseURL: account.instance.url, path: "streams/\(id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
var signedIn: Bool { false }
|
||||||
|
|
||||||
|
var subscriptions: Resource? { nil }
|
||||||
|
var feed: Resource? { nil }
|
||||||
|
var home: Resource? { nil }
|
||||||
|
var popular: Resource? { nil }
|
||||||
|
var playlists: Resource? { nil }
|
||||||
|
|
||||||
|
func channelSubscription(_: String) -> Resource? { nil }
|
||||||
|
|
||||||
|
func playlistVideo(_: String, _: String) -> Resource? { nil }
|
||||||
|
func playlistVideos(_: String) -> Resource? { nil }
|
||||||
|
|
||||||
private func pathPattern(_ path: String) -> String {
|
private func pathPattern(_ path: String) -> String {
|
||||||
"**\(path)"
|
"**\(path)"
|
||||||
}
|
}
|
||||||
|
|
||||||
func streams(id: Video.ID) -> Resource {
|
|
||||||
resource(baseURL: account.instance.url, path: "streams/\(id)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -226,8 +226,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
var externalMetadata = [
|
var externalMetadata = [
|
||||||
makeMetadataItem(.commonIdentifierTitle, value: video.title),
|
makeMetadataItem(.commonIdentifierTitle, value: video.title),
|
||||||
makeMetadataItem(.quickTimeMetadataGenre, value: video.genre),
|
makeMetadataItem(.quickTimeMetadataGenre, value: video.genre ?? ""),
|
||||||
makeMetadataItem(.commonIdentifierDescription, value: video.description)
|
makeMetadataItem(.commonIdentifierDescription, value: video.description ?? "")
|
||||||
]
|
]
|
||||||
if let thumbnailData = try? Data(contentsOf: video.thumbnailURL(quality: .medium)!),
|
if let thumbnailData = try? Data(contentsOf: video.thumbnailURL(quality: .medium)!),
|
||||||
let image = UIImage(data: thumbnailData),
|
let image = UIImage(data: thumbnailData),
|
||||||
|
@ -104,22 +104,12 @@ extension PlayerModel {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoResource(_ id: Video.ID) -> Resource {
|
|
||||||
accounts.invidious.video(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadDetails(_ video: Video?, onSuccess: @escaping (Video) -> Void) {
|
private func loadDetails(_ video: Video?, onSuccess: @escaping (Video) -> Void) {
|
||||||
guard video != nil else {
|
guard video != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !video!.streams.isEmpty {
|
accounts.api.video(video!.videoID).load().onSuccess { response in
|
||||||
logger.critical("not loading video details again")
|
|
||||||
onSuccess(video!)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
videoResource(video!.videoID).load().onSuccess { response in
|
|
||||||
if let video: Video = response.typedContent() {
|
if let video: Video = response.typedContent() {
|
||||||
onSuccess(video)
|
onSuccess(video)
|
||||||
}
|
}
|
||||||
|
@ -23,57 +23,28 @@ extension PlayerModel {
|
|||||||
var instancesWithLoadedStreams = [Instance]()
|
var instancesWithLoadedStreams = [Instance]()
|
||||||
|
|
||||||
instances.all.forEach { instance in
|
instances.all.forEach { instance in
|
||||||
switch instance.app {
|
fetchStreams(instance.anonymous.video(video.videoID), instance: instance, video: video) { _ in
|
||||||
case .piped:
|
self.completeIfAllInstancesLoaded(
|
||||||
fetchPipedStreams(instance, video: video) { _ in
|
instance: instance,
|
||||||
self.completeIfAllInstancesLoaded(
|
streams: self.availableStreams,
|
||||||
instance: instance,
|
instancesWithLoadedStreams: &instancesWithLoadedStreams,
|
||||||
streams: self.availableStreams,
|
completionHandler: completionHandler
|
||||||
instancesWithLoadedStreams: &instancesWithLoadedStreams,
|
)
|
||||||
completionHandler: completionHandler
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case .invidious:
|
|
||||||
fetchInvidiousStreams(instance, video: video) { _ in
|
|
||||||
self.completeIfAllInstancesLoaded(
|
|
||||||
instance: instance,
|
|
||||||
streams: self.availableStreams,
|
|
||||||
instancesWithLoadedStreams: &instancesWithLoadedStreams,
|
|
||||||
completionHandler: completionHandler
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchInvidiousStreams(
|
private func fetchStreams(
|
||||||
_ instance: Instance,
|
_ resource: Resource,
|
||||||
|
instance: Instance,
|
||||||
video: Video,
|
video: Video,
|
||||||
onCompletion: @escaping (ResponseInfo) -> Void = { _ in }
|
onCompletion: @escaping (ResponseInfo) -> Void = { _ in }
|
||||||
) {
|
) {
|
||||||
invidious(instance)
|
resource
|
||||||
.video(video.videoID)
|
|
||||||
.load()
|
.load()
|
||||||
.onSuccess { response in
|
.onSuccess { response in
|
||||||
if let video: Video = response.typedContent() {
|
if let video: Video = response.typedContent() {
|
||||||
self.availableStreams += self.streamsWithAssetsFromInstance(instance: instance, streams: video.streams)
|
self.availableStreams += self.streamsWithInstance(instance: instance, streams: video.streams)
|
||||||
}
|
|
||||||
}
|
|
||||||
.onCompletion(onCompletion)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchPipedStreams(
|
|
||||||
_ instance: Instance,
|
|
||||||
video: Video,
|
|
||||||
onCompletion: @escaping (ResponseInfo) -> Void = { _ in }
|
|
||||||
) {
|
|
||||||
piped(instance)
|
|
||||||
.streams(id: video.videoID)
|
|
||||||
.load()
|
|
||||||
.onSuccess { response in
|
|
||||||
if let pipedStreams: [Stream] = response.typedContent() {
|
|
||||||
self.availableStreams += self.streamsWithInstance(instance: instance, streams: pipedStreams)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onCompletion(onCompletion)
|
.onCompletion(onCompletion)
|
||||||
|
@ -9,10 +9,6 @@ final class PlaylistsModel: ObservableObject {
|
|||||||
|
|
||||||
var accounts = AccountsModel()
|
var accounts = AccountsModel()
|
||||||
|
|
||||||
var api: InvidiousAPI {
|
|
||||||
accounts.invidious
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ playlists: [Playlist] = [Playlist]()) {
|
init(_ playlists: [Playlist] = [Playlist]()) {
|
||||||
self.playlists = playlists
|
self.playlists = playlists
|
||||||
}
|
}
|
||||||
@ -48,19 +44,19 @@ final class PlaylistsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addVideoToCurrentPlaylist(videoID: Video.ID, onSuccess: @escaping () -> Void = {}) {
|
func addVideoToCurrentPlaylist(videoID: Video.ID, onSuccess: @escaping () -> Void = {}) {
|
||||||
let resource = api.playlistVideos(currentPlaylist!.id)
|
let resource = accounts.api.playlistVideos(currentPlaylist!.id)
|
||||||
let body = ["videoId": videoID]
|
let body = ["videoId": videoID]
|
||||||
|
|
||||||
resource.request(.post, json: body).onSuccess { _ in
|
resource?.request(.post, json: body).onSuccess { _ in
|
||||||
self.load(force: true)
|
self.load(force: true)
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeVideoFromPlaylist(videoIndexID: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) {
|
func removeVideoFromPlaylist(videoIndexID: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) {
|
||||||
let resource = api.playlistVideo(playlistID, videoIndexID)
|
let resource = accounts.api.playlistVideo(playlistID, videoIndexID)
|
||||||
|
|
||||||
resource.request(.delete).onSuccess { _ in
|
resource?.request(.delete).onSuccess { _ in
|
||||||
self.load(force: true)
|
self.load(force: true)
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
@ -71,7 +67,7 @@ final class PlaylistsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var resource: Resource {
|
private var resource: Resource {
|
||||||
api.playlists
|
accounts.api.playlists!
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectedPlaylist: Playlist? {
|
private var selectedPlaylist: Playlist? {
|
||||||
|
@ -17,14 +17,10 @@ final class SearchModel: ObservableObject {
|
|||||||
resource?.isLoading ?? false
|
resource?.isLoading ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
var api: InvidiousAPI {
|
|
||||||
accounts.invidious
|
|
||||||
}
|
|
||||||
|
|
||||||
func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) {
|
func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) {
|
||||||
changeHandler(query)
|
changeHandler(query)
|
||||||
|
|
||||||
let newResource = api.search(query)
|
let newResource = accounts.api.search(query)
|
||||||
guard newResource != previousResource else {
|
guard newResource != previousResource else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -43,7 +39,7 @@ final class SearchModel: ObservableObject {
|
|||||||
func resetQuery(_ query: SearchQuery = SearchQuery()) {
|
func resetQuery(_ query: SearchQuery = SearchQuery()) {
|
||||||
self.query = query
|
self.query = query
|
||||||
|
|
||||||
let newResource = api.search(query)
|
let newResource = accounts.api.search(query)
|
||||||
guard newResource != previousResource else {
|
guard newResource != previousResource else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -87,7 +83,7 @@ final class SearchModel: ObservableObject {
|
|||||||
suggestionsDebounceTimer?.invalidate()
|
suggestionsDebounceTimer?.invalidate()
|
||||||
|
|
||||||
suggestionsDebounceTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
|
suggestionsDebounceTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
|
||||||
let resource = self.api.searchSuggestions(query: query)
|
let resource = self.accounts.api.searchSuggestions(query: query)
|
||||||
|
|
||||||
resource.addObserver(self.querySuggestions)
|
resource.addObserver(self.querySuggestions)
|
||||||
resource.loadIfNeeded()
|
resource.loadIfNeeded()
|
||||||
|
@ -6,12 +6,8 @@ final class SubscriptionsModel: ObservableObject {
|
|||||||
@Published var channels = [Channel]()
|
@Published var channels = [Channel]()
|
||||||
var accounts: AccountsModel
|
var accounts: AccountsModel
|
||||||
|
|
||||||
var api: InvidiousAPI {
|
var resource: Resource? {
|
||||||
accounts.invidious
|
accounts.api.subscriptions
|
||||||
}
|
|
||||||
|
|
||||||
var resource: Resource {
|
|
||||||
api.subscriptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(accounts: AccountsModel? = nil) {
|
init(accounts: AccountsModel? = nil) {
|
||||||
@ -35,7 +31,7 @@ final class SubscriptionsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) {
|
func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) {
|
||||||
let request = force ? resource.load() : resource.loadIfNeeded()
|
let request = force ? resource?.load() : resource?.loadIfNeeded()
|
||||||
|
|
||||||
request?
|
request?
|
||||||
.onSuccess { resource in
|
.onSuccess { resource in
|
||||||
@ -50,7 +46,7 @@ final class SubscriptionsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func performRequest(_ channelID: String, method: RequestMethod, onSuccess: @escaping () -> Void = {}) {
|
fileprivate func performRequest(_ channelID: String, method: RequestMethod, onSuccess: @escaping () -> Void = {}) {
|
||||||
api.channelSubscription(channelID).request(method).onCompletion { _ in
|
accounts.api.channelSubscription(channelID)?.request(method).onCompletion { _ in
|
||||||
self.load(force: true, onSuccess: onSuccess)
|
self.load(force: true, onSuccess: onSuccess)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,29 @@ import SwiftyJSON
|
|||||||
struct Thumbnail {
|
struct Thumbnail {
|
||||||
enum Quality: String, CaseIterable {
|
enum Quality: String, CaseIterable {
|
||||||
case maxres, maxresdefault, sddefault, high, medium, `default`, start, middle, end
|
case maxres, maxresdefault, sddefault, high, medium, `default`, start, middle, end
|
||||||
|
|
||||||
|
var filename: String {
|
||||||
|
switch self {
|
||||||
|
case .maxres:
|
||||||
|
return "maxres"
|
||||||
|
case .maxresdefault:
|
||||||
|
return "maxresdefault"
|
||||||
|
case .sddefault:
|
||||||
|
return "sddefault"
|
||||||
|
case .high:
|
||||||
|
return "hqdefault"
|
||||||
|
case .medium:
|
||||||
|
return "mqdefault"
|
||||||
|
case .default:
|
||||||
|
return "default"
|
||||||
|
case .start:
|
||||||
|
return "1"
|
||||||
|
case .middle:
|
||||||
|
return "2"
|
||||||
|
case .end:
|
||||||
|
return "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var url: URL
|
var url: URL
|
||||||
|
@ -12,8 +12,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
var length: TimeInterval
|
var length: TimeInterval
|
||||||
var published: String
|
var published: String
|
||||||
var views: Int
|
var views: Int
|
||||||
var description: String
|
var description: String?
|
||||||
var genre: String
|
var genre: String?
|
||||||
|
|
||||||
// index used when in the Playlist
|
// index used when in the Playlist
|
||||||
let indexID: String?
|
let indexID: String?
|
||||||
@ -38,8 +38,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
length: TimeInterval,
|
length: TimeInterval,
|
||||||
published: String,
|
published: String,
|
||||||
views: Int,
|
views: Int,
|
||||||
description: String,
|
description: String? = nil,
|
||||||
genre: String,
|
genre: String? = nil,
|
||||||
channel: Channel,
|
channel: Channel,
|
||||||
thumbnails: [Thumbnail] = [],
|
thumbnails: [Thumbnail] = [],
|
||||||
indexID: String? = nil,
|
indexID: String? = nil,
|
||||||
@ -48,7 +48,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
publishedAt: Date? = nil,
|
publishedAt: Date? = nil,
|
||||||
likes: Int? = nil,
|
likes: Int? = nil,
|
||||||
dislikes: Int? = nil,
|
dislikes: Int? = nil,
|
||||||
keywords: [String] = []
|
keywords: [String] = [],
|
||||||
|
streams: [Stream] = []
|
||||||
) {
|
) {
|
||||||
self.id = id ?? UUID().uuidString
|
self.id = id ?? UUID().uuidString
|
||||||
self.videoID = videoID
|
self.videoID = videoID
|
||||||
@ -68,6 +69,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.dislikes = dislikes
|
self.dislikes = dislikes
|
||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
|
self.streams = streams
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ json: JSON) {
|
init(_ json: JSON) {
|
||||||
@ -169,7 +171,11 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func thumbnailURL(quality: Thumbnail.Quality) -> URL? {
|
func thumbnailURL(quality: Thumbnail.Quality) -> URL? {
|
||||||
thumbnails.first { $0.quality == quality }?.url
|
if let url = thumbnails.first(where: { $0.quality == quality })?.url.absoluteString {
|
||||||
|
return URL(string: url.replacingOccurrences(of: "hqdefault", with: quality.filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
private static func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
||||||
|
24
Model/VideosAPI.swift
Normal file
24
Model/VideosAPI.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Foundation
|
||||||
|
import Siesta
|
||||||
|
|
||||||
|
protocol VideosAPI {
|
||||||
|
var signedIn: Bool { get }
|
||||||
|
|
||||||
|
func channel(_ id: String) -> Resource
|
||||||
|
func trending(country: Country, category: TrendingCategory?) -> Resource
|
||||||
|
func search(_ query: SearchQuery) -> Resource
|
||||||
|
func searchSuggestions(query: String) -> Resource
|
||||||
|
|
||||||
|
func video(_ id: Video.ID) -> Resource
|
||||||
|
|
||||||
|
var subscriptions: Resource? { get }
|
||||||
|
var feed: Resource? { get }
|
||||||
|
var home: Resource? { get }
|
||||||
|
var popular: Resource? { get }
|
||||||
|
var playlists: Resource? { get }
|
||||||
|
|
||||||
|
func channelSubscription(_ id: String) -> Resource?
|
||||||
|
|
||||||
|
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
|
||||||
|
func playlistVideos(_ id: String) -> Resource?
|
||||||
|
}
|
33
Model/VideosApp.swift
Normal file
33
Model/VideosApp.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum VideosApp: String, CaseIterable {
|
||||||
|
case invidious, piped
|
||||||
|
|
||||||
|
var name: String {
|
||||||
|
rawValue.capitalized
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsAccounts: Bool {
|
||||||
|
self == .invidious
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsPopular: Bool {
|
||||||
|
self == .invidious
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsSearchFilters: Bool {
|
||||||
|
self == .invidious
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsSubscriptions: Bool {
|
||||||
|
supportsAccounts
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsTrendingCategories: Bool {
|
||||||
|
self == .invidious
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsUserPlaylists: Bool {
|
||||||
|
self == .invidious
|
||||||
|
}
|
||||||
|
}
|
@ -79,8 +79,6 @@
|
|||||||
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||||
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||||
3743CA4A270EF79400E4D32B /* SwiftUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3743CA49270EF79400E4D32B /* SwiftUIKit */; };
|
|
||||||
3743CA4C270EF7A500E4D32B /* SwiftUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3743CA4B270EF7A500E4D32B /* SwiftUIKit */; };
|
|
||||||
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
||||||
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
||||||
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
||||||
@ -136,6 +134,12 @@
|
|||||||
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; };
|
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; };
|
||||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; };
|
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; };
|
||||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; };
|
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; };
|
||||||
|
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
|
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
|
376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
|
376A33E42720CB35000C1D6B /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33E32720CB35000C1D6B /* Account.swift */; };
|
||||||
|
376A33E52720CB35000C1D6B /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33E32720CB35000C1D6B /* Account.swift */; };
|
||||||
|
376A33E62720CB35000C1D6B /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33E32720CB35000C1D6B /* Account.swift */; };
|
||||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; };
|
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; };
|
||||||
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; };
|
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; };
|
||||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; };
|
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; };
|
||||||
@ -292,6 +296,11 @@
|
|||||||
37D4B19826717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
37D4B19826717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
||||||
37D4B19926717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
37D4B19926717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
||||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 37D4B19C2671817900C925CA /* SwiftyJSON */; };
|
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 37D4B19C2671817900C925CA /* SwiftyJSON */; };
|
||||||
|
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D526DD2720AC4400ED2F5E /* VideosAPI.swift */; };
|
||||||
|
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D526DD2720AC4400ED2F5E /* VideosAPI.swift */; };
|
||||||
|
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 */; };
|
||||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
||||||
37DD87C8271C9CFE0027CBF9 /* 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 */; };
|
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
||||||
@ -399,8 +408,11 @@
|
|||||||
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
|
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
|
||||||
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
|
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
|
||||||
37666BA927023AF000F869E5 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
|
37666BA927023AF000F869E5 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
|
||||||
|
376A33DF2720CAD6000C1D6B /* VideosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosApp.swift; sourceTree = "<group>"; };
|
||||||
|
376A33E32720CB35000C1D6B /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||||
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInRequiredView.swift; sourceTree = "<group>"; };
|
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInRequiredView.swift; sourceTree = "<group>"; };
|
||||||
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; };
|
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
|
37725DF327204139006D4D4B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
37732FEF2703A26300F04329 /* ValidationStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationStatusView.swift; sourceTree = "<group>"; };
|
37732FEF2703A26300F04329 /* ValidationStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationStatusView.swift; sourceTree = "<group>"; };
|
||||||
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
||||||
@ -465,6 +477,8 @@
|
|||||||
37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
|
37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
|
||||||
37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
|
37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
|
||||||
37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
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>"; };
|
||||||
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
|
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
|
||||||
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
|
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
|
||||||
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
|
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
|
||||||
@ -491,7 +505,6 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
3743CA4A270EF79400E4D32B /* SwiftUIKit in Frameworks */,
|
|
||||||
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */,
|
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */,
|
||||||
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */,
|
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */,
|
||||||
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */,
|
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */,
|
||||||
@ -506,7 +519,6 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */,
|
37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */,
|
||||||
3743CA4C270EF7A500E4D32B /* SwiftUIKit in Frameworks */,
|
|
||||||
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */,
|
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */,
|
||||||
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */,
|
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */,
|
||||||
37BD07C02698AC97003EBB87 /* Siesta in Frameworks */,
|
37BD07C02698AC97003EBB87 /* Siesta in Frameworks */,
|
||||||
@ -750,6 +762,7 @@
|
|||||||
37D4B0C12671614700C925CA /* Shared */ = {
|
37D4B0C12671614700C925CA /* Shared */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
37D526E12720B49200ED2F5E /* Gestures */,
|
||||||
3761AC0526F0F96100AA496F /* Modifiers */,
|
3761AC0526F0F96100AA496F /* Modifiers */,
|
||||||
371AAE2326CEB9E800901972 /* Navigation */,
|
371AAE2326CEB9E800901972 /* Navigation */,
|
||||||
371AAE2426CEBA4100901972 /* Player */,
|
371AAE2426CEBA4100901972 /* Player */,
|
||||||
@ -823,6 +836,7 @@
|
|||||||
37D4B1B72672CFE300C925CA /* Model */ = {
|
37D4B1B72672CFE300C925CA /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
376A33E32720CB35000C1D6B /* Account.swift */,
|
||||||
37001562271B1F250049C794 /* AccountsModel.swift */,
|
37001562271B1F250049C794 /* AccountsModel.swift */,
|
||||||
37484C3026FCB8F900287258 /* AccountValidator.swift */,
|
37484C3026FCB8F900287258 /* AccountValidator.swift */,
|
||||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||||
@ -851,10 +865,20 @@
|
|||||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
||||||
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
||||||
37D4B19626717E1500C925CA /* Video.swift */,
|
37D4B19626717E1500C925CA /* Video.swift */,
|
||||||
|
37D526DD2720AC4400ED2F5E /* VideosAPI.swift */,
|
||||||
|
376A33DF2720CAD6000C1D6B /* VideosApp.swift */,
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
37D526E12720B49200ED2F5E /* Gestures */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */,
|
||||||
|
);
|
||||||
|
path = Gestures;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
37FD43E1270472060073EE42 /* Settings */ = {
|
37FD43E1270472060073EE42 /* Settings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -905,7 +929,6 @@
|
|||||||
37BD07B82698AB2E003EBB87 /* Siesta */,
|
37BD07B82698AB2E003EBB87 /* Siesta */,
|
||||||
37BD07C62698B27B003EBB87 /* Introspect */,
|
37BD07C62698B27B003EBB87 /* Introspect */,
|
||||||
37BADCA42699FB72009BE4FB /* Alamofire */,
|
37BADCA42699FB72009BE4FB /* Alamofire */,
|
||||||
3743CA49270EF79400E4D32B /* SwiftUIKit */,
|
|
||||||
);
|
);
|
||||||
productName = "Pearvidious (iOS)";
|
productName = "Pearvidious (iOS)";
|
||||||
productReference = 37D4B0C92671614900C925CA /* Pearvidious.app */;
|
productReference = 37D4B0C92671614900C925CA /* Pearvidious.app */;
|
||||||
@ -931,7 +954,6 @@
|
|||||||
37BD07BD2698AC96003EBB87 /* Defaults */,
|
37BD07BD2698AC96003EBB87 /* Defaults */,
|
||||||
37BD07BF2698AC97003EBB87 /* Siesta */,
|
37BD07BF2698AC97003EBB87 /* Siesta */,
|
||||||
37BADCA6269A552E009BE4FB /* Alamofire */,
|
37BADCA6269A552E009BE4FB /* Alamofire */,
|
||||||
3743CA4B270EF7A500E4D32B /* SwiftUIKit */,
|
|
||||||
);
|
);
|
||||||
productName = "Pearvidious (macOS)";
|
productName = "Pearvidious (macOS)";
|
||||||
productReference = 37D4B0CF2671614900C925CA /* Pearvidious.app */;
|
productReference = 37D4B0CF2671614900C925CA /* Pearvidious.app */;
|
||||||
@ -1075,7 +1097,6 @@
|
|||||||
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */,
|
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */,
|
||||||
37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||||
37BADCA32699FB72009BE4FB /* XCRemoteSwiftPackageReference "Alamofire" */,
|
37BADCA32699FB72009BE4FB /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||||
3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */,
|
|
||||||
);
|
);
|
||||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -1273,6 +1294,7 @@
|
|||||||
37484C1926FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
37484C1926FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||||
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */,
|
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
37F64FE426FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
37F64FE426FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
||||||
|
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
||||||
@ -1298,6 +1320,7 @@
|
|||||||
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||||
37E2EEAB270656EC00170416 /* PlayerControlsView.swift in Sources */,
|
37E2EEAB270656EC00170416 /* PlayerControlsView.swift in Sources */,
|
||||||
|
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||||
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
@ -1319,6 +1342,7 @@
|
|||||||
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||||
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
37A9965A26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */,
|
37A9965A26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */,
|
||||||
|
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
37484C2526FC83E000287258 /* InstanceFormView.swift in Sources */,
|
37484C2526FC83E000287258 /* InstanceFormView.swift in Sources */,
|
||||||
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||||
@ -1330,6 +1354,7 @@
|
|||||||
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
||||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
|
37D526E32720B4BE00ED2F5E /* View+SwipeGesture.swift in Sources */,
|
||||||
37732FF42703D32400F04329 /* Sidebar.swift in Sources */,
|
37732FF42703D32400F04329 /* Sidebar.swift in Sources */,
|
||||||
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
||||||
37484C2926FC83FF00287258 /* AccountFormView.swift in Sources */,
|
37484C2926FC83FF00287258 /* AccountFormView.swift in Sources */,
|
||||||
@ -1393,16 +1418,19 @@
|
|||||||
37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
|
37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
|
||||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
||||||
|
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||||
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||||
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
|
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
37484C2626FC83E000287258 /* InstanceFormView.swift in Sources */,
|
37484C2626FC83E000287258 /* InstanceFormView.swift in Sources */,
|
||||||
|
37D526E42720B4BE00ED2F5E /* View+SwipeGesture.swift in Sources */,
|
||||||
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */,
|
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||||
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||||
377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
||||||
|
376A33E52720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||||
37A9965F26D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
37A9965F26D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||||
37F4AE7326828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
37F4AE7326828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
||||||
@ -1420,6 +1448,7 @@
|
|||||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
|
37725DF62720420C006D4D4B /* AppDelegate.swift in Sources */,
|
||||||
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
37732FF12703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
37732FF12703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
||||||
@ -1428,6 +1457,7 @@
|
|||||||
3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
|
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
|
||||||
|
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
|
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
|
||||||
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
@ -1496,6 +1526,7 @@
|
|||||||
37732FF22703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
37732FF22703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
||||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
||||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
|
376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||||
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */,
|
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
@ -1510,6 +1541,7 @@
|
|||||||
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||||
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
|
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
@ -1542,6 +1574,7 @@
|
|||||||
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
||||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
|
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
@ -2237,14 +2270,6 @@
|
|||||||
minimumVersion = 5.0.0;
|
minimumVersion = 5.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/danielsaidi/SwiftUIKit.git";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 2.0.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
||||||
@ -2293,16 +2318,6 @@
|
|||||||
package = 372915E22687E33E00F5A35B /* XCRemoteSwiftPackageReference "Defaults" */;
|
package = 372915E22687E33E00F5A35B /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||||
productName = Defaults;
|
productName = Defaults;
|
||||||
};
|
};
|
||||||
3743CA49270EF79400E4D32B /* SwiftUIKit */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */;
|
|
||||||
productName = SwiftUIKit;
|
|
||||||
};
|
|
||||||
3743CA4B270EF7A500E4D32B /* SwiftUIKit */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */;
|
|
||||||
productName = SwiftUIKit;
|
|
||||||
};
|
|
||||||
377FC7D4267A080300A6BBAF /* SwiftyJSON */ = {
|
377FC7D4267A080300A6BBAF /* SwiftyJSON */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
|
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
|
||||||
|
@ -46,15 +46,6 @@
|
|||||||
"version": "0.1.3"
|
"version": "0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"package": "SwiftUIKit",
|
|
||||||
"repositoryURL": "https://github.com/danielsaidi/SwiftUIKit.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "ad509355ba9bc87f8375a297c3df93acd42e6c01",
|
|
||||||
"version": "2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"package": "SwiftyJSON",
|
"package": "SwiftyJSON",
|
||||||
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
||||||
|
8
Shared/AppDelegate.swift
Normal file
8
Shared/AppDelegate.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate.swift
|
||||||
|
// Pearvidious
|
||||||
|
//
|
||||||
|
// Created by Arkadiusz Fal on 20/10/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
@ -9,13 +9,13 @@ extension Defaults.Keys {
|
|||||||
.init(app: .piped, id: pipedInstanceID, name: "Public", url: "https://pipedapi.kavin.rocks"),
|
.init(app: .piped, id: pipedInstanceID, name: "Public", url: "https://pipedapi.kavin.rocks"),
|
||||||
.init(app: .invidious, id: invidiousInstanceID, name: "Private", url: "https://invidious.home.arekf.net")
|
.init(app: .invidious, id: invidiousInstanceID, name: "Private", url: "https://invidious.home.arekf.net")
|
||||||
])
|
])
|
||||||
static let accounts = Key<[Instance.Account]>("accounts", default: [
|
static let accounts = Key<[Account]>("accounts", default: [
|
||||||
.init(instanceID: invidiousInstanceID,
|
.init(instanceID: invidiousInstanceID,
|
||||||
name: "arekf",
|
name: "arekf",
|
||||||
url: "https://invidious.home.arekf.net",
|
url: "https://invidious.home.arekf.net",
|
||||||
sid: "ki55SJbaQmm0bOxUWctGAQLYPQRgk-CXDPw5Dp4oBmI=")
|
sid: "ki55SJbaQmm0bOxUWctGAQLYPQRgk-CXDPw5Dp4oBmI=")
|
||||||
])
|
])
|
||||||
static let lastAccountID = Key<Instance.Account.ID?>("lastAccountID")
|
static let lastAccountID = Key<Account.ID?>("lastAccountID")
|
||||||
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
||||||
|
|
||||||
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
||||||
|
9
Shared/Gestures/GestureTimer.swift
Normal file
9
Shared/Gestures/GestureTimer.swift
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
//
|
||||||
|
// GestureTimer.swift
|
||||||
|
// SwiftUIKit
|
||||||
|
//
|
||||||
|
// Created by Daniel Saidi on 2021-02-17.
|
||||||
|
// Copyright © 2021 Daniel Saidi. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
22
Shared/Gestures/View+SwipeGesture.swift
Normal file
22
Shared/Gestures/View+SwipeGesture.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func onSwipeGesture(
|
||||||
|
up: @escaping () -> Void = {},
|
||||||
|
down: @escaping () -> Void = {}
|
||||||
|
) -> some View {
|
||||||
|
gesture(
|
||||||
|
DragGesture(minimumDistance: 10)
|
||||||
|
.onEnded { gesture in
|
||||||
|
let translation = gesture.translation
|
||||||
|
|
||||||
|
if abs(translation.height) > 100_000 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isUp = translation.height < 0
|
||||||
|
isUp ? up() : down()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -22,11 +22,11 @@ struct AccountsMenuView: View {
|
|||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var allAccounts: [Instance.Account] {
|
private var allAccounts: [Account] {
|
||||||
accounts + instances.map(\.anonymousAccount)
|
accounts + instances.map(\.anonymousAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func accountButtonTitle(account: Instance.Account) -> String {
|
private func accountButtonTitle(account: Account) -> String {
|
||||||
instances.count > 1 ? "\(account.description) — \(account.instance.description)" : account.description
|
instances.count > 1 ? "\(account.description) — \(account.instance.description)" : account.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,17 +32,18 @@ struct Sidebar: View {
|
|||||||
Label("Watch Now", systemImage: "play.circle")
|
Label("Watch Now", systemImage: "play.circle")
|
||||||
.accessibility(label: Text("Watch Now"))
|
.accessibility(label: Text("Watch Now"))
|
||||||
}
|
}
|
||||||
|
if accounts.app.supportsSubscriptions && accounts.signedIn {
|
||||||
if accounts.signedIn {
|
|
||||||
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: $navigation.tabSelection) {
|
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: $navigation.tabSelection) {
|
||||||
Label("Subscriptions", systemImage: "star.circle")
|
Label("Subscriptions", systemImage: "star.circle")
|
||||||
.accessibility(label: Text("Subscriptions"))
|
.accessibility(label: Text("Subscriptions"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: $navigation.tabSelection) {
|
if accounts.app.supportsPopular {
|
||||||
Label("Popular", systemImage: "chart.bar")
|
NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: $navigation.tabSelection) {
|
||||||
.accessibility(label: Text("Popular"))
|
Label("Popular", systemImage: "chart.bar")
|
||||||
|
.accessibility(label: Text("Popular"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) {
|
NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) {
|
||||||
|
@ -13,7 +13,7 @@ struct PlayerQueueView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.listStyle(.groupedWithInsets)
|
.listStyle(.inset)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
#else
|
#else
|
||||||
|
@ -16,6 +16,7 @@ struct VideoDetails: View {
|
|||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
@ -86,7 +87,8 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
guard video != nil else {
|
guard video != nil, accounts.app.supportsSubscriptions else {
|
||||||
|
subscribed = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,41 +157,42 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Spacer()
|
if accounts.app.supportsSubscriptions {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
if subscribed {
|
if subscribed {
|
||||||
Button("Unsubscribe") {
|
|
||||||
confirmationShown = true
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.tint(.gray)
|
|
||||||
#endif
|
|
||||||
.confirmationDialog("Are you you want to unsubscribe from \(video!.channel.name)?", isPresented: $confirmationShown) {
|
|
||||||
Button("Unsubscribe") {
|
Button("Unsubscribe") {
|
||||||
subscriptions.unsubscribe(video!.channel.id)
|
confirmationShown = true
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.tint(.gray)
|
||||||
|
#endif
|
||||||
|
.confirmationDialog("Are you you want to unsubscribe from \(video!.channel.name)?", isPresented: $confirmationShown) {
|
||||||
|
Button("Unsubscribe") {
|
||||||
|
subscriptions.unsubscribe(video!.channel.id)
|
||||||
|
|
||||||
|
withAnimation {
|
||||||
|
subscribed.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button("Subscribe") {
|
||||||
|
subscriptions.subscribe(video!.channel.id)
|
||||||
|
|
||||||
withAnimation {
|
withAnimation {
|
||||||
subscribed.toggle()
|
subscribed.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.tint(.blue)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Button("Subscribe") {
|
|
||||||
subscriptions.subscribe(video!.channel.id)
|
|
||||||
|
|
||||||
withAnimation {
|
|
||||||
subscribed.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
}
|
}
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.buttonBorderShape(.roundedRectangle)
|
||||||
}
|
}
|
||||||
.font(.system(size: 13))
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.buttonBorderShape(.roundedRectangle)
|
|
||||||
}
|
}
|
||||||
Divider()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,7 +267,10 @@ struct VideoDetails: View {
|
|||||||
Group {
|
Group {
|
||||||
if let video = player.currentItem?.video {
|
if let video = player.currentItem?.video {
|
||||||
Group {
|
Group {
|
||||||
publishedDateSection
|
HStack {
|
||||||
|
publishedDateSection
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
@ -274,8 +280,13 @@ struct VideoDetails: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(video.description)
|
if let description = video.description {
|
||||||
.font(.caption)
|
Text(description)
|
||||||
|
.font(.caption)
|
||||||
|
} else {
|
||||||
|
Text("No description")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
|
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -2,9 +2,6 @@ import AVKit
|
|||||||
import Defaults
|
import Defaults
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
#if !os(tvOS)
|
|
||||||
import SwiftUIKit
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct VideoPlayerView: View {
|
struct VideoPlayerView: View {
|
||||||
static let defaultAspectRatio: Double = 1.77777778
|
static let defaultAspectRatio: Double = 1.77777778
|
||||||
|
@ -170,7 +170,7 @@ struct PlaylistFormView: View {
|
|||||||
|
|
||||||
let body = ["title": name, "privacy": visibility.rawValue]
|
let body = ["title": name, "privacy": visibility.rawValue]
|
||||||
|
|
||||||
resource.request(editing ? .patch : .post, json: body).onSuccess { response in
|
resource?.request(editing ? .patch : .post, json: body).onSuccess { response in
|
||||||
if let modifiedPlaylist: Playlist = response.typedContent() {
|
if let modifiedPlaylist: Playlist = response.typedContent() {
|
||||||
playlist = modifiedPlaylist
|
playlist = modifiedPlaylist
|
||||||
}
|
}
|
||||||
@ -181,7 +181,7 @@ struct PlaylistFormView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource? {
|
||||||
editing ? api.playlist(playlist.id) : api.playlists
|
editing ? api.playlist(playlist.id) : api.playlists
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +227,7 @@ struct PlaylistFormView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deletePlaylistAndDismiss() {
|
func deletePlaylistAndDismiss() {
|
||||||
api.playlist(playlist.id).request(.delete).onSuccess { _ in
|
api.playlist(playlist.id)?.request(.delete).onSuccess { _ in
|
||||||
playlist = nil
|
playlist = nil
|
||||||
playlists.load(force: true)
|
playlists.load(force: true)
|
||||||
dismiss()
|
dismiss()
|
||||||
|
@ -3,7 +3,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct AccountFormView: View {
|
struct AccountFormView: View {
|
||||||
let instance: Instance
|
let instance: Instance
|
||||||
var selectedAccount: Binding<Instance.Account?>?
|
var selectedAccount: Binding<Account?>?
|
||||||
|
|
||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var sid = ""
|
@State private var sid = ""
|
||||||
@ -134,7 +134,7 @@ struct AccountFormView: View {
|
|||||||
AccountValidator(
|
AccountValidator(
|
||||||
app: .constant(instance.app),
|
app: .constant(instance.app),
|
||||||
url: instance.url,
|
url: instance.url,
|
||||||
account: Instance.Account(instanceID: instance.id, url: instance.url, sid: sid),
|
account: Account(instanceID: instance.id, url: instance.url, sid: sid),
|
||||||
id: $sid,
|
id: $sid,
|
||||||
isValid: $isValid,
|
isValid: $isValid,
|
||||||
isValidated: $isValidated,
|
isValidated: $isValidated,
|
||||||
|
@ -14,8 +14,8 @@ struct AccountsSettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
VStack {
|
||||||
if instance.supportsAccounts {
|
if instance.app.supportsAccounts {
|
||||||
accounts
|
accounts
|
||||||
} else {
|
} else {
|
||||||
Text("Accounts are not supported for the application of this instance")
|
Text("Accounts are not supported for the application of this instance")
|
||||||
@ -68,7 +68,7 @@ struct AccountsSettingsView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeAccount(_ account: Instance.Account) {
|
private func removeAccount(_ account: Account) {
|
||||||
AccountsModel.remove(account)
|
AccountsModel.remove(account)
|
||||||
accountsChanged.toggle()
|
accountsChanged.toggle()
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ struct InstanceFormView: View {
|
|||||||
|
|
||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var url = ""
|
@State private var url = ""
|
||||||
@State private var app = Instance.App.invidious
|
@State private var app = VideosApp.invidious
|
||||||
|
|
||||||
@State private var isValid = false
|
@State private var isValid = false
|
||||||
@State private var isValidated = false
|
@State private var isValidated = false
|
||||||
@ -75,7 +75,7 @@ struct InstanceFormView: View {
|
|||||||
private var formFields: some View {
|
private var formFields: some View {
|
||||||
Group {
|
Group {
|
||||||
Picker("Application", selection: $app) {
|
Picker("Application", selection: $app) {
|
||||||
ForEach(Instance.App.allCases, id: \.self) { app in
|
ForEach(VideosApp.allCases, id: \.self) { app in
|
||||||
Text(app.rawValue.capitalized).tag(app)
|
Text(app.rawValue.capitalized).tag(app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ struct InstancesSettingsView: View {
|
|||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
|
||||||
@State private var selectedInstanceID: Instance.ID?
|
@State private var selectedInstanceID: Instance.ID?
|
||||||
@State private var selectedAccount: Instance.Account?
|
@State private var selectedAccount: Account?
|
||||||
|
|
||||||
@State private var presentingInstanceForm = false
|
@State private var presentingInstanceForm = false
|
||||||
@State private var savedFormInstanceID: Instance.ID?
|
@State private var savedFormInstanceID: Instance.ID?
|
||||||
|
@ -18,10 +18,12 @@ struct TrendingView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource {
|
||||||
let resource = accounts.invidious.trending(category: category, country: country)
|
let newResource: Resource
|
||||||
resource.addObserver(store)
|
|
||||||
|
|
||||||
return resource
|
newResource = accounts.api.trending(country: country, category: category)
|
||||||
|
newResource.addObserver(store)
|
||||||
|
|
||||||
|
return newResource
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -56,20 +58,26 @@ struct TrendingView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
ToolbarItemGroup {
|
ToolbarItemGroup {
|
||||||
categoryButton
|
if accounts.app.supportsTrendingCategories {
|
||||||
|
categoryButton
|
||||||
|
}
|
||||||
countryButton
|
countryButton
|
||||||
}
|
}
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
ToolbarItemGroup(placement: .bottomBar) {
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
Group {
|
Group {
|
||||||
HStack {
|
if accounts.app.supportsTrendingCategories {
|
||||||
Text("Category")
|
HStack {
|
||||||
.foregroundColor(.secondary)
|
Text("Category")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
categoryButton
|
categoryButton
|
||||||
// only way to disable Menu animation is to
|
// only way to disable Menu animation is to
|
||||||
// force redraw of the view when it changes
|
// force redraw of the view when it changes
|
||||||
.id(UUID())
|
.id(UUID())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
@ -97,11 +105,13 @@ struct TrendingView: View {
|
|||||||
|
|
||||||
var toolbar: some View {
|
var toolbar: some View {
|
||||||
HStack {
|
HStack {
|
||||||
HStack {
|
if accounts.app.supportsTrendingCategories {
|
||||||
Text("Category")
|
HStack {
|
||||||
.foregroundColor(.secondary)
|
Text("Category")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
categoryButton
|
categoryButton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
@ -7,6 +7,7 @@ struct VideoBanner: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: 12) {
|
HStack(alignment: .center, spacing: 12) {
|
||||||
smallThumbnail
|
smallThumbnail
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(video.title)
|
Text(video.title)
|
||||||
.truncationMode(.middle)
|
.truncationMode(.middle)
|
||||||
|
@ -99,7 +99,7 @@ struct ChannelVideosView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource {
|
||||||
let resource = accounts.invidious.channel(channel.id)
|
let resource = accounts.api.channel(channel.id)
|
||||||
resource.addObserver(store)
|
resource.addObserver(store)
|
||||||
|
|
||||||
return resource
|
return resource
|
||||||
@ -107,14 +107,16 @@ struct ChannelVideosView: View {
|
|||||||
|
|
||||||
var subscriptionToggleButton: some View {
|
var subscriptionToggleButton: some View {
|
||||||
Group {
|
Group {
|
||||||
if subscriptions.isSubscribing(channel.id) {
|
if accounts.app.supportsSubscriptions && accounts.signedIn {
|
||||||
Button("Unsubscribe") {
|
if subscriptions.isSubscribing(channel.id) {
|
||||||
navigation.presentUnsubscribeAlert(channel)
|
Button("Unsubscribe") {
|
||||||
}
|
navigation.presentUnsubscribeAlert(channel)
|
||||||
} else {
|
}
|
||||||
Button("Subscribe") {
|
} else {
|
||||||
subscriptions.subscribe(channel.id) {
|
Button("Subscribe") {
|
||||||
navigation.sidebarSectionChanged.toggle()
|
subscriptions.subscribe(channel.id) {
|
||||||
|
navigation.sidebarSectionChanged.toggle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,16 @@ struct PopularView: View {
|
|||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource? {
|
||||||
accounts.invidious.popular
|
accounts.api.popular
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
PlayerControlsView {
|
PlayerControlsView {
|
||||||
VideosCellsVertical(videos: store.collection)
|
VideosCellsVertical(videos: store.collection)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
resource.addObserver(store)
|
resource?.addObserver(store)
|
||||||
resource.loadIfNeeded()
|
resource?.loadIfNeeded()
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.navigationTitle("Popular")
|
.navigationTitle("Popular")
|
||||||
|
@ -19,6 +19,7 @@ struct SearchView: View {
|
|||||||
|
|
||||||
@Environment(\.navigationStyle) private var navigationStyle
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SearchModel> private var state
|
@EnvironmentObject<SearchModel> private var state
|
||||||
|
|
||||||
@ -37,7 +38,9 @@ struct SearchView: View {
|
|||||||
} else {
|
} else {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
filtersHorizontalStack
|
if accounts.app.supportsSearchFilters {
|
||||||
|
filtersHorizontalStack
|
||||||
|
}
|
||||||
|
|
||||||
VideosCellsHorizontal(videos: state.store.collection)
|
VideosCellsHorizontal(videos: state.store.collection)
|
||||||
}
|
}
|
||||||
@ -61,27 +64,28 @@ struct SearchView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
ToolbarItemGroup(placement: toolbarPlacement) {
|
ToolbarItemGroup(placement: toolbarPlacement) {
|
||||||
Section {
|
if accounts.app.supportsSearchFilters {
|
||||||
#if os(macOS)
|
Section {
|
||||||
HStack {
|
#if os(macOS)
|
||||||
Text("Sort:")
|
HStack {
|
||||||
.foregroundColor(.secondary)
|
Text("Sort:")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
searchSortOrderPicker
|
searchSortOrderPicker
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
Menu("Sort: \(searchSortOrder.name)") {
|
Menu("Sort: \(searchSortOrder.name)") {
|
||||||
searchSortOrderPicker
|
searchSortOrderPicker
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
filtersMenu
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
filtersMenu
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
@ -6,12 +6,8 @@ struct SubscriptionsView: View {
|
|||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
|
||||||
var api: InvidiousAPI {
|
var feed: Resource? {
|
||||||
accounts.invidious
|
accounts.api.feed
|
||||||
}
|
|
||||||
|
|
||||||
var feed: Resource {
|
|
||||||
api.feed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -32,9 +28,9 @@ 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 ? api.home.load() : api.home.loadIfNeeded() {
|
if let request = force ? accounts.api.home?.load() : accounts.api.home?.loadIfNeeded() {
|
||||||
request.onSuccess { _ in
|
request.onSuccess { _ in
|
||||||
loadFeed(force: force)
|
loadFeed(force: force)
|
||||||
}
|
}
|
||||||
@ -44,6 +40,6 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func loadFeed(force: Bool = false) {
|
fileprivate func loadFeed(force: Bool = false) {
|
||||||
_ = force ? feed.load() : feed.loadIfNeeded()
|
_ = force ? feed?.load() : feed?.loadIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
@Environment(\.inNavigationView) private var inNavigationView
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@ -25,18 +26,22 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
Section {
|
Section {
|
||||||
openChannelButton
|
openChannelButton
|
||||||
subscriptionButton
|
if accounts.app.supportsSubscriptions {
|
||||||
|
subscriptionButton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
if accounts.app.supportsUserPlaylists {
|
||||||
if navigation.tabSelection != .playlists {
|
Section {
|
||||||
addToPlaylistButton
|
if navigation.tabSelection != .playlists {
|
||||||
} else if let playlist = playlists.currentPlaylist {
|
addToPlaylistButton
|
||||||
removeFromPlaylistButton(playlistID: playlist.id)
|
} else if let playlist = playlists.currentPlaylist {
|
||||||
}
|
removeFromPlaylistButton(playlistID: playlist.id)
|
||||||
|
}
|
||||||
|
|
||||||
if case let .playlist(id) = navigation.tabSelection {
|
if case let .playlist(id) = navigation.tabSelection {
|
||||||
removeFromPlaylistButton(playlistID: id)
|
removeFromPlaylistButton(playlistID: id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,14 +3,14 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct WatchNowSection: View {
|
struct WatchNowSection: View {
|
||||||
let resource: Resource
|
let resource: Resource?
|
||||||
let label: String
|
let label: String
|
||||||
|
|
||||||
@StateObject private var store = Store<[Video]>()
|
@StateObject private var store = Store<[Video]>()
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
|
||||||
init(resource: Resource, label: String) {
|
init(resource: Resource?, label: String) {
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
self.label = label
|
self.label = label
|
||||||
}
|
}
|
||||||
@ -18,11 +18,11 @@ struct WatchNowSection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
WatchNowSectionBody(label: label, videos: store.collection)
|
WatchNowSectionBody(label: label, videos: store.collection)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
resource.addObserver(store)
|
resource?.addObserver(store)
|
||||||
resource.loadIfNeeded()
|
resource?.loadIfNeeded()
|
||||||
}
|
}
|
||||||
.onChange(of: accounts.current) { _ in
|
.onChange(of: accounts.current) { _ in
|
||||||
resource.load()
|
resource?.load()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,22 +5,22 @@ import SwiftUI
|
|||||||
struct WatchNowView: View {
|
struct WatchNowView: View {
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
|
||||||
var api: InvidiousAPI! {
|
|
||||||
accounts.invidious
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
PlayerControlsView {
|
PlayerControlsView {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
if !accounts.current.isNil {
|
if !accounts.current.isNil {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
if api.signedIn {
|
if accounts.api.signedIn {
|
||||||
WatchNowSection(resource: api.feed, label: "Subscriptions")
|
WatchNowSection(resource: accounts.api.feed, label: "Subscriptions")
|
||||||
|
}
|
||||||
|
if accounts.app.supportsPopular {
|
||||||
|
WatchNowSection(resource: accounts.api.popular, label: "Popular")
|
||||||
|
}
|
||||||
|
WatchNowSection(resource: accounts.api.trending(country: .pl, category: .default), label: "Trending")
|
||||||
|
if accounts.app.supportsTrendingCategories {
|
||||||
|
WatchNowSection(resource: accounts.api.trending(country: .pl, category: .movies), label: "Movies")
|
||||||
|
WatchNowSection(resource: accounts.api.trending(country: .pl, category: .music), label: "Music")
|
||||||
}
|
}
|
||||||
WatchNowSection(resource: api.popular, label: "Popular")
|
|
||||||
WatchNowSection(resource: api.trending(category: .default, country: .pl), label: "Trending")
|
|
||||||
WatchNowSection(resource: api.trending(category: .movies, country: .pl), label: "Movies")
|
|
||||||
WatchNowSection(resource: api.trending(category: .music, country: .pl), label: "Music")
|
|
||||||
|
|
||||||
// TODO: adding sections to view
|
// TODO: adding sections to view
|
||||||
// ===================
|
// ===================
|
||||||
|
@ -3,7 +3,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct InstancesSettingsView: View {
|
struct InstancesSettingsView: View {
|
||||||
@State private var selectedInstanceID: Instance.ID?
|
@State private var selectedInstanceID: Instance.ID?
|
||||||
@State private var selectedAccount: Instance.Account?
|
@State private var selectedAccount: Account?
|
||||||
|
|
||||||
@State private var presentingAccountForm = false
|
@State private var presentingAccountForm = false
|
||||||
@State private var presentingInstanceForm = false
|
@State private var presentingInstanceForm = false
|
||||||
@ -34,7 +34,7 @@ struct InstancesSettingsView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !selectedInstance.isNil, selectedInstance.supportsAccounts {
|
if !selectedInstance.isNil, selectedInstance.app.supportsAccounts {
|
||||||
Text("Accounts")
|
Text("Accounts")
|
||||||
List(selection: $selectedAccount) {
|
List(selection: $selectedAccount) {
|
||||||
if selectedInstanceAccounts.isEmpty {
|
if selectedInstanceAccounts.isEmpty {
|
||||||
@ -67,7 +67,7 @@ struct InstancesSettingsView: View {
|
|||||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
if selectedInstance != nil, !selectedInstance.supportsAccounts {
|
if selectedInstance != nil, !selectedInstance.app.supportsAccounts {
|
||||||
Text("Accounts are not supported for the application of this instance")
|
Text("Accounts are not supported for the application of this instance")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
@ -81,7 +81,7 @@ struct InstancesSettingsView: View {
|
|||||||
selectedAccount = nil
|
selectedAccount = nil
|
||||||
presentingAccountForm = true
|
presentingAccountForm = true
|
||||||
}
|
}
|
||||||
.disabled(!selectedInstance.supportsAccounts)
|
.disabled(!selectedInstance.app.supportsAccounts)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ struct InstancesSettingsView: View {
|
|||||||
InstancesModel.find(selectedInstanceID)
|
InstancesModel.find(selectedInstanceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectedInstanceAccounts: [Instance.Account] {
|
private var selectedInstanceAccounts: [Account] {
|
||||||
guard selectedInstance != nil else {
|
guard selectedInstance != nil else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -32,15 +32,15 @@ struct AccountSelectionView: View {
|
|||||||
.id(UUID())
|
.id(UUID())
|
||||||
}
|
}
|
||||||
|
|
||||||
var allAccounts: [Instance.Account] {
|
var allAccounts: [Account] {
|
||||||
accounts + instances.map(\.anonymousAccount)
|
accounts + instances.map(\.anonymousAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var nextAccount: Instance.Account? {
|
private var nextAccount: Account? {
|
||||||
allAccounts.next(after: accountsModel.current)
|
allAccounts.next(after: accountsModel.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountButtonTitle(account: Instance.Account! = nil) -> String {
|
func accountButtonTitle(account: Account! = nil) -> String {
|
||||||
guard account != nil else {
|
guard account != nil else {
|
||||||
return "Not selected"
|
return "Not selected"
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TVNavigationView: View {
|
struct TVNavigationView: View {
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@ -13,21 +14,27 @@ struct TVNavigationView: View {
|
|||||||
.tabItem { Text("Watch Now") }
|
.tabItem { Text("Watch Now") }
|
||||||
.tag(TabSelection.watchNow)
|
.tag(TabSelection.watchNow)
|
||||||
|
|
||||||
SubscriptionsView()
|
if accounts.app.supportsSubscriptions {
|
||||||
.tabItem { Text("Subscriptions") }
|
SubscriptionsView()
|
||||||
.tag(TabSelection.subscriptions)
|
.tabItem { Text("Subscriptions") }
|
||||||
|
.tag(TabSelection.subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
PopularView()
|
if accounts.app.supportsPopular {
|
||||||
.tabItem { Text("Popular") }
|
PopularView()
|
||||||
.tag(TabSelection.popular)
|
.tabItem { Text("Popular") }
|
||||||
|
.tag(TabSelection.popular)
|
||||||
|
}
|
||||||
|
|
||||||
TrendingView()
|
TrendingView()
|
||||||
.tabItem { Text("Trending") }
|
.tabItem { Text("Trending") }
|
||||||
.tag(TabSelection.trending)
|
.tag(TabSelection.trending)
|
||||||
|
|
||||||
PlaylistsView()
|
if accounts.app.supportsUserPlaylists {
|
||||||
.tabItem { Text("Playlists") }
|
PlaylistsView()
|
||||||
.tag(TabSelection.playlists)
|
.tabItem { Text("Playlists") }
|
||||||
|
.tag(TabSelection.playlists)
|
||||||
|
}
|
||||||
|
|
||||||
NowPlayingView()
|
NowPlayingView()
|
||||||
.tabItem { Text("Now Playing") }
|
.tabItem { Text("Now Playing") }
|
||||||
|
Loading…
Reference in New Issue
Block a user