diff --git a/Fixtures/Instance+Fixtures.swift b/Fixtures/Instance+Fixtures.swift new file mode 100644 index 00000000..2fb458f1 --- /dev/null +++ b/Fixtures/Instance+Fixtures.swift @@ -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") + ]) + } +} diff --git a/Model/Instance.swift b/Model/Instance.swift new file mode 100644 index 00000000..7437de4d --- /dev/null +++ b/Model/Instance.swift @@ -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[.. 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) + } +} diff --git a/Model/InstanceAccountValidator.swift b/Model/InstanceAccountValidator.swift new file mode 100644 index 00000000..038de316 --- /dev/null +++ b/Model/InstanceAccountValidator.swift @@ -0,0 +1,108 @@ +import Foundation +import Siesta +import SwiftUI + +final class InstanceAccountValidator: Service { + let url: String + let account: Instance.Account? + + var formObjectID: Binding + var valid: Binding + var validated: Binding + var error: Binding? + + init( + url: String, + account: Instance.Account? = nil, + formObjectID: Binding, + valid: Binding, + validated: Binding, + error: Binding? = 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") + } +} diff --git a/Model/InstancesModel.swift b/Model/InstancesModel.swift new file mode 100644 index 00000000..4faaecc9 --- /dev/null +++ b/Model/InstancesModel.swift @@ -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) + } + } + } +} diff --git a/Model/InvidiousAPI.swift b/Model/InvidiousAPI.swift index 479655f9..fc74ed2a 100644 --- a/Model/InvidiousAPI.swift +++ b/Model/InvidiousAPI.swift @@ -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) -> [Video] in + configureTransformer(pathPattern("popular"), requestMethods: [.get]) { (content: Entity) -> [Video] in content.json.arrayValue.map(Video.init) } - configureTransformer("/trending", requestMethods: [.get]) { (content: Entity) -> [Video] in + configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity) -> [Video] in content.json.arrayValue.map(Video.init) } - configureTransformer("/search", requestMethods: [.get]) { (content: Entity) -> [Video] in + configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity) -> [Video] in content.json.arrayValue.map(Video.init) } - configureTransformer("/search/suggestions", requestMethods: [.get]) { (content: Entity) -> [String] in + configureTransformer(pathPattern("search/suggestions"), requestMethods: [.get]) { (content: Entity) -> [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) -> [Playlist] in + configureTransformer(pathPattern("auth/playlists"), requestMethods: [.get]) { (content: Entity) -> [Playlist] in content.json.arrayValue.map(Playlist.init) } - configureTransformer("/auth/playlists/*", requestMethods: [.get]) { (content: Entity) -> Playlist in + configureTransformer(pathPattern("auth/playlists/*"), requestMethods: [.get]) { (content: Entity) -> Playlist in Playlist(content.json) } - configureTransformer("/auth/playlists", requestMethods: [.post, .patch]) { (content: Entity) -> Playlist in + configureTransformer(pathPattern("auth/playlists"), requestMethods: [.post, .patch]) { (content: Entity) -> 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) -> [Video] in + configureTransformer(pathPattern("auth/feed"), requestMethods: [.get]) { (content: Entity) -> [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) -> [Channel] in + configureTransformer(pathPattern("auth/subscriptions"), requestMethods: [.get]) { (content: Entity) -> [Channel] in content.json.arrayValue.map(Channel.init) } - configureTransformer("/channels/*", requestMethods: [.get]) { (content: Entity) -> Channel in + configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity) -> Channel in Channel(json: content.json) } - configureTransformer("/channels/*/latest", requestMethods: [.get]) { (content: Entity) -> [Video] in + configureTransformer(pathPattern("channels/*/latest"), requestMethods: [.get]) { (content: Entity) -> [Video] in content.json.arrayValue.map(Video.init) } - configureTransformer("/videos/*", requestMethods: [.get]) { (content: Entity) -> Video in + configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity) -> 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()) } diff --git a/Model/NavigationState.swift b/Model/NavigationModel.swift similarity index 91% rename from Model/NavigationState.swift rename to Model/NavigationModel.swift index 20aaf040..ed9164b0 100644 --- a/Model/NavigationState.swift +++ b/Model/NavigationModel.swift @@ -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 diff --git a/Model/PlaybackState.swift b/Model/PlaybackModel.swift similarity index 93% rename from Model/PlaybackState.swift rename to Model/PlaybackModel.swift index 30cc6b8b..bade756f 100644 --- a/Model/PlaybackState.swift +++ b/Model/PlaybackModel.swift @@ -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? diff --git a/Model/PlayerState.swift b/Model/PlayerModel.swift similarity index 89% rename from Model/PlayerState.swift rename to Model/PlayerModel.swift index 69d8858f..a2d0f950 100644 --- a/Model/PlayerState.swift +++ b/Model/PlayerModel.swift @@ -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() } } diff --git a/Model/Playlists.swift b/Model/Playlists.swift deleted file mode 100644 index 328564a3..00000000 --- a/Model/Playlists.swift +++ /dev/null @@ -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 - } - } - } -} diff --git a/Model/PlaylistsModel.swift b/Model/PlaylistsModel.swift new file mode 100644 index 00000000..54344b0f --- /dev/null +++ b/Model/PlaylistsModel.swift @@ -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 = [] + } + } +} diff --git a/Model/Profile.swift b/Model/Profile.swift deleted file mode 100644 index d1b8f888..00000000 --- a/Model/Profile.swift +++ /dev/null @@ -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)! - } - } -} diff --git a/Model/Recents.swift b/Model/Recents.swift index ccc99f19..a0cb9dba 100644 --- a/Model/Recents.swift +++ b/Model/Recents.swift @@ -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"], diff --git a/Model/SearchState.swift b/Model/SearchModel.swift similarity index 84% rename from Model/SearchState.swift rename to Model/SearchModel.swift index 2149aba1..a27b3ff9 100644 --- a/Model/SearchState.swift +++ b/Model/SearchModel.swift @@ -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 } diff --git a/Model/Stream.swift b/Model/Stream.swift index da152622..6beafaa3 100644 --- a/Model/Stream.swift +++ b/Model/Stream.swift @@ -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 { diff --git a/Model/Subscriptions.swift b/Model/Subscriptions.swift deleted file mode 100644 index 80bf4db0..00000000 --- a/Model/Subscriptions.swift +++ /dev/null @@ -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) - } - } -} diff --git a/Model/SubscriptionsModel.swift b/Model/SubscriptionsModel.swift new file mode 100644 index 00000000..b1d32932 --- /dev/null +++ b/Model/SubscriptionsModel.swift @@ -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) + } + } +} diff --git a/Model/Video.swift b/Model/Video.swift index c175bc63..122886b9 100644 --- a/Model/Video.swift +++ b/Model/Video.swift @@ -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? { diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 325e076b..2703fa68 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -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 = ""; }; 3705B181267B4E4900704544 /* TrendingCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCategory.swift; sourceTree = ""; }; - 3711403E26B206A6005B3555 /* SearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchState.swift; sourceTree = ""; }; + 3711403E26B206A6005B3555 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; 3714166E267A8ACC006CA35D /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; 37141672267A8E10006CA35D /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; 37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; - 371F2F19269B43D300E4A7AB /* NavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationState.swift; sourceTree = ""; }; + 371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = ""; }; 372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 373CFABD26966115003CB2C6 /* CoverSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionView.swift; sourceTree = ""; }; 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionRowView.swift; sourceTree = ""; }; @@ -285,17 +320,28 @@ 3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = ""; }; 3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = ""; }; 3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = ""; }; + 37484C1826FC837400287258 /* PlaybackSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettingsView.swift; sourceTree = ""; }; + 37484C1C26FC83A400287258 /* InstancesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettingsView.swift; sourceTree = ""; }; + 37484C2026FC83C400287258 /* AccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsView.swift; sourceTree = ""; }; + 37484C2426FC83E000287258 /* InstanceFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceFormView.swift; sourceTree = ""; }; + 37484C2826FC83FF00287258 /* AccountFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFormView.swift; sourceTree = ""; }; + 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceDetailsSettingsView.swift; sourceTree = ""; }; + 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceAccountValidator.swift; sourceTree = ""; }; + 375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = ""; }; 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = ""; }; 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = ""; }; 376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = ""; }; 376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = ""; }; 376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = ""; }; + 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInRequiredView.swift; sourceTree = ""; }; + 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = ""; }; 37754C9C26B7500000DBD602 /* VideosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideosView.swift; sourceTree = ""; }; 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = ""; }; - 3788AC2226F683DE00F6BAA9 /* WatchNowPlaylistSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowPlaylistSection.swift; sourceTree = ""; }; 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSection.swift; sourceTree = ""; }; 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSectionBody.swift; sourceTree = ""; }; + 378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; + 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsMenuView.swift; sourceTree = ""; }; 37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = ""; }; 3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = ""; }; @@ -307,17 +353,18 @@ 37AAF28F26740715007FC770 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; 37AAF29926740A01007FC770 /* VideosListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosListView.swift; sourceTree = ""; }; 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = ""; }; + 37B044B626F7AB9000E1419D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = ""; }; - 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = ""; }; + 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerModel.swift; sourceTree = ""; }; 37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = ""; }; 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = ""; }; 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = ""; }; 37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = ""; }; 37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = ""; }; - 37B81B0426D2CEDA00675966 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = ""; }; + 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackModel.swift; sourceTree = ""; }; 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVideosView.swift; sourceTree = ""; }; 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelVideosView.swift; sourceTree = ""; }; - 37BA794226DBA973002A0235 /* Playlists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlists.swift; sourceTree = ""; }; + 37BA794226DBA973002A0235 /* PlaylistsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsModel.swift; sourceTree = ""; }; 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarSubscriptions.swift; sourceTree = ""; }; 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarPlaylists.swift; sourceTree = ""; }; 37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = ""; }; @@ -334,7 +381,6 @@ 37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; 37C194C626F6A9C8005D3B96 /* Recents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recents.swift; sourceTree = ""; }; 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = ""; }; - 37C7A1DB267CE9D90010EAD6 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = ""; }; 37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = ""; }; 37D4B0C22671614700C925CA /* PearvidiousApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousApp.swift; sourceTree = ""; }; @@ -353,11 +399,12 @@ 37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = ""; }; 37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = ""; }; 37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 37E64DD026D597EB00C71877 /* Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscriptions.swift; sourceTree = ""; }; + 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = ""; }; 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = ""; }; 37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = ""; }; 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = ""; }; 37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosCellsVertical.swift; sourceTree = ""; }; + 37F64FE326FE70A60081B69E /* RedrawOnViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnViewModifier.swift; sourceTree = ""; }; /* 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 = ""; }; + 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 = ""; + }; 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; }; diff --git a/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/Shared/Assets.xcassets/AccentColor.colorset/Contents.json index eb878970..b5f34f32 100644 --- a/Shared/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -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" } ], diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index f4c22eb2..09a9b627 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -5,6 +5,8 @@ extension Defaults.Keys { static let layout = Key("listingLayout", default: .cells) #endif + static let instances = Key<[Instance]>("instances", default: []) + static let searchSortOrder = Key("searchSortOrder", default: .relevance) static let searchDate = Key("searchDate") static let searchDuration = Key("searchDuration") @@ -14,6 +16,7 @@ extension Defaults.Keys { static let videoIDToAddToPlaylist = Key("videoIDToAddToPlaylist") static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) + static let quality = Key("quality", default: .hd720pFirstThenBest) } enum ListingLayout: String, CaseIterable, Identifiable, Defaults.Serializable { diff --git a/Shared/Modifiers/RedrawOnViewModifier.swift b/Shared/Modifiers/RedrawOnViewModifier.swift new file mode 100644 index 00000000..0e6ee171 --- /dev/null +++ b/Shared/Modifiers/RedrawOnViewModifier.swift @@ -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)) + } +} diff --git a/Shared/Modifiers/UnsubscribeAlertModifier.swift b/Shared/Modifiers/UnsubscribeAlertModifier.swift index 7a73093e..56068a0b 100644 --- a/Shared/Modifiers/UnsubscribeAlertModifier.swift +++ b/Shared/Modifiers/UnsubscribeAlertModifier.swift @@ -2,13 +2,13 @@ import Foundation import SwiftUI struct UnsubscribeAlertModifier: ViewModifier { - @EnvironmentObject private var navigationState - @EnvironmentObject private var subscriptions + @EnvironmentObject private var navigation + @EnvironmentObject 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)" } diff --git a/Shared/Navigation/AccountsMenuView.swift b/Shared/Navigation/AccountsMenuView.swift new file mode 100644 index 00000000..a9bb0fa1 --- /dev/null +++ b/Shared/Navigation/AccountsMenuView.swift @@ -0,0 +1,33 @@ +import Defaults +import SwiftUI + +struct AccountsMenuView: View { + @EnvironmentObject 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 + } +} diff --git a/Shared/Navigation/AppSidebarNavigation.swift b/Shared/Navigation/AppSidebarNavigation.swift index 4bb0f1cc..9fc27a91 100644 --- a/Shared/Navigation/AppSidebarNavigation.swift +++ b/Shared/Navigation/AppSidebarNavigation.swift @@ -12,16 +12,18 @@ struct AppSidebarNavigation: View { } } - @EnvironmentObject private var navigationState - @EnvironmentObject private var playlists + @EnvironmentObject private var api + @EnvironmentObject private var instances + @EnvironmentObject private var navigation + @EnvironmentObject private var playlists @EnvironmentObject private var recents - @EnvironmentObject private var searchState - @EnvironmentObject private var subscriptions + @EnvironmentObject private var search + @EnvironmentObject private var subscriptions @State private var didApplyPrimaryViewWorkAround = false var selection: Binding { - 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,25 +112,27 @@ struct AppSidebarNavigation: View { AppSidebarRecents(selection: selection) .id("recentlyOpened") - AppSidebarSubscriptions(selection: selection) - AppSidebarPlaylists(selection: selection) + + 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")) } - NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: selection) { - Label("Subscriptions", systemImage: "star.circle") - .accessibility(label: Text("Subscriptions")) + 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) { @@ -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" } } diff --git a/Shared/Navigation/AppSidebarPlaylists.swift b/Shared/Navigation/AppSidebarPlaylists.swift index 12018689..1f71b5dc 100644 --- a/Shared/Navigation/AppSidebarPlaylists.swift +++ b/Shared/Navigation/AppSidebarPlaylists.swift @@ -1,14 +1,14 @@ import SwiftUI struct AppSidebarPlaylists: View { - @EnvironmentObject private var navigationState - @EnvironmentObject private var playlists + @EnvironmentObject private var navigation + @EnvironmentObject 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) diff --git a/Shared/Navigation/AppSidebarRecents.swift b/Shared/Navigation/AppSidebarRecents.swift index 1a919298..22e590dd 100644 --- a/Shared/Navigation/AppSidebarRecents.swift +++ b/Shared/Navigation/AppSidebarRecents.swift @@ -4,7 +4,7 @@ import SwiftUI struct AppSidebarRecents: View { @Binding var selection: TabSelection? - @EnvironmentObject private var navigationState + @EnvironmentObject private var navigation @EnvironmentObject 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") { diff --git a/Shared/Navigation/AppSidebarSubscriptions.swift b/Shared/Navigation/AppSidebarSubscriptions.swift index 67924961..b7f3c39d 100644 --- a/Shared/Navigation/AppSidebarSubscriptions.swift +++ b/Shared/Navigation/AppSidebarSubscriptions.swift @@ -1,8 +1,9 @@ +import Defaults import SwiftUI struct AppSidebarSubscriptions: View { - @EnvironmentObject private var navigationState - @EnvironmentObject private var subscriptions + @EnvironmentObject private var navigation + @EnvironmentObject 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)" } diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index cceaa1ca..cdaedd3c 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -2,14 +2,15 @@ import Defaults import SwiftUI struct AppTabNavigation: View { - @EnvironmentObject private var navigationState - @EnvironmentObject private var searchState + @EnvironmentObject private var navigation + @EnvironmentObject private var search @EnvironmentObject 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 { - SearchView() - .searchable(text: $searchState.queryText, placement: .navigationBarDrawer(displayMode: .always)) { - ForEach(searchState.querySuggestions.collection, id: \.self) { suggestion in - Text(suggestion) - .searchCompletion(suggestion) + LazyView( + SearchView() + .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) - } - .onSubmit(of: .search) { - searchState.changeQuery { query in - query.query = searchState.queryText + .onChange(of: search.queryText) { query in + search.loadSuggestions(query) } + .onSubmit(of: .search) { + 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 + } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 3816d4d8..9a9ed5b1 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -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 private var api + @EnvironmentObject 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) } } diff --git a/Shared/PearvidiousApp.swift b/Shared/PearvidiousApp.swift index 598b4b27..7ee070cb 100644 --- a/Shared/PearvidiousApp.swift +++ b/Shared/PearvidiousApp.swift @@ -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) } } diff --git a/Shared/Player/PlaybackBar.swift b/Shared/Player/PlaybackBar.swift index 854c2297..e94509b7 100644 --- a/Shared/Player/PlaybackBar.swift +++ b/Shared/Player/PlaybackBar.swift @@ -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" diff --git a/Shared/Player/Player.swift b/Shared/Player/Player.swift index 888164da..b915bafa 100644 --- a/Shared/Player/Player.swift +++ b/Shared/Player/Player.swift @@ -1,7 +1,9 @@ +import Defaults import SwiftUI struct Player: UIViewControllerRepresentable { - @EnvironmentObject private var playbackState + @EnvironmentObject private var api + @EnvironmentObject 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 } diff --git a/Shared/Player/PlayerViewController.swift b/Shared/Player/PlayerViewController.swift index c8c35583..7fb1700f 100644 --- a/Shared/Player/PlayerViewController.swift +++ b/Shared/Player/PlayerViewController.swift @@ -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 } } diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index ba7b1cbf..16b2067a 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -2,7 +2,7 @@ import Foundation import SwiftUI struct VideoDetails: View { - @EnvironmentObject private var subscriptions + @EnvironmentObject 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()) } } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 7a1a0d30..1e09a8af 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -12,31 +12,31 @@ struct VideoPlayerView: View { #endif } - @EnvironmentObject private var navigationState - @EnvironmentObject private var playbackState - - @ObservedObject private var store = Store