mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Settings for iOS/macOS
This commit is contained in:
parent
433725c5e8
commit
a7da3b9468
10
Fixtures/Instance+Fixtures.swift
Normal file
10
Fixtures/Instance+Fixtures.swift
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Instance {
|
||||||
|
static var fixture: Instance {
|
||||||
|
Instance(name: "Home", url: "https://invidious.home.net", accounts: [
|
||||||
|
.init(id: UUID(), name: "Evelyn", url: "https://invidious.home.net", sid: "abc"),
|
||||||
|
.init(id: UUID(), name: "Jake", url: "https://invidious.home.net", sid: "xyz")
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
146
Model/Instance.swift
Normal file
146
Model/Instance.swift
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
||||||
|
struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||||
|
static var bridge = AccountsBridge()
|
||||||
|
|
||||||
|
let id: UUID?
|
||||||
|
var name: String?
|
||||||
|
let url: String
|
||||||
|
let sid: String
|
||||||
|
|
||||||
|
init(id: UUID? = nil, name: String? = nil, url: String, sid: String) {
|
||||||
|
self.id = id ?? UUID()
|
||||||
|
self.name = name
|
||||||
|
self.url = url
|
||||||
|
self.sid = sid
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.uuidString ?? "",
|
||||||
|
"name": value.name ?? "",
|
||||||
|
"url": value.url,
|
||||||
|
"sid": value.sid
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
|
guard
|
||||||
|
let object = object,
|
||||||
|
let url = object["url"],
|
||||||
|
let sid = object["sid"]
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = object["name"] ?? ""
|
||||||
|
|
||||||
|
return Account(name: name, url: url, sid: sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var bridge = InstancesBridge()
|
||||||
|
|
||||||
|
let id: UUID?
|
||||||
|
let name: String
|
||||||
|
let url: String
|
||||||
|
var accounts = [Account]()
|
||||||
|
|
||||||
|
init(id: UUID? = nil, name: String, url: String, accounts: [Account] = []) {
|
||||||
|
self.id = id ?? UUID()
|
||||||
|
self.name = name
|
||||||
|
self.url = url
|
||||||
|
self.accounts = accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
name.isEmpty ? url : "\(name) (\(url))"
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortDescription: String {
|
||||||
|
name.isEmpty ? url : name
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymousAccount: Account {
|
||||||
|
Account(name: "Anonymous", url: url, sid: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InstancesBridge: Defaults.Bridge {
|
||||||
|
typealias Value = Instance
|
||||||
|
typealias Serializable = [String: String]
|
||||||
|
|
||||||
|
func serialize(_ value: Value?) -> Serializable? {
|
||||||
|
guard let value = value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"id": value.id?.uuidString ?? "",
|
||||||
|
"name": value.name,
|
||||||
|
"url": value.url,
|
||||||
|
"accounts": value.accounts.map { "\($0.id!):\($0.name ?? ""):\($0.sid)" }.joined(separator: ";")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
|
guard
|
||||||
|
let object = object,
|
||||||
|
let id = object["id"],
|
||||||
|
let url = object["url"]
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = object["name"] ?? ""
|
||||||
|
let accounts = object["accounts"] ?? ""
|
||||||
|
let uuid = UUID(uuidString: id)
|
||||||
|
|
||||||
|
var instance = Instance(id: uuid, name: name, url: url)
|
||||||
|
|
||||||
|
accounts.split(separator: ";").forEach { sid in
|
||||||
|
let components = sid.components(separatedBy: ":")
|
||||||
|
|
||||||
|
let id = components[0]
|
||||||
|
let name = components[1]
|
||||||
|
let sid = components[2]
|
||||||
|
|
||||||
|
let uuid = UUID(uuidString: id)
|
||||||
|
instance.accounts.append(Account(id: uuid, name: name, url: instance.url, sid: sid))
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(url)
|
||||||
|
}
|
||||||
|
}
|
108
Model/InstanceAccountValidator.swift
Normal file
108
Model/InstanceAccountValidator.swift
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import Foundation
|
||||||
|
import Siesta
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class InstanceAccountValidator: Service {
|
||||||
|
let url: String
|
||||||
|
let account: Instance.Account?
|
||||||
|
|
||||||
|
var formObjectID: Binding<String>
|
||||||
|
var valid: Binding<Bool>
|
||||||
|
var validated: Binding<Bool>
|
||||||
|
var error: Binding<String?>?
|
||||||
|
|
||||||
|
init(
|
||||||
|
url: String,
|
||||||
|
account: Instance.Account? = nil,
|
||||||
|
formObjectID: Binding<String>,
|
||||||
|
valid: Binding<Bool>,
|
||||||
|
validated: Binding<Bool>,
|
||||||
|
error: Binding<String?>? = nil
|
||||||
|
) {
|
||||||
|
self.url = url
|
||||||
|
self.account = account
|
||||||
|
self.formObjectID = formObjectID
|
||||||
|
self.valid = valid
|
||||||
|
self.validated = validated
|
||||||
|
self.error = error
|
||||||
|
|
||||||
|
super.init(baseURL: url)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
configure("/api/v1/auth/feed", requestMethods: [.get]) {
|
||||||
|
guard self.account != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.headers["Cookie"] = self.cookieHeader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateInstance() {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
stats
|
||||||
|
.load()
|
||||||
|
.onSuccess { _ in
|
||||||
|
guard self.url == self.formObjectID.wrappedValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.valid.wrappedValue = true
|
||||||
|
self.error?.wrappedValue = nil
|
||||||
|
self.validated.wrappedValue = true
|
||||||
|
}
|
||||||
|
.onFailure { error in
|
||||||
|
guard self.url == self.formObjectID.wrappedValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.valid.wrappedValue = false
|
||||||
|
self.error?.wrappedValue = error.userMessage
|
||||||
|
self.validated.wrappedValue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAccount() {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
feed
|
||||||
|
.load()
|
||||||
|
.onSuccess { _ in
|
||||||
|
guard self.account!.sid == self.formObjectID.wrappedValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.valid.wrappedValue = true
|
||||||
|
self.validated.wrappedValue = true
|
||||||
|
}
|
||||||
|
.onFailure { _ in
|
||||||
|
guard self.account!.sid == self.formObjectID.wrappedValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.valid.wrappedValue = false
|
||||||
|
self.validated.wrappedValue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
valid.wrappedValue = false
|
||||||
|
validated.wrappedValue = false
|
||||||
|
error?.wrappedValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookieHeader: String {
|
||||||
|
"SID=\(account!.sid)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats: Resource {
|
||||||
|
resource("/api/v1/stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
var feed: Resource {
|
||||||
|
resource("/api/v1/auth/feed")
|
||||||
|
}
|
||||||
|
}
|
51
Model/InstancesModel.swift
Normal file
51
Model/InstancesModel.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class InstancesModel: ObservableObject {
|
||||||
|
var defaultAccount: Instance.Account! {
|
||||||
|
Defaults[.instances].first?.accounts.first
|
||||||
|
}
|
||||||
|
|
||||||
|
func find(_ id: Instance.ID?) -> Instance? {
|
||||||
|
guard id != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Defaults[.instances].first { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func accounts(_ id: Instance.ID?) -> [Instance.Account] {
|
||||||
|
find(id)?.accounts ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(name: String, url: String) -> Instance {
|
||||||
|
let instance = Instance(name: name, url: url)
|
||||||
|
Defaults[.instances].append(instance)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ instance: Instance) {
|
||||||
|
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
||||||
|
Defaults[.instances].remove(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAccount(instance: Instance, name: String, sid: String) -> Instance.Account {
|
||||||
|
let account = Instance.Account(name: name, url: instance.url, sid: sid)
|
||||||
|
|
||||||
|
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
||||||
|
Defaults[.instances][index].accounts.append(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAccount(instance: Instance, account: Instance.Account) {
|
||||||
|
if let instanceIndex = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
||||||
|
if let accountIndex = Defaults[.instances][instanceIndex].accounts.firstIndex(where: { $0.id == account.id }) {
|
||||||
|
Defaults[.instances][instanceIndex].accounts.remove(at: accountIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,55 +3,108 @@ import Foundation
|
|||||||
import Siesta
|
import Siesta
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class InvidiousAPI: Service {
|
final class InvidiousAPI: Service, ObservableObject {
|
||||||
static let shared = InvidiousAPI()
|
static let basePath = "/api/v1"
|
||||||
|
|
||||||
static let instance = "https://invidious.home.arekf.net"
|
@Published var account: Instance.Account!
|
||||||
|
|
||||||
static func proxyURLForAsset(_ url: String) -> URL? {
|
@Published var validInstance = true
|
||||||
guard let instanceURLComponents = URLComponents(string: InvidiousAPI.instance),
|
@Published var signedIn = true
|
||||||
var urlComponents = URLComponents(string: url) else { return nil }
|
|
||||||
|
|
||||||
urlComponents.scheme = instanceURLComponents.scheme
|
|
||||||
urlComponents.host = instanceURLComponents.host
|
|
||||||
|
|
||||||
return urlComponents.url
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
// TODO: remove
|
||||||
|
setAccount(.init(id: UUID(), name: "", url: "https://invidious.home.arekf.net", sid: "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAccount(_ account: Instance.Account) {
|
||||||
|
self.account = account
|
||||||
|
|
||||||
|
validInstance = false
|
||||||
|
signedIn = false
|
||||||
|
|
||||||
|
configure()
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate() {
|
||||||
|
validateInstance()
|
||||||
|
validateSID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateInstance() {
|
||||||
|
guard !validInstance else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
home
|
||||||
|
.load()
|
||||||
|
.onSuccess { _ in
|
||||||
|
self.validInstance = true
|
||||||
|
}
|
||||||
|
.onFailure { _ in
|
||||||
|
self.validInstance = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSID() {
|
||||||
|
guard !signedIn else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed
|
||||||
|
.load()
|
||||||
|
.onSuccess { _ in
|
||||||
|
self.signedIn = true
|
||||||
|
}
|
||||||
|
.onFailure { _ in
|
||||||
|
self.signedIn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func proxyURLForAsset(_ url: String) -> URL? {
|
||||||
|
URL(string: url)
|
||||||
|
// TODO: Switching instances, move up to player
|
||||||
|
// guard let instanceURLComponents = URLComponents(string: InvidiousAPI.instance),
|
||||||
|
// var urlComponents = URLComponents(string: url) else { return nil }
|
||||||
|
//
|
||||||
|
// urlComponents.scheme = instanceURLComponents.scheme
|
||||||
|
// urlComponents.host = instanceURLComponents.host
|
||||||
|
//
|
||||||
|
// return urlComponents.url
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure() {
|
||||||
SiestaLog.Category.enabled = .common
|
SiestaLog.Category.enabled = .common
|
||||||
|
|
||||||
let SwiftyJSONTransformer =
|
let SwiftyJSONTransformer =
|
||||||
ResponseContentTransformer(transformErrors: true) { JSON($0.content as AnyObject) }
|
ResponseContentTransformer(transformErrors: true) { JSON($0.content as AnyObject) }
|
||||||
|
|
||||||
super.init(baseURL: "\(InvidiousAPI.instance)/api/v1")
|
|
||||||
|
|
||||||
configure {
|
configure {
|
||||||
|
$0.headers["Cookie"] = self.cookieHeader
|
||||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||||
}
|
}
|
||||||
|
|
||||||
configure(requestMethods: [.get]) {
|
|
||||||
$0.headers["Cookie"] = self.authHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
configure("**", requestMethods: [.post]) {
|
configure("**", requestMethods: [.post]) {
|
||||||
$0.headers["Cookie"] = self.authHeader
|
|
||||||
$0.pipeline[.parsing].removeTransformers()
|
$0.pipeline[.parsing].removeTransformers()
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/popular", requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("popular"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(Video.init)
|
content.json.arrayValue.map(Video.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/trending", requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(Video.init)
|
content.json.arrayValue.map(Video.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/search", requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(Video.init)
|
content.json.arrayValue.map(Video.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/search/suggestions", requestMethods: [.get]) { (content: Entity<JSON>) -> [String] in
|
configureTransformer(pathPattern("search/suggestions"), requestMethods: [.get]) { (content: Entity<JSON>) -> [String] in
|
||||||
if let suggestions = content.json.dictionaryValue["suggestions"] {
|
if let suggestions = content.json.dictionaryValue["suggestions"] {
|
||||||
return suggestions.arrayValue.map(String.init)
|
return suggestions.arrayValue.map(String.init)
|
||||||
}
|
}
|
||||||
@ -59,20 +112,20 @@ final class InvidiousAPI: Service {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/auth/playlists", requestMethods: [.get]) { (content: Entity<JSON>) -> [Playlist] in
|
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Playlist] in
|
||||||
content.json.arrayValue.map(Playlist.init)
|
content.json.arrayValue.map(Playlist.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/auth/playlists/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
|
configureTransformer(pathPattern("auth/playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
|
||||||
Playlist(content.json)
|
Playlist(content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/auth/playlists", requestMethods: [.post, .patch]) { (content: Entity<Data>) -> Playlist in
|
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.post, .patch]) { (content: Entity<Data>) -> Playlist in
|
||||||
// hacky, to verify if possible to get it in easier way
|
// hacky, to verify if possible to get it in easier way
|
||||||
Playlist(JSON(parseJSON: String(data: content.content, encoding: .utf8)!))
|
Playlist(JSON(parseJSON: String(data: content.content, encoding: .utf8)!))
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/auth/feed", requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("auth/feed"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
if let feedVideos = content.json.dictionaryValue["videos"] {
|
if let feedVideos = content.json.dictionaryValue["videos"] {
|
||||||
return feedVideos.arrayValue.map(Video.init)
|
return feedVideos.arrayValue.map(Video.init)
|
||||||
}
|
}
|
||||||
@ -80,69 +133,83 @@ final class InvidiousAPI: Service {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/auth/subscriptions", requestMethods: [.get]) { (content: Entity<JSON>) -> [Channel] in
|
configureTransformer(pathPattern("auth/subscriptions"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Channel] in
|
||||||
content.json.arrayValue.map(Channel.init)
|
content.json.arrayValue.map(Channel.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/channels/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
||||||
Channel(json: content.json)
|
Channel(json: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/channels/*/latest", requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("channels/*/latest"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(Video.init)
|
content.json.arrayValue.map(Video.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer("/videos/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
|
configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
|
||||||
Video(content.json)
|
Video(content.json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var authHeader: String? = "SID=\(Profile().sid)"
|
fileprivate func pathPattern(_ path: String) -> String {
|
||||||
|
"**\(InvidiousAPI.basePath)/\(path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func basePathAppending(_ path: String) -> String {
|
||||||
|
"\(InvidiousAPI.basePath)/\(path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cookieHeader: String {
|
||||||
|
"SID=\(account.sid)"
|
||||||
|
}
|
||||||
|
|
||||||
var popular: Resource {
|
var popular: Resource {
|
||||||
resource("/popular")
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/popular")
|
||||||
}
|
}
|
||||||
|
|
||||||
func trending(category: TrendingCategory, country: Country) -> Resource {
|
func trending(category: TrendingCategory, country: Country) -> Resource {
|
||||||
resource("/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: InvidiousAPI.instance, path: "/feed/subscriptions")
|
resource(baseURL: account.url, path: "/feed/subscriptions")
|
||||||
}
|
}
|
||||||
|
|
||||||
var feed: Resource {
|
var feed: Resource {
|
||||||
resource("/auth/feed")
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/auth/feed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats: Resource {
|
||||||
|
resource(baseURL: account.url, path: basePathAppending("stats"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptions: Resource {
|
var subscriptions: Resource {
|
||||||
resource("/auth/subscriptions")
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelSubscription(_ id: String) -> Resource {
|
func channelSubscription(_ id: String) -> Resource {
|
||||||
resource("/auth/subscriptions").child(id)
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")).child(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func channel(_ id: String) -> Resource {
|
func channel(_ id: String) -> Resource {
|
||||||
resource("/channels/\(id)")
|
resource(baseURL: account.url, path: basePathAppending("channels/\(id)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelVideos(_ id: String) -> Resource {
|
func channelVideos(_ id: String) -> Resource {
|
||||||
resource("/channels/\(id)/latest")
|
resource(baseURL: account.url, path: basePathAppending("channels/\(id)/latest"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func video(_ id: String) -> Resource {
|
func video(_ id: String) -> Resource {
|
||||||
resource("/videos/\(id)")
|
resource(baseURL: account.url, path: basePathAppending("videos/\(id)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var playlists: Resource {
|
var playlists: Resource {
|
||||||
resource("/auth/playlists")
|
resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func playlist(_ id: String) -> Resource {
|
func playlist(_ id: String) -> Resource {
|
||||||
resource("/auth/playlists/\(id)")
|
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func playlistVideos(_ id: String) -> Resource {
|
func playlistVideos(_ id: String) -> Resource {
|
||||||
@ -154,7 +221,7 @@ final class InvidiousAPI: Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func search(_ query: SearchQuery) -> Resource {
|
func search(_ query: SearchQuery) -> Resource {
|
||||||
var resource = resource("/search")
|
var resource = resource(baseURL: account.url, path: basePathAppending("search"))
|
||||||
.withParam("q", searchQuery(query.query))
|
.withParam("q", searchQuery(query.query))
|
||||||
.withParam("sort_by", query.sortBy.parameter)
|
.withParam("sort_by", query.sortBy.parameter)
|
||||||
|
|
||||||
@ -170,7 +237,7 @@ final class InvidiousAPI: Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func searchSuggestions(query: String) -> Resource {
|
func searchSuggestions(query: String) -> Resource {
|
||||||
resource("/search/suggestions")
|
resource(baseURL: account.url, path: basePathAppending("search/suggestions"))
|
||||||
.withParam("q", query.lowercased())
|
.withParam("q", query.lowercased())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class NavigationState: ObservableObject {
|
final class NavigationModel: ObservableObject {
|
||||||
enum TabSelection: Hashable {
|
enum TabSelection: Hashable {
|
||||||
case watchNow, subscriptions, popular, trending, playlists, channel(String), playlist(String), recentlyOpened(String), search
|
case watchNow, subscriptions, popular, trending, playlists, channel(String), playlist(String), recentlyOpened(String), search
|
||||||
}
|
}
|
||||||
@ -22,6 +22,8 @@ final class NavigationState: ObservableObject {
|
|||||||
@Published var isChannelOpen = false
|
@Published var isChannelOpen = false
|
||||||
@Published var sidebarSectionChanged = false
|
@Published var sidebarSectionChanged = false
|
||||||
|
|
||||||
|
@Published var presentingSettings = false
|
||||||
|
|
||||||
func playVideo(_ video: Video) {
|
func playVideo(_ video: Video) {
|
||||||
self.video = video
|
self.video = video
|
||||||
showingVideo = true
|
showingVideo = true
|
||||||
@ -56,4 +58,4 @@ final class NavigationState: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias TabSelection = NavigationState.TabSelection
|
typealias TabSelection = NavigationModel.TabSelection
|
@ -1,7 +1,7 @@
|
|||||||
import CoreMedia
|
import CoreMedia
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class PlaybackState: ObservableObject {
|
final class PlaybackModel: ObservableObject {
|
||||||
@Published var live = false
|
@Published var live = false
|
||||||
@Published var stream: Stream?
|
@Published var stream: Stream?
|
||||||
@Published var time: CMTime?
|
@Published var time: CMTime?
|
@ -5,7 +5,7 @@ import Logging
|
|||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
final class PlayerState: ObservableObject {
|
final class PlayerModel: ObservableObject {
|
||||||
let logger = Logger(label: "net.arekf.Pearvidious.ps")
|
let logger = Logger(label: "net.arekf.Pearvidious.ps")
|
||||||
|
|
||||||
var video: Video!
|
var video: Video!
|
||||||
@ -19,17 +19,19 @@ final class PlayerState: ObservableObject {
|
|||||||
private(set) var currentRate: Float = 0.0
|
private(set) var currentRate: Float = 0.0
|
||||||
static let availableRates: [Double] = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
static let availableRates: [Double] = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
||||||
|
|
||||||
var playbackState: PlaybackState
|
var api: InvidiousAPI
|
||||||
|
var playback: PlaybackModel
|
||||||
var timeObserver: Any?
|
var timeObserver: Any?
|
||||||
|
|
||||||
let maxResolution: Stream.Resolution?
|
let resolution: Stream.ResolutionSetting?
|
||||||
|
|
||||||
var playingOutsideViewController = false
|
var playingOutsideViewController = false
|
||||||
|
|
||||||
init(_ video: Video? = nil, playbackState: PlaybackState, maxResolution: Stream.Resolution? = nil) {
|
init(_ video: Video? = nil, playback: PlaybackModel, api: InvidiousAPI, resolution: Stream.ResolutionSetting? = nil) {
|
||||||
self.video = video
|
self.video = video
|
||||||
self.playbackState = playbackState
|
self.playback = playback
|
||||||
self.maxResolution = maxResolution
|
self.api = api
|
||||||
|
self.resolution = resolution
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -41,7 +43,7 @@ final class PlayerState: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackState.reset()
|
playback.reset()
|
||||||
|
|
||||||
loadExtendedVideoDetails(video) { video in
|
loadExtendedVideoDetails(video) { video in
|
||||||
self.video = video
|
self.video = video
|
||||||
@ -54,22 +56,26 @@ final class PlayerState: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
InvidiousAPI.shared.video(video!.id).load().onSuccess { response in
|
api.video(video!.id).load().onSuccess { response in
|
||||||
if let video: Video = response.typedContent() {
|
if let video: Video = response.typedContent() {
|
||||||
onSuccess(video)
|
onSuccess(video)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestedResolution: Bool {
|
||||||
|
resolution != nil && resolution != .hd720pFirstThenBest
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate func playVideo(_ video: Video) {
|
fileprivate func playVideo(_ video: Video) {
|
||||||
playbackState.live = video.live
|
playback.live = video.live
|
||||||
|
|
||||||
if video.live {
|
if video.live {
|
||||||
playHlsUrl()
|
playHlsUrl()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let stream = maxResolution != nil ? video.streamWithResolution(maxResolution!) : video.defaultStream
|
let stream = requestedResolution ? video.streamWithResolution(resolution!.value) : video.defaultStream
|
||||||
|
|
||||||
guard stream != nil else {
|
guard stream != nil else {
|
||||||
return
|
return
|
||||||
@ -78,7 +84,7 @@ final class PlayerState: ObservableObject {
|
|||||||
Task {
|
Task {
|
||||||
await self.loadStream(stream!)
|
await self.loadStream(stream!)
|
||||||
|
|
||||||
if stream != video.bestStream {
|
if resolution == .hd720pFirstThenBest {
|
||||||
await self.loadBestStream()
|
await self.loadBestStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,9 +97,7 @@ final class PlayerState: ObservableObject {
|
|||||||
|
|
||||||
fileprivate func loadStream(_ stream: Stream) async {
|
fileprivate func loadStream(_ stream: Stream) async {
|
||||||
if stream.oneMeaningfullAsset {
|
if stream.oneMeaningfullAsset {
|
||||||
DispatchQueue.main.async {
|
playStream(stream)
|
||||||
self.playStream(stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@ -111,11 +115,11 @@ final class PlayerState: ObservableObject {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.saveTime()
|
self.saveTime()
|
||||||
self.player?.replaceCurrentItem(with: self.playerItemWithMetadata(for: stream))
|
self.player?.replaceCurrentItem(with: self.playerItemWithMetadata(for: stream))
|
||||||
self.playbackState.stream = stream
|
self.playback.stream = stream
|
||||||
if self.timeObserver == nil {
|
if self.timeObserver == nil {
|
||||||
self.addTimeObserver()
|
self.addTimeObserver()
|
||||||
}
|
}
|
||||||
self.player?.playImmediately(atRate: 1.0)
|
self.player?.play()
|
||||||
self.seekToSavedTime()
|
self.seekToSavedTime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,7 +271,7 @@ final class PlayerState: ObservableObject {
|
|||||||
self.player.rate = self.currentRate
|
self.player.rate = self.currentRate
|
||||||
}
|
}
|
||||||
|
|
||||||
self.playbackState.time = self.player.currentTime()
|
self.playback.time = self.player.currentTime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Siesta
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
final class Playlists: ObservableObject {
|
|
||||||
@Published var playlists = [Playlist]()
|
|
||||||
|
|
||||||
var resource: Resource {
|
|
||||||
InvidiousAPI.shared.playlists
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
var all: [Playlist] {
|
|
||||||
playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
|
||||||
}
|
|
||||||
|
|
||||||
func find(id: Playlist.ID) -> Playlist? {
|
|
||||||
all.first { $0.id == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
func reload() {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func load() {
|
|
||||||
resource.load().onSuccess { resource in
|
|
||||||
if let playlists: [Playlist] = resource.typedContent() {
|
|
||||||
self.playlists = playlists
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
35
Model/PlaylistsModel.swift
Normal file
35
Model/PlaylistsModel.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import Foundation
|
||||||
|
import Siesta
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class PlaylistsModel: ObservableObject {
|
||||||
|
@Published var playlists = [Playlist]()
|
||||||
|
|
||||||
|
@Published var api: InvidiousAPI!
|
||||||
|
|
||||||
|
var resource: Resource {
|
||||||
|
api.playlists
|
||||||
|
}
|
||||||
|
|
||||||
|
var all: [Playlist] {
|
||||||
|
playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func find(id: Playlist.ID) -> Playlist? {
|
||||||
|
all.first { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func load(force: Bool = false) {
|
||||||
|
let request = force ? resource.load() : resource.loadIfNeeded()
|
||||||
|
|
||||||
|
request?
|
||||||
|
.onSuccess { resource in
|
||||||
|
if let playlists: [Playlist] = resource.typedContent() {
|
||||||
|
self.playlists = playlists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { _ in
|
||||||
|
self.playlists = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
import Defaults
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct Profile {
|
|
||||||
var defaultStreamResolution: DefaultStreamResolution = .hd1080p
|
|
||||||
|
|
||||||
var skippedSegmentsCategories = [String]() // SponsorBlockSegmentsProvider.categories
|
|
||||||
|
|
||||||
var sid = "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="
|
|
||||||
|
|
||||||
var cellsColumns = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DefaultStreamResolution: String {
|
|
||||||
case hd720pFirstThenBest, hd1080p, hd720p, sd480p, sd360p, sd240p, sd144p
|
|
||||||
|
|
||||||
var value: Stream.Resolution {
|
|
||||||
switch self {
|
|
||||||
case .hd720pFirstThenBest:
|
|
||||||
return .hd720p
|
|
||||||
default:
|
|
||||||
return Stream.Resolution(rawValue: rawValue)!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -103,7 +103,7 @@ struct RecentItemBridge: Defaults.Bridge {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
func deserialize(_ object: Serializable?) -> RecentItem? {
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
guard
|
guard
|
||||||
let object = object,
|
let object = object,
|
||||||
let type = object["type"],
|
let type = object["type"],
|
||||||
|
@ -2,30 +2,23 @@ import Defaults
|
|||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class SearchState: ObservableObject {
|
final class SearchModel: ObservableObject {
|
||||||
@Published var store = Store<[Video]>()
|
@Published var store = Store<[Video]>()
|
||||||
|
|
||||||
|
@Published var api: InvidiousAPI!
|
||||||
@Published var query = SearchQuery()
|
@Published var query = SearchQuery()
|
||||||
|
|
||||||
@Published var queryText = ""
|
@Published var queryText = ""
|
||||||
|
|
||||||
@Published var querySuggestions = Store<[String]>()
|
@Published var querySuggestions = Store<[String]>()
|
||||||
|
|
||||||
private var previousResource: Resource?
|
private var previousResource: Resource?
|
||||||
private var resource: Resource!
|
private var resource: Resource!
|
||||||
|
|
||||||
init() {
|
|
||||||
let newQuery = query
|
|
||||||
query = newQuery
|
|
||||||
|
|
||||||
resource = InvidiousAPI.shared.search(newQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isLoading: Bool {
|
var isLoading: Bool {
|
||||||
resource.isLoading
|
resource.isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadQuerySuggestions(_ query: String) {
|
func loadSuggestions(_ query: String) {
|
||||||
let resource = InvidiousAPI.shared.searchSuggestions(query: query)
|
let resource = api.searchSuggestions(query: query)
|
||||||
|
|
||||||
resource.addObserver(querySuggestions)
|
resource.addObserver(querySuggestions)
|
||||||
resource.loadIfNeeded()
|
resource.loadIfNeeded()
|
||||||
@ -44,7 +37,7 @@ final class SearchState: ObservableObject {
|
|||||||
func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) {
|
func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) {
|
||||||
changeHandler(query)
|
changeHandler(query)
|
||||||
|
|
||||||
let newResource = InvidiousAPI.shared.search(query)
|
let newResource = api.search(query)
|
||||||
guard newResource != previousResource else {
|
guard newResource != previousResource else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -60,7 +53,7 @@ final class SearchState: ObservableObject {
|
|||||||
func resetQuery(_ query: SearchQuery) {
|
func resetQuery(_ query: SearchQuery) {
|
||||||
self.query = query
|
self.query = query
|
||||||
|
|
||||||
let newResource = InvidiousAPI.shared.search(query)
|
let newResource = api.search(query)
|
||||||
guard newResource != previousResource else {
|
guard newResource != previousResource else {
|
||||||
return
|
return
|
||||||
}
|
}
|
@ -1,9 +1,32 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// swiftlint:disable:next final_class
|
// swiftlint:disable:next final_class
|
||||||
class Stream: Equatable, Hashable {
|
class Stream: Equatable, Hashable {
|
||||||
enum Resolution: String, CaseIterable, Comparable {
|
enum ResolutionSetting: String, Defaults.Serializable, CaseIterable {
|
||||||
|
case hd720pFirstThenBest, hd1080p, hd720p, sd480p, sd360p, sd240p, sd144p
|
||||||
|
|
||||||
|
var value: Stream.Resolution {
|
||||||
|
switch self {
|
||||||
|
case .hd720pFirstThenBest:
|
||||||
|
return .hd720p
|
||||||
|
default:
|
||||||
|
return Stream.Resolution(rawValue: rawValue)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .hd720pFirstThenBest:
|
||||||
|
return "Default: adaptive"
|
||||||
|
default:
|
||||||
|
return "\(value.height)p".replacingOccurrences(of: " ", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable {
|
||||||
case hd1080p, hd720p, sd480p, sd360p, sd240p, sd144p
|
case hd1080p, hd720p, sd480p, sd360p, sd240p, sd144p
|
||||||
|
|
||||||
var height: Int {
|
var height: Int {
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Siesta
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
final class Subscriptions: ObservableObject {
|
|
||||||
@Published var channels = [Channel]()
|
|
||||||
|
|
||||||
var resource: Resource {
|
|
||||||
InvidiousAPI.shared.subscriptions
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
var all: [Channel] {
|
|
||||||
channels.sorted { $0.name.lowercased() < $1.name.lowercased() }
|
|
||||||
}
|
|
||||||
|
|
||||||
func subscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
|
||||||
performChannelSubscriptionRequest(channelID, method: .post, onSuccess: onSuccess)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unsubscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
|
||||||
performChannelSubscriptionRequest(channelID, method: .delete, onSuccess: onSuccess)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSubscribing(_ channelID: String) -> Bool {
|
|
||||||
channels.contains { $0.id == channelID }
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func load(onSuccess: @escaping () -> Void = {}) {
|
|
||||||
resource.load().onSuccess { resource in
|
|
||||||
if let channels: [Channel] = resource.typedContent() {
|
|
||||||
self.channels = channels
|
|
||||||
onSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func performChannelSubscriptionRequest(_ channelID: String, method: RequestMethod, onSuccess: @escaping () -> Void = {}) {
|
|
||||||
InvidiousAPI.shared.channelSubscription(channelID).request(method).onCompletion { _ in
|
|
||||||
self.load(onSuccess: onSuccess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
53
Model/SubscriptionsModel.swift
Normal file
53
Model/SubscriptionsModel.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import Foundation
|
||||||
|
import Siesta
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class SubscriptionsModel: ObservableObject {
|
||||||
|
@Published var channels = [Channel]()
|
||||||
|
@Published var api: InvidiousAPI!
|
||||||
|
|
||||||
|
var resource: Resource {
|
||||||
|
api.subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
init(api: InvidiousAPI? = nil) {
|
||||||
|
self.api = api
|
||||||
|
}
|
||||||
|
|
||||||
|
var all: [Channel] {
|
||||||
|
channels.sorted { $0.name.lowercased() < $1.name.lowercased() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
||||||
|
performRequest(channelID, method: .post, onSuccess: onSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
|
||||||
|
performRequest(channelID, method: .delete, onSuccess: onSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSubscribing(_ channelID: String) -> Bool {
|
||||||
|
channels.contains { $0.id == channelID }
|
||||||
|
}
|
||||||
|
|
||||||
|
func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) {
|
||||||
|
let request = force ? resource.load() : resource.loadIfNeeded()
|
||||||
|
|
||||||
|
request?
|
||||||
|
.onSuccess { resource in
|
||||||
|
if let channels: [Channel] = resource.typedContent() {
|
||||||
|
self.channels = channels
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { _ in
|
||||||
|
self.channels = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func performRequest(_ channelID: String, method: RequestMethod, onSuccess: @escaping () -> Void = {}) {
|
||||||
|
api.channelSubscription(channelID).request(method).onCompletion { _ in
|
||||||
|
self.load(force: true, onSuccess: onSuccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -160,11 +160,7 @@ struct Video: Identifiable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func streamWithResolution(_ resolution: Stream.Resolution) -> Stream? {
|
func streamWithResolution(_ resolution: Stream.Resolution) -> Stream? {
|
||||||
selectableStreams.first { $0.resolution == resolution }
|
selectableStreams.first { $0.resolution == resolution } ?? defaultStream
|
||||||
}
|
|
||||||
|
|
||||||
func defaultStreamForProfile(_ profile: Profile) -> Stream? {
|
|
||||||
streamWithResolution(profile.defaultStreamResolution.value) ?? streams.first
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func thumbnailURL(quality: Thumbnail.Quality) -> URL? {
|
func thumbnailURL(quality: Thumbnail.Quality) -> URL? {
|
||||||
|
@ -11,9 +11,9 @@
|
|||||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
|
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
|
||||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
|
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
|
||||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
|
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; };
|
||||||
3711403F26B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; };
|
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
3711404026B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; };
|
3711404026B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
3711404126B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; };
|
3711404126B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
||||||
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
||||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
||||||
@ -23,9 +23,9 @@
|
|||||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; };
|
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; };
|
||||||
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; };
|
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; };
|
||||||
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; };
|
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; };
|
||||||
371F2F1A269B43D300E4A7AB /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationState.swift */; };
|
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||||
371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationState.swift */; };
|
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||||
371F2F1C269B43D300E4A7AB /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationState.swift */; };
|
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||||
372915E42687E33E00F5A35B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 372915E32687E33E00F5A35B /* Defaults */; };
|
372915E42687E33E00F5A35B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 372915E32687E33E00F5A35B /* Defaults */; };
|
||||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
@ -57,6 +57,30 @@
|
|||||||
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
|
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
|
||||||
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
|
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
|
||||||
3748187026A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
|
3748187026A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
|
||||||
|
37484C1926FC837400287258 /* PlaybackSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1826FC837400287258 /* PlaybackSettingsView.swift */; };
|
||||||
|
37484C1A26FC837400287258 /* PlaybackSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1826FC837400287258 /* PlaybackSettingsView.swift */; };
|
||||||
|
37484C1B26FC837400287258 /* PlaybackSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1826FC837400287258 /* PlaybackSettingsView.swift */; };
|
||||||
|
37484C1D26FC83A400287258 /* InstancesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1C26FC83A400287258 /* InstancesSettingsView.swift */; };
|
||||||
|
37484C1E26FC83A400287258 /* InstancesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1C26FC83A400287258 /* InstancesSettingsView.swift */; };
|
||||||
|
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1C26FC83A400287258 /* InstancesSettingsView.swift */; };
|
||||||
|
37484C2126FC83C400287258 /* AccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2026FC83C400287258 /* AccountSettingsView.swift */; };
|
||||||
|
37484C2226FC83C400287258 /* AccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2026FC83C400287258 /* AccountSettingsView.swift */; };
|
||||||
|
37484C2326FC83C400287258 /* AccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2026FC83C400287258 /* AccountSettingsView.swift */; };
|
||||||
|
37484C2526FC83E000287258 /* InstanceFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2426FC83E000287258 /* InstanceFormView.swift */; };
|
||||||
|
37484C2626FC83E000287258 /* InstanceFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2426FC83E000287258 /* InstanceFormView.swift */; };
|
||||||
|
37484C2726FC83E000287258 /* InstanceFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2426FC83E000287258 /* InstanceFormView.swift */; };
|
||||||
|
37484C2926FC83FF00287258 /* AccountFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2826FC83FF00287258 /* AccountFormView.swift */; };
|
||||||
|
37484C2A26FC83FF00287258 /* AccountFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2826FC83FF00287258 /* AccountFormView.swift */; };
|
||||||
|
37484C2B26FC83FF00287258 /* AccountFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2826FC83FF00287258 /* AccountFormView.swift */; };
|
||||||
|
37484C2D26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; };
|
||||||
|
37484C2E26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; };
|
||||||
|
37484C2F26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; };
|
||||||
|
37484C3126FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; };
|
||||||
|
37484C3226FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; };
|
||||||
|
37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; };
|
||||||
|
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||||
|
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||||
|
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||||
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
||||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
||||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
||||||
@ -74,6 +98,12 @@
|
|||||||
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; };
|
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; };
|
||||||
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 */; };
|
||||||
|
376B2E0726F920D600B1D64D /* 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 */; };
|
||||||
|
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; };
|
||||||
|
376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; };
|
||||||
|
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; };
|
||||||
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37754C9C26B7500000DBD602 /* VideosView.swift */; };
|
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37754C9C26B7500000DBD602 /* VideosView.swift */; };
|
||||||
37754C9E26B7500000DBD602 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37754C9C26B7500000DBD602 /* VideosView.swift */; };
|
37754C9E26B7500000DBD602 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37754C9C26B7500000DBD602 /* VideosView.swift */; };
|
||||||
37754C9F26B7500000DBD602 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37754C9C26B7500000DBD602 /* VideosView.swift */; };
|
37754C9F26B7500000DBD602 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37754C9C26B7500000DBD602 /* VideosView.swift */; };
|
||||||
@ -90,15 +120,17 @@
|
|||||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
|
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
|
||||||
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7EC267A0A0800A6BBAF /* SwiftyJSON */; };
|
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7EC267A0A0800A6BBAF /* SwiftyJSON */; };
|
||||||
377FC7F3267A0A0800A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7F2267A0A0800A6BBAF /* Logging */; };
|
377FC7F3267A0A0800A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7F2267A0A0800A6BBAF /* Logging */; };
|
||||||
3788AC2326F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2226F683DE00F6BAA9 /* WatchNowPlaylistSection.swift */; };
|
|
||||||
3788AC2426F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2226F683DE00F6BAA9 /* WatchNowPlaylistSection.swift */; };
|
|
||||||
3788AC2526F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2226F683DE00F6BAA9 /* WatchNowPlaylistSection.swift */; };
|
|
||||||
3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; };
|
3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; };
|
||||||
3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; };
|
3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; };
|
||||||
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; };
|
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; };
|
||||||
3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; };
|
3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; };
|
||||||
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; };
|
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; };
|
||||||
3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; };
|
3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; };
|
||||||
|
378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; };
|
||||||
|
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; };
|
||||||
|
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; };
|
||||||
|
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */; };
|
||||||
|
378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */; };
|
||||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
||||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||||
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||||
@ -124,12 +156,15 @@
|
|||||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||||
|
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B044B626F7AB9000E1419D /* SettingsView.swift */; };
|
||||||
|
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B044B626F7AB9000E1419D /* SettingsView.swift */; };
|
||||||
|
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B044B626F7AB9000E1419D /* SettingsView.swift */; };
|
||||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; };
|
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; };
|
||||||
37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; };
|
37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; };
|
||||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; };
|
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; };
|
||||||
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||||
37B767E02678C5BF0098BAA8 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 37B767DF2678C5BF0098BAA8 /* Logging */; };
|
37B767E02678C5BF0098BAA8 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 37B767DF2678C5BF0098BAA8 /* Logging */; };
|
||||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
||||||
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
||||||
@ -139,18 +174,18 @@
|
|||||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; };
|
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; };
|
||||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
||||||
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
||||||
37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
||||||
37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
37B81B0626D2CEDA00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
||||||
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
||||||
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
||||||
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
||||||
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
|
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
|
||||||
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
|
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
|
||||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
|
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
|
||||||
37BA794326DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
|
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* PlaylistsModel.swift */; };
|
||||||
37BA794426DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
|
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* PlaylistsModel.swift */; };
|
||||||
37BA794526DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
|
37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* PlaylistsModel.swift */; };
|
||||||
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */; };
|
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */; };
|
||||||
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */; };
|
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */; };
|
||||||
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */; };
|
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */; };
|
||||||
@ -193,9 +228,6 @@
|
|||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
||||||
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; };
|
|
||||||
37C7A1DD267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; };
|
|
||||||
37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; };
|
|
||||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
||||||
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
||||||
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
||||||
@ -217,9 +249,9 @@
|
|||||||
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 */; };
|
||||||
37E64DD126D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||||
37E64DD226D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||||
37E64DD326D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
||||||
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
||||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
||||||
@ -233,6 +265,9 @@
|
|||||||
37F4AE7226828F0900BD60EA /* VideosCellsVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */; };
|
37F4AE7226828F0900BD60EA /* VideosCellsVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */; };
|
||||||
37F4AE7326828F0900BD60EA /* VideosCellsVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */; };
|
37F4AE7326828F0900BD60EA /* VideosCellsVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */; };
|
||||||
37F4AE7426828F0900BD60EA /* VideosCellsVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */; };
|
37F4AE7426828F0900BD60EA /* VideosCellsVertical.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */; };
|
||||||
|
37F64FE426FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnViewModifier.swift */; };
|
||||||
|
37F64FE526FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnViewModifier.swift */; };
|
||||||
|
37F64FE626FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnViewModifier.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -269,11 +304,11 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
3705B17F267B4DFB00704544 /* TrendingCountry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCountry.swift; sourceTree = "<group>"; };
|
3705B17F267B4DFB00704544 /* TrendingCountry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCountry.swift; sourceTree = "<group>"; };
|
||||||
3705B181267B4E4900704544 /* TrendingCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCategory.swift; sourceTree = "<group>"; };
|
3705B181267B4E4900704544 /* TrendingCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCategory.swift; sourceTree = "<group>"; };
|
||||||
3711403E26B206A6005B3555 /* SearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchState.swift; sourceTree = "<group>"; };
|
3711403E26B206A6005B3555 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
|
||||||
3714166E267A8ACC006CA35D /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = "<group>"; };
|
3714166E267A8ACC006CA35D /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = "<group>"; };
|
||||||
37141672267A8E10006CA35D /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = "<group>"; };
|
37141672267A8E10006CA35D /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = "<group>"; };
|
||||||
37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||||
371F2F19269B43D300E4A7AB /* NavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationState.swift; sourceTree = "<group>"; };
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
|
||||||
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
|
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
|
||||||
373CFABD26966115003CB2C6 /* CoverSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionView.swift; sourceTree = "<group>"; };
|
373CFABD26966115003CB2C6 /* CoverSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionView.swift; sourceTree = "<group>"; };
|
||||||
373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionRowView.swift; sourceTree = "<group>"; };
|
373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionRowView.swift; sourceTree = "<group>"; };
|
||||||
@ -285,17 +320,28 @@
|
|||||||
3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = "<group>"; };
|
3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = "<group>"; };
|
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = "<group>"; };
|
3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = "<group>"; };
|
||||||
|
37484C1826FC837400287258 /* PlaybackSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
37484C1C26FC83A400287258 /* InstancesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
37484C2026FC83C400287258 /* AccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
37484C2426FC83E000287258 /* InstanceFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceFormView.swift; sourceTree = "<group>"; };
|
||||||
|
37484C2826FC83FF00287258 /* AccountFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFormView.swift; sourceTree = "<group>"; };
|
||||||
|
37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceDetailsSettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceAccountValidator.swift; sourceTree = "<group>"; };
|
||||||
|
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
|
||||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
||||||
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = "<group>"; };
|
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = "<group>"; };
|
||||||
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = "<group>"; };
|
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = "<group>"; };
|
||||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
|
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
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>"; };
|
||||||
37754C9C26B7500000DBD602 /* VideosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideosView.swift; sourceTree = "<group>"; };
|
37754C9C26B7500000DBD602 /* VideosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideosView.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>"; };
|
||||||
3788AC2226F683DE00F6BAA9 /* WatchNowPlaylistSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowPlaylistSection.swift; sourceTree = "<group>"; };
|
|
||||||
3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSection.swift; sourceTree = "<group>"; };
|
3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSection.swift; sourceTree = "<group>"; };
|
||||||
3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSectionBody.swift; sourceTree = "<group>"; };
|
3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSectionBody.swift; sourceTree = "<group>"; };
|
||||||
|
378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
||||||
|
378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsMenuView.swift; sourceTree = "<group>"; };
|
||||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = "<group>"; };
|
37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = "<group>"; };
|
||||||
3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
||||||
379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = "<group>"; };
|
379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = "<group>"; };
|
||||||
@ -307,17 +353,18 @@
|
|||||||
37AAF28F26740715007FC770 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
|
37AAF28F26740715007FC770 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
|
||||||
37AAF29926740A01007FC770 /* VideosListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosListView.swift; sourceTree = "<group>"; };
|
37AAF29926740A01007FC770 /* VideosListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosListView.swift; sourceTree = "<group>"; };
|
||||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
|
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
|
||||||
|
37B044B626F7AB9000E1419D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = "<group>"; };
|
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = "<group>"; };
|
||||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
|
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerModel.swift; sourceTree = "<group>"; };
|
||||||
37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = "<group>"; };
|
37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = "<group>"; };
|
||||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
|
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
|
||||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; };
|
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; };
|
||||||
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
|
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
|
||||||
37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = "<group>"; };
|
37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = "<group>"; };
|
||||||
37B81B0426D2CEDA00675966 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = "<group>"; };
|
37B81B0426D2CEDA00675966 /* PlaybackModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackModel.swift; sourceTree = "<group>"; };
|
||||||
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVideosView.swift; sourceTree = "<group>"; };
|
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVideosView.swift; sourceTree = "<group>"; };
|
||||||
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelVideosView.swift; sourceTree = "<group>"; };
|
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelVideosView.swift; sourceTree = "<group>"; };
|
||||||
37BA794226DBA973002A0235 /* Playlists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlists.swift; sourceTree = "<group>"; };
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsModel.swift; sourceTree = "<group>"; };
|
||||||
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarSubscriptions.swift; sourceTree = "<group>"; };
|
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarSubscriptions.swift; sourceTree = "<group>"; };
|
||||||
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarPlaylists.swift; sourceTree = "<group>"; };
|
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarPlaylists.swift; sourceTree = "<group>"; };
|
||||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = "<group>"; };
|
37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = "<group>"; };
|
||||||
@ -334,7 +381,6 @@
|
|||||||
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
|
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
|
||||||
37C194C626F6A9C8005D3B96 /* Recents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recents.swift; sourceTree = "<group>"; };
|
37C194C626F6A9C8005D3B96 /* Recents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recents.swift; sourceTree = "<group>"; };
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
||||||
37C7A1DB267CE9D90010EAD6 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
|
|
||||||
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = "<group>"; };
|
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = "<group>"; };
|
||||||
37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = "<group>"; };
|
37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = "<group>"; };
|
||||||
37D4B0C22671614700C925CA /* PearvidiousApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousApp.swift; sourceTree = "<group>"; };
|
37D4B0C22671614700C925CA /* PearvidiousApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousApp.swift; sourceTree = "<group>"; };
|
||||||
@ -353,11 +399,12 @@
|
|||||||
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>"; };
|
||||||
37E64DD026D597EB00C71877 /* Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscriptions.swift; sourceTree = "<group>"; };
|
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
|
||||||
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
|
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
|
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
|
||||||
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
|
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosCellsVertical.swift; sourceTree = "<group>"; };
|
37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosCellsVertical.swift; sourceTree = "<group>"; };
|
||||||
|
37F64FE326FE70A60081B69E /* RedrawOnViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnViewModifier.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -432,6 +479,7 @@
|
|||||||
371AAE2326CEB9E800901972 /* Navigation */ = {
|
371AAE2326CEB9E800901972 /* Navigation */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */,
|
||||||
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */,
|
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */,
|
||||||
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */,
|
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */,
|
||||||
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */,
|
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */,
|
||||||
@ -497,6 +545,7 @@
|
|||||||
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
|
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
|
||||||
37AAF27D26737323007FC770 /* PopularView.swift */,
|
37AAF27D26737323007FC770 /* PopularView.swift */,
|
||||||
37AAF27F26737550007FC770 /* SearchView.swift */,
|
37AAF27F26737550007FC770 /* SearchView.swift */,
|
||||||
|
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */,
|
||||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
||||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
||||||
);
|
);
|
||||||
@ -515,6 +564,7 @@
|
|||||||
3748186426A762300084E870 /* Fixtures */ = {
|
3748186426A762300084E870 /* Fixtures */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */,
|
||||||
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */,
|
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */,
|
||||||
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */,
|
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */,
|
||||||
3748186526A7627F0084E870 /* Video+Fixtures.swift */,
|
3748186526A7627F0084E870 /* Video+Fixtures.swift */,
|
||||||
@ -522,9 +572,24 @@
|
|||||||
path = Fixtures;
|
path = Fixtures;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
37484C1726FC836500287258 /* Settings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37484C2826FC83FF00287258 /* AccountFormView.swift */,
|
||||||
|
37484C2026FC83C400287258 /* AccountSettingsView.swift */,
|
||||||
|
37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */,
|
||||||
|
37484C2426FC83E000287258 /* InstanceFormView.swift */,
|
||||||
|
37484C1C26FC83A400287258 /* InstancesSettingsView.swift */,
|
||||||
|
37484C1826FC837400287258 /* PlaybackSettingsView.swift */,
|
||||||
|
37B044B626F7AB9000E1419D /* SettingsView.swift */,
|
||||||
|
);
|
||||||
|
path = Settings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3761AC0526F0F96100AA496F /* Modifiers */ = {
|
3761AC0526F0F96100AA496F /* Modifiers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
37F64FE326FE70A60081B69E /* RedrawOnViewModifier.swift */,
|
||||||
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */,
|
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */,
|
||||||
);
|
);
|
||||||
path = Modifiers;
|
path = Modifiers;
|
||||||
@ -540,7 +605,6 @@
|
|||||||
3788AC2126F683AB00F6BAA9 /* Watch Now */ = {
|
3788AC2126F683AB00F6BAA9 /* Watch Now */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3788AC2226F683DE00F6BAA9 /* WatchNowPlaylistSection.swift */,
|
|
||||||
3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */,
|
3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */,
|
||||||
3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */,
|
3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */,
|
||||||
37A9965D26D6F9B9006E3224 /* WatchNowView.swift */,
|
37A9965D26D6F9B9006E3224 /* WatchNowView.swift */,
|
||||||
@ -618,6 +682,7 @@
|
|||||||
371AAE2326CEB9E800901972 /* Navigation */,
|
371AAE2326CEB9E800901972 /* Navigation */,
|
||||||
371AAE2426CEBA4100901972 /* Player */,
|
371AAE2426CEBA4100901972 /* Player */,
|
||||||
371AAE2626CEBF1600901972 /* Playlists */,
|
371AAE2626CEBF1600901972 /* Playlists */,
|
||||||
|
37484C1726FC836500287258 /* Settings */,
|
||||||
371AAE2526CEBF0B00901972 /* Trending */,
|
371AAE2526CEBF0B00901972 /* Trending */,
|
||||||
371AAE2726CEBF4700901972 /* Videos */,
|
371AAE2726CEBF4700901972 /* Videos */,
|
||||||
371AAE2826CEC7D900901972 /* Views */,
|
371AAE2826CEC7D900901972 /* Views */,
|
||||||
@ -686,23 +751,25 @@
|
|||||||
children = (
|
children = (
|
||||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||||
37141672267A8E10006CA35D /* Country.swift */,
|
37141672267A8E10006CA35D /* Country.swift */,
|
||||||
|
378E50FA26FE8B9F00F49626 /* Instance.swift */,
|
||||||
|
37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */,
|
||||||
|
375DFB5726F9DA010013F468 /* InstancesModel.swift */,
|
||||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
||||||
371F2F19269B43D300E4A7AB /* NavigationState.swift */,
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||||
37B81B0426D2CEDA00675966 /* PlaybackState.swift */,
|
37B81B0426D2CEDA00675966 /* PlaybackModel.swift */,
|
||||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
|
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
||||||
376578882685471400D4EA09 /* Playlist.swift */,
|
376578882685471400D4EA09 /* Playlist.swift */,
|
||||||
37BA794226DBA973002A0235 /* Playlists.swift */,
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||||
37C7A1DB267CE9D90010EAD6 /* Profile.swift */,
|
|
||||||
37C194C626F6A9C8005D3B96 /* Recents.swift */,
|
37C194C626F6A9C8005D3B96 /* Recents.swift */,
|
||||||
|
3711403E26B206A6005B3555 /* SearchModel.swift */,
|
||||||
373CFACA26966264003CB2C6 /* SearchQuery.swift */,
|
373CFACA26966264003CB2C6 /* SearchQuery.swift */,
|
||||||
3711403E26B206A6005B3555 /* SearchState.swift */,
|
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||||
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
||||||
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */,
|
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */,
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
||||||
3797758A2689345500DD52A8 /* Store.swift */,
|
3797758A2689345500DD52A8 /* Store.swift */,
|
||||||
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
||||||
37E64DD026D597EB00C71877 /* Subscriptions.swift */,
|
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */,
|
||||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
||||||
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
||||||
37D4B19626717E1500C925CA /* Video.swift */,
|
37D4B19626717E1500C925CA /* Video.swift */,
|
||||||
@ -1001,19 +1068,24 @@
|
|||||||
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
||||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
|
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
|
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */,
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
||||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */,
|
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */,
|
||||||
37C194C726F6A9C8005D3B96 /* Recents.swift in Sources */,
|
37C194C726F6A9C8005D3B96 /* Recents.swift in Sources */,
|
||||||
3711403F26B206A6005B3555 /* SearchState.swift in Sources */,
|
37484C1926FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||||
|
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
|
37F64FE426FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
||||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||||
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
||||||
|
37484C2126FC83C400287258 /* AccountSettingsView.swift in Sources */,
|
||||||
37A9965E26D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
37A9965E26D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||||
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
37F4AE7226828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
37F4AE7226828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
||||||
@ -1023,25 +1095,27 @@
|
|||||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
37E64DD126D597EB00C71877 /* Subscriptions.swift in Sources */,
|
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */,
|
37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */,
|
||||||
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */,
|
|
||||||
3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */,
|
373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */,
|
||||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
|
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||||
37BA794326DBA973002A0235 /* Playlists.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 */,
|
||||||
3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
||||||
3788AC2326F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */,
|
|
||||||
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||||
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
|
37484C2D26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */,
|
||||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
|
37484C3126FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||||
|
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
||||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
|
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||||
@ -1050,8 +1124,10 @@
|
|||||||
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 */,
|
||||||
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
37484C2526FC83E000287258 /* InstanceFormView.swift in Sources */,
|
||||||
|
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||||
|
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
||||||
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
@ -1061,8 +1137,11 @@
|
|||||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
||||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
||||||
371F2F1A269B43D300E4A7AB /* NavigationState.swift in Sources */,
|
37484C2926FC83FF00287258 /* AccountFormView.swift in Sources */,
|
||||||
|
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
|
378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
|
37484C1D26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||||
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||||
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
3797758B2689345500DD52A8 /* Store.swift in Sources */,
|
3797758B2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
@ -1081,22 +1160,31 @@
|
|||||||
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */,
|
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||||
|
37484C1E26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||||
|
37484C2E26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */,
|
||||||
|
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
|
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||||
|
37484C1A26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||||
|
37484C3226FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||||
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
3788AC2426F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */,
|
|
||||||
37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||||
|
37F64FE526FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
||||||
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
|
378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */,
|
||||||
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */,
|
377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||||
37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */,
|
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
|
37B81B0626D2CEDA00675966 /* PlaybackModel.swift in Sources */,
|
||||||
|
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
|
3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
373CFAC32696616C003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
373CFAC32696616C003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
||||||
@ -1105,6 +1193,7 @@
|
|||||||
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
|
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
|
37484C2626FC83E000287258 /* InstanceFormView.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 */,
|
||||||
@ -1118,10 +1207,9 @@
|
|||||||
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
|
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||||
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||||
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
37E64DD226D597EB00C71877 /* Subscriptions.swift in Sources */,
|
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
37C7A1DD267CE9D90010EAD6 /* Profile.swift in Sources */,
|
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
|
||||||
37754C9E26B7500000DBD602 /* VideosView.swift in Sources */,
|
37754C9E26B7500000DBD602 /* VideosView.swift in Sources */,
|
||||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
@ -1131,16 +1219,19 @@
|
|||||||
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 */,
|
||||||
3711404026B206A6005B3555 /* SearchState.swift in Sources */,
|
3711404026B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
|
37484C2A26FC83FF00287258 /* AccountFormView.swift in Sources */,
|
||||||
|
37484C2226FC83C400287258 /* AccountSettingsView.swift in Sources */,
|
||||||
37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||||
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
|
376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
37BA794426DBA973002A0235 /* Playlists.swift in Sources */,
|
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -1169,9 +1260,11 @@
|
|||||||
3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
|
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
37BE0BD426A1D47D0092E2DB /* Player.swift in Sources */,
|
37BE0BD426A1D47D0092E2DB /* Player.swift in Sources */,
|
||||||
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
|
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
37F4AE7426828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
37F4AE7426828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
||||||
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||||
37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
@ -1179,24 +1272,26 @@
|
|||||||
373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */,
|
373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */,
|
||||||
37A9965C26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */,
|
37A9965C26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */,
|
||||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||||
|
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */,
|
37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */,
|
||||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
37E64DD326D597EB00C71877 /* Subscriptions.swift in Sources */,
|
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */,
|
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */,
|
373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */,
|
||||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
||||||
37D4B18E26717B3800C925CA /* VideoView.swift in Sources */,
|
37D4B18E26717B3800C925CA /* VideoView.swift in Sources */,
|
||||||
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
||||||
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||||
|
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||||
37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */,
|
37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */,
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
@ -1204,30 +1299,37 @@
|
|||||||
3748186C26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186C26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||||
371F2F1C269B43D300E4A7AB /* NavigationState.swift in Sources */,
|
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
37BA794526DBA973002A0235 /* Playlists.swift in Sources */,
|
37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
|
37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
|
37484C2326FC83C400287258 /* AccountSettingsView.swift in Sources */,
|
||||||
37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */,
|
37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */,
|
||||||
|
37F64FE626FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
||||||
37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */,
|
37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */,
|
||||||
|
37484C2B26FC83FF00287258 /* AccountFormView.swift in Sources */,
|
||||||
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
3711404126B206A6005B3555 /* SearchState.swift in Sources */,
|
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
|
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||||
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
||||||
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
37141675267A8E10006CA35D /* Country.swift in Sources */,
|
37141675267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
|
37484C2726FC83E000287258 /* InstanceFormView.swift in Sources */,
|
||||||
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
||||||
373CFAC42696616C003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
373CFAC42696616C003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
||||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||||
|
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
3788AC2526F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */,
|
|
||||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
|
37484C1B26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||||
372915E82687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E82687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */,
|
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */,
|
||||||
3797758D2689345500DD52A8 /* Store.swift in Sources */,
|
3797758D2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
|
37484C2F26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
{
|
{
|
||||||
"colors" : [
|
"colors" : [
|
||||||
{
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.416",
|
||||||
|
"green" : "0.256",
|
||||||
|
"red" : "0.837"
|
||||||
|
}
|
||||||
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -5,6 +5,8 @@ extension Defaults.Keys {
|
|||||||
static let layout = Key<ListingLayout>("listingLayout", default: .cells)
|
static let layout = Key<ListingLayout>("listingLayout", default: .cells)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static let instances = Key<[Instance]>("instances", default: [])
|
||||||
|
|
||||||
static let searchSortOrder = Key<SearchQuery.SortOrder>("searchSortOrder", default: .relevance)
|
static let searchSortOrder = Key<SearchQuery.SortOrder>("searchSortOrder", default: .relevance)
|
||||||
static let searchDate = Key<SearchQuery.Date?>("searchDate")
|
static let searchDate = Key<SearchQuery.Date?>("searchDate")
|
||||||
static let searchDuration = Key<SearchQuery.Duration?>("searchDuration")
|
static let searchDuration = Key<SearchQuery.Duration?>("searchDuration")
|
||||||
@ -14,6 +16,7 @@ extension Defaults.Keys {
|
|||||||
static let videoIDToAddToPlaylist = Key<String?>("videoIDToAddToPlaylist")
|
static let videoIDToAddToPlaylist = Key<String?>("videoIDToAddToPlaylist")
|
||||||
|
|
||||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||||
|
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ListingLayout: String, CaseIterable, Identifiable, Defaults.Serializable {
|
enum ListingLayout: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
|
19
Shared/Modifiers/RedrawOnViewModifier.swift
Normal file
19
Shared/Modifiers/RedrawOnViewModifier.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RedrawOnViewModifier: ViewModifier {
|
||||||
|
@State private var changeFlag: Bool
|
||||||
|
|
||||||
|
init(changeFlag: Bool) {
|
||||||
|
self.changeFlag = changeFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content.opacity(changeFlag ? 1 : 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func redrawOn(change flag: Bool) -> some View {
|
||||||
|
modifier(RedrawOnViewModifier(changeFlag: flag))
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,13 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UnsubscribeAlertModifier: ViewModifier {
|
struct UnsubscribeAlertModifier: ViewModifier {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) {
|
.alert(unsubscribeAlertTitle, isPresented: $navigation.presentingUnsubscribeAlert) {
|
||||||
if let channel = navigationState.channelToUnsubscribe {
|
if let channel = navigation.channelToUnsubscribe {
|
||||||
Button("Unsubscribe", role: .destructive) {
|
Button("Unsubscribe", role: .destructive) {
|
||||||
subscriptions.unsubscribe(channel.id)
|
subscriptions.unsubscribe(channel.id)
|
||||||
}
|
}
|
||||||
@ -17,7 +17,7 @@ struct UnsubscribeAlertModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var unsubscribeAlertTitle: String {
|
var unsubscribeAlertTitle: String {
|
||||||
if let channel = navigationState.channelToUnsubscribe {
|
if let channel = navigation.channelToUnsubscribe {
|
||||||
return "Unsubscribe from \(channel.name)"
|
return "Unsubscribe from \(channel.name)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
33
Shared/Navigation/AccountsMenuView.swift
Normal file
33
Shared/Navigation/AccountsMenuView.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AccountsMenuView: View {
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
|
@Default(.instances) private var instances
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Menu {
|
||||||
|
ForEach(instances, id: \.self) { instance in
|
||||||
|
Button(accountButtonTitle(instance: instance, account: instance.anonymousAccount)) {
|
||||||
|
api.setAccount(instance.anonymousAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(instance.accounts, id: \.self) { account in
|
||||||
|
Button(accountButtonTitle(instance: instance, account: account)) {
|
||||||
|
api.setAccount(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label(api.account?.name ?? "Accounts", systemImage: "person.crop.circle")
|
||||||
|
.labelStyle(.titleAndIcon)
|
||||||
|
}
|
||||||
|
.disabled(instances.isEmpty)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountButtonTitle(instance: Instance, account: Instance.Account) -> String {
|
||||||
|
instances.count > 1 ? "\(account.description) — \(instance.shortDescription)" : account.description
|
||||||
|
}
|
||||||
|
}
|
@ -12,16 +12,18 @@ struct AppSidebarNavigation: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
@EnvironmentObject<Playlists> private var playlists
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@EnvironmentObject<Recents> private var recents
|
@EnvironmentObject<Recents> private var recents
|
||||||
@EnvironmentObject<SearchState> private var searchState
|
@EnvironmentObject<SearchModel> private var search
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
@State private var didApplyPrimaryViewWorkAround = false
|
@State private var didApplyPrimaryViewWorkAround = false
|
||||||
|
|
||||||
var selection: Binding<TabSelection?> {
|
var selection: Binding<TabSelection?> {
|
||||||
navigationState.tabSelectionOptionalBinding
|
navigation.tabSelectionOptionalBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -30,11 +32,10 @@ struct AppSidebarNavigation: View {
|
|||||||
// workaround for an empty supplementary view on launch
|
// workaround for an empty supplementary view on launch
|
||||||
// the supplementary view is determined by the default selection inside the
|
// the supplementary view is determined by the default selection inside the
|
||||||
// primary view, but the primary view is not loaded so its selection is not read
|
// primary view, but the primary view is not loaded so its selection is not read
|
||||||
// We work around that by briefly showing the primary view.
|
// We work around that by showing the primary view
|
||||||
if !didApplyPrimaryViewWorkAround, let splitVC = viewController.children.first as? UISplitViewController {
|
if !didApplyPrimaryViewWorkAround, let splitVC = viewController.children.first as? UISplitViewController {
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
splitVC.show(.primary)
|
splitVC.show(.primary)
|
||||||
splitVC.hide(.primary)
|
|
||||||
}
|
}
|
||||||
didApplyPrimaryViewWorkAround = true
|
didApplyPrimaryViewWorkAround = true
|
||||||
}
|
}
|
||||||
@ -44,31 +45,33 @@ struct AppSidebarNavigation: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sidebarMinWidth: Double = 280
|
||||||
|
|
||||||
var content: some View {
|
var content: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
sidebar
|
sidebar
|
||||||
.frame(minWidth: 180)
|
.toolbar { toolbarContent }
|
||||||
|
.frame(minWidth: sidebarMinWidth)
|
||||||
|
|
||||||
Text("Select section")
|
Text("Select section")
|
||||||
}
|
}
|
||||||
.environment(\.navigationStyle, .sidebar)
|
.environment(\.navigationStyle, .sidebar)
|
||||||
.searchable(text: $searchState.queryText, placement: .sidebar) {
|
.searchable(text: $search.queryText, placement: .sidebar) {
|
||||||
ForEach(searchState.querySuggestions.collection, id: \.self) { suggestion in
|
ForEach(search.querySuggestions.collection, id: \.self) { suggestion in
|
||||||
Text(suggestion)
|
Text(suggestion)
|
||||||
.searchCompletion(suggestion)
|
.searchCompletion(suggestion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: searchState.queryText) { query in
|
.onChange(of: search.queryText) { query in
|
||||||
searchState.loadQuerySuggestions(query)
|
search.loadSuggestions(query)
|
||||||
}
|
}
|
||||||
.onSubmit(of: .search) {
|
.onSubmit(of: .search) {
|
||||||
searchState.changeQuery { query in
|
search.changeQuery { query in
|
||||||
query.query = searchState.queryText
|
query.query = search.queryText
|
||||||
}
|
}
|
||||||
|
recents.open(RecentItem(from: search.queryText))
|
||||||
|
|
||||||
recents.open(RecentItem(from: searchState.queryText))
|
navigation.tabSelection = .search
|
||||||
|
|
||||||
navigationState.tabSelection = .search
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +83,8 @@ struct AppSidebarNavigation: View {
|
|||||||
.id(group)
|
.id(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
.onChange(of: navigationState.sidebarSectionChanged) { _ in
|
.onChange(of: navigation.sidebarSectionChanged) { _ in
|
||||||
scrollScrollViewToItem(scrollView: scrollView, for: navigationState.tabSelection)
|
scrollScrollViewToItem(scrollView: scrollView, for: navigation.tabSelection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background {
|
.background {
|
||||||
@ -93,13 +96,7 @@ struct AppSidebarNavigation: View {
|
|||||||
.listStyle(.sidebar)
|
.listStyle(.sidebar)
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
#if os(macOS)
|
toolbarContent
|
||||||
ToolbarItemGroup {
|
|
||||||
Button(action: toggleSidebar) {
|
|
||||||
Image(systemName: "sidebar.left").help("Toggle Sidebar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,26 +112,28 @@ struct AppSidebarNavigation: View {
|
|||||||
|
|
||||||
AppSidebarRecents(selection: selection)
|
AppSidebarRecents(selection: selection)
|
||||||
.id("recentlyOpened")
|
.id("recentlyOpened")
|
||||||
|
|
||||||
|
if api.signedIn {
|
||||||
AppSidebarSubscriptions(selection: selection)
|
AppSidebarSubscriptions(selection: selection)
|
||||||
AppSidebarPlaylists(selection: selection)
|
AppSidebarPlaylists(selection: selection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var mainNavigationLinks: some View {
|
var mainNavigationLinks: some View {
|
||||||
Section("Videos") {
|
Section("Videos") {
|
||||||
NavigationLink(tag: TabSelection.watchNow, selection: selection) {
|
NavigationLink(destination: LazyView(WatchNowView()), tag: TabSelection.watchNow, selection: selection) {
|
||||||
WatchNowView()
|
|
||||||
}
|
|
||||||
label: {
|
|
||||||
Label("Watch Now", systemImage: "play.circle")
|
Label("Watch Now", systemImage: "play.circle")
|
||||||
.accessibility(label: Text("Watch Now"))
|
.accessibility(label: Text("Watch Now"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if api.signedIn {
|
||||||
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: selection) {
|
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: selection) {
|
||||||
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: selection) {
|
NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: selection) {
|
||||||
Label("Popular", systemImage: "chart.bar")
|
Label("Popular", systemImage: "chart.bar")
|
||||||
@ -145,11 +144,6 @@ struct AppSidebarNavigation: View {
|
|||||||
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
||||||
.accessibility(label: Text("Trending"))
|
.accessibility(label: Text("Trending"))
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink(destination: LazyView(PlaylistsView()), tag: TabSelection.playlists, selection: selection) {
|
|
||||||
Label("Playlists", systemImage: "list.and.film")
|
|
||||||
.accessibility(label: Text("Playlists"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +159,36 @@ struct AppSidebarNavigation: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var toolbarContent: some ToolbarContent {
|
||||||
|
Group {
|
||||||
|
#if os(iOS)
|
||||||
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: { navigation.presentingSettings = true }) {
|
||||||
|
Image(systemName: "gearshape.2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ToolbarItem(placement: accountsMenuToolbarItemPlacement) {
|
||||||
|
AccountsMenuView()
|
||||||
|
.help(
|
||||||
|
"Switch Instances and Accounts\n" +
|
||||||
|
"Current Instance: \n" +
|
||||||
|
"\(api.account?.url ?? "Not Set")\n" +
|
||||||
|
"Current User: \(api.account?.description ?? "Not set")"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountsMenuToolbarItemPlacement: ToolbarItemPlacement {
|
||||||
|
#if os(iOS)
|
||||||
|
return .bottomBar
|
||||||
|
#else
|
||||||
|
return .automatic
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private func toggleSidebar() {
|
private func toggleSidebar() {
|
||||||
NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
|
NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
|
||||||
@ -177,6 +201,6 @@ struct AppSidebarNavigation: View {
|
|||||||
|
|
||||||
let symbolName = firstLetter?.range(of: regex, options: .regularExpression) != nil ? firstLetter! : "questionmark"
|
let symbolName = firstLetter?.range(of: regex, options: .regularExpression) != nil ? firstLetter! : "questionmark"
|
||||||
|
|
||||||
return "\(symbolName).square"
|
return "\(symbolName).circle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AppSidebarPlaylists: View {
|
struct AppSidebarPlaylists: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<Playlists> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
|
||||||
@Binding var selection: TabSelection?
|
@Binding var selection: TabSelection?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section(header: Text("Playlists")) {
|
Section(header: Text("Playlists")) {
|
||||||
ForEach(playlists.all) { playlist in
|
ForEach(playlists.playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }) { playlist in
|
||||||
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $selection) {
|
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $selection) {
|
||||||
LazyView(PlaylistVideosView(playlist))
|
LazyView(PlaylistVideosView(playlist))
|
||||||
} label: {
|
} label: {
|
||||||
@ -18,7 +18,7 @@ struct AppSidebarPlaylists: View {
|
|||||||
.id(playlist.id)
|
.id(playlist.id)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Edit") {
|
Button("Edit") {
|
||||||
navigationState.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
navigation.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,11 +26,14 @@ struct AppSidebarPlaylists: View {
|
|||||||
newPlaylistButton
|
newPlaylistButton
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
playlists.load()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPlaylistButton: some View {
|
var newPlaylistButton: some View {
|
||||||
Button(action: { navigationState.presentNewPlaylistForm() }) {
|
Button(action: { navigation.presentNewPlaylistForm() }) {
|
||||||
Label("New Playlist", systemImage: "plus.square")
|
Label("New Playlist", systemImage: "plus.circle")
|
||||||
}
|
}
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
@ -4,7 +4,7 @@ import SwiftUI
|
|||||||
struct AppSidebarRecents: View {
|
struct AppSidebarRecents: View {
|
||||||
@Binding var selection: TabSelection?
|
@Binding var selection: TabSelection?
|
||||||
|
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<Recents> private var recents
|
@EnvironmentObject<Recents> private var recents
|
||||||
|
|
||||||
@Default(.recentlyOpened) private var recentItems
|
@Default(.recentlyOpened) private var recentItems
|
||||||
@ -18,7 +18,7 @@ struct AppSidebarRecents: View {
|
|||||||
switch recent.type {
|
switch recent.type {
|
||||||
case .channel:
|
case .channel:
|
||||||
RecentNavigationLink(recent: recent, selection: $selection) {
|
RecentNavigationLink(recent: recent, selection: $selection) {
|
||||||
LazyView(ChannelVideosView(Channel(id: recent.id, name: recent.title)))
|
LazyView(ChannelVideosView(channel: Channel(id: recent.id, name: recent.title)))
|
||||||
}
|
}
|
||||||
case .query:
|
case .query:
|
||||||
RecentNavigationLink(recent: recent, selection: $selection, systemImage: "magnifyingglass") {
|
RecentNavigationLink(recent: recent, selection: $selection, systemImage: "magnifyingglass") {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AppSidebarSubscriptions: View {
|
struct AppSidebarSubscriptions: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
@Binding var selection: TabSelection?
|
@Binding var selection: TabSelection?
|
||||||
|
|
||||||
@ -10,22 +11,25 @@ struct AppSidebarSubscriptions: View {
|
|||||||
Section(header: Text("Subscriptions")) {
|
Section(header: Text("Subscriptions")) {
|
||||||
ForEach(subscriptions.all) { channel in
|
ForEach(subscriptions.all) { channel in
|
||||||
NavigationLink(tag: TabSelection.channel(channel.id), selection: $selection) {
|
NavigationLink(tag: TabSelection.channel(channel.id), selection: $selection) {
|
||||||
LazyView(ChannelVideosView(channel))
|
LazyView(ChannelVideosView(channel: channel))
|
||||||
} label: {
|
} label: {
|
||||||
Label(channel.name, systemImage: AppSidebarNavigation.symbolSystemImage(channel.name))
|
Label(channel.name, systemImage: AppSidebarNavigation.symbolSystemImage(channel.name))
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Unsubscribe") {
|
Button("Unsubscribe") {
|
||||||
navigationState.presentUnsubscribeAlert(channel)
|
navigation.presentUnsubscribeAlert(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(UnsubscribeAlertModifier())
|
.modifier(UnsubscribeAlertModifier())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
subscriptions.load()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var unsubscribeAlertTitle: String {
|
var unsubscribeAlertTitle: String {
|
||||||
if let channel = navigationState.channelToUnsubscribe {
|
if let channel = navigation.channelToUnsubscribe {
|
||||||
return "Unsubscribe from \(channel.name)"
|
return "Unsubscribe from \(channel.name)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,15 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AppTabNavigation: View {
|
struct AppTabNavigation: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<SearchState> private var searchState
|
@EnvironmentObject<SearchModel> private var search
|
||||||
@EnvironmentObject<Recents> private var recents
|
@EnvironmentObject<Recents> private var recents
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $navigationState.tabSelection) {
|
TabView(selection: $navigation.tabSelection) {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
WatchNowView()
|
LazyView(WatchNowView())
|
||||||
|
.toolbar { toolbarContent }
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Watch Now", systemImage: "play.circle")
|
Label("Watch Now", systemImage: "play.circle")
|
||||||
@ -18,7 +19,8 @@ struct AppTabNavigation: View {
|
|||||||
.tag(TabSelection.watchNow)
|
.tag(TabSelection.watchNow)
|
||||||
|
|
||||||
NavigationView {
|
NavigationView {
|
||||||
SubscriptionsView()
|
LazyView(SubscriptionsView())
|
||||||
|
.toolbar { toolbarContent }
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Subscriptions", systemImage: "star.circle.fill")
|
Label("Subscriptions", systemImage: "star.circle.fill")
|
||||||
@ -29,7 +31,8 @@ struct AppTabNavigation: View {
|
|||||||
// TODO: reenable with settings
|
// TODO: reenable with settings
|
||||||
// ============================
|
// ============================
|
||||||
// NavigationView {
|
// NavigationView {
|
||||||
// PopularView()
|
// LazyView(PopularView())
|
||||||
|
// .toolbar { toolbarContent }
|
||||||
// }
|
// }
|
||||||
// .tabItem {
|
// .tabItem {
|
||||||
// Label("Popular", systemImage: "chart.bar")
|
// Label("Popular", systemImage: "chart.bar")
|
||||||
@ -38,7 +41,8 @@ struct AppTabNavigation: View {
|
|||||||
// .tag(TabSelection.popular)
|
// .tag(TabSelection.popular)
|
||||||
|
|
||||||
NavigationView {
|
NavigationView {
|
||||||
TrendingView()
|
LazyView(TrendingView())
|
||||||
|
.toolbar { toolbarContent }
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
||||||
@ -47,7 +51,8 @@ struct AppTabNavigation: View {
|
|||||||
.tag(TabSelection.trending)
|
.tag(TabSelection.trending)
|
||||||
|
|
||||||
NavigationView {
|
NavigationView {
|
||||||
PlaylistsView()
|
LazyView(PlaylistsView())
|
||||||
|
.toolbar { toolbarContent }
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Playlists", systemImage: "list.and.film")
|
Label("Playlists", systemImage: "list.and.film")
|
||||||
@ -56,25 +61,28 @@ struct AppTabNavigation: View {
|
|||||||
.tag(TabSelection.playlists)
|
.tag(TabSelection.playlists)
|
||||||
|
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
LazyView(
|
||||||
SearchView()
|
SearchView()
|
||||||
.searchable(text: $searchState.queryText, placement: .navigationBarDrawer(displayMode: .always)) {
|
.toolbar { toolbarContent }
|
||||||
ForEach(searchState.querySuggestions.collection, id: \.self) { suggestion in
|
.searchable(text: $search.queryText, placement: .navigationBarDrawer(displayMode: .always)) {
|
||||||
|
ForEach(search.querySuggestions.collection, id: \.self) { suggestion in
|
||||||
Text(suggestion)
|
Text(suggestion)
|
||||||
.searchCompletion(suggestion)
|
.searchCompletion(suggestion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: searchState.queryText) { query in
|
.onChange(of: search.queryText) { query in
|
||||||
searchState.loadQuerySuggestions(query)
|
search.loadSuggestions(query)
|
||||||
}
|
}
|
||||||
.onSubmit(of: .search) {
|
.onSubmit(of: .search) {
|
||||||
searchState.changeQuery { query in
|
search.changeQuery { query in
|
||||||
query.query = searchState.queryText
|
query.query = search.queryText
|
||||||
}
|
}
|
||||||
|
|
||||||
recents.open(RecentItem(from: searchState.queryText))
|
recents.open(RecentItem(from: search.queryText))
|
||||||
|
|
||||||
navigationState.tabSelection = .search
|
navigation.tabSelection = .search
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Search", systemImage: "magnifyingglass")
|
Label("Search", systemImage: "magnifyingglass")
|
||||||
@ -83,7 +91,7 @@ struct AppTabNavigation: View {
|
|||||||
.tag(TabSelection.search)
|
.tag(TabSelection.search)
|
||||||
}
|
}
|
||||||
.environment(\.navigationStyle, .tab)
|
.environment(\.navigationStyle, .tab)
|
||||||
.sheet(isPresented: $navigationState.isChannelOpen, onDismiss: {
|
.sheet(isPresented: $navigation.isChannelOpen, onDismiss: {
|
||||||
if let channel = recents.presentedChannel {
|
if let channel = recents.presentedChannel {
|
||||||
let recent = RecentItem(from: channel)
|
let recent = RecentItem(from: channel)
|
||||||
recents.close(recent)
|
recents.close(recent)
|
||||||
@ -91,10 +99,26 @@ struct AppTabNavigation: View {
|
|||||||
}) {
|
}) {
|
||||||
if recents.presentedChannel != nil {
|
if recents.presentedChannel != nil {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ChannelVideosView(recents.presentedChannel!)
|
ChannelVideosView(channel: recents.presentedChannel!)
|
||||||
.environment(\.inNavigationView, true)
|
.environment(\.inNavigationView, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var toolbarContent: some ToolbarContent {
|
||||||
|
#if os(iOS)
|
||||||
|
Group {
|
||||||
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||||
|
Button(action: { navigation.presentingSettings = true }) {
|
||||||
|
Image(systemName: "gearshape.2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
AccountsMenuView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@StateObject private var navigationState = NavigationState()
|
@StateObject private var navigation = NavigationModel()
|
||||||
@StateObject private var playbackState = PlaybackState()
|
@StateObject private var playback = PlaybackModel()
|
||||||
@StateObject private var playlists = Playlists()
|
|
||||||
@StateObject private var recents = Recents()
|
@StateObject private var recents = Recents()
|
||||||
@StateObject private var searchState = SearchState()
|
|
||||||
@StateObject private var subscriptions = Subscriptions()
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@ -26,29 +27,29 @@ struct ContentView: View {
|
|||||||
TVNavigationView()
|
TVNavigationView()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.environmentObject(navigation)
|
||||||
|
.environmentObject(playback)
|
||||||
|
.environmentObject(recents)
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.sheet(isPresented: $navigationState.showingVideo) {
|
.sheet(isPresented: $navigation.showingVideo) {
|
||||||
if let video = navigationState.video {
|
if let video = navigation.video {
|
||||||
VideoPlayerView(video)
|
VideoPlayerView(video)
|
||||||
|
|
||||||
#if !os(iOS)
|
#if !os(iOS)
|
||||||
.frame(minWidth: 550, minHeight: 720)
|
.frame(minWidth: 550, minHeight: 720)
|
||||||
.onExitCommand {
|
.onExitCommand {
|
||||||
navigationState.showingVideo = false
|
navigation.showingVideo = false
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $navigationState.presentingPlaylistForm) {
|
.sheet(isPresented: $navigation.presentingPlaylistForm) {
|
||||||
PlaylistFormView(playlist: $navigationState.editedPlaylist)
|
PlaylistFormView(playlist: $navigation.editedPlaylist)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $navigation.presentingSettings) {
|
||||||
|
SettingsView()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
.environmentObject(navigationState)
|
|
||||||
.environmentObject(playbackState)
|
|
||||||
.environmentObject(playlists)
|
|
||||||
.environmentObject(recents)
|
|
||||||
.environmentObject(searchState)
|
|
||||||
.environmentObject(subscriptions)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,50 @@
|
|||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct PearvidiousApp: App {
|
struct PearvidiousApp: App {
|
||||||
|
@StateObject private var api = InvidiousAPI()
|
||||||
|
@StateObject private var instances = InstancesModel()
|
||||||
|
@StateObject private var playlists = PlaylistsModel()
|
||||||
|
@StateObject private var search = SearchModel()
|
||||||
|
@StateObject private var subscriptions = SubscriptionsModel()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.onAppear(perform: configureAPI)
|
||||||
|
.environmentObject(api)
|
||||||
|
.environmentObject(instances)
|
||||||
|
.environmentObject(playlists)
|
||||||
|
.environmentObject(search)
|
||||||
|
.environmentObject(subscriptions)
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.commands {
|
.commands {
|
||||||
SidebarCommands()
|
SidebarCommands()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
Settings {
|
||||||
|
SettingsView()
|
||||||
|
.onAppear(perform: configureAPI)
|
||||||
|
.environmentObject(api)
|
||||||
|
.environmentObject(instances)
|
||||||
|
.environmentObject(playlists)
|
||||||
|
.environmentObject(subscriptions)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func configureAPI() {
|
||||||
|
subscriptions.api = api
|
||||||
|
playlists.api = api
|
||||||
|
|
||||||
|
guard api.account == nil, instances.defaultAccount != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.setAccount(instances.defaultAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ struct PlaybackBar: View {
|
|||||||
let video: Video
|
let video: Video
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@EnvironmentObject private var playbackState: PlaybackState
|
@EnvironmentObject private var playback: PlaybackModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
@ -18,7 +18,7 @@ struct PlaybackBar: View {
|
|||||||
.frame(minWidth: 60, maxWidth: .infinity)
|
.frame(minWidth: 60, maxWidth: .infinity)
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
if playbackState.stream != nil {
|
if playback.stream != nil {
|
||||||
Text(currentStreamString)
|
Text(currentStreamString)
|
||||||
} else {
|
} else {
|
||||||
if video.live {
|
if video.live {
|
||||||
@ -38,19 +38,19 @@ struct PlaybackBar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentStreamString: String {
|
var currentStreamString: String {
|
||||||
playbackState.stream != nil ? "\(playbackState.stream!.resolution.height)p" : ""
|
playback.stream != nil ? "\(playback.stream!.resolution.height)p" : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var playbackStatus: String {
|
var playbackStatus: String {
|
||||||
guard playbackState.time != nil else {
|
guard playback.time != nil else {
|
||||||
if playbackState.live {
|
if playback.live {
|
||||||
return "LIVE"
|
return "LIVE"
|
||||||
} else {
|
} else {
|
||||||
return "loading..."
|
return "loading..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let remainingSeconds = video.length - playbackState.time!.seconds
|
let remainingSeconds = video.length - playback.time!.seconds
|
||||||
|
|
||||||
if remainingSeconds < 60 {
|
if remainingSeconds < 60 {
|
||||||
return "less than a minute"
|
return "less than a minute"
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct Player: UIViewControllerRepresentable {
|
struct Player: UIViewControllerRepresentable {
|
||||||
@EnvironmentObject<PlaybackState> private var playbackState
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<PlaybackModel> private var playback
|
||||||
|
|
||||||
var video: Video?
|
var video: Video?
|
||||||
|
|
||||||
@ -9,7 +11,10 @@ struct Player: UIViewControllerRepresentable {
|
|||||||
let controller = PlayerViewController()
|
let controller = PlayerViewController()
|
||||||
|
|
||||||
controller.video = video
|
controller.video = video
|
||||||
controller.playbackState = playbackState
|
controller.playback = playback
|
||||||
|
controller.api = api
|
||||||
|
|
||||||
|
controller.resolution = Defaults[.quality]
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,13 @@ import SwiftUI
|
|||||||
final class PlayerViewController: UIViewController {
|
final class PlayerViewController: UIViewController {
|
||||||
var video: Video!
|
var video: Video!
|
||||||
|
|
||||||
|
var api: InvidiousAPI!
|
||||||
var playerLoaded = false
|
var playerLoaded = false
|
||||||
var player = AVPlayer()
|
var player = AVPlayer()
|
||||||
var playerState: PlayerState!
|
var playerModel: PlayerModel!
|
||||||
var playbackState: PlaybackState!
|
var playback: PlaybackModel!
|
||||||
var playerViewController = AVPlayerViewController()
|
var playerViewController = AVPlayerViewController()
|
||||||
|
var resolution: Stream.ResolutionSetting!
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
@ -22,7 +24,7 @@ final class PlayerViewController: UIViewController {
|
|||||||
|
|
||||||
override func viewDidDisappear(_ animated: Bool) {
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if !playerState.playingOutsideViewController {
|
if !playerModel.playingOutsideViewController {
|
||||||
playerViewController.player?.replaceCurrentItem(with: nil)
|
playerViewController.player?.replaceCurrentItem(with: nil)
|
||||||
playerViewController.player = nil
|
playerViewController.player = nil
|
||||||
|
|
||||||
@ -34,15 +36,15 @@ final class PlayerViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadPlayer() {
|
func loadPlayer() {
|
||||||
playerState = PlayerState(playbackState: playbackState)
|
playerModel = PlayerModel(playback: playback, api: api, resolution: resolution)
|
||||||
|
|
||||||
guard !playerLoaded else {
|
guard !playerLoaded else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
playerState.player = player
|
playerModel.player = player
|
||||||
playerViewController.player = playerState.player
|
playerViewController.player = playerModel.player
|
||||||
playerState.loadVideo(video)
|
playerModel.loadVideo(video)
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
present(playerViewController, animated: false)
|
present(playerViewController, animated: false)
|
||||||
@ -95,7 +97,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {
|
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {
|
||||||
playerState.playingOutsideViewController = false
|
playerModel.playingOutsideViewController = false
|
||||||
dismiss(animated: false)
|
dismiss(animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +105,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
_: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||||
) {
|
) {
|
||||||
playerState.playingOutsideViewController = true
|
playerModel.playingOutsideViewController = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerViewController(
|
func playerViewController(
|
||||||
@ -112,7 +114,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
) {
|
) {
|
||||||
coordinator.animate(alongsideTransition: nil) { context in
|
coordinator.animate(alongsideTransition: nil) { context in
|
||||||
if !context.isCancelled {
|
if !context.isCancelled {
|
||||||
self.playerState.playingOutsideViewController = false
|
self.playerModel.playingOutsideViewController = false
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if self.traitCollection.verticalSizeClass == .compact {
|
if self.traitCollection.verticalSizeClass == .compact {
|
||||||
@ -124,10 +126,10 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
|
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
|
||||||
playerState.playingOutsideViewController = true
|
playerModel.playingOutsideViewController = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
|
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
|
||||||
playerState.playingOutsideViewController = false
|
playerModel.playingOutsideViewController = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoDetails: View {
|
struct VideoDetails: View {
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
@State private var subscribed = false
|
@State private var subscribed = false
|
||||||
@State private var confirmationShown = false
|
@State private var confirmationShown = false
|
||||||
@ -186,6 +186,6 @@ struct VideoDetails: View {
|
|||||||
struct VideoDetails_Previews: PreviewProvider {
|
struct VideoDetails_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VideoDetails(video: Video.fixture)
|
VideoDetails(video: Video.fixture)
|
||||||
.environmentObject(Subscriptions())
|
.environmentObject(SubscriptionsModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,31 +12,31 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@StateObject private var store = Store<Video>()
|
||||||
@EnvironmentObject<PlaybackState> private var playbackState
|
|
||||||
|
|
||||||
@ObservedObject private var store = Store<Video>()
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<PlaybackModel> private var playback
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource {
|
||||||
InvidiousAPI.shared.video(video.id)
|
api.video(video.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
var video: Video
|
var video: Video
|
||||||
|
|
||||||
init(_ video: Video) {
|
init(_ video: Video) {
|
||||||
self.video = video
|
self.video = video
|
||||||
resource.addObserver(store)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
Player(video: video)
|
Player(video: video)
|
||||||
.environmentObject(playbackState)
|
.environmentObject(playback)
|
||||||
#else
|
#else
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@ -49,8 +49,8 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
Player(video: video)
|
Player(video: video)
|
||||||
.environmentObject(playbackState)
|
.environmentObject(playback)
|
||||||
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: playbackState.aspectRatio))
|
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: playback.aspectRatio))
|
||||||
}
|
}
|
||||||
.background(.black)
|
.background(.black)
|
||||||
|
|
||||||
@ -73,13 +73,13 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.modifier(VideoDetailsPaddingModifier(geometry: geometry, aspectRatio: playbackState.aspectRatio))
|
.modifier(VideoDetailsPaddingModifier(geometry: geometry, aspectRatio: playback.aspectRatio))
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.2), value: playbackState.aspectRatio)
|
.animation(.linear(duration: 0.2), value: playback.aspectRatio)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
resource.addObserver(store)
|
||||||
resource.loadIfNeeded()
|
resource.loadIfNeeded()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
@ -109,7 +109,7 @@ struct VideoPlayerView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: .constant(true)) {
|
.sheet(isPresented: .constant(true)) {
|
||||||
VideoPlayerView(Video.fixture)
|
VideoPlayerView(Video.fixture)
|
||||||
.environmentObject(NavigationState())
|
.environmentObject(NavigationModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlaylistFormView: View {
|
struct PlaylistFormView: View {
|
||||||
|
@Binding var playlist: Playlist!
|
||||||
|
|
||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var visibility = Playlist.Visibility.public
|
@State private var visibility = Playlist.Visibility.public
|
||||||
|
|
||||||
@ -10,11 +12,10 @@ struct PlaylistFormView: View {
|
|||||||
|
|
||||||
@FocusState private var focused: Bool
|
@FocusState private var focused: Bool
|
||||||
|
|
||||||
@Binding var playlist: Playlist!
|
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject<Playlists> private var playlists
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
|
||||||
var editing: Bool {
|
var editing: Bool {
|
||||||
playlist != nil
|
playlist != nil
|
||||||
@ -33,6 +34,8 @@ struct PlaylistFormView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}.keyboardShortcut(.cancelAction)
|
}.keyboardShortcut(.cancelAction)
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
Form {
|
Form {
|
||||||
TextField("Name", text: $name, onCommit: validate)
|
TextField("Name", text: $name, onCommit: validate)
|
||||||
.frame(maxWidth: 450)
|
.frame(maxWidth: 450)
|
||||||
@ -46,8 +49,7 @@ struct PlaylistFormView: View {
|
|||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
}
|
}
|
||||||
Divider()
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
HStack {
|
HStack {
|
||||||
if editing {
|
if editing {
|
||||||
deletePlaylistButton
|
deletePlaylistButton
|
||||||
@ -59,11 +61,14 @@ struct PlaylistFormView: View {
|
|||||||
.disabled(!valid)
|
.disabled(!valid)
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
}
|
}
|
||||||
|
.frame(minHeight: 35)
|
||||||
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
.onChange(of: name) { _ in validate() }
|
.onChange(of: name) { _ in validate() }
|
||||||
.onAppear(perform: initializeForm)
|
.onAppear(perform: initializeForm)
|
||||||
.padding(.horizontal)
|
#if os(iOS)
|
||||||
#if !os(iOS)
|
.padding(.vertical)
|
||||||
|
#else
|
||||||
.frame(width: 400, height: 150)
|
.frame(width: 400, height: 150)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -141,14 +146,14 @@ struct PlaylistFormView: View {
|
|||||||
playlist = modifiedPlaylist
|
playlist = modifiedPlaylist
|
||||||
}
|
}
|
||||||
|
|
||||||
playlists.reload()
|
playlists.load(force: true)
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource {
|
||||||
editing ? InvidiousAPI.shared.playlist(playlist.id) : InvidiousAPI.shared.playlists
|
editing ? api.playlist(playlist.id) : api.playlists
|
||||||
}
|
}
|
||||||
|
|
||||||
var visibilityButton: some View {
|
var visibilityButton: some View {
|
||||||
@ -189,9 +194,9 @@ struct PlaylistFormView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deletePlaylistAndDismiss() {
|
func deletePlaylistAndDismiss() {
|
||||||
let resource = InvidiousAPI.shared.playlist(playlist.id)
|
api.playlist(playlist.id).request(.delete).onSuccess { _ in
|
||||||
resource.request(.delete).onSuccess { _ in
|
|
||||||
playlist = nil
|
playlist = nil
|
||||||
|
playlists.load(force: true)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,9 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlaylistsView: View {
|
struct PlaylistsView: View {
|
||||||
@ObservedObject private var store = Store<[Playlist]>()
|
@StateObject private var store = Store<[Playlist]>()
|
||||||
|
|
||||||
@Default(.selectedPlaylistID) private var selectedPlaylistID
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
@State private var showingNewPlaylist = false
|
@State private var showingNewPlaylist = false
|
||||||
@State private var createdPlaylist: Playlist?
|
@State private var createdPlaylist: Playlist?
|
||||||
@ -13,27 +13,18 @@ struct PlaylistsView: View {
|
|||||||
@State private var showingEditPlaylist = false
|
@State private var showingEditPlaylist = false
|
||||||
@State private var editedPlaylist: Playlist?
|
@State private var editedPlaylist: Playlist?
|
||||||
|
|
||||||
var resource: Resource {
|
@Default(.selectedPlaylistID) private var selectedPlaylistID
|
||||||
InvidiousAPI.shared.playlists
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
var resource: Resource {
|
||||||
resource.addObserver(store)
|
api.playlists
|
||||||
}
|
}
|
||||||
|
|
||||||
var videos: [Video] {
|
var videos: [Video] {
|
||||||
currentPlaylist?.videos ?? []
|
currentPlaylist?.videos ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
var videosViewMaxHeight: Double {
|
|
||||||
#if os(tvOS)
|
|
||||||
videos.isEmpty ? 150 : .infinity
|
|
||||||
#else
|
|
||||||
videos.isEmpty ? 0 : .infinity
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
SignInRequiredView(title: "Playlists") {
|
||||||
VStack {
|
VStack {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
toolbar
|
toolbar
|
||||||
@ -47,15 +38,7 @@ struct PlaylistsView: View {
|
|||||||
} else {
|
} else {
|
||||||
VideosView(videos: videos)
|
VideosView(videos: videos)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#if os(iOS)
|
|
||||||
toolbar
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.overlay(Divider().offset(x: 0, y: -2), alignment: .topTrailing)
|
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) {
|
.fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) {
|
||||||
@ -85,21 +68,37 @@ struct PlaylistsView: View {
|
|||||||
#endif
|
#endif
|
||||||
newPlaylistButton
|
newPlaylistButton
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
|
Group {
|
||||||
|
if store.collection.isEmpty {
|
||||||
|
Text("No Playlists")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("Current Playlist")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
selectPlaylistButton
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if currentPlaylist != nil {
|
||||||
|
editPlaylistButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
resource.addObserver(store)
|
||||||
|
|
||||||
resource.loadIfNeeded()?.onSuccess { _ in
|
resource.loadIfNeeded()?.onSuccess { _ in
|
||||||
selectPlaylist(selectedPlaylistID)
|
selectPlaylist(selectedPlaylistID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
|
||||||
.navigationTitle("Playlists")
|
|
||||||
#elseif os(iOS)
|
|
||||||
.navigationBarItems(trailing: newPlaylistButton)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
var scaledToolbar: some View {
|
|
||||||
toolbar.scaleEffect(0.85)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var toolbar: some View {
|
var toolbar: some View {
|
||||||
@ -233,6 +232,6 @@ struct PlaylistsView: View {
|
|||||||
struct PlaylistsView_Provider: PreviewProvider {
|
struct PlaylistsView_Provider: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
PlaylistsView()
|
PlaylistsView()
|
||||||
.environmentObject(NavigationState())
|
.environmentObject(NavigationModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
117
Shared/Settings/AccountFormView.swift
Normal file
117
Shared/Settings/AccountFormView.swift
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AccountFormView: View {
|
||||||
|
let instance: Instance
|
||||||
|
var selectedAccount: Binding<Instance.Account?>?
|
||||||
|
|
||||||
|
@State private var name = ""
|
||||||
|
@State private var sid = ""
|
||||||
|
|
||||||
|
@State private var valid = false
|
||||||
|
@State private var validated = false
|
||||||
|
|
||||||
|
@FocusState private var focused: Bool
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Text("Add Account")
|
||||||
|
.font(.title2.bold())
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Form {
|
||||||
|
TextField("Name", text: $name, prompt: Text("Account Name (optional)"))
|
||||||
|
.focused($focused)
|
||||||
|
|
||||||
|
TextField("SID", text: $sid, prompt: Text("Invidious SID Cookie"))
|
||||||
|
}
|
||||||
|
.onAppear(perform: initializeForm)
|
||||||
|
.onChange(of: sid) { _ in validate() }
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
.padding(.horizontal)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||||
|
.foregroundColor(valid ? .green : .red)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(valid ? "Account found" : "Invalid account details")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.opacity(validated ? 1 : 0)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Save", action: submitForm)
|
||||||
|
.disabled(!valid)
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.frame(minHeight: 35)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
.padding(.vertical)
|
||||||
|
#else
|
||||||
|
.frame(width: 400, height: 145)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeForm() {
|
||||||
|
focused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate() {
|
||||||
|
guard !sid.isEmpty else {
|
||||||
|
validator.reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.validateAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitForm() {
|
||||||
|
guard valid else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let account = instances.addAccount(instance: instance, name: name, sid: sid)
|
||||||
|
selectedAccount?.wrappedValue = account
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var validator: InstanceAccountValidator {
|
||||||
|
InstanceAccountValidator(
|
||||||
|
url: instance.url,
|
||||||
|
account: Instance.Account(url: instance.url, sid: sid),
|
||||||
|
formObjectID: $sid,
|
||||||
|
valid: $valid,
|
||||||
|
validated: $validated
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccountFormView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
AccountFormView(instance: Instance.fixture)
|
||||||
|
}
|
||||||
|
}
|
36
Shared/Settings/AccountSettingsView.swift
Normal file
36
Shared/Settings/AccountSettingsView.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AccountSettingsView: View {
|
||||||
|
let instance: Instance
|
||||||
|
let account: Instance.Account
|
||||||
|
@Binding var selectedAccount: Instance.Account?
|
||||||
|
|
||||||
|
@State private var presentingRemovalConfirmationDialog = false
|
||||||
|
|
||||||
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(account.description)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("Remove", role: .destructive) {
|
||||||
|
presentingRemovalConfirmationDialog = true
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
"Are you sure you want to remove \(account.description) account?",
|
||||||
|
isPresented: $presentingRemovalConfirmationDialog
|
||||||
|
) {
|
||||||
|
Button("Remove", role: .destructive) {
|
||||||
|
instances.removeAccount(instance: instance, account: account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.opacity(account == selectedAccount ? 1 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
Shared/Settings/InstanceDetailsSettingsView.swift
Normal file
45
Shared/Settings/InstanceDetailsSettingsView.swift
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InstanceDetailsSettingsView: View {
|
||||||
|
let instanceID: Instance.ID?
|
||||||
|
|
||||||
|
@State private var accountsChanged = false
|
||||||
|
@State private var presentingAccountForm = false
|
||||||
|
|
||||||
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
|
|
||||||
|
var instance: Instance! {
|
||||||
|
instances.find(instanceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section(header: Text("Accounts")) {
|
||||||
|
ForEach(instance.accounts, id: \.self) { account in
|
||||||
|
Text(account.description)
|
||||||
|
#if !os(tvOS)
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||||
|
Button("Remove", role: .destructive) {
|
||||||
|
instances.removeAccount(instance: instance, account: account)
|
||||||
|
accountsChanged.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.redrawOn(change: accountsChanged)
|
||||||
|
|
||||||
|
Button("Add account...") {
|
||||||
|
presentingAccountForm = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
.navigationTitle(instance.shortDescription)
|
||||||
|
.sheet(isPresented: $presentingAccountForm, onDismiss: { accountsChanged.toggle() }) {
|
||||||
|
AccountFormView(instance: instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
126
Shared/Settings/InstanceFormView.swift
Normal file
126
Shared/Settings/InstanceFormView.swift
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InstanceFormView: View {
|
||||||
|
@Binding var savedInstanceID: Instance.ID?
|
||||||
|
|
||||||
|
@State private var name = ""
|
||||||
|
@State private var url = ""
|
||||||
|
|
||||||
|
@State private var valid = false
|
||||||
|
@State private var validated = false
|
||||||
|
@State private var validationError: String?
|
||||||
|
|
||||||
|
@FocusState private var nameFieldFocused: Bool
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@EnvironmentObject<InstancesModel> private var instancesModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Text("Add Instance")
|
||||||
|
.font(.title2.bold())
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Form {
|
||||||
|
TextField("Name", text: $name, prompt: Text("Instance Name (optional)"))
|
||||||
|
.frame(maxWidth: 450)
|
||||||
|
.focused($nameFieldFocused)
|
||||||
|
|
||||||
|
TextField("URL", text: $url, prompt: Text("https://invidious.home.net"))
|
||||||
|
.frame(maxWidth: 450)
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.padding(.horizontal)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||||
|
.foregroundColor(valid ? .green : .red)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(valid ? "Connected successfully" : "Connection failed")
|
||||||
|
if !valid {
|
||||||
|
Text(validationError ?? "Unknown Error")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minHeight: 40)
|
||||||
|
}
|
||||||
|
.opacity(validated ? 1 : 0)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Save", action: submitForm)
|
||||||
|
.disabled(!valid)
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.onChange(of: url) { _ in validate() }
|
||||||
|
.onAppear(perform: initializeForm)
|
||||||
|
#if os(iOS)
|
||||||
|
.padding(.vertical)
|
||||||
|
#else
|
||||||
|
.frame(width: 400, height: 150)
|
||||||
|
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var validator: InstanceAccountValidator {
|
||||||
|
InstanceAccountValidator(
|
||||||
|
url: url,
|
||||||
|
formObjectID: $url,
|
||||||
|
valid: $valid,
|
||||||
|
validated: $validated,
|
||||||
|
error: $validationError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate() {
|
||||||
|
valid = false
|
||||||
|
validated = false
|
||||||
|
validationError = nil
|
||||||
|
|
||||||
|
guard !url.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.validateInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeForm() {
|
||||||
|
nameFieldFocused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitForm() {
|
||||||
|
guard valid else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savedInstanceID = instancesModel.add(name: name, url: url).id
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InstanceFormView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
InstanceFormView(savedInstanceID: .constant(nil))
|
||||||
|
}
|
||||||
|
}
|
168
Shared/Settings/InstancesSettingsView.swift
Normal file
168
Shared/Settings/InstancesSettingsView.swift
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InstancesSettingsView: View {
|
||||||
|
@Default(.instances) private var instances
|
||||||
|
@EnvironmentObject<InstancesModel> private var instancesModel
|
||||||
|
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
|
||||||
|
@State private var selectedInstanceID: Instance.ID?
|
||||||
|
@State private var selectedAccount: Instance.Account?
|
||||||
|
|
||||||
|
@State private var presentingAccountForm = false
|
||||||
|
@State private var presentingInstanceForm = false
|
||||||
|
@State private var savedFormInstanceID: Instance.ID?
|
||||||
|
|
||||||
|
@State private var presentingConfirmationDialog = false
|
||||||
|
@State private var presentingInstanceDetails = false
|
||||||
|
|
||||||
|
var selectedInstance: Instance! {
|
||||||
|
instancesModel.find(selectedInstanceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
#if os(iOS)
|
||||||
|
Section(header: instancesHeader) {
|
||||||
|
ForEach(instances, id: \.self) { instance in
|
||||||
|
Button(action: {
|
||||||
|
self.selectedInstanceID = instance.id
|
||||||
|
self.presentingInstanceDetails = true
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(instance.description)
|
||||||
|
Spacer()
|
||||||
|
NavigationLink(
|
||||||
|
isActive: .constant(false),
|
||||||
|
destination: { EmptyView() },
|
||||||
|
label: { EmptyView() }
|
||||||
|
)
|
||||||
|
.frame(maxWidth: 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||||
|
Button("Remove", role: .destructive) {
|
||||||
|
instancesModel.remove(instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Add Instance...") {
|
||||||
|
presentingInstanceForm = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#else
|
||||||
|
Section {
|
||||||
|
Text("Instance")
|
||||||
|
|
||||||
|
if !instances.isEmpty {
|
||||||
|
Picker("Instance", selection: $selectedInstanceID) {
|
||||||
|
ForEach(instances, id: \.url) { instance in
|
||||||
|
Text(instance.description).tag(Optional(instance.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
} else {
|
||||||
|
Text("You have no instances configured")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let instance = selectedInstance {
|
||||||
|
if instance.accounts.isEmpty {
|
||||||
|
Text("You have no accounts for this instance")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("Accounts")
|
||||||
|
List(selection: $selectedAccount) {
|
||||||
|
ForEach(instance.accounts, id: \.self) { account in
|
||||||
|
AccountSettingsView(instance: instance, account: account,
|
||||||
|
selectedAccount: $selectedAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedInstance != nil {
|
||||||
|
HStack {
|
||||||
|
Button("Add Account...") {
|
||||||
|
selectedAccount = nil
|
||||||
|
presentingAccountForm = true
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Remove Instance", role: .destructive) {
|
||||||
|
presentingConfirmationDialog = true
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
"Are you sure you want to remove \(selectedInstance!.description) instance?",
|
||||||
|
isPresented: $presentingConfirmationDialog
|
||||||
|
) {
|
||||||
|
Button("Remove Instance", role: .destructive) {
|
||||||
|
instancesModel.remove(selectedInstance!)
|
||||||
|
selectedInstanceID = instances.last?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Add Instance...") {
|
||||||
|
presentingInstanceForm = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
.onAppear {
|
||||||
|
selectedInstanceID = instances.first?.id
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $presentingAccountForm) {
|
||||||
|
AccountFormView(instance: selectedInstance, selectedAccount: $selectedAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $presentingInstanceForm, onDismiss: setSelectedInstanceToFormInstance) {
|
||||||
|
InstanceFormView(savedInstanceID: $savedFormInstanceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var instancesHeader: some View {
|
||||||
|
Text("Instances").background(instanceDetailsNavigationLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
var instanceDetailsNavigationLink: some View {
|
||||||
|
NavigationLink(
|
||||||
|
isActive: $presentingInstanceDetails,
|
||||||
|
destination: { InstanceDetailsSettingsView(instanceID: selectedInstanceID) },
|
||||||
|
label: { EmptyView() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSelectedInstanceToFormInstance() {
|
||||||
|
if let id = savedFormInstanceID {
|
||||||
|
selectedInstanceID = id
|
||||||
|
savedFormInstanceID = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InstancesSettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
InstancesSettingsView()
|
||||||
|
}
|
||||||
|
}
|
25
Shared/Settings/PlaybackSettingsView.swift
Normal file
25
Shared/Settings/PlaybackSettingsView.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PlaybackSettingsView: View {
|
||||||
|
@Default(.quality) private var quality
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(header: Text("Quality")) {
|
||||||
|
Picker("Quality", selection: $quality) {
|
||||||
|
ForEach(Stream.ResolutionSetting.allCases, id: \.self) { resolution in
|
||||||
|
Text(resolution.description).tag(resolution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
.pickerStyle(.automatic)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
Spacer()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
Shared/Settings/SettingsView.swift
Normal file
66
Shared/Settings/SettingsView.swift
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
private enum Tabs: Hashable {
|
||||||
|
case playback, instances
|
||||||
|
}
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
TabView {
|
||||||
|
Form {
|
||||||
|
InstancesSettingsView()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Instances", systemImage: "server.rack")
|
||||||
|
}
|
||||||
|
.tag(Tabs.instances)
|
||||||
|
|
||||||
|
Form {
|
||||||
|
PlaybackSettingsView()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Playback", systemImage: "play.rectangle.on.rectangle.fill")
|
||||||
|
}
|
||||||
|
.tag(Tabs.playback)
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.frame(width: 400, height: 270)
|
||||||
|
#else
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
InstancesSettingsView()
|
||||||
|
PlaybackSettingsView()
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
.navigationTitle("Settings")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsView()
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(width: 600, height: 300)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ struct TrendingCountry: View {
|
|||||||
static let prompt = "Country Name or Code"
|
static let prompt = "Country Name or Code"
|
||||||
@Binding var selectedCountry: Country?
|
@Binding var selectedCountry: Country?
|
||||||
|
|
||||||
@ObservedObject private var store = Store(Country.allCases)
|
@StateObject private var store = Store(Country.allCases)
|
||||||
|
|
||||||
@State private var query: String = ""
|
@State private var query: String = ""
|
||||||
@State private var selection: Country?
|
@State private var selection: Country?
|
||||||
|
@ -2,18 +2,85 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TrendingView: View {
|
struct TrendingView: View {
|
||||||
|
@StateObject private var store = Store<[Video]>()
|
||||||
|
|
||||||
@State private var category: TrendingCategory = .default
|
@State private var category: TrendingCategory = .default
|
||||||
@State private var country: Country! = .pl
|
@State private var country: Country! = .pl
|
||||||
@State private var selectingCountry = false
|
@State private var presentingCountrySelection = false
|
||||||
|
|
||||||
@ObservedObject private var store = Store<[Video]>()
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource {
|
||||||
InvidiousAPI.shared.trending(category: category, country: country)
|
let resource = api.trending(category: category, country: country)
|
||||||
|
resource.addObserver(store)
|
||||||
|
|
||||||
|
return resource
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .center, spacing: 2) {
|
||||||
|
#if os(tvOS)
|
||||||
|
toolbar
|
||||||
|
.scaleEffect(0.85)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if store.collection.isEmpty {
|
||||||
|
Text("Loading")
|
||||||
|
}
|
||||||
|
|
||||||
|
VideosView(videos: store.collection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.fullScreenCover(isPresented: $presentingCountrySelection) {
|
||||||
|
TrendingCountry(selectedCountry: $country)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
.sheet(isPresented: $presentingCountrySelection) {
|
||||||
|
TrendingCountry(selectedCountry: $country)
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(minWidth: 400, minHeight: 400)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.navigationTitle("Trending")
|
||||||
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup {
|
||||||
|
categoryButton
|
||||||
|
countryButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#elseif os(iOS)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
|
Group {
|
||||||
|
HStack {
|
||||||
|
Text("Category")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
categoryButton
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Country")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
countryButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
.onChange(of: resource) { resource in
|
||||||
|
resource.load()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
resource.addObserver(store)
|
resource.addObserver(store)
|
||||||
|
resource.loadIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var toolbar: some View {
|
var toolbar: some View {
|
||||||
@ -38,67 +105,21 @@ struct TrendingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Section {
|
|
||||||
VStack(alignment: .center, spacing: 2) {
|
|
||||||
#if os(tvOS)
|
|
||||||
toolbar
|
|
||||||
.scaleEffect(0.85)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
VideosView(videos: store.collection)
|
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
toolbar
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.overlay(Divider().offset(x: 0, y: -2), alignment: .topTrailing)
|
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if os(tvOS)
|
|
||||||
.fullScreenCover(isPresented: $selectingCountry, onDismiss: { setCountry(country) }) {
|
|
||||||
TrendingCountry(selectedCountry: $country)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
.sheet(isPresented: $selectingCountry, onDismiss: { setCountry(country) }) {
|
|
||||||
TrendingCountry(selectedCountry: $country)
|
|
||||||
#if os(macOS)
|
|
||||||
.frame(minWidth: 400, minHeight: 400)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
.navigationTitle("Trending")
|
|
||||||
#endif
|
|
||||||
#if os(macOS)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItemGroup {
|
|
||||||
categoryButton
|
|
||||||
countryButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
.onAppear {
|
|
||||||
resource.loadIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var categoryButton: some View {
|
var categoryButton: some View {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
Button(category.name) {
|
Button(category.name) {
|
||||||
setCategory(category.next())
|
self.category = category.next()
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
ForEach(TrendingCategory.allCases) { category in
|
ForEach(TrendingCategory.allCases) { category in
|
||||||
Button(category.name) { setCategory(category) }
|
Button(category.name) { self.category = category }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
Menu(category.name) {
|
Menu(category.name) {
|
||||||
ForEach(TrendingCategory.allCases) { category in
|
ForEach(TrendingCategory.allCases) { category in
|
||||||
Button(action: { setCategory(category) }) {
|
Button(action: { self.category = category }) {
|
||||||
if category == self.category {
|
if category == self.category {
|
||||||
Label(category.name, systemImage: "checkmark")
|
Label(category.name, systemImage: "checkmark")
|
||||||
} else {
|
} else {
|
||||||
@ -112,30 +133,17 @@ struct TrendingView: View {
|
|||||||
|
|
||||||
var countryButton: some View {
|
var countryButton: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
selectingCountry.toggle()
|
presentingCountrySelection.toggle()
|
||||||
resource.removeObservers(ownedBy: store)
|
resource.removeObservers(ownedBy: store)
|
||||||
}) {
|
}) {
|
||||||
Text("\(country.flag) \(country.id)")
|
Text("\(country.flag) \(country.id)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func setCategory(_ category: TrendingCategory) {
|
|
||||||
resource.removeObservers(ownedBy: store)
|
|
||||||
self.category = category
|
|
||||||
resource.addObserver(store)
|
|
||||||
resource.loadIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func setCountry(_ country: Country) {
|
|
||||||
self.country = country
|
|
||||||
resource.addObserver(store)
|
|
||||||
resource.loadIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TrendingView_Previews: PreviewProvider {
|
struct TrendingView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
TrendingView()
|
TrendingView()
|
||||||
.environmentObject(NavigationState())
|
.environmentObject(NavigationModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoView: View {
|
struct VideoView: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
@ -21,7 +21,7 @@ struct VideoView: View {
|
|||||||
content
|
content
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(action: { navigationState.playVideo(video) }) {
|
Button(action: { navigation.playVideo(video) }) {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,7 +125,7 @@ struct VideoView: View {
|
|||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
thumbnail
|
thumbnail
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
videoDetail(video.title, lineLimit: additionalDetailsAvailable ? 2 : 3)
|
videoDetail(video.title, lineLimit: additionalDetailsAvailable ? 2 : 3)
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.frame(minHeight: additionalDetailsAvailable ? 80 : 120, alignment: .top)
|
.frame(minHeight: additionalDetailsAvailable ? 80 : 120, alignment: .top)
|
||||||
@ -155,7 +155,9 @@ struct VideoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: 30, alignment: .top)
|
.frame(minHeight: 30, alignment: .top)
|
||||||
|
#if os(tvOS)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading)
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading)
|
||||||
|
@ -20,7 +20,7 @@ struct VideosCellsHorizontal: View {
|
|||||||
.padding(.trailing, 20)
|
.padding(.trailing, 20)
|
||||||
.padding(.bottom, 40)
|
.padding(.bottom, 40)
|
||||||
#else
|
#else
|
||||||
.frame(maxWidth: 300)
|
.frame(width: 300)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,6 +32,11 @@ struct VideosCellsHorizontal: View {
|
|||||||
.padding(.vertical, 20)
|
.padding(.vertical, 20)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
if let video = videos.first {
|
||||||
|
scrollView.scrollTo(video.id, anchor: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
.onChange(of: videos) { [videos] newVideos in
|
.onChange(of: videos) { [videos] newVideos in
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
guard !videos.isEmpty, let video = newVideos.first else {
|
guard !videos.isEmpty, let video = newVideos.first else {
|
||||||
@ -45,7 +50,7 @@ struct VideosCellsHorizontal: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.frame(height: 560)
|
.frame(height: 560)
|
||||||
#else
|
#else
|
||||||
.frame(height: 320)
|
.frame(height: 280)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
@ -55,7 +60,7 @@ struct VideosCellsHorizontal: View {
|
|||||||
struct VideoCellsHorizontal_Previews: PreviewProvider {
|
struct VideoCellsHorizontal_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VideosCellsHorizontal(videos: Video.allFixtures)
|
VideosCellsHorizontal(videos: Video.allFixtures)
|
||||||
.environmentObject(NavigationState())
|
.environmentObject(NavigationModel())
|
||||||
.environmentObject(Subscriptions())
|
.environmentObject(SubscriptionsModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,6 @@ struct VideoCellsView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VideosView(videos: Video.allFixtures)
|
VideosView(videos: Video.allFixtures)
|
||||||
.frame(minWidth: 1000)
|
.frame(minWidth: 1000)
|
||||||
.environmentObject(NavigationState())
|
.environmentObject(NavigationModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideosView: View {
|
struct VideosView: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@Default(.layout) private var layout
|
@Default(.layout) private var layout
|
||||||
|
@ -4,8 +4,11 @@ import SwiftUI
|
|||||||
struct ChannelVideosView: View {
|
struct ChannelVideosView: View {
|
||||||
let channel: Channel
|
let channel: Channel
|
||||||
|
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@StateObject private var store = Store<Channel>()
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
@Environment(\.inNavigationView) private var inNavigationView
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
|
||||||
@ -14,16 +17,8 @@ struct ChannelVideosView: View {
|
|||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ObservedObject private var store = Store<Channel>()
|
|
||||||
|
|
||||||
@Namespace private var focusNamespace
|
@Namespace private var focusNamespace
|
||||||
|
|
||||||
init(_ channel: Channel) {
|
|
||||||
self.channel = channel
|
|
||||||
|
|
||||||
resource.addObserver(store)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@ -55,12 +50,11 @@ struct ChannelVideosView: View {
|
|||||||
#endif
|
#endif
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: subscriptionToolbarItemPlacement) {
|
ToolbarItem {
|
||||||
HStack {
|
HStack {
|
||||||
if let channel = store.item, let subscribers = channel.subscriptionsString {
|
Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers")
|
||||||
Text("**\(subscribers)** subscribers")
|
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||||
|
|
||||||
subscriptionToggleButton
|
subscriptionToggleButton
|
||||||
}
|
}
|
||||||
@ -77,13 +71,19 @@ struct ChannelVideosView: View {
|
|||||||
#endif
|
#endif
|
||||||
.modifier(UnsubscribeAlertModifier())
|
.modifier(UnsubscribeAlertModifier())
|
||||||
.onAppear {
|
.onAppear {
|
||||||
resource.loadIfNeeded()
|
if store.item.isNil {
|
||||||
|
resource.addObserver(store)
|
||||||
|
resource.load()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(navigationTitle)
|
.navigationTitle(navigationTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource {
|
||||||
InvidiousAPI.shared.channel(channel.id)
|
let resource = api.channel(channel.id)
|
||||||
|
resource.addObserver(store)
|
||||||
|
|
||||||
|
return resource
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@ -94,7 +94,7 @@ struct ChannelVideosView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return .status
|
return .automatic
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -102,12 +102,12 @@ struct ChannelVideosView: View {
|
|||||||
Group {
|
Group {
|
||||||
if subscriptions.isSubscribing(channel.id) {
|
if subscriptions.isSubscribing(channel.id) {
|
||||||
Button("Unsubscribe") {
|
Button("Unsubscribe") {
|
||||||
navigationState.presentUnsubscribeAlert(channel)
|
navigation.presentUnsubscribeAlert(channel)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button("Subscribe") {
|
Button("Subscribe") {
|
||||||
subscriptions.subscribe(channel.id) {
|
subscriptions.subscribe(channel.id) {
|
||||||
navigationState.sidebarSectionChanged.toggle()
|
navigation.sidebarSectionChanged.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,22 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PopularView: View {
|
struct PopularView: View {
|
||||||
@ObservedObject private var store = Store<[Video]>()
|
@StateObject private var store = Store<[Video]>()
|
||||||
|
|
||||||
var resource = InvidiousAPI.shared.popular
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
init() {
|
var resource: Resource {
|
||||||
resource.addObserver(store)
|
api.popular
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VideosView(videos: store.collection)
|
VideosView(videos: store.collection)
|
||||||
|
.onAppear {
|
||||||
|
resource.addObserver(store)
|
||||||
|
resource.loadIfNeeded()
|
||||||
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.navigationTitle("Popular")
|
.navigationTitle("Popular")
|
||||||
#endif
|
#endif
|
||||||
.onAppear {
|
|
||||||
resource.loadIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ struct SearchView: View {
|
|||||||
@Default(.searchDuration) private var searchDuration
|
@Default(.searchDuration) private var searchDuration
|
||||||
|
|
||||||
@EnvironmentObject<Recents> private var recents
|
@EnvironmentObject<Recents> private var recents
|
||||||
@EnvironmentObject<SearchState> private var state
|
@EnvironmentObject<SearchModel> private var state
|
||||||
|
|
||||||
@Environment(\.navigationStyle) private var navigationStyle
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ struct SearchView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.opacity(recentsChanged ? 1 : 1)
|
.redrawOn(change: recentsChanged)
|
||||||
|
|
||||||
clearAllButton
|
clearAllButton
|
||||||
}
|
}
|
||||||
|
73
Shared/Views/SignInRequiredView.swift
Normal file
73
Shared/Views/SignInRequiredView.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SignInRequiredView<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@Default(.instances) private var instances
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
|
||||||
|
init(title: String, @ViewBuilder content: @escaping () -> Content) {
|
||||||
|
self.title = title
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if api.signedIn {
|
||||||
|
content
|
||||||
|
} else {
|
||||||
|
prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.navigationTitle(title)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var prompt: some View {
|
||||||
|
VStack(spacing: 30) {
|
||||||
|
Text("Sign In Required")
|
||||||
|
.font(.title2.bold())
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if instances.isEmpty {
|
||||||
|
Text("You need to create an instance and accounts\nto access **\(title)** section")
|
||||||
|
} else {
|
||||||
|
Text("You need to select an account\nto access **\(title)** section")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.title3)
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
if instances.isEmpty {
|
||||||
|
openSettingsButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var openSettingsButton: some View {
|
||||||
|
Button(action: {
|
||||||
|
#if os(macOS)
|
||||||
|
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||||
|
#else
|
||||||
|
navigation.presentingSettings = true
|
||||||
|
#endif
|
||||||
|
}) {
|
||||||
|
Text("Open Settings")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SignInRequiredView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SignInRequiredView(title: "Subscriptions") {
|
||||||
|
Text("Only when signed in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,46 @@
|
|||||||
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SubscriptionsView: View {
|
struct SubscriptionsView: View {
|
||||||
@ObservedObject private var store = Store<[Video]>()
|
@StateObject private var store = Store<[Video]>()
|
||||||
|
|
||||||
var resource = InvidiousAPI.shared.feed
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
init() {
|
var feed: Resource {
|
||||||
resource.addObserver(store)
|
api.feed
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
SignInRequiredView(title: "Subscriptions") {
|
||||||
VideosView(videos: store.collection)
|
VideosView(videos: store.collection)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if let home = InvidiousAPI.shared.home.loadIfNeeded() {
|
loadResources()
|
||||||
home.onSuccess { _ in
|
|
||||||
resource.loadIfNeeded()
|
|
||||||
}
|
}
|
||||||
} else {
|
.onChange(of: api.account) { _ in
|
||||||
resource.loadIfNeeded()
|
loadResources(force: true)
|
||||||
|
}
|
||||||
|
.onChange(of: feed) { _ in
|
||||||
|
loadResources(force: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
resource.load()
|
loadResources(force: true)
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
}
|
||||||
.navigationTitle("Subscriptions")
|
|
||||||
#endif
|
fileprivate func loadResources(force: Bool = false) {
|
||||||
|
feed.addObserver(store)
|
||||||
|
|
||||||
|
if let request = force ? api.home.load() : api.home.loadIfNeeded() {
|
||||||
|
request.onSuccess { _ in
|
||||||
|
loadFeed(force: force)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadFeed(force: force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func loadFeed(force: Bool = false) {
|
||||||
|
_ = force ? feed.load() : feed.loadIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,23 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoContextMenuView: View {
|
struct VideoContextMenuView: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<Recents> private var recents
|
@EnvironmentObject<Recents> private var recents
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
let video: Video
|
let video: Video
|
||||||
|
|
||||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||||
@Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist
|
@Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist
|
||||||
|
|
||||||
@State private var subscribed = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
openChannelButton
|
openChannelButton
|
||||||
|
|
||||||
subscriptionButton
|
subscriptionButton
|
||||||
.opacity(subscribed ? 1 : 1)
|
|
||||||
|
|
||||||
if navigationState.tabSelection == .playlists {
|
if navigation.tabSelection == .playlists {
|
||||||
removeFromPlaylistButton
|
removeFromPlaylistButton
|
||||||
} else {
|
} else {
|
||||||
addToPlaylistButton
|
addToPlaylistButton
|
||||||
@ -32,9 +30,9 @@ struct VideoContextMenuView: View {
|
|||||||
Button("\(video.author) Channel") {
|
Button("\(video.author) Channel") {
|
||||||
let recent = RecentItem(from: video.channel)
|
let recent = RecentItem(from: video.channel)
|
||||||
recents.open(recent)
|
recents.open(recent)
|
||||||
navigationState.tabSelection = .recentlyOpened(recent.tag)
|
navigation.tabSelection = .recentlyOpened(recent.tag)
|
||||||
navigationState.isChannelOpen = true
|
navigation.isChannelOpen = true
|
||||||
navigationState.sidebarSectionChanged.toggle()
|
navigation.sidebarSectionChanged.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,13 +43,13 @@ struct VideoContextMenuView: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
subscriptions.unsubscribe(video.channel.id)
|
subscriptions.unsubscribe(video.channel.id)
|
||||||
#else
|
#else
|
||||||
navigationState.presentUnsubscribeAlert(video.channel)
|
navigation.presentUnsubscribeAlert(video.channel)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button("Subscribe") {
|
Button("Subscribe") {
|
||||||
subscriptions.subscribe(video.channel.id) {
|
subscriptions.subscribe(video.channel.id) {
|
||||||
navigationState.sidebarSectionChanged.toggle()
|
navigation.sidebarSectionChanged.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,9 +65,9 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
var removeFromPlaylistButton: some View {
|
var removeFromPlaylistButton: some View {
|
||||||
Button("Remove from playlist", role: .destructive) {
|
Button("Remove from playlist", role: .destructive) {
|
||||||
let resource = InvidiousAPI.shared.playlistVideo(Defaults[.selectedPlaylistID]!, video.indexID!)
|
let resource = api.playlistVideo(Defaults[.selectedPlaylistID]!, video.indexID!)
|
||||||
resource.request(.delete).onSuccess { _ in
|
resource.request(.delete).onSuccess { _ in
|
||||||
InvidiousAPI.shared.playlists.load()
|
api.playlists.load()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import Siesta
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct WatchNowPlaylistSection: View {
|
|
||||||
@ObservedObject private var store = Store<Playlist>()
|
|
||||||
|
|
||||||
let id: String
|
|
||||||
|
|
||||||
var resource: Resource {
|
|
||||||
InvidiousAPI.shared.playlist(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(id: String) {
|
|
||||||
self.id = id
|
|
||||||
|
|
||||||
resource.addObserver(store)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
WatchNowSectionBody(label: store.item?.title ?? "Loading", videos: store.item?.videos ?? [])
|
|
||||||
.onAppear {
|
|
||||||
resource.loadIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +1,28 @@
|
|||||||
|
import Defaults
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct WatchNowSection: View {
|
struct WatchNowSection: View {
|
||||||
@ObservedObject private var store = Store<[Video]>()
|
|
||||||
|
|
||||||
let resource: Resource
|
let resource: Resource
|
||||||
let label: String
|
let label: String
|
||||||
|
|
||||||
|
@StateObject private var store = Store<[Video]>()
|
||||||
|
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
init(resource: Resource, label: String) {
|
init(resource: Resource, label: String) {
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
self.label = label
|
self.label = label
|
||||||
|
|
||||||
self.resource.addObserver(store)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WatchNowSectionBody(label: label, videos: store.collection)
|
WatchNowSectionBody(label: label, videos: store.collection)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
resource.loadIfNeeded()
|
resource.addObserver(store)
|
||||||
|
resource.load()
|
||||||
|
}
|
||||||
|
.onChange(of: api.account) { _ in
|
||||||
|
resource.load()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
|
import Defaults
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct WatchNowView: View {
|
struct WatchNowView: View {
|
||||||
init() {
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
InvidiousAPI.shared.home.loadIfNeeded()
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
if api.validInstance {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
WatchNowSection(resource: InvidiousAPI.shared.feed, label: "Subscriptions")
|
if api.signedIn {
|
||||||
WatchNowSection(resource: InvidiousAPI.shared.popular, label: "Popular")
|
WatchNowSection(resource: api.feed, label: "Subscriptions")
|
||||||
WatchNowSection(resource: InvidiousAPI.shared.trending(category: .default, country: .pl), label: "Trending")
|
}
|
||||||
WatchNowSection(resource: InvidiousAPI.shared.trending(category: .movies, country: .pl), label: "Movies")
|
WatchNowSection(resource: api.popular, label: "Popular")
|
||||||
WatchNowSection(resource: InvidiousAPI.shared.trending(category: .music, country: .pl), label: "Music")
|
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
|
||||||
// ===================
|
// ===================
|
||||||
@ -21,6 +24,7 @@ struct WatchNowView: View {
|
|||||||
// WatchNowSection(resource: InvidiousAPI.shared.channelVideos("UCBJycsmduvYEL83R_U4JriQ"), label: "MKBHD")
|
// WatchNowSection(resource: InvidiousAPI.shared.channelVideos("UCBJycsmduvYEL83R_U4JriQ"), label: "MKBHD")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
#else
|
#else
|
||||||
@ -36,7 +40,7 @@ struct WatchNowView: View {
|
|||||||
struct WatchNowView_Previews: PreviewProvider {
|
struct WatchNowView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
WatchNowView()
|
WatchNowView()
|
||||||
.environmentObject(Subscriptions())
|
.environmentObject(SubscriptionsModel())
|
||||||
.environmentObject(NavigationState())
|
.environmentObject(NavigationModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct Player: NSViewControllerRepresentable {
|
struct Player: NSViewControllerRepresentable {
|
||||||
@EnvironmentObject<PlaybackState> private var playbackState
|
|
||||||
|
|
||||||
var video: Video!
|
var video: Video!
|
||||||
|
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
@EnvironmentObject<PlaybackModel> private var playback
|
||||||
|
|
||||||
func makeNSViewController(context _: Context) -> PlayerViewController {
|
func makeNSViewController(context _: Context) -> PlayerViewController {
|
||||||
let controller = PlayerViewController()
|
let controller = PlayerViewController()
|
||||||
|
|
||||||
controller.video = video
|
controller.video = video
|
||||||
controller.playbackState = playbackState
|
controller.playback = playback
|
||||||
|
controller.api = api
|
||||||
|
|
||||||
|
controller.resolution = Defaults[.quality]
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
@ -4,36 +4,40 @@ import SwiftUI
|
|||||||
final class PlayerViewController: NSViewController {
|
final class PlayerViewController: NSViewController {
|
||||||
var video: Video!
|
var video: Video!
|
||||||
|
|
||||||
|
var api: InvidiousAPI!
|
||||||
var player = AVPlayer()
|
var player = AVPlayer()
|
||||||
var playerState: PlayerState!
|
var playerModel: PlayerModel!
|
||||||
var playbackState: PlaybackState!
|
var playback: PlaybackModel!
|
||||||
var playerView = AVPlayerView()
|
var playerView = AVPlayerView()
|
||||||
|
var resolution: Stream.ResolutionSetting!
|
||||||
|
|
||||||
override func viewDidDisappear() {
|
override func viewDidDisappear() {
|
||||||
playerView.player?.replaceCurrentItem(with: nil)
|
playerView.player?.replaceCurrentItem(with: nil)
|
||||||
playerView.player = nil
|
playerView.player = nil
|
||||||
|
|
||||||
playerState.player = nil
|
playerModel.player = nil
|
||||||
playerState = nil
|
playerModel = nil
|
||||||
|
|
||||||
super.viewDidDisappear()
|
super.viewDidDisappear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
playerState = PlayerState(playbackState: playbackState)
|
playerModel = PlayerModel(playback: playback, api: api, resolution: resolution)
|
||||||
|
|
||||||
guard playerState.player == nil else {
|
guard playerModel.player == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
playerState.player = player
|
playerModel.player = player
|
||||||
playerView.player = playerState.player
|
playerView.player = playerModel.player
|
||||||
|
|
||||||
playerView.allowsPictureInPicturePlayback = true
|
playerView.allowsPictureInPicturePlayback = true
|
||||||
playerView.showsFullScreenToggleButton = true
|
playerView.showsFullScreenToggleButton = true
|
||||||
|
|
||||||
view = playerView
|
view = playerView
|
||||||
|
|
||||||
playerState.loadVideo(video)
|
DispatchQueue.main.async {
|
||||||
|
self.playerModel.loadVideo(self.video)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AddToPlaylistView: View {
|
struct AddToPlaylistView: View {
|
||||||
@ObservedObject private var store = Store<[Playlist]>()
|
@StateObject private var store = Store<[Playlist]>()
|
||||||
|
|
||||||
@State private var selectedPlaylist: Playlist?
|
@State private var selectedPlaylist: Playlist?
|
||||||
|
|
||||||
@ -11,8 +11,10 @@ struct AddToPlaylistView: View {
|
|||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
var resource: Resource {
|
var resource: Resource {
|
||||||
InvidiousAPI.shared.playlists
|
api.playlists
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -85,12 +87,12 @@ struct AddToPlaylistView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let resource = InvidiousAPI.shared.playlistVideos(currentPlaylist!.id)
|
let resource = 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
|
||||||
Defaults.reset(.videoIDToAddToPlaylist)
|
Defaults.reset(.videoIDToAddToPlaylist)
|
||||||
InvidiousAPI.shared.playlists.load()
|
api.playlists.load()
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct OptionsView: View {
|
struct OptionsView: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
|
||||||
@Default(.layout) private var layout
|
@Default(.layout) private var layout
|
||||||
|
|
||||||
@ -28,6 +28,8 @@ struct OptionsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
SettingsView()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 800)
|
.frame(maxWidth: 800)
|
||||||
|
|
||||||
@ -42,7 +44,7 @@ struct OptionsView: View {
|
|||||||
|
|
||||||
var tabSelectionOptions: some View {
|
var tabSelectionOptions: some View {
|
||||||
VStack {
|
VStack {
|
||||||
switch navigationState.tabSelection {
|
switch navigation.tabSelection {
|
||||||
case .search:
|
case .search:
|
||||||
SearchOptionsView()
|
SearchOptionsView()
|
||||||
|
|
||||||
|
@ -2,17 +2,17 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TVNavigationView: View {
|
struct TVNavigationView: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlaybackState> private var playbackState
|
@EnvironmentObject<PlaybackModel> private var playback
|
||||||
@EnvironmentObject<Recents> private var recents
|
@EnvironmentObject<Recents> private var recents
|
||||||
@EnvironmentObject<SearchState> private var searchState
|
@EnvironmentObject<SearchModel> private var search
|
||||||
|
|
||||||
@State private var showingOptions = false
|
@State private var showingOptions = false
|
||||||
|
|
||||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $navigationState.tabSelection) {
|
TabView(selection: $navigation.tabSelection) {
|
||||||
WatchNowView()
|
WatchNowView()
|
||||||
.tabItem { Text("Watch Now") }
|
.tabItem { Text("Watch Now") }
|
||||||
.tag(TabSelection.watchNow)
|
.tag(TabSelection.watchNow)
|
||||||
@ -34,30 +34,30 @@ struct TVNavigationView: View {
|
|||||||
.tag(TabSelection.playlists)
|
.tag(TabSelection.playlists)
|
||||||
|
|
||||||
SearchView()
|
SearchView()
|
||||||
.searchable(text: $searchState.queryText) {
|
.searchable(text: $search.queryText) {
|
||||||
ForEach(searchState.querySuggestions.collection, id: \.self) { suggestion in
|
ForEach(search.querySuggestions.collection, id: \.self) { suggestion in
|
||||||
Text(suggestion)
|
Text(suggestion)
|
||||||
.searchCompletion(suggestion)
|
.searchCompletion(suggestion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: searchState.queryText) { newQuery in
|
.onChange(of: search.queryText) { newQuery in
|
||||||
searchState.loadQuerySuggestions(newQuery)
|
search.loadSuggestions(newQuery)
|
||||||
searchState.changeQuery { query in query.query = newQuery }
|
search.changeQuery { query in query.query = newQuery }
|
||||||
}
|
}
|
||||||
.tabItem { Image(systemName: "magnifyingglass") }
|
.tabItem { Image(systemName: "magnifyingglass") }
|
||||||
.tag(TabSelection.search)
|
.tag(TabSelection.search)
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $showingOptions) { OptionsView() }
|
.fullScreenCover(isPresented: $showingOptions) { OptionsView() }
|
||||||
.fullScreenCover(isPresented: $showingAddToPlaylist) { AddToPlaylistView() }
|
.fullScreenCover(isPresented: $showingAddToPlaylist) { AddToPlaylistView() }
|
||||||
.fullScreenCover(isPresented: $navigationState.showingVideo) {
|
.fullScreenCover(isPresented: $navigation.showingVideo) {
|
||||||
if let video = navigationState.video {
|
if let video = navigation.video {
|
||||||
VideoPlayerView(video)
|
VideoPlayerView(video)
|
||||||
.environmentObject(playbackState)
|
.environmentObject(playback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $navigationState.isChannelOpen) {
|
.fullScreenCover(isPresented: $navigation.isChannelOpen) {
|
||||||
if let channel = recents.presentedChannel {
|
if let channel = recents.presentedChannel {
|
||||||
ChannelVideosView(channel)
|
ChannelVideosView(channel: channel)
|
||||||
.background(.thickMaterial)
|
.background(.thickMaterial)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user