Locations manifest, reorganized instances settings

This commit is contained in:
Arkadiusz Fal 2022-07-01 23:28:32 +02:00
parent 0f46bb179a
commit 7a9a490558
28 changed files with 686 additions and 214 deletions

View 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 }
}
}

View File

@ -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())

View File

@ -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 {

View File

@ -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

View File

@ -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,

View File

@ -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
} }

View 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)
}
}

View 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
)
}
}

View File

@ -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
View 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
}
}

View File

@ -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

View File

@ -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)"
} }
} }

View File

@ -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)

View File

@ -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] {

View File

@ -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,

View File

@ -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)

View File

@ -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
} }
} }

View 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()
}
}

View File

@ -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
} }

View 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())
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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 */,

View File

@ -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
} }
} }

View File

@ -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
} }
} }

View File

@ -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)