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