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