mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +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 SwiftyJSON
|
||||
|
||||
final class InvidiousAPI: Service {
|
||||
static let shared = InvidiousAPI()
|
||||
final class InvidiousAPI: Service, ObservableObject {
|
||||
static let basePath = "/api/v1"
|
||||
|
||||
static let instance = "https://invidious.home.arekf.net"
|
||||
@Published var account: Instance.Account!
|
||||
|
||||
static func proxyURLForAsset(_ url: String) -> URL? {
|
||||
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
|
||||
}
|
||||
@Published var validInstance = true
|
||||
@Published var signedIn = true
|
||||
|
||||
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
|
||||
|
||||
let SwiftyJSONTransformer =
|
||||
ResponseContentTransformer(transformErrors: true) { JSON($0.content as AnyObject) }
|
||||
|
||||
super.init(baseURL: "\(InvidiousAPI.instance)/api/v1")
|
||||
|
||||
configure {
|
||||
$0.headers["Cookie"] = self.cookieHeader
|
||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||
}
|
||||
|
||||
configure(requestMethods: [.get]) {
|
||||
$0.headers["Cookie"] = self.authHeader
|
||||
}
|
||||
|
||||
configure("**", requestMethods: [.post]) {
|
||||
$0.headers["Cookie"] = self.authHeader
|
||||
$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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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"] {
|
||||
return suggestions.arrayValue.map(String.init)
|
||||
}
|
||||
@ -59,20 +112,20 @@ final class InvidiousAPI: Service {
|
||||
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)
|
||||
}
|
||||
|
||||
configureTransformer("/auth/playlists/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
|
||||
configureTransformer(pathPattern("auth/playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
|
||||
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
|
||||
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"] {
|
||||
return feedVideos.arrayValue.map(Video.init)
|
||||
}
|
||||
@ -80,69 +133,83 @@ final class InvidiousAPI: Service {
|
||||
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)
|
||||
}
|
||||
|
||||
configureTransformer("/channels/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
||||
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
||||
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)
|
||||
}
|
||||
|
||||
configureTransformer("/videos/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
|
||||
configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
|
||||
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 {
|
||||
resource("/popular")
|
||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/popular")
|
||||
}
|
||||
|
||||
func trending(category: TrendingCategory, country: Country) -> Resource {
|
||||
resource("/trending")
|
||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/trending")
|
||||
.withParam("type", category.name)
|
||||
.withParam("region", country.rawValue)
|
||||
}
|
||||
|
||||
var home: Resource {
|
||||
resource(baseURL: InvidiousAPI.instance, path: "/feed/subscriptions")
|
||||
resource(baseURL: account.url, path: "/feed/subscriptions")
|
||||
}
|
||||
|
||||
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 {
|
||||
resource("/auth/subscriptions")
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
}
|
||||
|
||||
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 {
|
||||
resource("/channels/\(id)")
|
||||
resource(baseURL: account.url, path: basePathAppending("channels/\(id)"))
|
||||
}
|
||||
|
||||
func channelVideos(_ id: String) -> Resource {
|
||||
resource("/channels/\(id)/latest")
|
||||
resource(baseURL: account.url, path: basePathAppending("channels/\(id)/latest"))
|
||||
}
|
||||
|
||||
func video(_ id: String) -> Resource {
|
||||
resource("/videos/\(id)")
|
||||
resource(baseURL: account.url, path: basePathAppending("videos/\(id)"))
|
||||
}
|
||||
|
||||
var playlists: Resource {
|
||||
resource("/auth/playlists")
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||
}
|
||||
|
||||
func playlist(_ id: String) -> Resource {
|
||||
resource("/auth/playlists/\(id)")
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||
}
|
||||
|
||||
func playlistVideos(_ id: String) -> Resource {
|
||||
@ -154,7 +221,7 @@ final class InvidiousAPI: Service {
|
||||
}
|
||||
|
||||
func search(_ query: SearchQuery) -> Resource {
|
||||
var resource = resource("/search")
|
||||
var resource = resource(baseURL: account.url, path: basePathAppending("search"))
|
||||
.withParam("q", searchQuery(query.query))
|
||||
.withParam("sort_by", query.sortBy.parameter)
|
||||
|
||||
@ -170,7 +237,7 @@ final class InvidiousAPI: Service {
|
||||
}
|
||||
|
||||
func searchSuggestions(query: String) -> Resource {
|
||||
resource("/search/suggestions")
|
||||
resource(baseURL: account.url, path: basePathAppending("search/suggestions"))
|
||||
.withParam("q", query.lowercased())
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class NavigationState: ObservableObject {
|
||||
final class NavigationModel: ObservableObject {
|
||||
enum TabSelection: Hashable {
|
||||
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 sidebarSectionChanged = false
|
||||
|
||||
@Published var presentingSettings = false
|
||||
|
||||
func playVideo(_ video: Video) {
|
||||
self.video = video
|
||||
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 Foundation
|
||||
|
||||
final class PlaybackState: ObservableObject {
|
||||
final class PlaybackModel: ObservableObject {
|
||||
@Published var live = false
|
||||
@Published var stream: Stream?
|
||||
@Published var time: CMTime?
|
@ -5,7 +5,7 @@ import Logging
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
final class PlayerState: ObservableObject {
|
||||
final class PlayerModel: ObservableObject {
|
||||
let logger = Logger(label: "net.arekf.Pearvidious.ps")
|
||||
|
||||
var video: Video!
|
||||
@ -19,17 +19,19 @@ final class PlayerState: ObservableObject {
|
||||
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]
|
||||
|
||||
var playbackState: PlaybackState
|
||||
var api: InvidiousAPI
|
||||
var playback: PlaybackModel
|
||||
var timeObserver: Any?
|
||||
|
||||
let maxResolution: Stream.Resolution?
|
||||
let resolution: Stream.ResolutionSetting?
|
||||
|
||||
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.playbackState = playbackState
|
||||
self.maxResolution = maxResolution
|
||||
self.playback = playback
|
||||
self.api = api
|
||||
self.resolution = resolution
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -41,7 +43,7 @@ final class PlayerState: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
playbackState.reset()
|
||||
playback.reset()
|
||||
|
||||
loadExtendedVideoDetails(video) { video in
|
||||
self.video = video
|
||||
@ -54,22 +56,26 @@ final class PlayerState: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
InvidiousAPI.shared.video(video!.id).load().onSuccess { response in
|
||||
api.video(video!.id).load().onSuccess { response in
|
||||
if let video: Video = response.typedContent() {
|
||||
onSuccess(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var requestedResolution: Bool {
|
||||
resolution != nil && resolution != .hd720pFirstThenBest
|
||||
}
|
||||
|
||||
fileprivate func playVideo(_ video: Video) {
|
||||
playbackState.live = video.live
|
||||
playback.live = video.live
|
||||
|
||||
if video.live {
|
||||
playHlsUrl()
|
||||
return
|
||||
}
|
||||
|
||||
let stream = maxResolution != nil ? video.streamWithResolution(maxResolution!) : video.defaultStream
|
||||
let stream = requestedResolution ? video.streamWithResolution(resolution!.value) : video.defaultStream
|
||||
|
||||
guard stream != nil else {
|
||||
return
|
||||
@ -78,7 +84,7 @@ final class PlayerState: ObservableObject {
|
||||
Task {
|
||||
await self.loadStream(stream!)
|
||||
|
||||
if stream != video.bestStream {
|
||||
if resolution == .hd720pFirstThenBest {
|
||||
await self.loadBestStream()
|
||||
}
|
||||
}
|
||||
@ -91,9 +97,7 @@ final class PlayerState: ObservableObject {
|
||||
|
||||
fileprivate func loadStream(_ stream: Stream) async {
|
||||
if stream.oneMeaningfullAsset {
|
||||
DispatchQueue.main.async {
|
||||
self.playStream(stream)
|
||||
}
|
||||
playStream(stream)
|
||||
|
||||
return
|
||||
} else {
|
||||
@ -111,11 +115,11 @@ final class PlayerState: ObservableObject {
|
||||
DispatchQueue.main.async {
|
||||
self.saveTime()
|
||||
self.player?.replaceCurrentItem(with: self.playerItemWithMetadata(for: stream))
|
||||
self.playbackState.stream = stream
|
||||
self.playback.stream = stream
|
||||
if self.timeObserver == nil {
|
||||
self.addTimeObserver()
|
||||
}
|
||||
self.player?.playImmediately(atRate: 1.0)
|
||||
self.player?.play()
|
||||
self.seekToSavedTime()
|
||||
}
|
||||
}
|
||||
@ -267,7 +271,7 @@ final class PlayerState: ObservableObject {
|
||||
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
|
||||
let object = object,
|
||||
let type = object["type"],
|
||||
|
@ -2,30 +2,23 @@ import Defaults
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
final class SearchState: ObservableObject {
|
||||
final class SearchModel: ObservableObject {
|
||||
@Published var store = Store<[Video]>()
|
||||
|
||||
@Published var api: InvidiousAPI!
|
||||
@Published var query = SearchQuery()
|
||||
|
||||
@Published var queryText = ""
|
||||
|
||||
@Published var querySuggestions = Store<[String]>()
|
||||
|
||||
private var previousResource: Resource?
|
||||
private var resource: Resource!
|
||||
|
||||
init() {
|
||||
let newQuery = query
|
||||
query = newQuery
|
||||
|
||||
resource = InvidiousAPI.shared.search(newQuery)
|
||||
}
|
||||
|
||||
var isLoading: Bool {
|
||||
resource.isLoading
|
||||
}
|
||||
|
||||
func loadQuerySuggestions(_ query: String) {
|
||||
let resource = InvidiousAPI.shared.searchSuggestions(query: query)
|
||||
func loadSuggestions(_ query: String) {
|
||||
let resource = api.searchSuggestions(query: query)
|
||||
|
||||
resource.addObserver(querySuggestions)
|
||||
resource.loadIfNeeded()
|
||||
@ -44,7 +37,7 @@ final class SearchState: ObservableObject {
|
||||
func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) {
|
||||
changeHandler(query)
|
||||
|
||||
let newResource = InvidiousAPI.shared.search(query)
|
||||
let newResource = api.search(query)
|
||||
guard newResource != previousResource else {
|
||||
return
|
||||
}
|
||||
@ -60,7 +53,7 @@ final class SearchState: ObservableObject {
|
||||
func resetQuery(_ query: SearchQuery) {
|
||||
self.query = query
|
||||
|
||||
let newResource = InvidiousAPI.shared.search(query)
|
||||
let newResource = api.search(query)
|
||||
guard newResource != previousResource else {
|
||||
return
|
||||
}
|
@ -1,9 +1,32 @@
|
||||
import AVFoundation
|
||||
import Defaults
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable:next final_class
|
||||
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
|
||||
|
||||
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? {
|
||||
selectableStreams.first { $0.resolution == resolution }
|
||||
}
|
||||
|
||||
func defaultStreamForProfile(_ profile: Profile) -> Stream? {
|
||||
streamWithResolution(profile.defaultStreamResolution.value) ?? streams.first
|
||||
selectableStreams.first { $0.resolution == resolution } ?? defaultStream
|
||||
}
|
||||
|
||||
func thumbnailURL(quality: Thumbnail.Quality) -> URL? {
|
||||
|
@ -11,9 +11,9 @@
|
||||
3705B182267B4E4900704544 /* 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 */; };
|
||||
3711403F26B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; };
|
||||
3711404026B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; };
|
||||
3711404126B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; };
|
||||
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||
3711404026B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||
3711404126B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||
3714166F267A8ACC006CA35D /* 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 */; };
|
||||
@ -23,9 +23,9 @@
|
||||
37152EEA26EFEB95004FB96D /* 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 */; };
|
||||
371F2F1A269B43D300E4A7AB /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationState.swift */; };
|
||||
371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationState.swift */; };
|
||||
371F2F1C269B43D300E4A7AB /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationState.swift */; };
|
||||
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||
372915E42687E33E00F5A35B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 372915E32687E33E00F5A35B /* Defaults */; };
|
||||
372915E62687E3B900F5A35B /* 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 */; };
|
||||
3748186F26A769D60084E870 /* 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 */; };
|
||||
3761ABFE26F0F8DE00AA496F /* 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 */; };
|
||||
376578922685490700D4EA09 /* 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 */; };
|
||||
37754C9E26B7500000DBD602 /* 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 */; };
|
||||
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7EC267A0A0800A6BBAF /* SwiftyJSON */; };
|
||||
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 */; };
|
||||
3788AC2826F6840700F6BAA9 /* 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 */; };
|
||||
3788AC2C26F6842D00F6BAA9 /* 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 */; };
|
||||
37977583268922F600DD52A8 /* 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 */; };
|
||||
37AAF2A126741C97007FC770 /* 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 */; };
|
||||
37B17DA1268A1F89006AEE9B /* 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 */; };
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||
37B767E02678C5BF0098BAA8 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 37B767DF2678C5BF0098BAA8 /* Logging */; };
|
||||
37B81AF926D2C9A700675966 /* 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 */; };
|
||||
37B81B0226D2CAE700675966 /* 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 */; };
|
||||
37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
||||
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
||||
37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
||||
37B81B0626D2CEDA00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
||||
37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
||||
37BA793B26DB8EE4002A0235 /* 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 */; };
|
||||
37BA793F26DB8F97002A0235 /* 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 */; };
|
||||
37BA794326DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
|
||||
37BA794426DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
|
||||
37BA794526DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
|
||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* PlaylistsModel.swift */; };
|
||||
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* PlaylistsModel.swift */; };
|
||||
37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* PlaylistsModel.swift */; };
|
||||
37BA794726DC2E56002A0235 /* 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 */; };
|
||||
@ -193,9 +228,6 @@
|
||||
37C7A1D6267BFD9D0010EAD6 /* 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 */; };
|
||||
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 */; };
|
||||
37CEE4BE2677B670005A1EFE /* 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 */; };
|
||||
37D4B19926717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 37D4B19C2671817900C925CA /* SwiftyJSON */; };
|
||||
37E64DD126D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
||||
37E64DD226D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
||||
37E64DD326D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
||||
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||
37EAD86B267B9C5600D9E01B /* 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 */; };
|
||||
@ -233,6 +265,9 @@
|
||||
37F4AE7226828F0900BD60EA /* 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 */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -269,11 +304,11 @@
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -285,17 +320,28 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -307,17 +353,18 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -334,7 +381,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -353,11 +399,12 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -432,6 +479,7 @@
|
||||
371AAE2326CEB9E800901972 /* Navigation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */,
|
||||
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */,
|
||||
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */,
|
||||
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */,
|
||||
@ -497,6 +545,7 @@
|
||||
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
|
||||
37AAF27D26737323007FC770 /* PopularView.swift */,
|
||||
37AAF27F26737550007FC770 /* SearchView.swift */,
|
||||
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */,
|
||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
||||
);
|
||||
@ -515,6 +564,7 @@
|
||||
3748186426A762300084E870 /* Fixtures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */,
|
||||
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */,
|
||||
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */,
|
||||
3748186526A7627F0084E870 /* Video+Fixtures.swift */,
|
||||
@ -522,9 +572,24 @@
|
||||
path = Fixtures;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37F64FE326FE70A60081B69E /* RedrawOnViewModifier.swift */,
|
||||
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */,
|
||||
);
|
||||
path = Modifiers;
|
||||
@ -540,7 +605,6 @@
|
||||
3788AC2126F683AB00F6BAA9 /* Watch Now */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3788AC2226F683DE00F6BAA9 /* WatchNowPlaylistSection.swift */,
|
||||
3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */,
|
||||
3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */,
|
||||
37A9965D26D6F9B9006E3224 /* WatchNowView.swift */,
|
||||
@ -618,6 +682,7 @@
|
||||
371AAE2326CEB9E800901972 /* Navigation */,
|
||||
371AAE2426CEBA4100901972 /* Player */,
|
||||
371AAE2626CEBF1600901972 /* Playlists */,
|
||||
37484C1726FC836500287258 /* Settings */,
|
||||
371AAE2526CEBF0B00901972 /* Trending */,
|
||||
371AAE2726CEBF4700901972 /* Videos */,
|
||||
371AAE2826CEC7D900901972 /* Views */,
|
||||
@ -686,23 +751,25 @@
|
||||
children = (
|
||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||
37141672267A8E10006CA35D /* Country.swift */,
|
||||
378E50FA26FE8B9F00F49626 /* Instance.swift */,
|
||||
37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */,
|
||||
375DFB5726F9DA010013F468 /* InstancesModel.swift */,
|
||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
||||
371F2F19269B43D300E4A7AB /* NavigationState.swift */,
|
||||
37B81B0426D2CEDA00675966 /* PlaybackState.swift */,
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
|
||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||
37B81B0426D2CEDA00675966 /* PlaybackModel.swift */,
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
||||
376578882685471400D4EA09 /* Playlist.swift */,
|
||||
37BA794226DBA973002A0235 /* Playlists.swift */,
|
||||
37C7A1DB267CE9D90010EAD6 /* Profile.swift */,
|
||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||
37C194C626F6A9C8005D3B96 /* Recents.swift */,
|
||||
3711403E26B206A6005B3555 /* SearchModel.swift */,
|
||||
373CFACA26966264003CB2C6 /* SearchQuery.swift */,
|
||||
3711403E26B206A6005B3555 /* SearchState.swift */,
|
||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
||||
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */,
|
||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
||||
3797758A2689345500DD52A8 /* Store.swift */,
|
||||
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
||||
37E64DD026D597EB00C71877 /* Subscriptions.swift */,
|
||||
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */,
|
||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
||||
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
||||
37D4B19626717E1500C925CA /* Video.swift */,
|
||||
@ -1001,19 +1068,24 @@
|
||||
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */,
|
||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
37754C9D26B7500000DBD602 /* VideosView.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 */,
|
||||
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
||||
37484C2126FC83C400287258 /* AccountSettingsView.swift in Sources */,
|
||||
37A9965E26D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37F4AE7226828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
||||
@ -1023,25 +1095,27 @@
|
||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
37E64DD126D597EB00C71877 /* Subscriptions.swift in Sources */,
|
||||
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */,
|
||||
37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */,
|
||||
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||
3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||
373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */,
|
||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||
37BA794326DBA973002A0235 /* Playlists.swift in Sources */,
|
||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||
3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
||||
3788AC2326F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */,
|
||||
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
37484C2D26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */,
|
||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||
37484C3126FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
@ -1050,8 +1124,10 @@
|
||||
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.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 */,
|
||||
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
||||
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
||||
@ -1061,8 +1137,11 @@
|
||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
||||
372915E62687E3B900F5A35B /* Defaults.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 */,
|
||||
378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||
37484C1D26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||
3797758B2689345500DD52A8 /* Store.swift in Sources */,
|
||||
@ -1081,22 +1160,31 @@
|
||||
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
||||
371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */,
|
||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||
37484C1E26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||
37484C2E26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */,
|
||||
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
37484C1A26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||
37484C3226FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||
3788AC2426F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */,
|
||||
37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||
37F64FE526FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
||||
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||
378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */,
|
||||
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
37152EEB26EFEB95004FB96D /* LazyView.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 */,
|
||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
373CFAC32696616C003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
||||
@ -1105,6 +1193,7 @@
|
||||
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
37484C2626FC83E000287258 /* InstanceFormView.swift in Sources */,
|
||||
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||
377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||
@ -1118,10 +1207,9 @@
|
||||
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
|
||||
37E64DD226D597EB00C71877 /* Subscriptions.swift in Sources */,
|
||||
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
37C7A1DD267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||
37754C9E26B7500000DBD602 /* VideosView.swift in Sources */,
|
||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||
@ -1131,16 +1219,19 @@
|
||||
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||
37BD07C12698AD3B003EBB87 /* TrendingCountry.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 */,
|
||||
373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||
376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||
37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||
37BA794426DBA973002A0235 /* Playlists.swift in Sources */,
|
||||
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -1169,9 +1260,11 @@
|
||||
3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||
37BE0BD426A1D47D0092E2DB /* Player.swift in Sources */,
|
||||
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||
37F4AE7426828F0900BD60EA /* VideosCellsVertical.swift in Sources */,
|
||||
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||
37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */,
|
||||
@ -1179,24 +1272,26 @@
|
||||
373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */,
|
||||
37A9965C26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */,
|
||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */,
|
||||
37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */,
|
||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
37E64DD326D597EB00C71877 /* Subscriptions.swift in Sources */,
|
||||
37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||
373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */,
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||
3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
||||
37D4B18E26717B3800C925CA /* VideoView.swift in Sources */,
|
||||
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
||||
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||
37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */,
|
||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
@ -1204,30 +1299,37 @@
|
||||
3748186C26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||
371F2F1C269B43D300E4A7AB /* NavigationState.swift in Sources */,
|
||||
37BA794526DBA973002A0235 /* Playlists.swift in Sources */,
|
||||
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||
37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||
37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37484C2326FC83C400287258 /* AccountSettingsView.swift in Sources */,
|
||||
37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */,
|
||||
37F64FE626FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
||||
37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */,
|
||||
37484C2B26FC83FF00287258 /* AccountFormView.swift in Sources */,
|
||||
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||
3711404126B206A6005B3555 /* SearchState.swift in Sources */,
|
||||
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
||||
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
37141675267A8E10006CA35D /* Country.swift in Sources */,
|
||||
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||
37484C2726FC83E000287258 /* InstanceFormView.swift in Sources */,
|
||||
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
||||
373CFAC42696616C003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
3788AC2526F683DE00F6BAA9 /* WatchNowPlaylistSection.swift in Sources */,
|
||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37484C1B26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||
372915E82687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */,
|
||||
3797758D2689345500DD52A8 /* Store.swift in Sources */,
|
||||
37484C2F26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1,6 +1,15 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.416",
|
||||
"green" : "0.256",
|
||||
"red" : "0.837"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
@ -5,6 +5,8 @@ extension Defaults.Keys {
|
||||
static let layout = Key<ListingLayout>("listingLayout", default: .cells)
|
||||
#endif
|
||||
|
||||
static let instances = Key<[Instance]>("instances", default: [])
|
||||
|
||||
static let searchSortOrder = Key<SearchQuery.SortOrder>("searchSortOrder", default: .relevance)
|
||||
static let searchDate = Key<SearchQuery.Date?>("searchDate")
|
||||
static let searchDuration = Key<SearchQuery.Duration?>("searchDuration")
|
||||
@ -14,6 +16,7 @@ extension Defaults.Keys {
|
||||
static let videoIDToAddToPlaylist = Key<String?>("videoIDToAddToPlaylist")
|
||||
|
||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
struct UnsubscribeAlertModifier: ViewModifier {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) {
|
||||
if let channel = navigationState.channelToUnsubscribe {
|
||||
.alert(unsubscribeAlertTitle, isPresented: $navigation.presentingUnsubscribeAlert) {
|
||||
if let channel = navigation.channelToUnsubscribe {
|
||||
Button("Unsubscribe", role: .destructive) {
|
||||
subscriptions.unsubscribe(channel.id)
|
||||
}
|
||||
@ -17,7 +17,7 @@ struct UnsubscribeAlertModifier: ViewModifier {
|
||||
}
|
||||
|
||||
var unsubscribeAlertTitle: String {
|
||||
if let channel = navigationState.channelToUnsubscribe {
|
||||
if let channel = navigation.channelToUnsubscribe {
|
||||
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<Playlists> private var playlists
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<Recents> private var recents
|
||||
@EnvironmentObject<SearchState> private var searchState
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@State private var didApplyPrimaryViewWorkAround = false
|
||||
|
||||
var selection: Binding<TabSelection?> {
|
||||
navigationState.tabSelectionOptionalBinding
|
||||
navigation.tabSelectionOptionalBinding
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -30,11 +32,10 @@ struct AppSidebarNavigation: View {
|
||||
// workaround for an empty supplementary view on launch
|
||||
// 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
|
||||
// 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 {
|
||||
UIView.performWithoutAnimation {
|
||||
splitVC.show(.primary)
|
||||
splitVC.hide(.primary)
|
||||
}
|
||||
didApplyPrimaryViewWorkAround = true
|
||||
}
|
||||
@ -44,31 +45,33 @@ struct AppSidebarNavigation: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
let sidebarMinWidth: Double = 280
|
||||
|
||||
var content: some View {
|
||||
NavigationView {
|
||||
sidebar
|
||||
.frame(minWidth: 180)
|
||||
.toolbar { toolbarContent }
|
||||
.frame(minWidth: sidebarMinWidth)
|
||||
|
||||
Text("Select section")
|
||||
}
|
||||
.environment(\.navigationStyle, .sidebar)
|
||||
.searchable(text: $searchState.queryText, placement: .sidebar) {
|
||||
ForEach(searchState.querySuggestions.collection, id: \.self) { suggestion in
|
||||
.searchable(text: $search.queryText, placement: .sidebar) {
|
||||
ForEach(search.querySuggestions.collection, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchState.queryText) { query in
|
||||
searchState.loadQuerySuggestions(query)
|
||||
.onChange(of: search.queryText) { query in
|
||||
search.loadSuggestions(query)
|
||||
}
|
||||
.onSubmit(of: .search) {
|
||||
searchState.changeQuery { query in
|
||||
query.query = searchState.queryText
|
||||
search.changeQuery { query in
|
||||
query.query = search.queryText
|
||||
}
|
||||
recents.open(RecentItem(from: search.queryText))
|
||||
|
||||
recents.open(RecentItem(from: searchState.queryText))
|
||||
|
||||
navigationState.tabSelection = .search
|
||||
navigation.tabSelection = .search
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,8 +83,8 @@ struct AppSidebarNavigation: View {
|
||||
.id(group)
|
||||
}
|
||||
|
||||
.onChange(of: navigationState.sidebarSectionChanged) { _ in
|
||||
scrollScrollViewToItem(scrollView: scrollView, for: navigationState.tabSelection)
|
||||
.onChange(of: navigation.sidebarSectionChanged) { _ in
|
||||
scrollScrollViewToItem(scrollView: scrollView, for: navigation.tabSelection)
|
||||
}
|
||||
}
|
||||
.background {
|
||||
@ -93,13 +96,7 @@ struct AppSidebarNavigation: View {
|
||||
.listStyle(.sidebar)
|
||||
}
|
||||
.toolbar {
|
||||
#if os(macOS)
|
||||
ToolbarItemGroup {
|
||||
Button(action: toggleSidebar) {
|
||||
Image(systemName: "sidebar.left").help("Toggle Sidebar")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
toolbarContent
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,26 +112,28 @@ struct AppSidebarNavigation: View {
|
||||
|
||||
AppSidebarRecents(selection: selection)
|
||||
.id("recentlyOpened")
|
||||
|
||||
if api.signedIn {
|
||||
AppSidebarSubscriptions(selection: selection)
|
||||
AppSidebarPlaylists(selection: selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mainNavigationLinks: some View {
|
||||
Section("Videos") {
|
||||
NavigationLink(tag: TabSelection.watchNow, selection: selection) {
|
||||
WatchNowView()
|
||||
}
|
||||
label: {
|
||||
NavigationLink(destination: LazyView(WatchNowView()), tag: TabSelection.watchNow, selection: selection) {
|
||||
Label("Watch Now", systemImage: "play.circle")
|
||||
.accessibility(label: Text("Watch Now"))
|
||||
}
|
||||
|
||||
if api.signedIn {
|
||||
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: selection) {
|
||||
Label("Subscriptions", systemImage: "star.circle")
|
||||
.accessibility(label: Text("Subscriptions"))
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: selection) {
|
||||
Label("Popular", systemImage: "chart.bar")
|
||||
@ -145,11 +144,6 @@ struct AppSidebarNavigation: View {
|
||||
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
||||
.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)
|
||||
private func toggleSidebar() {
|
||||
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"
|
||||
|
||||
return "\(symbolName).square"
|
||||
return "\(symbolName).circle"
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AppSidebarPlaylists: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<Playlists> private var playlists
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
|
||||
@Binding var selection: TabSelection?
|
||||
|
||||
var body: some View {
|
||||
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) {
|
||||
LazyView(PlaylistVideosView(playlist))
|
||||
} label: {
|
||||
@ -18,7 +18,7 @@ struct AppSidebarPlaylists: View {
|
||||
.id(playlist.id)
|
||||
.contextMenu {
|
||||
Button("Edit") {
|
||||
navigationState.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
||||
navigation.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,11 +26,14 @@ struct AppSidebarPlaylists: View {
|
||||
newPlaylistButton
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.onAppear {
|
||||
playlists.load()
|
||||
}
|
||||
}
|
||||
|
||||
var newPlaylistButton: some View {
|
||||
Button(action: { navigationState.presentNewPlaylistForm() }) {
|
||||
Label("New Playlist", systemImage: "plus.square")
|
||||
Button(action: { navigation.presentNewPlaylistForm() }) {
|
||||
Label("New Playlist", systemImage: "plus.circle")
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.buttonStyle(.plain)
|
||||
|
@ -4,7 +4,7 @@ import SwiftUI
|
||||
struct AppSidebarRecents: View {
|
||||
@Binding var selection: TabSelection?
|
||||
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<Recents> private var recents
|
||||
|
||||
@Default(.recentlyOpened) private var recentItems
|
||||
@ -18,7 +18,7 @@ struct AppSidebarRecents: View {
|
||||
switch recent.type {
|
||||
case .channel:
|
||||
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:
|
||||
RecentNavigationLink(recent: recent, selection: $selection, systemImage: "magnifyingglass") {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct AppSidebarSubscriptions: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@Binding var selection: TabSelection?
|
||||
|
||||
@ -10,22 +11,25 @@ struct AppSidebarSubscriptions: View {
|
||||
Section(header: Text("Subscriptions")) {
|
||||
ForEach(subscriptions.all) { channel in
|
||||
NavigationLink(tag: TabSelection.channel(channel.id), selection: $selection) {
|
||||
LazyView(ChannelVideosView(channel))
|
||||
LazyView(ChannelVideosView(channel: channel))
|
||||
} label: {
|
||||
Label(channel.name, systemImage: AppSidebarNavigation.symbolSystemImage(channel.name))
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Unsubscribe") {
|
||||
navigationState.presentUnsubscribeAlert(channel)
|
||||
navigation.presentUnsubscribeAlert(channel)
|
||||
}
|
||||
}
|
||||
.modifier(UnsubscribeAlertModifier())
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
subscriptions.load()
|
||||
}
|
||||
}
|
||||
|
||||
var unsubscribeAlertTitle: String {
|
||||
if let channel = navigationState.channelToUnsubscribe {
|
||||
if let channel = navigation.channelToUnsubscribe {
|
||||
return "Unsubscribe from \(channel.name)"
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,15 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct AppTabNavigation: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<SearchState> private var searchState
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
@EnvironmentObject<Recents> private var recents
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $navigationState.tabSelection) {
|
||||
TabView(selection: $navigation.tabSelection) {
|
||||
NavigationView {
|
||||
WatchNowView()
|
||||
LazyView(WatchNowView())
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.tabItem {
|
||||
Label("Watch Now", systemImage: "play.circle")
|
||||
@ -18,7 +19,8 @@ struct AppTabNavigation: View {
|
||||
.tag(TabSelection.watchNow)
|
||||
|
||||
NavigationView {
|
||||
SubscriptionsView()
|
||||
LazyView(SubscriptionsView())
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.tabItem {
|
||||
Label("Subscriptions", systemImage: "star.circle.fill")
|
||||
@ -29,7 +31,8 @@ struct AppTabNavigation: View {
|
||||
// TODO: reenable with settings
|
||||
// ============================
|
||||
// NavigationView {
|
||||
// PopularView()
|
||||
// LazyView(PopularView())
|
||||
// .toolbar { toolbarContent }
|
||||
// }
|
||||
// .tabItem {
|
||||
// Label("Popular", systemImage: "chart.bar")
|
||||
@ -38,7 +41,8 @@ struct AppTabNavigation: View {
|
||||
// .tag(TabSelection.popular)
|
||||
|
||||
NavigationView {
|
||||
TrendingView()
|
||||
LazyView(TrendingView())
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.tabItem {
|
||||
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
|
||||
@ -47,7 +51,8 @@ struct AppTabNavigation: View {
|
||||
.tag(TabSelection.trending)
|
||||
|
||||
NavigationView {
|
||||
PlaylistsView()
|
||||
LazyView(PlaylistsView())
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.tabItem {
|
||||
Label("Playlists", systemImage: "list.and.film")
|
||||
@ -56,25 +61,28 @@ struct AppTabNavigation: View {
|
||||
.tag(TabSelection.playlists)
|
||||
|
||||
NavigationView {
|
||||
LazyView(
|
||||
SearchView()
|
||||
.searchable(text: $searchState.queryText, placement: .navigationBarDrawer(displayMode: .always)) {
|
||||
ForEach(searchState.querySuggestions.collection, id: \.self) { suggestion in
|
||||
.toolbar { toolbarContent }
|
||||
.searchable(text: $search.queryText, placement: .navigationBarDrawer(displayMode: .always)) {
|
||||
ForEach(search.querySuggestions.collection, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchState.queryText) { query in
|
||||
searchState.loadQuerySuggestions(query)
|
||||
.onChange(of: search.queryText) { query in
|
||||
search.loadSuggestions(query)
|
||||
}
|
||||
.onSubmit(of: .search) {
|
||||
searchState.changeQuery { query in
|
||||
query.query = searchState.queryText
|
||||
search.changeQuery { query in
|
||||
query.query = search.queryText
|
||||
}
|
||||
|
||||
recents.open(RecentItem(from: searchState.queryText))
|
||||
recents.open(RecentItem(from: search.queryText))
|
||||
|
||||
navigationState.tabSelection = .search
|
||||
navigation.tabSelection = .search
|
||||
}
|
||||
)
|
||||
}
|
||||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
@ -83,7 +91,7 @@ struct AppTabNavigation: View {
|
||||
.tag(TabSelection.search)
|
||||
}
|
||||
.environment(\.navigationStyle, .tab)
|
||||
.sheet(isPresented: $navigationState.isChannelOpen, onDismiss: {
|
||||
.sheet(isPresented: $navigation.isChannelOpen, onDismiss: {
|
||||
if let channel = recents.presentedChannel {
|
||||
let recent = RecentItem(from: channel)
|
||||
recents.close(recent)
|
||||
@ -91,10 +99,26 @@ struct AppTabNavigation: View {
|
||||
}) {
|
||||
if recents.presentedChannel != nil {
|
||||
NavigationView {
|
||||
ChannelVideosView(recents.presentedChannel!)
|
||||
ChannelVideosView(channel: recents.presentedChannel!)
|
||||
.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
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var navigationState = NavigationState()
|
||||
@StateObject private var playbackState = PlaybackState()
|
||||
@StateObject private var playlists = Playlists()
|
||||
@StateObject private var navigation = NavigationModel()
|
||||
@StateObject private var playback = PlaybackModel()
|
||||
@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)
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@ -26,29 +27,29 @@ struct ContentView: View {
|
||||
TVNavigationView()
|
||||
#endif
|
||||
}
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(playback)
|
||||
.environmentObject(recents)
|
||||
#if !os(tvOS)
|
||||
.sheet(isPresented: $navigationState.showingVideo) {
|
||||
if let video = navigationState.video {
|
||||
.sheet(isPresented: $navigation.showingVideo) {
|
||||
if let video = navigation.video {
|
||||
VideoPlayerView(video)
|
||||
|
||||
#if !os(iOS)
|
||||
.frame(minWidth: 550, minHeight: 720)
|
||||
.onExitCommand {
|
||||
navigationState.showingVideo = false
|
||||
navigation.showingVideo = false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $navigationState.presentingPlaylistForm) {
|
||||
PlaylistFormView(playlist: $navigationState.editedPlaylist)
|
||||
.sheet(isPresented: $navigation.presentingPlaylistForm) {
|
||||
PlaylistFormView(playlist: $navigation.editedPlaylist)
|
||||
}
|
||||
.sheet(isPresented: $navigation.presentingSettings) {
|
||||
SettingsView()
|
||||
}
|
||||
#endif
|
||||
.environmentObject(navigationState)
|
||||
.environmentObject(playbackState)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(recents)
|
||||
.environmentObject(searchState)
|
||||
.environmentObject(subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,50 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
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 {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.onAppear(perform: configureAPI)
|
||||
.environmentObject(api)
|
||||
.environmentObject(instances)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(search)
|
||||
.environmentObject(subscriptions)
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.commands {
|
||||
SidebarCommands()
|
||||
}
|
||||
#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
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject private var playbackState: PlaybackState
|
||||
@EnvironmentObject private var playback: PlaybackModel
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@ -18,7 +18,7 @@ struct PlaybackBar: View {
|
||||
.frame(minWidth: 60, maxWidth: .infinity)
|
||||
|
||||
VStack {
|
||||
if playbackState.stream != nil {
|
||||
if playback.stream != nil {
|
||||
Text(currentStreamString)
|
||||
} else {
|
||||
if video.live {
|
||||
@ -38,19 +38,19 @@ struct PlaybackBar: View {
|
||||
}
|
||||
|
||||
var currentStreamString: String {
|
||||
playbackState.stream != nil ? "\(playbackState.stream!.resolution.height)p" : ""
|
||||
playback.stream != nil ? "\(playback.stream!.resolution.height)p" : ""
|
||||
}
|
||||
|
||||
var playbackStatus: String {
|
||||
guard playbackState.time != nil else {
|
||||
if playbackState.live {
|
||||
guard playback.time != nil else {
|
||||
if playback.live {
|
||||
return "LIVE"
|
||||
} else {
|
||||
return "loading..."
|
||||
}
|
||||
}
|
||||
|
||||
let remainingSeconds = video.length - playbackState.time!.seconds
|
||||
let remainingSeconds = video.length - playback.time!.seconds
|
||||
|
||||
if remainingSeconds < 60 {
|
||||
return "less than a minute"
|
||||
|
@ -1,7 +1,9 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct Player: UIViewControllerRepresentable {
|
||||
@EnvironmentObject<PlaybackState> private var playbackState
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<PlaybackModel> private var playback
|
||||
|
||||
var video: Video?
|
||||
|
||||
@ -9,7 +11,10 @@ struct Player: UIViewControllerRepresentable {
|
||||
let controller = PlayerViewController()
|
||||
|
||||
controller.video = video
|
||||
controller.playbackState = playbackState
|
||||
controller.playback = playback
|
||||
controller.api = api
|
||||
|
||||
controller.resolution = Defaults[.quality]
|
||||
|
||||
return controller
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ import SwiftUI
|
||||
final class PlayerViewController: UIViewController {
|
||||
var video: Video!
|
||||
|
||||
var api: InvidiousAPI!
|
||||
var playerLoaded = false
|
||||
var player = AVPlayer()
|
||||
var playerState: PlayerState!
|
||||
var playbackState: PlaybackState!
|
||||
var playerModel: PlayerModel!
|
||||
var playback: PlaybackModel!
|
||||
var playerViewController = AVPlayerViewController()
|
||||
var resolution: Stream.ResolutionSetting!
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
@ -22,7 +24,7 @@ final class PlayerViewController: UIViewController {
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
#if os(iOS)
|
||||
if !playerState.playingOutsideViewController {
|
||||
if !playerModel.playingOutsideViewController {
|
||||
playerViewController.player?.replaceCurrentItem(with: nil)
|
||||
playerViewController.player = nil
|
||||
|
||||
@ -34,15 +36,15 @@ final class PlayerViewController: UIViewController {
|
||||
}
|
||||
|
||||
func loadPlayer() {
|
||||
playerState = PlayerState(playbackState: playbackState)
|
||||
playerModel = PlayerModel(playback: playback, api: api, resolution: resolution)
|
||||
|
||||
guard !playerLoaded else {
|
||||
return
|
||||
}
|
||||
|
||||
playerState.player = player
|
||||
playerViewController.player = playerState.player
|
||||
playerState.loadVideo(video)
|
||||
playerModel.player = player
|
||||
playerViewController.player = playerModel.player
|
||||
playerModel.loadVideo(video)
|
||||
|
||||
#if os(tvOS)
|
||||
present(playerViewController, animated: false)
|
||||
@ -95,7 +97,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
||||
}
|
||||
|
||||
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {
|
||||
playerState.playingOutsideViewController = false
|
||||
playerModel.playingOutsideViewController = false
|
||||
dismiss(animated: false)
|
||||
}
|
||||
|
||||
@ -103,7 +105,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
||||
_: AVPlayerViewController,
|
||||
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||
) {
|
||||
playerState.playingOutsideViewController = true
|
||||
playerModel.playingOutsideViewController = true
|
||||
}
|
||||
|
||||
func playerViewController(
|
||||
@ -112,7 +114,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
||||
) {
|
||||
coordinator.animate(alongsideTransition: nil) { context in
|
||||
if !context.isCancelled {
|
||||
self.playerState.playingOutsideViewController = false
|
||||
self.playerModel.playingOutsideViewController = false
|
||||
|
||||
#if os(iOS)
|
||||
if self.traitCollection.verticalSizeClass == .compact {
|
||||
@ -124,10 +126,10 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
||||
}
|
||||
|
||||
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
|
||||
playerState.playingOutsideViewController = true
|
||||
playerModel.playingOutsideViewController = true
|
||||
}
|
||||
|
||||
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
|
||||
playerState.playingOutsideViewController = false
|
||||
playerModel.playingOutsideViewController = false
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct VideoDetails: View {
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@State private var subscribed = false
|
||||
@State private var confirmationShown = false
|
||||
@ -186,6 +186,6 @@ struct VideoDetails: View {
|
||||
struct VideoDetails_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideoDetails(video: Video.fixture)
|
||||
.environmentObject(Subscriptions())
|
||||
.environmentObject(SubscriptionsModel())
|
||||
}
|
||||
}
|
||||
|
@ -12,31 +12,31 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<PlaybackState> private var playbackState
|
||||
|
||||
@ObservedObject private var store = Store<Video>()
|
||||
@StateObject private var store = Store<Video>()
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlaybackModel> private var playback
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.video(video.id)
|
||||
api.video(video.id)
|
||||
}
|
||||
|
||||
var video: Video
|
||||
|
||||
init(_ video: Video) {
|
||||
self.video = video
|
||||
resource.addObserver(store)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
#if os(tvOS)
|
||||
Player(video: video)
|
||||
.environmentObject(playbackState)
|
||||
.environmentObject(playback)
|
||||
#else
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: 0) {
|
||||
@ -49,8 +49,8 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
|
||||
Player(video: video)
|
||||
.environmentObject(playbackState)
|
||||
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: playbackState.aspectRatio))
|
||||
.environmentObject(playback)
|
||||
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: playback.aspectRatio))
|
||||
}
|
||||
.background(.black)
|
||||
|
||||
@ -73,13 +73,13 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#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
|
||||
}
|
||||
|
||||
.onAppear {
|
||||
resource.addObserver(store)
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
.onDisappear {
|
||||
@ -109,7 +109,7 @@ struct VideoPlayerView_Previews: PreviewProvider {
|
||||
}
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
VideoPlayerView(Video.fixture)
|
||||
.environmentObject(NavigationState())
|
||||
.environmentObject(NavigationModel())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct PlaylistFormView: View {
|
||||
@Binding var playlist: Playlist!
|
||||
|
||||
@State private var name = ""
|
||||
@State private var visibility = Playlist.Visibility.public
|
||||
|
||||
@ -10,11 +12,10 @@ struct PlaylistFormView: View {
|
||||
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
@Binding var playlist: Playlist!
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject<Playlists> private var playlists
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
|
||||
var editing: Bool {
|
||||
playlist != nil
|
||||
@ -33,6 +34,8 @@ struct PlaylistFormView: View {
|
||||
dismiss()
|
||||
}.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Form {
|
||||
TextField("Name", text: $name, onCommit: validate)
|
||||
.frame(maxWidth: 450)
|
||||
@ -46,8 +49,7 @@ struct PlaylistFormView: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
Divider()
|
||||
.padding(.vertical, 4)
|
||||
|
||||
HStack {
|
||||
if editing {
|
||||
deletePlaylistButton
|
||||
@ -59,11 +61,14 @@ struct PlaylistFormView: View {
|
||||
.disabled(!valid)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.onChange(of: name) { _ in validate() }
|
||||
.onAppear(perform: initializeForm)
|
||||
.padding(.horizontal)
|
||||
#if !os(iOS)
|
||||
#if os(iOS)
|
||||
.padding(.vertical)
|
||||
#else
|
||||
.frame(width: 400, height: 150)
|
||||
#endif
|
||||
|
||||
@ -141,14 +146,14 @@ struct PlaylistFormView: View {
|
||||
playlist = modifiedPlaylist
|
||||
}
|
||||
|
||||
playlists.reload()
|
||||
playlists.load(force: true)
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
var resource: Resource {
|
||||
editing ? InvidiousAPI.shared.playlist(playlist.id) : InvidiousAPI.shared.playlists
|
||||
editing ? api.playlist(playlist.id) : api.playlists
|
||||
}
|
||||
|
||||
var visibilityButton: some View {
|
||||
@ -189,9 +194,9 @@ struct PlaylistFormView: View {
|
||||
}
|
||||
|
||||
func deletePlaylistAndDismiss() {
|
||||
let resource = InvidiousAPI.shared.playlist(playlist.id)
|
||||
resource.request(.delete).onSuccess { _ in
|
||||
api.playlist(playlist.id).request(.delete).onSuccess { _ in
|
||||
playlist = nil
|
||||
playlists.load(force: true)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
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 createdPlaylist: Playlist?
|
||||
@ -13,27 +13,18 @@ struct PlaylistsView: View {
|
||||
@State private var showingEditPlaylist = false
|
||||
@State private var editedPlaylist: Playlist?
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.playlists
|
||||
}
|
||||
@Default(.selectedPlaylistID) private var selectedPlaylistID
|
||||
|
||||
init() {
|
||||
resource.addObserver(store)
|
||||
var resource: Resource {
|
||||
api.playlists
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
currentPlaylist?.videos ?? []
|
||||
}
|
||||
|
||||
var videosViewMaxHeight: Double {
|
||||
#if os(tvOS)
|
||||
videos.isEmpty ? 150 : .infinity
|
||||
#else
|
||||
videos.isEmpty ? 0 : .infinity
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SignInRequiredView(title: "Playlists") {
|
||||
VStack {
|
||||
#if os(tvOS)
|
||||
toolbar
|
||||
@ -47,15 +38,7 @@ struct PlaylistsView: View {
|
||||
} else {
|
||||
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)
|
||||
.fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) {
|
||||
@ -85,21 +68,37 @@ struct PlaylistsView: View {
|
||||
#endif
|
||||
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 {
|
||||
resource.addObserver(store)
|
||||
|
||||
resource.loadIfNeeded()?.onSuccess { _ in
|
||||
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 {
|
||||
@ -233,6 +232,6 @@ struct PlaylistsView: View {
|
||||
struct PlaylistsView_Provider: PreviewProvider {
|
||||
static var previews: some View {
|
||||
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"
|
||||
@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 selection: Country?
|
||||
|
@ -2,18 +2,85 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct TrendingView: View {
|
||||
@StateObject private var store = Store<[Video]>()
|
||||
|
||||
@State private var category: TrendingCategory = .default
|
||||
@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 {
|
||||
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.loadIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
#if os(tvOS)
|
||||
Button(category.name) {
|
||||
setCategory(category.next())
|
||||
self.category = category.next()
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(TrendingCategory.allCases) { category in
|
||||
Button(category.name) { setCategory(category) }
|
||||
Button(category.name) { self.category = category }
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
Menu(category.name) {
|
||||
ForEach(TrendingCategory.allCases) { category in
|
||||
Button(action: { setCategory(category) }) {
|
||||
Button(action: { self.category = category }) {
|
||||
if category == self.category {
|
||||
Label(category.name, systemImage: "checkmark")
|
||||
} else {
|
||||
@ -112,30 +133,17 @@ struct TrendingView: View {
|
||||
|
||||
var countryButton: some View {
|
||||
Button(action: {
|
||||
selectingCountry.toggle()
|
||||
presentingCountrySelection.toggle()
|
||||
resource.removeObservers(ownedBy: store)
|
||||
}) {
|
||||
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 {
|
||||
static var previews: some View {
|
||||
TrendingView()
|
||||
.environmentObject(NavigationState())
|
||||
.environmentObject(NavigationModel())
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct VideoView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@ -21,7 +21,7 @@ struct VideoView: View {
|
||||
content
|
||||
}
|
||||
} else {
|
||||
Button(action: { navigationState.playVideo(video) }) {
|
||||
Button(action: { navigation.playVideo(video) }) {
|
||||
content
|
||||
}
|
||||
}
|
||||
@ -125,7 +125,7 @@ struct VideoView: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
thumbnail
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
videoDetail(video.title, lineLimit: additionalDetailsAvailable ? 2 : 3)
|
||||
#if os(tvOS)
|
||||
.frame(minHeight: additionalDetailsAvailable ? 80 : 120, alignment: .top)
|
||||
@ -155,7 +155,9 @@ struct VideoView: View {
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 30, alignment: .top)
|
||||
#if os(tvOS)
|
||||
.padding(.bottom, 10)
|
||||
#endif
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading)
|
||||
|
@ -20,7 +20,7 @@ struct VideosCellsHorizontal: View {
|
||||
.padding(.trailing, 20)
|
||||
.padding(.bottom, 40)
|
||||
#else
|
||||
.frame(maxWidth: 300)
|
||||
.frame(width: 300)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,11 @@ struct VideosCellsHorizontal: View {
|
||||
.padding(.vertical, 20)
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
if let video = videos.first {
|
||||
scrollView.scrollTo(video.id, anchor: .leading)
|
||||
}
|
||||
}
|
||||
.onChange(of: videos) { [videos] newVideos in
|
||||
#if !os(tvOS)
|
||||
guard !videos.isEmpty, let video = newVideos.first else {
|
||||
@ -45,7 +50,7 @@ struct VideosCellsHorizontal: View {
|
||||
#if os(tvOS)
|
||||
.frame(height: 560)
|
||||
#else
|
||||
.frame(height: 320)
|
||||
.frame(height: 280)
|
||||
#endif
|
||||
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
@ -55,7 +60,7 @@ struct VideosCellsHorizontal: View {
|
||||
struct VideoCellsHorizontal_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideosCellsHorizontal(videos: Video.allFixtures)
|
||||
.environmentObject(NavigationState())
|
||||
.environmentObject(Subscriptions())
|
||||
.environmentObject(NavigationModel())
|
||||
.environmentObject(SubscriptionsModel())
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,6 @@ struct VideoCellsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideosView(videos: Video.allFixtures)
|
||||
.frame(minWidth: 1000)
|
||||
.environmentObject(NavigationState())
|
||||
.environmentObject(NavigationModel())
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct VideosView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
#if os(tvOS)
|
||||
@Default(.layout) private var layout
|
||||
|
@ -4,8 +4,11 @@ import SwiftUI
|
||||
struct ChannelVideosView: View {
|
||||
let channel: Channel
|
||||
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
@StateObject private var store = Store<Channel>()
|
||||
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
|
||||
@ -14,16 +17,8 @@ struct ChannelVideosView: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
#endif
|
||||
|
||||
@ObservedObject private var store = Store<Channel>()
|
||||
|
||||
@Namespace private var focusNamespace
|
||||
|
||||
init(_ channel: Channel) {
|
||||
self.channel = channel
|
||||
|
||||
resource.addObserver(store)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
#if os(tvOS)
|
||||
@ -55,12 +50,11 @@ struct ChannelVideosView: View {
|
||||
#endif
|
||||
#if !os(tvOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: subscriptionToolbarItemPlacement) {
|
||||
ToolbarItem {
|
||||
HStack {
|
||||
if let channel = store.item, let subscribers = channel.subscriptionsString {
|
||||
Text("**\(subscribers)** subscribers")
|
||||
Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||
|
||||
subscriptionToggleButton
|
||||
}
|
||||
@ -77,13 +71,19 @@ struct ChannelVideosView: View {
|
||||
#endif
|
||||
.modifier(UnsubscribeAlertModifier())
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
if store.item.isNil {
|
||||
resource.addObserver(store)
|
||||
resource.load()
|
||||
}
|
||||
}
|
||||
.navigationTitle(navigationTitle)
|
||||
}
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.channel(channel.id)
|
||||
let resource = api.channel(channel.id)
|
||||
resource.addObserver(store)
|
||||
|
||||
return resource
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
@ -94,7 +94,7 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
return .status
|
||||
return .automatic
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -102,12 +102,12 @@ struct ChannelVideosView: View {
|
||||
Group {
|
||||
if subscriptions.isSubscribing(channel.id) {
|
||||
Button("Unsubscribe") {
|
||||
navigationState.presentUnsubscribeAlert(channel)
|
||||
navigation.presentUnsubscribeAlert(channel)
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptions.subscribe(channel.id) {
|
||||
navigationState.sidebarSectionChanged.toggle()
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,22 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
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() {
|
||||
resource.addObserver(store)
|
||||
var resource: Resource {
|
||||
api.popular
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VideosView(videos: store.collection)
|
||||
.onAppear {
|
||||
resource.addObserver(store)
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle("Popular")
|
||||
#endif
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ struct SearchView: View {
|
||||
@Default(.searchDuration) private var searchDuration
|
||||
|
||||
@EnvironmentObject<Recents> private var recents
|
||||
@EnvironmentObject<SearchState> private var state
|
||||
@EnvironmentObject<SearchModel> private var state
|
||||
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
|
||||
@ -85,7 +85,7 @@ struct SearchView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.opacity(recentsChanged ? 1 : 1)
|
||||
.redrawOn(change: recentsChanged)
|
||||
|
||||
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
|
||||
|
||||
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() {
|
||||
resource.addObserver(store)
|
||||
var feed: Resource {
|
||||
api.feed
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SignInRequiredView(title: "Subscriptions") {
|
||||
VideosView(videos: store.collection)
|
||||
.onAppear {
|
||||
if let home = InvidiousAPI.shared.home.loadIfNeeded() {
|
||||
home.onSuccess { _ in
|
||||
resource.loadIfNeeded()
|
||||
loadResources()
|
||||
}
|
||||
} else {
|
||||
resource.loadIfNeeded()
|
||||
.onChange(of: api.account) { _ in
|
||||
loadResources(force: true)
|
||||
}
|
||||
.onChange(of: feed) { _ in
|
||||
loadResources(force: true)
|
||||
}
|
||||
}
|
||||
.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
|
||||
|
||||
struct VideoContextMenuView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<Recents> private var recents
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
let video: Video
|
||||
|
||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||
@Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist
|
||||
|
||||
@State private var subscribed = false
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
openChannelButton
|
||||
|
||||
subscriptionButton
|
||||
.opacity(subscribed ? 1 : 1)
|
||||
|
||||
if navigationState.tabSelection == .playlists {
|
||||
if navigation.tabSelection == .playlists {
|
||||
removeFromPlaylistButton
|
||||
} else {
|
||||
addToPlaylistButton
|
||||
@ -32,9 +30,9 @@ struct VideoContextMenuView: View {
|
||||
Button("\(video.author) Channel") {
|
||||
let recent = RecentItem(from: video.channel)
|
||||
recents.open(recent)
|
||||
navigationState.tabSelection = .recentlyOpened(recent.tag)
|
||||
navigationState.isChannelOpen = true
|
||||
navigationState.sidebarSectionChanged.toggle()
|
||||
navigation.tabSelection = .recentlyOpened(recent.tag)
|
||||
navigation.isChannelOpen = true
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,13 +43,13 @@ struct VideoContextMenuView: View {
|
||||
#if os(tvOS)
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
#else
|
||||
navigationState.presentUnsubscribeAlert(video.channel)
|
||||
navigation.presentUnsubscribeAlert(video.channel)
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptions.subscribe(video.channel.id) {
|
||||
navigationState.sidebarSectionChanged.toggle()
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,9 +65,9 @@ struct VideoContextMenuView: View {
|
||||
|
||||
var removeFromPlaylistButton: some View {
|
||||
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
|
||||
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 SwiftUI
|
||||
|
||||
struct WatchNowSection: View {
|
||||
@ObservedObject private var store = Store<[Video]>()
|
||||
|
||||
let resource: Resource
|
||||
let label: String
|
||||
|
||||
@StateObject private var store = Store<[Video]>()
|
||||
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
|
||||
init(resource: Resource, label: String) {
|
||||
self.resource = resource
|
||||
self.label = label
|
||||
|
||||
self.resource.addObserver(store)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WatchNowSectionBody(label: label, videos: store.collection)
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
resource.addObserver(store)
|
||||
resource.load()
|
||||
}
|
||||
.onChange(of: api.account) { _ in
|
||||
resource.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,22 @@
|
||||
import Defaults
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct WatchNowView: View {
|
||||
init() {
|
||||
InvidiousAPI.shared.home.loadIfNeeded()
|
||||
}
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
if api.validInstance {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
WatchNowSection(resource: InvidiousAPI.shared.feed, label: "Subscriptions")
|
||||
WatchNowSection(resource: InvidiousAPI.shared.popular, label: "Popular")
|
||||
WatchNowSection(resource: InvidiousAPI.shared.trending(category: .default, country: .pl), label: "Trending")
|
||||
WatchNowSection(resource: InvidiousAPI.shared.trending(category: .movies, country: .pl), label: "Movies")
|
||||
WatchNowSection(resource: InvidiousAPI.shared.trending(category: .music, country: .pl), label: "Music")
|
||||
if api.signedIn {
|
||||
WatchNowSection(resource: api.feed, label: "Subscriptions")
|
||||
}
|
||||
WatchNowSection(resource: api.popular, label: "Popular")
|
||||
WatchNowSection(resource: api.trending(category: .default, country: .pl), label: "Trending")
|
||||
WatchNowSection(resource: api.trending(category: .movies, country: .pl), label: "Movies")
|
||||
WatchNowSection(resource: api.trending(category: .music, country: .pl), label: "Music")
|
||||
|
||||
// TODO: adding sections to view
|
||||
// ===================
|
||||
@ -21,6 +24,7 @@ struct WatchNowView: View {
|
||||
// WatchNowSection(resource: InvidiousAPI.shared.channelVideos("UCBJycsmduvYEL83R_U4JriQ"), label: "MKBHD")
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
#else
|
||||
@ -36,7 +40,7 @@ struct WatchNowView: View {
|
||||
struct WatchNowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WatchNowView()
|
||||
.environmentObject(Subscriptions())
|
||||
.environmentObject(NavigationState())
|
||||
.environmentObject(SubscriptionsModel())
|
||||
.environmentObject(NavigationModel())
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,20 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct Player: NSViewControllerRepresentable {
|
||||
@EnvironmentObject<PlaybackState> private var playbackState
|
||||
|
||||
var video: Video!
|
||||
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
@EnvironmentObject<PlaybackModel> private var playback
|
||||
|
||||
func makeNSViewController(context _: Context) -> PlayerViewController {
|
||||
let controller = PlayerViewController()
|
||||
|
||||
controller.video = video
|
||||
controller.playbackState = playbackState
|
||||
controller.playback = playback
|
||||
controller.api = api
|
||||
|
||||
controller.resolution = Defaults[.quality]
|
||||
|
||||
return controller
|
||||
}
|
||||
|
@ -4,36 +4,40 @@ import SwiftUI
|
||||
final class PlayerViewController: NSViewController {
|
||||
var video: Video!
|
||||
|
||||
var api: InvidiousAPI!
|
||||
var player = AVPlayer()
|
||||
var playerState: PlayerState!
|
||||
var playbackState: PlaybackState!
|
||||
var playerModel: PlayerModel!
|
||||
var playback: PlaybackModel!
|
||||
var playerView = AVPlayerView()
|
||||
var resolution: Stream.ResolutionSetting!
|
||||
|
||||
override func viewDidDisappear() {
|
||||
playerView.player?.replaceCurrentItem(with: nil)
|
||||
playerView.player = nil
|
||||
|
||||
playerState.player = nil
|
||||
playerState = nil
|
||||
playerModel.player = nil
|
||||
playerModel = nil
|
||||
|
||||
super.viewDidDisappear()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
playerState.player = player
|
||||
playerView.player = playerState.player
|
||||
playerModel.player = player
|
||||
playerView.player = playerModel.player
|
||||
|
||||
playerView.allowsPictureInPicturePlayback = true
|
||||
playerView.showsFullScreenToggleButton = true
|
||||
|
||||
view = playerView
|
||||
|
||||
playerState.loadVideo(video)
|
||||
DispatchQueue.main.async {
|
||||
self.playerModel.loadVideo(self.video)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct AddToPlaylistView: View {
|
||||
@ObservedObject private var store = Store<[Playlist]>()
|
||||
@StateObject private var store = Store<[Playlist]>()
|
||||
|
||||
@State private var selectedPlaylist: Playlist?
|
||||
|
||||
@ -11,8 +11,10 @@ struct AddToPlaylistView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject<InvidiousAPI> private var api
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.playlists
|
||||
api.playlists
|
||||
}
|
||||
|
||||
init() {
|
||||
@ -85,12 +87,12 @@ struct AddToPlaylistView: View {
|
||||
return
|
||||
}
|
||||
|
||||
let resource = InvidiousAPI.shared.playlistVideos(currentPlaylist!.id)
|
||||
let resource = api.playlistVideos(currentPlaylist!.id)
|
||||
let body = ["videoId": videoID]
|
||||
|
||||
resource.request(.post, json: body).onSuccess { _ in
|
||||
Defaults.reset(.videoIDToAddToPlaylist)
|
||||
InvidiousAPI.shared.playlists.load()
|
||||
api.playlists.load()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct OptionsView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
@Default(.layout) private var layout
|
||||
|
||||
@ -28,6 +28,8 @@ struct OptionsView: View {
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
SettingsView()
|
||||
}
|
||||
.frame(maxWidth: 800)
|
||||
|
||||
@ -42,7 +44,7 @@ struct OptionsView: View {
|
||||
|
||||
var tabSelectionOptions: some View {
|
||||
VStack {
|
||||
switch navigationState.tabSelection {
|
||||
switch navigation.tabSelection {
|
||||
case .search:
|
||||
SearchOptionsView()
|
||||
|
||||
|
@ -2,17 +2,17 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct TVNavigationView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<PlaybackState> private var playbackState
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlaybackModel> private var playback
|
||||
@EnvironmentObject<Recents> private var recents
|
||||
@EnvironmentObject<SearchState> private var searchState
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
|
||||
@State private var showingOptions = false
|
||||
|
||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $navigationState.tabSelection) {
|
||||
TabView(selection: $navigation.tabSelection) {
|
||||
WatchNowView()
|
||||
.tabItem { Text("Watch Now") }
|
||||
.tag(TabSelection.watchNow)
|
||||
@ -34,30 +34,30 @@ struct TVNavigationView: View {
|
||||
.tag(TabSelection.playlists)
|
||||
|
||||
SearchView()
|
||||
.searchable(text: $searchState.queryText) {
|
||||
ForEach(searchState.querySuggestions.collection, id: \.self) { suggestion in
|
||||
.searchable(text: $search.queryText) {
|
||||
ForEach(search.querySuggestions.collection, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchState.queryText) { newQuery in
|
||||
searchState.loadQuerySuggestions(newQuery)
|
||||
searchState.changeQuery { query in query.query = newQuery }
|
||||
.onChange(of: search.queryText) { newQuery in
|
||||
search.loadSuggestions(newQuery)
|
||||
search.changeQuery { query in query.query = newQuery }
|
||||
}
|
||||
.tabItem { Image(systemName: "magnifyingglass") }
|
||||
.tag(TabSelection.search)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showingOptions) { OptionsView() }
|
||||
.fullScreenCover(isPresented: $showingAddToPlaylist) { AddToPlaylistView() }
|
||||
.fullScreenCover(isPresented: $navigationState.showingVideo) {
|
||||
if let video = navigationState.video {
|
||||
.fullScreenCover(isPresented: $navigation.showingVideo) {
|
||||
if let video = navigation.video {
|
||||
VideoPlayerView(video)
|
||||
.environmentObject(playbackState)
|
||||
.environmentObject(playback)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $navigationState.isChannelOpen) {
|
||||
.fullScreenCover(isPresented: $navigation.isChannelOpen) {
|
||||
if let channel = recents.presentedChannel {
|
||||
ChannelVideosView(channel)
|
||||
ChannelVideosView(channel: channel)
|
||||
.background(.thickMaterial)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user