mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Locations manifest, reorganized instances settings
This commit is contained in:
parent
6f62f14adf
commit
4fcf57d755
8
Extensions/Sequence+Unique.swift
Normal file
8
Extensions/Sequence+Unique.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Sequence where Iterator.Element: Hashable {
|
||||||
|
func unique() -> [Iterator.Element] {
|
||||||
|
var seen: Set<Iterator.Element> = []
|
||||||
|
return filter { seen.insert($0).inserted }
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
.environmentObject(AccountsModel())
|
.environmentObject(AccountsModel())
|
||||||
.environmentObject(comments)
|
.environmentObject(comments)
|
||||||
.environmentObject(InstancesModel())
|
.environmentObject(InstancesModel())
|
||||||
|
.environmentObject(InstancesManifest())
|
||||||
.environmentObject(invidious)
|
.environmentObject(invidious)
|
||||||
.environmentObject(NavigationModel())
|
.environmentObject(NavigationModel())
|
||||||
.environmentObject(NetworkStateModel())
|
.environmentObject(NetworkStateModel())
|
||||||
|
@ -5,37 +5,50 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
static var bridge = AccountsBridge()
|
static var bridge = AccountsBridge()
|
||||||
|
|
||||||
let id: String
|
let id: String
|
||||||
let instanceID: String
|
let app: VideosApp
|
||||||
|
let instanceID: String?
|
||||||
var name: String?
|
var name: String?
|
||||||
let url: String
|
let url: String
|
||||||
let username: String
|
let username: String
|
||||||
let password: String?
|
let password: String?
|
||||||
var token: String?
|
var token: String?
|
||||||
let anonymous: Bool
|
let anonymous: Bool
|
||||||
|
let country: String?
|
||||||
|
let region: String?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String? = nil,
|
id: String? = nil,
|
||||||
|
app: VideosApp? = nil,
|
||||||
instanceID: String? = nil,
|
instanceID: String? = nil,
|
||||||
name: String? = nil,
|
name: String? = nil,
|
||||||
url: String? = nil,
|
url: String? = nil,
|
||||||
username: String? = nil,
|
username: String? = nil,
|
||||||
password: String? = nil,
|
password: String? = nil,
|
||||||
token: String? = nil,
|
token: String? = nil,
|
||||||
anonymous: Bool = false
|
anonymous: Bool = false,
|
||||||
|
country: String? = nil,
|
||||||
|
region: String? = nil
|
||||||
) {
|
) {
|
||||||
self.anonymous = anonymous
|
self.anonymous = anonymous
|
||||||
|
|
||||||
self.id = id ?? (anonymous ? "anonymous-\(instanceID!)" : UUID().uuidString)
|
self.id = id ?? (anonymous ? "anonymous-\(instanceID ?? url ?? UUID().uuidString)" : UUID().uuidString)
|
||||||
self.instanceID = instanceID ?? UUID().uuidString
|
self.app = app ?? .invidious
|
||||||
|
self.instanceID = instanceID
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = url ?? ""
|
self.url = url ?? ""
|
||||||
self.username = username ?? ""
|
self.username = username ?? ""
|
||||||
self.token = token
|
self.token = token
|
||||||
self.password = password ?? ""
|
self.password = password ?? ""
|
||||||
|
self.country = country
|
||||||
|
self.region = region
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance: Instance! {
|
var instance: Instance! {
|
||||||
Defaults[.instances].first { $0.id == instanceID }
|
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app, name: url, apiURL: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPublic: Bool {
|
||||||
|
instanceID.isNil
|
||||||
}
|
}
|
||||||
|
|
||||||
var shortUsername: String {
|
var shortUsername: String {
|
||||||
|
@ -3,7 +3,7 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class AccountValidator: Service {
|
final class AccountValidator: Service {
|
||||||
let app: Binding<VideosApp>
|
let app: Binding<VideosApp?>
|
||||||
let url: String
|
let url: String
|
||||||
let account: Account!
|
let account: Account!
|
||||||
|
|
||||||
@ -13,8 +13,10 @@ final class AccountValidator: Service {
|
|||||||
var isValidating: Binding<Bool>
|
var isValidating: Binding<Bool>
|
||||||
var error: Binding<String?>?
|
var error: Binding<String?>?
|
||||||
|
|
||||||
|
private var appsToValidateInstance = VideosApp.allCases
|
||||||
|
|
||||||
init(
|
init(
|
||||||
app: Binding<VideosApp>,
|
app: Binding<VideosApp?>,
|
||||||
url: String,
|
url: String,
|
||||||
account: Account? = nil,
|
account: Account? = nil,
|
||||||
id: Binding<String>,
|
id: Binding<String>,
|
||||||
@ -54,82 +56,128 @@ final class AccountValidator: Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func instanceValidationResource(_ app: VideosApp) -> Resource {
|
||||||
|
switch app {
|
||||||
|
case .invidious:
|
||||||
|
return resource("/api/v1/videos/dQw4w9WgXcQ")
|
||||||
|
|
||||||
|
case .piped:
|
||||||
|
return resource("/streams/dQw4w9WgXcQ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validateInstance() {
|
func validateInstance() {
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
neverGonnaGiveYouUp
|
guard let app = appsToValidateInstance.popLast() else { return }
|
||||||
|
tryValidatingUsing(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryValidatingUsing(_ app: VideosApp) {
|
||||||
|
instanceValidationResource(app)
|
||||||
.load()
|
.load()
|
||||||
.onSuccess { response in
|
.onSuccess { response in
|
||||||
guard self.url == self.formObjectID.wrappedValue else {
|
guard self.url == self.formObjectID.wrappedValue else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard !response.json.isEmpty else {
|
||||||
|
guard let app = self.appsToValidateInstance.popLast() else {
|
||||||
|
self.isValid.wrappedValue = false
|
||||||
|
self.isValidated.wrappedValue = true
|
||||||
|
self.isValidating.wrappedValue = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tryValidatingUsing(app)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let json = response.json.dictionaryValue
|
let json = response.json.dictionaryValue
|
||||||
let author = self.app.wrappedValue == .invidious ? json["author"] : json["uploader"]
|
let author = app == .invidious ? json["author"] : json["uploader"]
|
||||||
|
|
||||||
if author == "Rick Astley" {
|
if author == "Rick Astley" {
|
||||||
|
self.app.wrappedValue = app
|
||||||
self.isValid.wrappedValue = true
|
self.isValid.wrappedValue = true
|
||||||
self.error?.wrappedValue = nil
|
self.error?.wrappedValue = nil
|
||||||
} else {
|
} else {
|
||||||
self.isValid.wrappedValue = false
|
self.isValid.wrappedValue = false
|
||||||
}
|
}
|
||||||
|
self.isValidated.wrappedValue = true
|
||||||
|
self.isValidating.wrappedValue = false
|
||||||
}
|
}
|
||||||
.onFailure { error in
|
.onFailure { error in
|
||||||
guard self.url == self.formObjectID.wrappedValue else {
|
guard self.url == self.formObjectID.wrappedValue else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isValid.wrappedValue = false
|
if self.appsToValidateInstance.isEmpty {
|
||||||
self.error?.wrappedValue = error.userMessage
|
self.isValidating.wrappedValue = false
|
||||||
}
|
self.isValidated.wrappedValue = true
|
||||||
.onCompletion { _ in
|
self.isValid.wrappedValue = false
|
||||||
self.isValidated.wrappedValue = true
|
self.error?.wrappedValue = error.userMessage
|
||||||
self.isValidating.wrappedValue = false
|
} else {
|
||||||
|
guard let app = self.appsToValidateInstance.popLast() else { return }
|
||||||
|
self.tryValidatingUsing(app)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAccount() {
|
func validateAccount() {
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
accountRequest
|
guard let request = accountRequest else {
|
||||||
.onSuccess { response in
|
isValid.wrappedValue = false
|
||||||
guard self.account!.username == self.formObjectID.wrappedValue else {
|
isValidated.wrappedValue = true
|
||||||
return
|
isValidating.wrappedValue = false
|
||||||
}
|
|
||||||
|
|
||||||
switch self.app.wrappedValue {
|
return
|
||||||
case .invidious:
|
}
|
||||||
self.isValid.wrappedValue = true
|
|
||||||
case .piped:
|
|
||||||
let error = response.json.dictionaryValue["error"]?.string
|
|
||||||
let token = response.json.dictionaryValue["token"]?.string
|
|
||||||
self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true)
|
|
||||||
self.error!.wrappedValue = error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFailure { _ in
|
|
||||||
guard self.account!.username == self.formObjectID.wrappedValue else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.isValid.wrappedValue = false
|
request.onSuccess { response in
|
||||||
|
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
.onCompletion { _ in
|
|
||||||
self.isValidated.wrappedValue = true
|
switch self.app.wrappedValue {
|
||||||
self.isValidating.wrappedValue = false
|
case .invidious:
|
||||||
|
self.isValid.wrappedValue = true
|
||||||
|
case .piped:
|
||||||
|
let error = response.json.dictionaryValue["error"]?.string
|
||||||
|
let token = response.json.dictionaryValue["token"]?.string
|
||||||
|
self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true)
|
||||||
|
self.error!.wrappedValue = error
|
||||||
|
default:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.onFailure { _ in
|
||||||
|
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isValid.wrappedValue = false
|
||||||
|
}
|
||||||
|
.onCompletion { _ in
|
||||||
|
self.isValidated.wrappedValue = true
|
||||||
|
self.isValidating.wrappedValue = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var accountRequest: Request {
|
var accountRequest: Request? {
|
||||||
switch app.wrappedValue {
|
switch app.wrappedValue {
|
||||||
case .invidious:
|
case .invidious:
|
||||||
return feed.load()
|
return feed.load()
|
||||||
case .piped:
|
case .piped:
|
||||||
return login.request(.post, json: ["username": account.username, "password": account.password])
|
return login.request(.post, json: ["username": account.username, "password": account.password])
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
|
appsToValidateInstance = VideosApp.allCases
|
||||||
|
app.wrappedValue = nil
|
||||||
isValid.wrappedValue = false
|
isValid.wrappedValue = false
|
||||||
isValidated.wrappedValue = false
|
isValidated.wrappedValue = false
|
||||||
isValidating.wrappedValue = false
|
isValidating.wrappedValue = false
|
||||||
|
@ -12,7 +12,7 @@ struct AccountsBridge: Defaults.Bridge {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
"id": value.id,
|
"id": value.id,
|
||||||
"instanceID": value.instanceID,
|
"instanceID": value.instanceID ?? "",
|
||||||
"name": value.name ?? "",
|
"name": value.name ?? "",
|
||||||
"apiURL": value.url,
|
"apiURL": value.url,
|
||||||
"username": value.username,
|
"username": value.username,
|
||||||
|
@ -8,6 +8,8 @@ final class AccountsModel: ObservableObject {
|
|||||||
@Published private var invidious = InvidiousAPI()
|
@Published private var invidious = InvidiousAPI()
|
||||||
@Published private var piped = PipedAPI()
|
@Published private var piped = PipedAPI()
|
||||||
|
|
||||||
|
@Published var publicAccount: Account?
|
||||||
|
|
||||||
private var cancellables = [AnyCancellable]()
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
var all: [Account] {
|
var all: [Account] {
|
||||||
@ -70,7 +72,7 @@ final class AccountsModel: ObservableObject {
|
|||||||
piped.setAccount(account)
|
piped.setAccount(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[.lastAccountID] = account.anonymous ? nil : account.id
|
Defaults[.lastAccountID] = account.anonymous ? (account.isPublic ? "public" : nil) : account.id
|
||||||
Defaults[.lastInstanceID] = account.instanceID
|
Defaults[.lastInstanceID] = account.instanceID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
104
Model/InstancesManifest.swift
Normal file
104
Model/InstancesManifest.swift
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
import Siesta
|
||||||
|
import SwiftyJSON
|
||||||
|
|
||||||
|
final class InstancesManifest: Service, ObservableObject {
|
||||||
|
static let builtinManifestUrl = "https://r.yattee.stream/manifest.json"
|
||||||
|
static let shared = InstancesManifest()
|
||||||
|
|
||||||
|
@Published var instances = [ManifestedInstance]()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
configure {
|
||||||
|
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(
|
||||||
|
manifestURL,
|
||||||
|
requestMethods: [.get]
|
||||||
|
) { (content: Entity<JSON>
|
||||||
|
) -> [ManifestedInstance] in
|
||||||
|
guard let instances = content.json.dictionaryValue["instances"] else { return [] }
|
||||||
|
|
||||||
|
return instances.arrayValue.compactMap(self.extractInstance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPublicAccount(_ country: String?, accounts: AccountsModel, asCurrent: Bool = true) {
|
||||||
|
guard let country = country else {
|
||||||
|
accounts.publicAccount = nil
|
||||||
|
if asCurrent {
|
||||||
|
accounts.setCurrent(nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instancesList.load().onSuccess { response in
|
||||||
|
if let instances: [ManifestedInstance] = response.typedContent() {
|
||||||
|
guard let instance = instances.filter { $0.country == country }.randomElement() else { return }
|
||||||
|
let account = instance.anonymousAccount
|
||||||
|
accounts.publicAccount = account
|
||||||
|
if asCurrent {
|
||||||
|
accounts.setCurrent(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func changePublicAccount(_ accounts: AccountsModel, settings: SettingsModel) {
|
||||||
|
instancesList.load().onSuccess { response in
|
||||||
|
if let instances: [ManifestedInstance] = response.typedContent() {
|
||||||
|
let countryInstances = instances.filter { $0.country == Defaults[.countryOfPublicInstances] }
|
||||||
|
let region = countryInstances.first?.region ?? "Europe"
|
||||||
|
var regionInstances = instances.filter { $0.region == region }
|
||||||
|
|
||||||
|
if let publicAccountUrl = accounts.publicAccount?.url {
|
||||||
|
regionInstances = regionInstances.filter { $0.url.absoluteString != publicAccountUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let instance = regionInstances.randomElement() else {
|
||||||
|
settings.presentAlert(title: "Could not change location", message: "No locations available at the moment")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let account = instance.anonymousAccount
|
||||||
|
accounts.publicAccount = account
|
||||||
|
accounts.setCurrent(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractInstance(from json: JSON) -> ManifestedInstance? {
|
||||||
|
guard let app = json["app"].string,
|
||||||
|
let videosApp = VideosApp(rawValue: app.lowercased()),
|
||||||
|
let region = json["region"].string,
|
||||||
|
let country = json["country"].string,
|
||||||
|
let flag = json["flag"].string,
|
||||||
|
let url = json["url"].url else { return nil }
|
||||||
|
|
||||||
|
return ManifestedInstance(
|
||||||
|
app: videosApp,
|
||||||
|
country: country,
|
||||||
|
region: region,
|
||||||
|
flag: flag,
|
||||||
|
url: url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifestURL: String {
|
||||||
|
var url = Defaults[.instancesManifest]
|
||||||
|
|
||||||
|
if url.isEmpty {
|
||||||
|
url = Self.builtinManifestUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
var instancesList: Resource {
|
||||||
|
resource(absoluteURL: manifestURL)
|
||||||
|
}
|
||||||
|
}
|
30
Model/ManifestedInstance.swift
Normal file
30
Model/ManifestedInstance.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ManifestedInstance: Identifiable, Hashable {
|
||||||
|
let id = UUID().uuidString
|
||||||
|
let app: VideosApp
|
||||||
|
let country: String
|
||||||
|
let region: String
|
||||||
|
let flag: String
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
var instance: Instance {
|
||||||
|
.init(app: app, name: "Public - \(country)", apiURL: url.absoluteString)
|
||||||
|
}
|
||||||
|
|
||||||
|
var location: String {
|
||||||
|
"\(flag) \(country)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymousAccount: Account {
|
||||||
|
.init(
|
||||||
|
id: UUID().uuidString,
|
||||||
|
app: app,
|
||||||
|
name: location,
|
||||||
|
url: url.absoluteString,
|
||||||
|
anonymous: true,
|
||||||
|
country: country,
|
||||||
|
region: region
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -221,8 +221,9 @@ final class NavigationModel: ObservableObject {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentAlert(title: String, message: String) {
|
func presentAlert(title: String, message: String? = nil) {
|
||||||
alert = Alert(title: Text(title), message: Text(message))
|
let message = message.isNil ? nil : Text(message!)
|
||||||
|
alert = Alert(title: Text(title), message: message)
|
||||||
presentingAlert = true
|
presentingAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
Model/SettingsModel.swift
Normal file
18
Model/SettingsModel.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class SettingsModel: ObservableObject {
|
||||||
|
@Published var presentingAlert = false
|
||||||
|
@Published var alert = Alert(title: Text("Error"))
|
||||||
|
|
||||||
|
func presentAlert(title: String, message: String? = nil) {
|
||||||
|
let message = message.isNil ? nil : Text(message!)
|
||||||
|
alert = Alert(title: Text(title), message: message)
|
||||||
|
presentingAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentAlert(_ alert: Alert) {
|
||||||
|
self.alert = alert
|
||||||
|
presentingAlert = true
|
||||||
|
}
|
||||||
|
}
|
@ -18,16 +18,10 @@ extension Defaults.Keys {
|
|||||||
static let defaultForPlayerDetailsPageButtonLabelStyle = UIDevice.current.userInterfaceIdiom == .phone ? PlayerDetailsPageButtonLabelStyle.iconOnly : .iconAndText
|
static let defaultForPlayerDetailsPageButtonLabelStyle = UIDevice.current.userInterfaceIdiom == .phone ? PlayerDetailsPageButtonLabelStyle.iconOnly : .iconAndText
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static let kavinPipedInstanceID = "kavin-piped"
|
static let instancesManifest = Key<String>("instancesManifest", default: "")
|
||||||
static let instances = Key<[Instance]>("instances", default: [
|
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
|
||||||
.init(
|
|
||||||
app: .piped,
|
static let instances = Key<[Instance]>("instances", default: [])
|
||||||
id: kavinPipedInstanceID,
|
|
||||||
name: "Kavin",
|
|
||||||
apiURL: "https://pipedapi.kavin.rocks",
|
|
||||||
frontendURL: "https://piped.kavin.rocks"
|
|
||||||
)
|
|
||||||
])
|
|
||||||
static let accounts = Key<[Account]>("accounts", default: [])
|
static let accounts = Key<[Account]>("accounts", default: [])
|
||||||
static let lastAccountID = Key<Account.ID?>("lastAccountID")
|
static let lastAccountID = Key<Account.ID?>("lastAccountID")
|
||||||
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
||||||
@ -59,7 +53,7 @@ extension Defaults.Keys {
|
|||||||
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
||||||
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
||||||
static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", default: false)
|
static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", default: false)
|
||||||
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
|
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: nil)
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
||||||
#endif
|
#endif
|
||||||
|
@ -8,45 +8,50 @@ struct AccountsMenuView: View {
|
|||||||
@Default(.instances) private var instances
|
@Default(.instances) private var instances
|
||||||
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
|
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
|
||||||
|
|
||||||
var body: some View {
|
@ViewBuilder var body: some View {
|
||||||
Menu {
|
if !instances.isEmpty {
|
||||||
ForEach(allAccounts, id: \.id) { account in
|
Menu {
|
||||||
Button {
|
ForEach(allAccounts, id: \.id) { account in
|
||||||
model.setCurrent(account)
|
Button {
|
||||||
} label: {
|
model.setCurrent(account)
|
||||||
HStack {
|
} label: {
|
||||||
Text(accountButtonTitle(account: account))
|
HStack {
|
||||||
|
Text(accountButtonTitle(account: account))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if model.current == account {
|
if model.current == account {
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} label: {
|
||||||
} label: {
|
HStack {
|
||||||
HStack {
|
if !accountPickerDisplaysUsername || !(model.current?.isPublic ?? true) {
|
||||||
Image(systemName: "person.crop.circle")
|
Image(systemName: "globe")
|
||||||
if accountPickerDisplaysUsername {
|
}
|
||||||
label
|
|
||||||
.labelStyle(.titleOnly)
|
if accountPickerDisplaysUsername {
|
||||||
|
label
|
||||||
|
.labelStyle(.titleOnly)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(allAccounts.isEmpty)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
.disabled(instances.isEmpty)
|
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var label: some View {
|
private var label: some View {
|
||||||
Label(model.current?.description ?? "Select Account", systemImage: "person.crop.circle")
|
Label(model.current?.description ?? "Select Account", systemImage: "globe")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var allAccounts: [Account] {
|
private var allAccounts: [Account] {
|
||||||
accounts + instances.map(\.anonymousAccount)
|
accounts + instances.map(\.anonymousAccount) + [model.publicAccount].compactMap { $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func accountButtonTitle(account: Account) -> String {
|
private func accountButtonTitle(account: Account) -> String {
|
||||||
instances.count > 1 ? "\(account.description) — \(account.instance.description)" : account.description
|
account.isPublic ? account.description : "\(account.description) — \(account.instance.shortDescription)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ struct ContentView: View {
|
|||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SearchModel> private var search
|
@EnvironmentObject<SearchModel> private var search
|
||||||
|
@EnvironmentObject<SettingsModel> private var settings
|
||||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
|
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ struct ContentView: View {
|
|||||||
AppSidebarNavigation()
|
AppSidebarNavigation()
|
||||||
#elseif os(tvOS)
|
#elseif os(tvOS)
|
||||||
TVNavigationView()
|
TVNavigationView()
|
||||||
|
.environmentObject(settings)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onChange(of: accounts.signedIn) { _ in
|
.onChange(of: accounts.signedIn) { _ in
|
||||||
@ -105,10 +107,12 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
EmptyView().sheet(isPresented: $navigation.presentingSettings, onDismiss: openWelcomeScreenIfAccountEmpty) {
|
EmptyView().sheet(isPresented: $navigation.presentingSettings) {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
|
.environmentObject(settings)
|
||||||
|
.environmentObject(navigation)
|
||||||
.environmentObject(player)
|
.environmentObject(player)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -126,14 +130,6 @@ struct ContentView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func openWelcomeScreenIfAccountEmpty() {
|
|
||||||
guard Defaults[.instances].isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
navigation.presentingWelcomeScreen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoPlayer: some View {
|
var videoPlayer: some View {
|
||||||
VideoPlayerView()
|
VideoPlayerView()
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
|
@ -66,9 +66,7 @@ final class AppleAVPlayerViewController: UIViewController {
|
|||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
var infoViewControllers = [UIHostingController<AnyView>]()
|
var infoViewControllers = [UIHostingController<AnyView>]()
|
||||||
if CommentsModel.enabled {
|
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
|
||||||
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var queueSections = [NowPlayingView.ViewSection.playingNext]
|
var queueSections = [NowPlayingView.ViewSection.playingNext]
|
||||||
if Defaults[.showHistoryInPlayer] {
|
if Defaults[.showHistoryInPlayer] {
|
||||||
|
@ -118,6 +118,7 @@ struct AccountForm: View {
|
|||||||
var footer: some View {
|
var footer: some View {
|
||||||
HStack {
|
HStack {
|
||||||
AccountValidationStatus(
|
AccountValidationStatus(
|
||||||
|
app: .constant(instance.app),
|
||||||
isValid: $isValid,
|
isValid: $isValid,
|
||||||
isValidated: $isValidated,
|
isValidated: $isValidated,
|
||||||
isValidating: $isValidating,
|
isValidating: $isValidating,
|
||||||
|
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AccountValidationStatus: View {
|
struct AccountValidationStatus: View {
|
||||||
|
@Binding var app: VideosApp?
|
||||||
@Binding var isValid: Bool
|
@Binding var isValid: Bool
|
||||||
@Binding var isValidated: Bool
|
@Binding var isValidated: Bool
|
||||||
@Binding var isValidating: Bool
|
@Binding var isValidating: Bool
|
||||||
@ -16,7 +17,7 @@ struct AccountValidationStatus: View {
|
|||||||
.opacity(isValidating ? 1 : (isValidated ? 1 : 0))
|
.opacity(isValidating ? 1 : (isValidated ? 1 : 0))
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(isValid ? "Connected successfully" : "Connection failed")
|
Text(isValid ? "Connected successfully (\(app?.name ?? "Unknown"))" : "Connection failed")
|
||||||
if let error = error, !isValid {
|
if let error = error, !isValid {
|
||||||
Text(error)
|
Text(error)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
|
@ -11,6 +11,10 @@ struct AccountsNavigationLink: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
removeInstanceButton(instance)
|
removeInstanceButton(instance)
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
57
Shared/Settings/AdvancedSettings.swift
Normal file
57
Shared/Settings/AdvancedSettings.swift
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AdvancedSettings: View {
|
||||||
|
@Default(.instancesManifest) private var instancesManifest
|
||||||
|
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
#if os(macOS)
|
||||||
|
advancedSettings
|
||||||
|
Spacer()
|
||||||
|
#else
|
||||||
|
List {
|
||||||
|
advancedSettings
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(maxWidth: 1000)
|
||||||
|
#endif
|
||||||
|
.navigationTitle("Advanced")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var advancedSettings: some View {
|
||||||
|
Section(header: manifestHeader, footer: manifestFooter) {
|
||||||
|
TextField("URL", text: $instancesManifest)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
|
Section(header: SettingsHeader(text: "Debugging")) {
|
||||||
|
showMPVPlaybackStatsToggle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifestHeader: some View {
|
||||||
|
SettingsHeader(text: "Public Manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifestFooter: some View {
|
||||||
|
Text("You can create your own locations manifest and set its URL here to replace the built-in one")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showMPVPlaybackStatsToggle: some View {
|
||||||
|
Toggle("Show MPV playback statistics", isOn: $showMPVPlaybackStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AdvancedSettings_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
AdvancedSettings()
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,8 @@ struct InstanceForm: View {
|
|||||||
|
|
||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var url = ""
|
@State private var url = ""
|
||||||
@State private var app = VideosApp.invidious
|
|
||||||
|
|
||||||
|
@State private var app: VideosApp?
|
||||||
@State private var isValid = false
|
@State private var isValid = false
|
||||||
@State private var isValidated = false
|
@State private var isValidated = false
|
||||||
@State private var isValidating = false
|
@State private var isValidating = false
|
||||||
@ -27,7 +27,6 @@ struct InstanceForm: View {
|
|||||||
}
|
}
|
||||||
.frame(maxWidth: 1000)
|
.frame(maxWidth: 1000)
|
||||||
}
|
}
|
||||||
.onChange(of: app) { _ in validate() }
|
|
||||||
.onChange(of: url) { _ in validate() }
|
.onChange(of: url) { _ in validate() }
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
@ -35,13 +34,13 @@ struct InstanceForm: View {
|
|||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||||
.background(Color.background(scheme: colorScheme))
|
.background(Color.background(scheme: colorScheme))
|
||||||
#else
|
#else
|
||||||
.frame(width: 400, height: 190)
|
.frame(width: 400, height: 150)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Text("Add Instance")
|
Text("Add Location")
|
||||||
.font(.title2.bold())
|
.font(.title2.bold())
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -71,17 +70,9 @@ struct InstanceForm: View {
|
|||||||
|
|
||||||
private var formFields: some View {
|
private var formFields: some View {
|
||||||
Group {
|
Group {
|
||||||
Picker("Application", selection: $app) {
|
|
||||||
ForEach(VideosApp.allCases, id: \.self) { app in
|
|
||||||
Text(app.rawValue.capitalized).tag(app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.labelsHidden()
|
|
||||||
|
|
||||||
TextField("Name", text: $name)
|
TextField("Name", text: $name)
|
||||||
|
|
||||||
TextField("API URL", text: $url)
|
TextField("URL", text: $url)
|
||||||
|
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
@ -92,7 +83,13 @@ struct InstanceForm: View {
|
|||||||
|
|
||||||
private var footer: some View {
|
private var footer: some View {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
AccountValidationStatus(isValid: $isValid, isValidated: $isValidated, isValidating: $isValidating, error: $validationError)
|
AccountValidationStatus(
|
||||||
|
app: $app,
|
||||||
|
isValid: $isValid,
|
||||||
|
isValidated: $isValidated,
|
||||||
|
isValidating: $isValidating,
|
||||||
|
error: $validationError
|
||||||
|
)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -137,7 +134,7 @@ struct InstanceForm: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func submitForm() {
|
func submitForm() {
|
||||||
guard isValid else {
|
guard isValid, let app = app else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
119
Shared/Settings/LocationsSettings.swift
Normal file
119
Shared/Settings/LocationsSettings.swift
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LocationsSettings: View {
|
||||||
|
@State private var countries = [String]()
|
||||||
|
@State private var presentingInstanceForm = false
|
||||||
|
@State private var savedFormInstanceID: Instance.ID?
|
||||||
|
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<SettingsModel> private var model
|
||||||
|
|
||||||
|
@Default(.countryOfPublicInstances) private var countryOfPublicInstances
|
||||||
|
@Default(.instances) private var instances
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
#if os(macOS)
|
||||||
|
settings
|
||||||
|
Spacer()
|
||||||
|
#else
|
||||||
|
List {
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.onAppear(perform: loadCountries)
|
||||||
|
.onChange(of: countryOfPublicInstances) { newCountry in
|
||||||
|
InstancesManifest.shared.setPublicAccount(newCountry, accounts: accounts, asCurrent: accounts.current?.isPublic ?? true)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $presentingInstanceForm) {
|
||||||
|
InstanceForm(savedInstanceID: $savedFormInstanceID)
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(maxWidth: 1000)
|
||||||
|
#endif
|
||||||
|
.navigationTitle("Locations")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var settings: some View {
|
||||||
|
Section(header: SettingsHeader(text: "Public Locations"), footer: countryFooter) {
|
||||||
|
Picker("Country", selection: $countryOfPublicInstances) {
|
||||||
|
Text("Don't use public locations").tag(String?.none)
|
||||||
|
ForEach(countries, id: \.self) { country in
|
||||||
|
Text(country).tag(Optional(country))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.pickerStyle(.inline)
|
||||||
|
#endif
|
||||||
|
.disabled(countries.isEmpty)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
InstancesManifest.shared.changePublicAccount(accounts, settings: model)
|
||||||
|
} label: {
|
||||||
|
if let account = accounts.current, account.isPublic {
|
||||||
|
Text("Switch to other public location")
|
||||||
|
} else {
|
||||||
|
Text("Switch to public locations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(countryOfPublicInstances.isNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: SettingsHeader(text: "Custom Locations")) {
|
||||||
|
#if os(macOS)
|
||||||
|
InstancesSettings()
|
||||||
|
#else
|
||||||
|
ForEach(instances) { instance in
|
||||||
|
AccountsNavigationLink(instance: instance)
|
||||||
|
}
|
||||||
|
addInstanceButton
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var countryFooter: some View {
|
||||||
|
if let account = accounts.current {
|
||||||
|
let locationType = account.isPublic ? (account.country ?? "Unknown") : "Custom"
|
||||||
|
let description = account.isPublic ? account.url : account.instance?.description ?? "unknown"
|
||||||
|
|
||||||
|
Text("Current: \(locationType)\n\(description)")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
#if os(macOS)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCountries() {
|
||||||
|
InstancesManifest.shared.instancesList.load()
|
||||||
|
.onSuccess { response in
|
||||||
|
if let instances: [ManifestedInstance] = response.typedContent() {
|
||||||
|
self.countries = instances.map(\.country).unique().sorted()
|
||||||
|
}
|
||||||
|
}.onFailure { _ in
|
||||||
|
model.presentAlert(title: "Could not load locations manifest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var addInstanceButton: some View {
|
||||||
|
Button {
|
||||||
|
presentingInstanceForm = true
|
||||||
|
} label: {
|
||||||
|
Label("Add Location...", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LocationsSettings_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LocationsSettings()
|
||||||
|
.environmentObject(AccountsModel())
|
||||||
|
.environmentObject(NavigationModel())
|
||||||
|
}
|
||||||
|
}
|
@ -26,8 +26,6 @@ struct PlayerSettings: View {
|
|||||||
|
|
||||||
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
|
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
|
||||||
|
|
||||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
private var idiom: UIUserInterfaceIdiom {
|
private var idiom: UIUserInterfaceIdiom {
|
||||||
UIDevice.current.userInterfaceIdiom
|
UIDevice.current.userInterfaceIdiom
|
||||||
@ -103,10 +101,6 @@ struct PlayerSettings: View {
|
|||||||
lockOrientationInFullScreenToggle
|
lockOrientationInFullScreenToggle
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Section(header: SettingsHeader(text: "Debugging")) {
|
|
||||||
showMPVPlaybackStatsToggle
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,10 +227,6 @@ struct PlayerSettings: View {
|
|||||||
Toggle("Close PiP and open player when application enters foreground", isOn: $closePiPAndOpenPlayerOnEnteringForeground)
|
Toggle("Close PiP and open player when application enters foreground", isOn: $closePiPAndOpenPlayerOnEnteringForeground)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private var showMPVPlaybackStatsToggle: some View {
|
|
||||||
Toggle("Show MPV playback statistics", isOn: $showMPVPlaybackStats)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PlaybackSettings_Previews: PreviewProvider {
|
struct PlaybackSettings_Previews: PreviewProvider {
|
||||||
|
@ -5,10 +5,10 @@ import SwiftUI
|
|||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private enum Tabs: Hashable {
|
private enum Tabs: Hashable {
|
||||||
case instances, browsing, player, history, sponsorBlock, help
|
case browsing, player, history, sponsorBlock, locations, advanced, help
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var selection = Tabs.instances
|
@State private var selection = Tabs.browsing
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@ -18,24 +18,20 @@ struct SettingsView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@State private var presentingInstanceForm = false
|
@EnvironmentObject<SettingsModel> private var model
|
||||||
@State private var savedFormInstanceID: Instance.ID?
|
|
||||||
|
|
||||||
@Default(.instances) private var instances
|
@Default(.instances) private var instances
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
settings
|
||||||
|
.environmentObject(model)
|
||||||
|
.alert(isPresented: $model.presentingAlert) { model.alert }
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
TabView(selection: $selection) {
|
TabView(selection: $selection) {
|
||||||
Form {
|
|
||||||
InstancesSettings()
|
|
||||||
.environmentObject(accounts)
|
|
||||||
}
|
|
||||||
.tabItem {
|
|
||||||
Label("Instances", systemImage: "server.rack")
|
|
||||||
}
|
|
||||||
.tag(Tabs.instances)
|
|
||||||
|
|
||||||
Form {
|
Form {
|
||||||
BrowsingSettings()
|
BrowsingSettings()
|
||||||
}
|
}
|
||||||
@ -68,6 +64,22 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.tag(Tabs.sponsorBlock)
|
.tag(Tabs.sponsorBlock)
|
||||||
|
|
||||||
|
Form {
|
||||||
|
LocationsSettings()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Locations", systemImage: "globe")
|
||||||
|
}
|
||||||
|
.tag(Tabs.locations)
|
||||||
|
|
||||||
|
Group {
|
||||||
|
AdvancedSettings()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Advanced", systemImage: "wrench.and.screwdriver")
|
||||||
|
}
|
||||||
|
.tag(Tabs.advanced)
|
||||||
|
|
||||||
Form {
|
Form {
|
||||||
Help()
|
Help()
|
||||||
}
|
}
|
||||||
@ -88,9 +100,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $presentingInstanceForm) {
|
|
||||||
InstanceForm(savedInstanceID: $savedFormInstanceID)
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,16 +109,6 @@ struct SettingsView: View {
|
|||||||
List {
|
List {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
AccountSelectionView()
|
AccountSelectionView()
|
||||||
#endif
|
|
||||||
|
|
||||||
Section(header: Text("Instances")) {
|
|
||||||
ForEach(instances) { instance in
|
|
||||||
AccountsNavigationLink(instance: instance)
|
|
||||||
}
|
|
||||||
addInstanceButton
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
Divider()
|
Divider()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -144,6 +144,18 @@ struct SettingsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Label("SponsorBlock", systemImage: "dollarsign.circle")
|
Label("SponsorBlock", systemImage: "dollarsign.circle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
LocationsSettings()
|
||||||
|
} label: {
|
||||||
|
Label("Locations", systemImage: "globe")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
AdvancedSettings()
|
||||||
|
} label: {
|
||||||
|
Label("Advanced", systemImage: "wrench.and.screwdriver")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(footer: versionString) {
|
Section(footer: versionString) {
|
||||||
@ -175,8 +187,6 @@ struct SettingsView: View {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private var windowHeight: Double {
|
private var windowHeight: Double {
|
||||||
switch selection {
|
switch selection {
|
||||||
case .instances:
|
|
||||||
return 390
|
|
||||||
case .browsing:
|
case .browsing:
|
||||||
return 390
|
return 390
|
||||||
case .player:
|
case .player:
|
||||||
@ -185,6 +195,10 @@ struct SettingsView: View {
|
|||||||
return 480
|
return 480
|
||||||
case .sponsorBlock:
|
case .sponsorBlock:
|
||||||
return 660
|
return 660
|
||||||
|
case .locations:
|
||||||
|
return 480
|
||||||
|
case .advanced:
|
||||||
|
return 300
|
||||||
case .help:
|
case .help:
|
||||||
return 570
|
return 570
|
||||||
}
|
}
|
||||||
@ -197,14 +211,6 @@ struct SettingsView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addInstanceButton: some View {
|
|
||||||
Button {
|
|
||||||
presentingInstanceForm = true
|
|
||||||
} label: {
|
|
||||||
Label("Add Instance...", systemImage: "plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SettingsView_Previews: PreviewProvider {
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
@ -1,65 +1,93 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct WelcomeScreen: View {
|
struct WelcomeScreen: View {
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@State private var store = [ManifestedInstance]()
|
||||||
@Default(.accounts) private var allAccounts
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let welcomeScreen = VStack {
|
VStack(alignment: .leading) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("Welcome")
|
Text("Welcome to Yattee")
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
|
|
||||||
if allAccounts.isEmpty {
|
Text("Select location closest to you to get the best performance")
|
||||||
Text("To start, configure your Instances in Settings")
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
|
||||||
} else {
|
|
||||||
Text("To start, pick one of your accounts:")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
#if os(tvOS)
|
|
||||||
AccountSelectionView(showHeader: false)
|
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
ForEach(store.map(\.country).sorted(), id: \.self) { country in
|
||||||
Button {
|
Button {
|
||||||
|
Defaults[.countryOfPublicInstances] = country
|
||||||
|
InstancesManifest.shared.setPublicAccount(country, accounts: accounts)
|
||||||
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
} label: {
|
} label: {
|
||||||
Text("Start")
|
HStack(spacing: 10) {
|
||||||
}
|
if let flag = flag(country) {
|
||||||
.opacity(accounts.current.isNil ? 0 : 1)
|
Text(flag)
|
||||||
.disabled(accounts.current.isNil)
|
}
|
||||||
|
Text(country)
|
||||||
#else
|
#if !os(tvOS)
|
||||||
AccountsMenuView()
|
.foregroundColor(.white)
|
||||||
.onChange(of: accounts.current) { _ in
|
#endif
|
||||||
presentationMode.wrappedValue.dismiss()
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(tvOS)
|
||||||
.frame(maxWidth: 280)
|
.padding(8)
|
||||||
|
#else
|
||||||
|
.padding(4)
|
||||||
|
.background(RoundedRectangle(cornerRadius: 4).foregroundColor(Color.accentColor))
|
||||||
|
.padding(.bottom, 2)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
#endif
|
#endif
|
||||||
#endif
|
}
|
||||||
|
.padding(.horizontal, 30)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Text("This information will not be collected and it will be saved only on your device. You can change it later in settings.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
OpenSettingsButton()
|
#if !os(tvOS)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
Spacer()
|
OpenSettingsButton()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
resource.load().onSuccess { response in
|
||||||
|
if let instances: [ManifestedInstance] = response.typedContent() {
|
||||||
|
store = instances
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 400, minHeight: 400)
|
.frame(minWidth: 400, minHeight: 400)
|
||||||
|
#elseif os(tvOS)
|
||||||
|
.frame(maxWidth: 1000)
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
func flag(_ country: String) -> String? {
|
||||||
welcomeScreen
|
store.first { $0.country == country }?.flag
|
||||||
.interactiveDismissDisabled()
|
}
|
||||||
} else {
|
|
||||||
welcomeScreen
|
var resource: Resource {
|
||||||
}
|
InstancesManifest.shared.instancesList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ struct YatteeApp: App {
|
|||||||
@StateObject private var playlists = PlaylistsModel()
|
@StateObject private var playlists = PlaylistsModel()
|
||||||
@StateObject private var recents = RecentsModel()
|
@StateObject private var recents = RecentsModel()
|
||||||
@StateObject private var search = SearchModel()
|
@StateObject private var search = SearchModel()
|
||||||
|
@StateObject private var settings = SettingsModel()
|
||||||
@StateObject private var subscriptions = SubscriptionsModel()
|
@StateObject private var subscriptions = SubscriptionsModel()
|
||||||
@StateObject private var thumbnails = ThumbnailsModel()
|
@StateObject private var thumbnails = ThumbnailsModel()
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ struct YatteeApp: App {
|
|||||||
.environmentObject(playerTime)
|
.environmentObject(playerTime)
|
||||||
.environmentObject(playlists)
|
.environmentObject(playlists)
|
||||||
.environmentObject(recents)
|
.environmentObject(recents)
|
||||||
|
.environmentObject(settings)
|
||||||
.environmentObject(subscriptions)
|
.environmentObject(subscriptions)
|
||||||
.environmentObject(thumbnails)
|
.environmentObject(thumbnails)
|
||||||
.environmentObject(menu)
|
.environmentObject(menu)
|
||||||
@ -144,8 +146,10 @@ struct YatteeApp: App {
|
|||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
|
.environmentObject(navigation)
|
||||||
.environmentObject(player)
|
.environmentObject(player)
|
||||||
.environmentObject(playerControls)
|
.environmentObject(playerControls)
|
||||||
|
.environmentObject(settings)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -170,17 +174,23 @@ struct YatteeApp: App {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if let account = accounts.lastUsed ??
|
if Defaults[.lastAccountID] != "public",
|
||||||
instances.lastUsed?.anonymousAccount ??
|
let account = accounts.lastUsed ??
|
||||||
InstancesModel.all.first?.anonymousAccount
|
instances.lastUsed?.anonymousAccount ??
|
||||||
|
InstancesModel.all.first?.anonymousAccount
|
||||||
{
|
{
|
||||||
accounts.setCurrent(account)
|
accounts.setCurrent(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
if accounts.current.isNil {
|
let countryOfPublicInstances = Defaults[.countryOfPublicInstances]
|
||||||
|
if accounts.current.isNil, countryOfPublicInstances.isNil {
|
||||||
navigation.presentingWelcomeScreen = true
|
navigation.presentingWelcomeScreen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !countryOfPublicInstances.isNil {
|
||||||
|
InstancesManifest.shared.setPublicAccount(countryOfPublicInstances!, accounts: accounts, asCurrent: accounts.current.isNil)
|
||||||
|
}
|
||||||
|
|
||||||
playlists.accounts = accounts
|
playlists.accounts = accounts
|
||||||
search.accounts = accounts
|
search.accounts = accounts
|
||||||
subscriptions.accounts = accounts
|
subscriptions.accounts = accounts
|
||||||
|
@ -398,7 +398,6 @@
|
|||||||
37737786276F9858000521C1 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37737785276F9858000521C1 /* Windows.swift */; };
|
37737786276F9858000521C1 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37737785276F9858000521C1 /* Windows.swift */; };
|
||||||
3774123327387CB000423605 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
3774123327387CB000423605 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||||
3774123527387CC700423605 /* PipedAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3700155A271B0D4D0049C794 /* PipedAPI.swift */; };
|
|
||||||
3774124927387D2300423605 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; };
|
3774124927387D2300423605 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; };
|
||||||
3774124A27387D2300423605 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; };
|
3774124A27387D2300423605 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; };
|
||||||
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
||||||
@ -442,6 +441,18 @@
|
|||||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
||||||
377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
||||||
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
||||||
|
377ABC40286E4AD5009C986F /* InstancesManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC3F286E4AD5009C986F /* InstancesManifest.swift */; };
|
||||||
|
377ABC41286E4AD5009C986F /* InstancesManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC3F286E4AD5009C986F /* InstancesManifest.swift */; };
|
||||||
|
377ABC42286E4AD5009C986F /* InstancesManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC3F286E4AD5009C986F /* InstancesManifest.swift */; };
|
||||||
|
377ABC44286E4B74009C986F /* ManifestedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC43286E4B74009C986F /* ManifestedInstance.swift */; };
|
||||||
|
377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC43286E4B74009C986F /* ManifestedInstance.swift */; };
|
||||||
|
377ABC46286E4B74009C986F /* ManifestedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC43286E4B74009C986F /* ManifestedInstance.swift */; };
|
||||||
|
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC47286E5887009C986F /* Sequence+Unique.swift */; };
|
||||||
|
377ABC49286E5887009C986F /* Sequence+Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC47286E5887009C986F /* Sequence+Unique.swift */; };
|
||||||
|
377ABC4A286E5887009C986F /* Sequence+Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC47286E5887009C986F /* Sequence+Unique.swift */; };
|
||||||
|
377ABC4C286E6A78009C986F /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC4B286E6A78009C986F /* LocationsSettings.swift */; };
|
||||||
|
377ABC4D286E6A78009C986F /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC4B286E6A78009C986F /* LocationsSettings.swift */; };
|
||||||
|
377ABC4E286E6A78009C986F /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377ABC4B286E6A78009C986F /* LocationsSettings.swift */; };
|
||||||
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7D4267A080300A6BBAF /* SwiftyJSON */; };
|
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7D4267A080300A6BBAF /* SwiftyJSON */; };
|
||||||
377FC7DB267A080300A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7DA267A080300A6BBAF /* Logging */; };
|
377FC7DB267A080300A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7DA267A080300A6BBAF /* Logging */; };
|
||||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; };
|
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; };
|
||||||
@ -726,6 +737,12 @@
|
|||||||
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
||||||
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
||||||
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
||||||
|
37F0F4EA286F397E00C06C2E /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4E9286F397E00C06C2E /* SettingsModel.swift */; };
|
||||||
|
37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4E9286F397E00C06C2E /* SettingsModel.swift */; };
|
||||||
|
37F0F4EC286F397E00C06C2E /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4E9286F397E00C06C2E /* SettingsModel.swift */; };
|
||||||
|
37F0F4EE286F734400C06C2E /* AdvancedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */; };
|
||||||
|
37F0F4EF286F734400C06C2E /* AdvancedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */; };
|
||||||
|
37F0F4F0286F734400C06C2E /* AdvancedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */; };
|
||||||
37F13B62285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
37F13B62285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
||||||
37F13B63285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
37F13B63285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
||||||
37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
||||||
@ -1032,6 +1049,10 @@
|
|||||||
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||||
37737785276F9858000521C1 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
|
37737785276F9858000521C1 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
||||||
|
377ABC3F286E4AD5009C986F /* InstancesManifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstancesManifest.swift; sourceTree = "<group>"; };
|
||||||
|
377ABC43286E4B74009C986F /* ManifestedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManifestedInstance.swift; sourceTree = "<group>"; };
|
||||||
|
377ABC47286E5887009C986F /* Sequence+Unique.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Unique.swift"; sourceTree = "<group>"; };
|
||||||
|
377ABC4B286E6A78009C986F /* LocationsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettings.swift; sourceTree = "<group>"; };
|
||||||
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
|
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
|
||||||
3782B9512755667600990149 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
|
3782B9512755667600990149 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
|
||||||
3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+FocusRingType.swift"; sourceTree = "<group>"; };
|
3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+FocusRingType.swift"; sourceTree = "<group>"; };
|
||||||
@ -1162,6 +1183,8 @@
|
|||||||
37EBD8C927AF26C200F1C24B /* MPVBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVBackend.swift; sourceTree = "<group>"; };
|
37EBD8C927AF26C200F1C24B /* MPVBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVBackend.swift; sourceTree = "<group>"; };
|
||||||
37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; };
|
37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; };
|
||||||
37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = "<group>"; };
|
37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = "<group>"; };
|
||||||
|
37F0F4E9286F397E00C06C2E /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = "<group>"; };
|
||||||
|
37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettings.swift; sourceTree = "<group>"; };
|
||||||
37F13B61285E43C000B137E4 /* ControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlay.swift; sourceTree = "<group>"; };
|
37F13B61285E43C000B137E4 /* ControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlay.swift; sourceTree = "<group>"; };
|
||||||
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
|
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
37F4AD1A28612B23004D0F66 /* OpeningStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningStream.swift; sourceTree = "<group>"; };
|
37F4AD1A28612B23004D0F66 /* OpeningStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningStream.swift; sourceTree = "<group>"; };
|
||||||
@ -1620,11 +1643,13 @@
|
|||||||
37484C2826FC83FF00287258 /* AccountForm.swift */,
|
37484C2826FC83FF00287258 /* AccountForm.swift */,
|
||||||
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */,
|
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */,
|
||||||
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */,
|
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */,
|
||||||
|
37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */,
|
||||||
376BE50A27349108009AD608 /* BrowsingSettings.swift */,
|
376BE50A27349108009AD608 /* BrowsingSettings.swift */,
|
||||||
37579D5C27864F5F00FD0B98 /* Help.swift */,
|
37579D5C27864F5F00FD0B98 /* Help.swift */,
|
||||||
37BC50A72778A84700510953 /* HistorySettings.swift */,
|
37BC50A72778A84700510953 /* HistorySettings.swift */,
|
||||||
37484C2426FC83E000287258 /* InstanceForm.swift */,
|
37484C2426FC83E000287258 /* InstanceForm.swift */,
|
||||||
37484C2C26FC844700287258 /* InstanceSettings.swift */,
|
37484C2C26FC844700287258 /* InstanceSettings.swift */,
|
||||||
|
377ABC4B286E6A78009C986F /* LocationsSettings.swift */,
|
||||||
37484C1826FC837400287258 /* PlayerSettings.swift */,
|
37484C1826FC837400287258 /* PlayerSettings.swift */,
|
||||||
374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */,
|
374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */,
|
||||||
376BE50627347B57009AD608 /* SettingsHeader.swift */,
|
376BE50627347B57009AD608 /* SettingsHeader.swift */,
|
||||||
@ -1814,6 +1839,7 @@
|
|||||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
||||||
370B79C8286279810045DB77 /* NSObject+Swizzle.swift */,
|
370B79C8286279810045DB77 /* NSObject+Swizzle.swift */,
|
||||||
3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */,
|
3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */,
|
||||||
|
377ABC47286E5887009C986F /* Sequence+Unique.swift */,
|
||||||
3782B9512755667600990149 /* String+Format.swift */,
|
3782B9512755667600990149 /* String+Format.swift */,
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
||||||
370B79CB286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift */,
|
370B79CB286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift */,
|
||||||
@ -1949,6 +1975,8 @@
|
|||||||
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
||||||
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
||||||
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
||||||
|
377ABC3F286E4AD5009C986F /* InstancesManifest.swift */,
|
||||||
|
377ABC43286E4B74009C986F /* ManifestedInstance.swift */,
|
||||||
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
||||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||||
3756C2A92861151C00E4B059 /* NetworkStateModel.swift */,
|
3756C2A92861151C00E4B059 /* NetworkStateModel.swift */,
|
||||||
@ -1957,6 +1985,7 @@
|
|||||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||||
|
37F0F4E9286F397E00C06C2E /* SettingsModel.swift */,
|
||||||
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
||||||
3797758A2689345500DD52A8 /* Store.swift */,
|
3797758A2689345500DD52A8 /* Store.swift */,
|
||||||
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
||||||
@ -2628,6 +2657,7 @@
|
|||||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||||
|
377ABC40286E4AD5009C986F /* InstancesManifest.swift in Sources */,
|
||||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||||
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
@ -2660,6 +2690,7 @@
|
|||||||
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */,
|
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */,
|
||||||
37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
||||||
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
|
37F0F4EA286F397E00C06C2E /* SettingsModel.swift in Sources */,
|
||||||
378AE943274EF00A006A4EE1 /* Color+Background.swift in Sources */,
|
378AE943274EF00A006A4EE1 /* Color+Background.swift in Sources */,
|
||||||
37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */,
|
37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */,
|
||||||
376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||||
@ -2727,6 +2758,7 @@
|
|||||||
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
||||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
|
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
|
||||||
37520699285E8DD300CA655F /* Chapter.swift in Sources */,
|
37520699285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||||
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
@ -2761,7 +2793,9 @@
|
|||||||
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||||
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
||||||
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */,
|
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||||
|
377ABC4C286E6A78009C986F /* LocationsSettings.swift in Sources */,
|
||||||
376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */,
|
376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */,
|
||||||
|
37F0F4EE286F734400C06C2E /* AdvancedSettings.swift in Sources */,
|
||||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */,
|
37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||||
37599F30272B42810087F250 /* FavoriteItem.swift in Sources */,
|
37599F30272B42810087F250 /* FavoriteItem.swift in Sources */,
|
||||||
@ -2777,6 +2811,7 @@
|
|||||||
37484C2926FC83FF00287258 /* AccountForm.swift in Sources */,
|
37484C2926FC83FF00287258 /* AccountForm.swift in Sources */,
|
||||||
37E70927271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70927271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
|
377ABC44286E4B74009C986F /* ManifestedInstance.swift in Sources */,
|
||||||
37EBD8C627AF26B300F1C24B /* AVPlayerBackend.swift in Sources */,
|
37EBD8C627AF26B300F1C24B /* AVPlayerBackend.swift in Sources */,
|
||||||
375E45F527B1976B00BA7902 /* MPVOGLView.swift in Sources */,
|
375E45F527B1976B00BA7902 /* MPVOGLView.swift in Sources */,
|
||||||
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
@ -2809,6 +2844,7 @@
|
|||||||
files = (
|
files = (
|
||||||
3727B74B27872B880021C15E /* VisualEffectBlur-macOS.swift in Sources */,
|
3727B74B27872B880021C15E /* VisualEffectBlur-macOS.swift in Sources */,
|
||||||
374710062755291C00CE0F87 /* SearchField.swift in Sources */,
|
374710062755291C00CE0F87 /* SearchField.swift in Sources */,
|
||||||
|
37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */,
|
||||||
378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||||
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
||||||
37737786276F9858000521C1 /* Windows.swift in Sources */,
|
37737786276F9858000521C1 /* Windows.swift in Sources */,
|
||||||
@ -2828,6 +2864,7 @@
|
|||||||
37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
||||||
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
3751BA8427E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
|
3751BA8427E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
|
||||||
|
377ABC4D286E6A78009C986F /* LocationsSettings.swift in Sources */,
|
||||||
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
|
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
|
||||||
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
@ -2863,6 +2900,7 @@
|
|||||||
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||||
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||||
|
377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */,
|
||||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||||
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
||||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
@ -2925,12 +2963,14 @@
|
|||||||
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||||
37732FF52703D32400F04329 /* Sidebar.swift in Sources */,
|
37732FF52703D32400F04329 /* Sidebar.swift in Sources */,
|
||||||
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
|
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||||
|
377ABC49286E5887009C986F /* Sequence+Unique.swift in Sources */,
|
||||||
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||||
3784B23E2728B85300B09468 /* ShareButton.swift in Sources */,
|
3784B23E2728B85300B09468 /* ShareButton.swift in Sources */,
|
||||||
37BE0BDA26A214630092E2DB /* AppleAVPlayerViewController.swift in Sources */,
|
37BE0BDA26A214630092E2DB /* AppleAVPlayerViewController.swift in Sources */,
|
||||||
37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||||
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */,
|
374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */,
|
||||||
|
37F0F4EF286F734400C06C2E /* AdvancedSettings.swift in Sources */,
|
||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
@ -2984,6 +3024,7 @@
|
|||||||
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
||||||
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||||
|
377ABC41286E4AD5009C986F /* InstancesManifest.swift in Sources */,
|
||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
@ -3043,7 +3084,6 @@
|
|||||||
3774126627387D6D00423605 /* Array+Next.swift in Sources */,
|
3774126627387D6D00423605 /* Array+Next.swift in Sources */,
|
||||||
3774126727387D6D00423605 /* View+Borders.swift in Sources */,
|
3774126727387D6D00423605 /* View+Borders.swift in Sources */,
|
||||||
3774123327387CB000423605 /* Defaults.swift in Sources */,
|
3774123327387CB000423605 /* Defaults.swift in Sources */,
|
||||||
3774123527387CC700423605 /* PipedAPI.swift in Sources */,
|
|
||||||
3774124E27387D2300423605 /* Playlist.swift in Sources */,
|
3774124E27387D2300423605 /* Playlist.swift in Sources */,
|
||||||
3766AFD2273DA97D00686348 /* Int+FormatTests.swift in Sources */,
|
3766AFD2273DA97D00686348 /* Int+FormatTests.swift in Sources */,
|
||||||
3774124F27387D2300423605 /* SubscriptionsModel.swift in Sources */,
|
3774124F27387D2300423605 /* SubscriptionsModel.swift in Sources */,
|
||||||
@ -3079,6 +3119,7 @@
|
|||||||
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
|
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
|
||||||
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||||
|
37F0F4EC286F397E00C06C2E /* SettingsModel.swift in Sources */,
|
||||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||||
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */,
|
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */,
|
||||||
37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
||||||
@ -3134,6 +3175,7 @@
|
|||||||
37A9966026D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
37A9966026D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
||||||
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
3751B4B427836902000B7DF4 /* SearchPage.swift in Sources */,
|
3751B4B427836902000B7DF4 /* SearchPage.swift in Sources */,
|
||||||
|
377ABC46286E4B74009C986F /* ManifestedInstance.swift in Sources */,
|
||||||
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
37130A61277657300033018A /* PersistenceController.swift in Sources */,
|
37130A61277657300033018A /* PersistenceController.swift in Sources */,
|
||||||
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
@ -3142,6 +3184,7 @@
|
|||||||
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||||
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */,
|
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
|
377ABC4E286E6A78009C986F /* LocationsSettings.swift in Sources */,
|
||||||
3756C2A82861131100E4B059 /* NetworkState.swift in Sources */,
|
3756C2A82861131100E4B059 /* NetworkState.swift in Sources */,
|
||||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||||
@ -3168,6 +3211,7 @@
|
|||||||
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
||||||
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
||||||
|
377ABC42286E4AD5009C986F /* InstancesManifest.swift in Sources */,
|
||||||
37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */,
|
37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */,
|
||||||
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
|
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
@ -3192,8 +3236,10 @@
|
|||||||
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
|
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||||
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
|
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||||
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
||||||
|
37F0F4F0286F734400C06C2E /* AdvancedSettings.swift in Sources */,
|
||||||
373197DA2732060100EF734F /* RelatedView.swift in Sources */,
|
373197DA2732060100EF734F /* RelatedView.swift in Sources */,
|
||||||
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */,
|
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */,
|
||||||
|
377ABC4A286E5887009C986F /* Sequence+Unique.swift in Sources */,
|
||||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
|
@ -16,13 +16,12 @@ struct InstancesSettings: View {
|
|||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<SettingsModel> private var settings
|
||||||
|
|
||||||
@Default(.instances) private var instances
|
@Default(.instances) private var instances
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
SettingsHeader(text: "Instance")
|
|
||||||
|
|
||||||
if !instances.isEmpty {
|
if !instances.isEmpty {
|
||||||
Picker("Instance", selection: $selectedInstanceID) {
|
Picker("Instance", selection: $selectedInstanceID) {
|
||||||
ForEach(instances) { instance in
|
ForEach(instances) { instance in
|
||||||
@ -31,7 +30,7 @@ struct InstancesSettings: View {
|
|||||||
}
|
}
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
} else {
|
} else {
|
||||||
Text("You have no instances configured")
|
Text("You have no custom locations configured")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ struct InstancesSettings: View {
|
|||||||
|
|
||||||
let list = List(selection: $selectedAccount) {
|
let list = List(selection: $selectedAccount) {
|
||||||
if selectedInstanceAccounts.isEmpty {
|
if selectedInstanceAccounts.isEmpty {
|
||||||
Text("You have no accounts for this instance")
|
Text("You have no accounts for this location")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
ForEach(selectedInstanceAccounts) { account in
|
ForEach(selectedInstanceAccounts) { account in
|
||||||
@ -116,13 +115,11 @@ struct InstancesSettings: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("Remove Instance") {
|
Button("Remove Location") {
|
||||||
presentingInstanceRemovalConfirmation = true
|
presentingInstanceRemovalConfirmation = true
|
||||||
}
|
settings.presentAlert(Alert(
|
||||||
.alert(isPresented: $presentingInstanceRemovalConfirmation) {
|
|
||||||
Alert(
|
|
||||||
title: Text(
|
title: Text(
|
||||||
"Are you sure you want to remove \(selectedInstance!.longDescription) instance?"
|
"Are you sure you want to remove \(selectedInstance!.longDescription) location?"
|
||||||
),
|
),
|
||||||
message: Text("This cannot be undone"),
|
message: Text("This cannot be undone"),
|
||||||
primaryButton: .destructive(Text("Remove")) {
|
primaryButton: .destructive(Text("Remove")) {
|
||||||
@ -134,13 +131,13 @@ struct InstancesSettings: View {
|
|||||||
selectedInstanceID = instances.last?.id
|
selectedInstanceID = instances.last?.id
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Add Instance...") {
|
Button("Add Location...") {
|
||||||
presentingInstanceForm = true
|
presentingInstanceForm = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,13 @@ struct AccountSelectionView: View {
|
|||||||
@Default(.instances) private var instances
|
@Default(.instances) private var instances
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section(header: Text(showHeader ? "Current Account" : "")) {
|
Section(header: Text(showHeader ? "Current Location" : "")) {
|
||||||
Button(accountButtonTitle(account: accountsModel.current, long: true)) {
|
Button(accountButtonTitle(account: accountsModel.current)) {
|
||||||
if let account = nextAccount {
|
if let account = nextAccount {
|
||||||
accountsModel.setCurrent(account)
|
accountsModel.setCurrent(account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(instances.isEmpty)
|
.disabled(instances.isEmpty && Defaults[.countryOfPublicInstances].isNil)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
ForEach(allAccounts) { account in
|
ForEach(allAccounts) { account in
|
||||||
Button(accountButtonTitle(account: account)) {
|
Button(accountButtonTitle(account: account)) {
|
||||||
@ -32,20 +32,18 @@ struct AccountSelectionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allAccounts: [Account] {
|
var allAccounts: [Account] {
|
||||||
accounts + instances.map(\.anonymousAccount)
|
accounts + instances.map(\.anonymousAccount) + [accountsModel.publicAccount].compactMap { $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var nextAccount: Account? {
|
private var nextAccount: Account? {
|
||||||
allAccounts.next(after: accountsModel.current)
|
allAccounts.next(after: accountsModel.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountButtonTitle(account: Account! = nil, long: Bool = false) -> String {
|
func accountButtonTitle(account: Account! = nil) -> String {
|
||||||
guard account != nil else {
|
guard account != nil else {
|
||||||
return "Not selected"
|
return "Not selected"
|
||||||
}
|
}
|
||||||
|
|
||||||
let instanceDescription = long ? account.instance.longDescription : account.instance.description
|
return account.isPublic ? account.description : "\(account.description) — \(account.instance.shortDescription)"
|
||||||
|
|
||||||
return instances.count > 1 ? "\(account.description) — \(instanceDescription)" : account.description
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ struct TVNavigationView: View {
|
|||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
|
@EnvironmentObject<SettingsModel> private var settings
|
||||||
|
|
||||||
@Default(.visibleSections) private var visibleSections
|
@Default(.visibleSections) private var visibleSections
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ struct TVNavigationView: View {
|
|||||||
.tag(TabSelection.trending)
|
.tag(TabSelection.trending)
|
||||||
}
|
}
|
||||||
|
|
||||||
if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists {
|
if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists, accounts.signedIn {
|
||||||
PlaylistsView()
|
PlaylistsView()
|
||||||
.tabItem { Text("Playlists") }
|
.tabItem { Text("Playlists") }
|
||||||
.tag(TabSelection.playlists)
|
.tag(TabSelection.playlists)
|
||||||
@ -56,7 +57,6 @@ struct TVNavigationView: View {
|
|||||||
.tag(TabSelection.settings)
|
.tag(TabSelection.settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $navigation.presentingSettings) { SettingsView() }
|
|
||||||
.fullScreenCover(isPresented: $navigation.presentingAddToPlaylist) {
|
.fullScreenCover(isPresented: $navigation.presentingAddToPlaylist) {
|
||||||
if let video = navigation.videoToAddToPlaylist {
|
if let video = navigation.videoToAddToPlaylist {
|
||||||
AddToPlaylistView(video: video)
|
AddToPlaylistView(video: video)
|
||||||
|
Loading…
Reference in New Issue
Block a user